【算法通关村 Day1】链表的增删改查及链表中双指针法应用
链表青铜关卡
链表的增删改查
什么是链表?
链表(Linked List)是一种常用的数据结构,它由一系列节点组成,每个节点包含数据域和指针域。指针域存储了下一个节点的地址,从而建立起各节点之间的线性关系。
创建一个链表需要class类型,首先,我们需要定义链表节点。每个节点包含两部分:一个数据域(val
)和一个指向下一个节点的引用(next
)。(为简便将数据域类型设置为int) :
public class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
this.next = null;
}
}
-
插入操作
insertAtHead
: 在链表的头部插入一个新节点,首先创建一个新节点,并将新节点的next
指向当前的头节点,然后更新头节点指向新节点。insertAfterNode
: 在指定的节点后面插入一个新节点,需要将新节点的next
指向指定节点的next
,然后将指定节点的next
指向新节点。
-
删除操作
deleteNode
: 删除值为value
的节点。如果要删除的是头节点,直接更新头节点指向下一个节点。如果是链表中的其他节点,遍历链表找到该节点,并将前一个节点的next
指向要删除节点的下一个节点。
-
修改操作
updateNode
: 遍历链表查找值为oldValue
的节点,并将该节点的val
修改为newValue
。
-
查询操作
searchNode
: 遍历链表查找值为value
的节点,返回该节点的指针。如果未找到,返回null
。
public class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
this.next = null;
}
//1.1 在链表的头部插入一个新节点
public static ListNode insertAtHead(ListNode head, int value) {
ListNode newNode = new ListNode(value);
newNode.next = head;
head = newNode;
return head;
}
//1.2 在指定的节点后面插入一个新节点
public static ListNode insertAfterNode(ListNode node, int value) {
if (node == null) return null; //先检查指定节点是否为空
ListNode newNode = new ListNode(value);
newNode.next = node.next;
node.next = newNode;
return node;
}
//2. 删除:删除值为value的节点
public static boolean deleteNode(ListNode head, int value) {
if (head == null) return false;
//如果头节点就是要删除的节点
if (head.val == value) {
head = head.next;
return true;
}
ListNode current = head;
while (current.next != null && current.next.val != value) {
current = current.next;
}
if (current.next != null) {
current.next = current.next.next;
return true;
}
return false;
}
//3. 修改:修改值为 oldValue 的节点为 newValue
public static boolean updateNode(ListNode head, int oldValue, int newValue) {
ListNode current = head;
while (current != null) {
if (current.val == oldValue) {
current.val = newValue;
return true;
}
current = current.next;
}
return false;
}
//4. 查询:查找值为value的节点
public static ListNode searchNode(ListNode head, int value) {
ListNode current = head;
while (current != null) {
if (current.val == value) {
return current;
}
current = current.next;
}
return null;
}
//5.附加 打印链表
public static void printList(ListNode head) {
ListNode current = head;
while(current != null) {
System.out.println(current.val + " -> ");
current = current.next;
}
System.out.println("null");
}
//主程序测试
public static void main(String[] args) {
ListNode head = new ListNode(1);
//1.1 在链表的头部插入一个新节点
head = insertAtHead(head,2);
head = insertAtHead(head, 3);
printList(head);
//1.2 在指定的节点后面插入一个新节点
ListNode node2 = searchNode(head, 2);
if (node2 != null) {
insertAfterNode(node2, 4);
}
printList(head);
//2. 删除:删除值为value的节点
deleteNode(head, 4);
printList(head);
//3. 修改:修改值为 oldValue 的节点为 newValue
updateNode(head, 1, 100);
printList(head);
//4. 查询:查找值为value的节点
ListNode result = searchNode(head, 100);
if (result != null) {
System.out.println(result);
} else {
System.out.println("No ListNode Found");
}
}
}
链表白银关卡
两个链表第一个公共子节点问题
两个链表A和B,头节点已知,存在公共子节点c1,c1的位置和数量是不确定的,要求找到A,B的第一个公共子节点。
-
假设链表A和链表B的长度不相同:
- 当指针
h1
到达链表 A 的尾部时,它会被重定向到链表 B 的头部,继续遍历链表 B。 - 同样,当指针
h2
到达链表 B 的尾部时,它会被重定向到链表 A 的头部,继续遍历链表 A。 - 这样一来,两个指针最终会在公共节点处相遇。如果两个链表有公共部分,经过两次遍历(一次遍历链表 A,一次遍历链表 B),指针
h1
和h2
会最终相遇在第一个公共节点上。 - 如果没有公共节点,两者都将同时遍历完两个链表的长度,最终会相遇在
null
上。 -
由于
h1
和h2
在相遇时指向相同的节点,所以无论是返回h1
还是h2
都是正确的。最终返回的h1
就是两个链表的第一个公共节点。如果没有公共节点,返回null
。
- 当指针
public class IntersectionNode {
static class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
//双指针法
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode h1 = headA, h2 = headB;
while (h1 != h2) {
h1 = h1 == null ? headB : h1.next;
h2 = h2 == null ? headA : h2.next;
}
return h1;
}
public static void main(String[] args) {
IntersectionNode list = new IntersectionNode();
//创建链表A: 1 -> 2 -> 3
ListNode headA = new ListNode(1);
headA.next = new ListNode(2);
headA.next.next = new ListNode(3);
//创建链表B:6 -> 5 -> 4 -> 2 -> 3
ListNode headB = new ListNode(6);
headB.next = new ListNode(5);
headB.next.next = new ListNode(4);
headB.next.next.next = headA.next;
ListNode intersectionNode = list.getIntersectionNode(headA, headB);
if (intersectionNode != null) {
System.out.println("Intersection node value: " + intersectionNode.val);
} else {
System.out.println("No intersection");
}
}
}
结果:
Intersection node value: 2
- 时间复杂度: O(N + M),其中 N 和 M 分别是链表 A 和链表 B 的长度。每个指针最多遍历两个链表一次。
- 空间复杂度: O(1),只使用了常数的额外空间。
判断链表是否为回文序列
什么是回文序列?
如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。例如:
通过快慢指针找到链表的中间节点。具体来说:
slow
每次移动一步,fast
每次移动两步。- 当
fast
指针到达链表的末尾时,slow
正好到达链表的中间。
- 链表的长度为奇数时:
slow
会指向链表的中间节点,即正好有一个节点位于中间。 - 链表的长度为偶数时:
slow
会指向链表的第二个中间节点,即位于两个中间节点之间的位置。
反转指针:
nextTemp = curr.next
:保存当前节点的下一个节点,因为反转指针后,curr.next
会指向prev
,而不是curr.next
,所以我们需要提前保存。curr.next = prev
:反转当前节点的指针,使其指向前一个节点。prev = curr
:将prev
移动到当前节点,准备处理下一个节点。curr = nextTemp
:将curr
移动到下一个节点。
public class PalindromeLinkedList {
static class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
public boolean isPalindrome(ListNode head){
//1. 找到链表的中间节点
ListNode slow = head;
ListNode fast = head;
while (fast != null & fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 2. 反转链表的后半部分
ListNode reverseSecondHalf = reverse(slow);
//3. 比较前半部分和反转的后半部分
ListNode p1 = head;
ListNode p2 = reverseSecondHalf;
while (p2 != null) {
if (p1.val != p2.val) {
return false;
}
p1 = p1.next;
p2 = p2.next;
}
return true;
}
private ListNode reverse(ListNode head){
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
}
合并有序链表
存在链表A(1->2->4),和链表B(1->3->4),A和B都是有序链表,要求将A,B合成一个链表,合成后的链表包含A,B链表的所有元素,且仍然是有序的。
- 初始化一个新的虚拟头节点
headC
,它的val
设置为-1
,只是为了简化操作。 iter
是一个指针,用来遍历新链表,并构建合并后的链表。开始时,iter
指向虚拟头节点headC
。
-
遍历两个链表:
- 使用
while
循环来遍历两个链表。当两个链表都不为空时,比较它们的节点值:- 如果
headA.val < headB.val
,则将headA
连接到合并链表,并让headA
指向下一个节点。 - 否则,将
headB
连接到合并链表,并让headB
指向下一个节点。
- 如果
- 每次连接后,
iter
都移动到当前插入的节点(iter = iter.next
)。
- 使用
-
处理剩余节点:
- 当其中一个链表为空时,另一个链表可能还有剩余的节点。这时,只需要将剩余的节点直接连接到
iter
。 - 如果
headA
还不为空,直接将headA
剩余部分连接到iter
。 - 如果
headB
还不为空,直接将headB
剩余部分连接到iter
。
- 当其中一个链表为空时,另一个链表可能还有剩余的节点。这时,只需要将剩余的节点直接连接到
public class MergeTwoList {
static class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
public static ListNode mergeTwoList(ListNode headA, ListNode headB){
ListNode headC = new ListNode(-1);
ListNode iter = headC;
while (headA != null && headB != null) {
if (headA.val < headB.val) {
iter.next = headA;
headA = headA.next;
iter = iter.next;
} else {
iter.next = headB;
headB = headB.next;
iter = iter.next;
}
}
if (headA != null) {
iter.next = headA;
}
if (headB != null) {
iter.next = headB;
}
return headC.next;
}
}
链表经典问题之双指针
寻找中间节点
前面已经做过。思路即设置两个指针fast,slow同时指向head,fast每次走两步,slow每次走一步,fast为空时,slow即为中间节点。
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast!=null&&fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
寻找倒数第K个节点
设置两个指针fast,slow同时指向head,fast先走K步,然后fast和slow一起走,当fast为空时,slow即是倒数第K个节点。
public ListNode getKthFromEnd(ListNode head, int k){
ListNode fast = head;
ListNode slow = head;
while (fast != null && k > 0) {
fast = fast.next;
k--;
}
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
旋转链表
旋转链表,将链表每个节点向右移动 k
个位置。
可以观察到,每一个节点向后移动K步,可以直接将倒数第K个节点以及之后的节点拼接到链表头部。
class Solution {
public ListNode rotateRight(ListNode head, int k) {
// 如果链表为空或者只有一个节点,直接返回
if (head == null || head.next == null) {
return head;
}
//计算链表的长度
int length = 1;
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
length++;
}
//调整k,避免k大于链表长度
k = k % length;
//刚好是head无需旋转
if (k == 0) {
return head;
}
// 找到新的尾节点(倒数第k个节点)
ListNode newTail = head;
for(int i = 1; i < length - k; i++){
newTail = newTail.next;
}
ListNode newHead = newTail.next;
// 将链表断开并重新连接
newTail.next = null;
tail.next = head;
return newHead;
}
}
链表黄金关卡
链表中环的问题
使用两个指针,快指针一次走两步,慢指针一次走一步。当两个指针相遇时,证明链表有环。然后,如果快指针到达链表的末尾(即 fast == null
或 fast.next == null
),则链表无环。
假设链表中存在环,非环长度为a,环长度为b,则使用快慢指针一定会相遇。我们分析相遇时的情况。fast指针式slow指针速度的两倍,则f=2s,fast比slow指针多走了n个环的长度,f=s+nb,联立方程,得f=2nb s=nb,当s走到环的入口时s = a+nb,我们想要找到环的入口,只需要通过重新设置让fast指针指向head,每次走一步,slow与fast同步走,两者在一起走a步,在环入口相遇。
若题目要求返回是否存在环的布尔值(如Leetcode 141),只需调整返回值:
public boolean hasCycle(ListNode head) {
if (head == null) return false;
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
若还需要返回环的入口节点,则需调整函数,类型为ListNode,如果没有环则返回null。
public ListNode detectCycle(ListNode head) {
if (head == null) return null;
ListNode slow = head;
ListNode fast = head;
boolean hasCycle = false;
// 步骤1:检测是否有环
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
hasCycle = true;
break;
}
}
// 无环则返回null
if (!hasCycle) return null;
// 步骤2:找到环的入口
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}