02.List(链表)
0、 基础理论
链表(Linked List)是一种常见的数据结构,它由一系列节点(Node)组成,每个节点包含两个部分:数据域(Data)和指针域(Pointer)。
- 单链表,每个节点的指针域都指向下一个节点;
- 双链表,每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
- 环形链表,最后一个节点的指针域指向头结点。
节点定义:
public class ListNode {
int value; // 结点的值
ListNode next; // 下一个结点
ListNode prev; // 上一个结点
public ListNode() {
}
public ListNode(int value) {
this.value = value;
}
public ListNode(int value, ListNode next, ListNode prev) {
this.value = value;
this.next = next;
this.prev = prev;
}
}
1、 移除节点
删除链表中等于给定值 val 的所有节点。
示例 1: 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2: 输入:head = [], val = 1 输出:[]
示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]
1.1、设置一个虚拟头结点在进行删除操作
public static ListNode removeListNode(ListNode head, int target) {
ListNode mock = new ListNode();
mock.next = head;
ListNode node = mock;
while (node.next != null) {
if (node.next.value == target) {
node.next = node.next.next;
} else {
node = node.next;
}
}
return mock.next;
}
1.2、递归删除节点
public static ListNode removeListNodeRecursion(ListNode head, int target) {
if (head == null) {
return null;
}
// 假设 removeListNodeRecursion() 返回后面完整的已经去掉val节点的子链表
// 在当前递归层用当前节点接住后面的子链表
// 随后判断当前层的node是否需要被删除,如果是,就返回
// 也可以先判断是否需要删除当前node,但是这样条件语句会比较不好想
head.next = removeListNodeRecursion(head.next, target);
if (head.value == target) {
return head.next;
}
return head; // 实际上就是还原一个从尾部开始重新构建链表的过程
}
2、 链表反转
2.1、双指针法
首先定义一个 cur 指针,指向头结点,再定义一个 pre 指针,初始化为 null。然后开始反转,首先要把 cur->next 节点用 tmp 指针保存一下,也就是保存一下这个节点。接下来要改变 cur->next 的指向了,将 cur->next 指向 pre ,此时已经反转了第一个节点了。继续移动 pre 和 cur 指针,循环反转代码逻辑。最后,cur 指针已经指向了 null,循环结束,链表也反转完毕了。此时返回 pre 指针就可以了,pre 指针就指向了新的头结点。
public static ListNode reverseList(ListNode head) {
ListNode node = head;
ListNode prev = null;
ListNode temp = null;
while (node != null) {
temp = node.next;
node.next = prev;
prev = node;
node = temp;
}
return prev;
}
2.2、递归法
从后向前递归链表
public static ListNode reverseListRecursion(ListNode head) {
// 边缘条件判断
if (head == null) {
return null;
}
if (head.next == null) {
return head;
}
// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseListRecursion(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = last;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
3、 链表删除倒数第 n 个节点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
3.1、双指针法
双指针的经典应用,如果要删除倒数第 n 个节点,让 fast 移动 n 步,然后让 fast 和 slow 同时移动,直到 fast 指向链表末尾。删掉 slow 所指向的节点就可以了。具体分为如下几步:
- 定义 fast 指针和 slow 指针,初始值为虚拟头结点
- fast 首先走 n + 1 步 ,为什么是 n+1 呢,因为只有这样同时移动的时候 slow 才能指向删除节点的上一个节点(方便做删除操作)
- fast 和 slow 同时移动,直到 fast 指向末尾
- 删除 slow 指向的下一个节点
public static ListNode removeNodeFromTail(ListNode head, int no) {
//新建一个虚拟头节点指向head
ListNode mock = new ListNode(Integer.MIN_VALUE, head);
//快慢指针指向虚拟头节点
ListNode fast = mock;
ListNode slow = mock;
// 只要快慢指针相差 n 个结点即可
for (int i = 0; i < no + 1; i++) {
fast = fast.next;
}
// 同时移动快慢指针,直到快指针指向链表结尾
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
// 此时 slow 的位置就是待删除元素的前一个位置
// 检查 slow.next 是否为 null,以避免空指针异常
if (slow.next != null) {
slow.next = slow.next.next;
}
return mock.next; // 返回虚拟头结点后的链表
}
4、 链表相交
给定两个单链表的头节点 headA 和 headB ,请找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
图示两个链表在节点 c1 开始相交:
public static ListNode findIntersectNode(ListNode headA, ListNode headB) {
int lenA = 0;
ListNode nodeA = headA;
while (nodeA != null) {
lenA++;
nodeA = nodeA.next;
}
int lenB = 0;
ListNode nodeB = headB;
while (nodeB != null) {
lenB++;
nodeB = nodeB.next;
}
// 让nodeA为最长链表的头,lenA为其长度
if (lenB > lenA) {
int lenT = lenA;
lenA = lenB;
lenB = lenT;
ListNode nodeT = headA;
headA = headB;
headB = nodeT;
}
// 求长度差,让nodeA和nodeB在同一起点上(末尾位置对齐)
nodeA = headA;
nodeB = headB;
int step = lenA - lenB;
while (step-- > 0) {
nodeA = nodeA.next;
}
// 遍历nodeA和nodeB,遇到相同则直接返回
while (nodeA != null) {
if (nodeA == nodeB) {
return nodeA;
}
nodeA = nodeA.next;
nodeB = nodeB.next;
}
return null;
}
5、 环形链表
给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回 null。
判断链表是否有环
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast 指针每次移动两个节点,slow 指针每次移动一个节点,如果 fast 和 slow 指针在途中相遇,说明这个链表有环。
这是因为 fast 是走两步,slow 是走一步,fast 指针一定先进入环中,如果 fast 指针和 slow 指针相遇的话,一定是在环中相遇;其实相对于 slow 来说,fast 是一个节点一个节点的靠近 slow 的,所以 fast 一定可以和 slow 重合。
找到环的入口
快慢指针相遇后,再从链表头节点和相遇节点同时出发,直到相遇,相遇点即为环的入口(推导过程有点长,记住结论算了)。
public static ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
// 快慢指针相遇,此时从head和slow相遇点,同时查找直至相遇
if (fast == slow) {
// 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
ListNode nodeH = head;
ListNode nodeF = fast;
while (nodeF != nodeH) {
nodeH = nodeH.next;
nodeF = nodeF.next;
}
return nodeH;
}
}
return null;
}