203. 移除链表元素
文档讲解:代码随想录
视频讲解:B站视频
状态:使用虚拟头节点AC,同时了解到还可以直接使用本节点进行删除。这道题可以说是之后好几道题的基础,在链表中使用虚拟头节点是十分常见的操作。
首先介绍使用虚拟头节点对链表进行操作,这样做的好处是头节点往往需要特殊的操作,使得编辑头节点时需要额外写一段逻辑,如果使用虚拟头节点的话就没有这个问题。下面首先介绍使用虚拟头节点的方法。
删除链表元素可以分解为以下步骤:
- 索引移动到待删除节点的上一个节点处
- 待删除节点的上一个节点指针指向待删除节点的下一个节点
注意:使用指针遍历节点时,应该注意指针递增的时机,在该题中,若下一个节点不是目标节点,指针递增。具体代码如下:
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode virhead = new ListNode(-1, head);
ListNode result = head;
ListNode index = virhead;
while(index.next != null) {
if (index.next.val != val) { // 下一个节点不是目标节点,索引+1
index = index.next;
}
else index.next = index.next.next; //下一个节点是目标节点,删除指向下一个节点的指针
}
result = virhead.next;
return result;
}
}
然后介绍不使用虚拟节点的方法,这种方法删除头节点以外的节点时逻辑相同,都是在待删除节点的前一个节点上进行指针操作,但是在删除头结点时,需要在待删除节点上进行操作,因此在这里应该分情况讨论。
- 待删除节点不是头节点,指针正常遍历到待删除节点的上一个节点进行操作
- 待删除节点是头节点,将头节点指向下一个节点。
如何判断待删除的节点为头节点?这需要在正常遍历流程开始之前先使用一个循环判断,直到头节点不是目标节点,再进行常规遍历操作。流程图如下:
具体实现代码如下:
public ListNode removeElements(ListNode head, int val) {
while (head != null && head.val == val) { //操作头节点知道头节点不是目标元素
head = head.next;
}
// 已经为null,提前退出
if (head == null) {
return head;
}
// 已确定当前head.val != val
ListNode pre = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
707. 设计链表
文档讲解:代码随想录
视频讲解:B站视频
状态:多次debug后AC,这道题十分考验对于链表的操作能力,在做题时经常因为对链表操作掌握不熟练而疏忽删除节点,增加节点等操作中的一些细节。这道题需要以后回来复习。
这道题需要设计的有链表类的字段、节点类设计,get方法、insert方法以及delete方法。
-
链表类字段
参考标准库中的
LinkedList
库,需要一个指向链表头部的指针和一个指向链表尾部的指针,以及一个表示链表长度的量:class MyLinkedList { private int size; private Node head; private Node tail; public MyLinkedList() { this.size = 0; this.head = null; this.tail = null; } }
-
节点内部类
将节点类作为链表类的内部类,这样节点类对外隐藏,创建节点首先需要一个链表类的实例,保证了封装性。单链表的节点类设计如下:
private class Node { private int val; private Node next; private Node(int val, Node next) { this.val = val; this.next = next; } }
-
get方法
这个方法需要得到指定索引处的链表元素,使用遍历得到元素,方法如下:
public int get(int index) { if (index < 0 || index >= size) return -1; Node current = head; for (int i = 0; i < index; i++) { current = current.next; } return current.val; }
-
insert方法设计
这一类方法包含三个方法
addAtHead
方法需要改变头节点,根据size大小也需要改变尾节点;addAtTail
方法需要改变尾节点,根据size大小也需要改变头节点;addAtIndex
方法需要插入元素,在插入到头部或者尾部时可以调用函数解决。
具体方法实现如下:
public void addAtHead(int val) { Node newHead = new Node(val, head); head = newHead; if (size == 0) { tail = newHead; } size++; } public void addAtTail(int val) { Node newTail = new Node(val, null); if (size == 0) { head = newTail; tail = newTail; } else { tail.next = newTail; tail = newTail; } size++; } public void addAtIndex(int index, int val) { if (index < 0 || index > size) return; if (index == 0) { addAtHead(val); return; } if (index == size) { addAtTail(val); return; } Node prev = head; for (int i = 0; i < index - 1; i++) { prev = prev.next; } Node newNode = new Node(val, prev.next); prev.next = newNode; size++; }
-
deleteAtIndex
方法这个方法删除指定索引处的节点,整体方法与第一道题移除节点相同,需要注意的是,如果移除的是头节点或者尾节点,相应的指针也需要改变。
public void deleteAtIndex(int index) { if (index < 0 || index >= size) return; if (index == 0) { head = head.next; if (size == 1) { tail = null; } } else { Node prev = head; for (int i = 0; i < index - 1; i++) { prev = prev.next; } prev.next = prev.next.next; if (index == size - 1) { tail = prev; } } size--; }
注意:操作链表时要特别留心链表的临界条件。
206. 反转链表
文档讲解:代码随想录
视频讲解:B站视频
状态:使用迭代法更容易让人理解,但是递归代码少,再尝试使用递归求解时碰壁,这道题需要以后回来复习。
使用迭代方法时,反转链表需要我们获取前一个节点和当前节点,使得当前节点指向前一个节点,并且也需要一个临时变量来存储后一个节点,因此流程如下:
- 获得后一个节点并存储在临时变量中
- 将当前节点指向上一个节点
- 索引移动至临时变量所在的节点
直到当前节点为null,就可以返回前一个节点作为头节点。
代码如下:
class Solution {
public ListNode reverseList(ListNode head) {
// 迭代解决
// 两个指针,一个指向当前节点,一个指向前一个节点
var index = head;
ListNode prev = null;
// 迭代
while(index != null) {
var temp = index.next;
index.next = prev;
// 两个索引进行移动
prev = index;
index = temp;
}
return prev
}
递归法只讨论给定函数声明的递归方法:
- 递归每次传入参数不一致,函数栈的上一层传入参数是在下一层的基础上+1;
- 递归的退出条件是遍历到链表的最后一个元素,返回当前元素作为头节点,并且一直返回到栈底
- 每一个函数栈需要完成当前索引所在节点的下一个节点的链表反转,并负责将最顶层函数栈的节点返回到main函数中,这是因为最后一个节点不做反转直接返回,所以导致当前节点的反转操作是在前一个节点处完成的。
具体代码如下:
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null) return null;
if (head.next == null) return head;
// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
}