1. 今日收获:链表理论基础,移除元素,设计和反转链表
2. 链表理论基础
(1)链表每个结点同时储存了本结点的值和指向下一个结点的指针,Java链表结点定义如下:
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
(2)链表可以分为单链表,双链表,循环链表
(3)与数组不同,链表在内存中不是连续存放的;链表更适合应用于增删较多、查询较少的场景
3. 虚拟头结点,例题 203.移除链表元素
题目链接:203. 移除链表元素 - 力扣(LeetCode)
思想:定义一个指针,始终判断其指向的下一个元素的值是否满足条件。设置虚拟头结点指向头结点,可以统一链表中头结点和后续结点的操作
方法一:不设置虚拟头结点,需要对头结点和其他结点分别操作
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 不使用虚拟头节点,头节点和后续节点要分开操作
// 处理头节点相等的情况
while (head!=null && head.val==val){
head=head.next;
}
// 处理后续节点相等的情况,cur指向目标节点的上一个节点
ListNode cur=head;
while (cur!=null && cur.next!=null){ // cur没有指向最后一个节点
if (cur.next.val==val){
cur.next=cur.next.next; // 删除目标节点
}else {
cur=cur.next;
}
}
return head;
}
}
方法二:设置虚拟头结点,统一所有结点的操作
class Solution {
public ListNode removeElements(ListNode head, int val) {
// 使用虚拟头节点,统一头节点和后续节点的操作
ListNode dummyHead = new ListNode();
dummyHead.next=head;
ListNode cur=dummyHead;
while(cur!=null && cur.next!=null){
if (cur.next.val==val){
cur.next=cur.next.next;
}else {
cur=cur.next;
}
}
return dummyHead.next;
}
}
总结:本题可以学习到两个思路。首先是对链表进行增加删除操作时,需要始终定义变量指向目标结点的上一个结点;第二就是设置虚拟头结点的方法,可以统一链表中所有结点的操作。因为头结点和其他结点的区别就是没有指针指向它,虚拟头结点可以让链表的头结点不再“孤单”,避免其“与众不同”
4. 链表初始化,例题 707.设计链表
思想:在链表初始化时定义的头结点属于虚拟头结点,后续的增加或删除方法都在用cur指针遍历寻找操作位置
方法:head是虚拟头结点
class MyLinkedList {
private ListNode head; // 头节点
private int size;
public MyLinkedList() {
head = new ListNode(0);
size=0;
}
public int get(int index) {
if(index<0||index>=size){
return -1;
}
ListNode cur=head;
for (int i=0;i<=index;i++){
cur=cur.next;
}
return cur.val;
}
public void addAtHead(int val) {
ListNode newHead = new ListNode(val);
newHead.next=head.next;
head.next=newHead;
size++;
}
public void addAtTail(int val) {
ListNode newTail=new ListNode(val);
newTail.next=null;
// 寻找最后的节点
ListNode cur=head;
while (cur.next!=null){
cur=cur.next;
}
// 在cur后插入节点
cur.next=newTail;
size++;
}
public void addAtIndex(int index, int val) {
ListNode newNode = new ListNode(val);
ListNode cur=head;
while(cur!=null && index!=0){ // 下标为index节点的前一个节点
cur=cur.next;
index--;
}
if (cur!=null){
newNode.next=cur.next;
cur.next=newNode;
size++;
}
}
public void deleteAtIndex(int index) {
ListNode cur=head;
while(index!=0){ // 删除索引的前一个节点
cur=cur.next;
index--;
}
if (cur!=null && cur.next!=null){
cur.next=cur.next.next;
size--;
}
}
}
class ListNode{
int val;
ListNode next;
public ListNode(){}
public ListNode(int val){
this.val=val;
}
public ListNode(int val,ListNode next){
this.val=val;
this.next=next;
}
}
总结:1. 链表构造方法中新建的结点是虚拟头结点,刚开始做以为是链表的头结点。如果不在构造函数中创建结点,也可以在addAtHead中判断当前头结点是否为空,如果为空则作为头结点,不为空则插入结点,这个思路没有题解简单。
2. 做本题的过程中一直报空指针错误,后续一直在“补漏洞”做判断。在获取当前指针的下一个结点或下下个结点时要注意判断当前指针是否为空或下个结点是否为空。
5. 前后指针赋值顺序,例题 206. 反转链表
思想:定义指针分别指向要反转方向的当前指针和前一个指针,每次反转前还要保存当前指针的下一个指针,确保当前指针反转后还能“找到原来的路”
方法一:双指针解法
class Solution {
public ListNode reverseList(ListNode head) {
// 双指针解法
ListNode cur=head;
ListNode pre=null;
while (cur!=null){
ListNode temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
return pre;
}
}
方法二:递归
class Solution {
public ListNode reverseList(ListNode head) {
// 递归解法
ListNode pre=null;
ListNode cur=head;
return reverse(cur,pre);
}
public ListNode reverse(ListNode cur,ListNode pre){
if (cur==null){
return pre;
}
ListNode temp=cur.next;
cur.next=pre;
return reverse(temp,cur);
}
}
总结:结合两种算法可以更深入地了解递归。根据双指针解法可以推出递归的解法,每次要重复的操作就是根据当前指针和前一个指针反转当前指针的方向,也就是方法一中循环体的内容,而递归的终止条件就是循环跳出的条件。