算法训练营day04 | 链表part2,24. 两两交换链表中的节点,19.删除链表的倒数第N个节点,面试题 02.07. 链表相交,142.环形链表II

54 阅读7分钟

文章来源:代码随想录

24. 两两交换链表中的节点

题目链接

做题小结

理清链表的指针指向哪里就可以理清思路。需要注意next node = null的问题。

if(head == null){
    return head;
}else if(head.next == null){
    return head;
}

ListNode dummy = null;
ListNode pred = head;
ListNode curr = head.next;

while(curr != null){

    if(dummy == null){
        head = curr;
    }else{
        dummy.next = curr;
    }
    dummy = pred;
    pred.next = curr.next;
    curr.next = pred;
    if((dummy.next == null) || (dummy.next.next == null)){
        break;
    }
    pred = pred.next;
    curr = pred.next;

}

return head;

随想录代码

文章链接

随想录的方法用到了虚拟头节点来简化对head的操作。

ListNode dummy = new ListNode(0, head);
ListNode cur = dummy;
while (cur.next != null && cur.next.next != null) {
    ListNode node1 = cur.next;// 第 1 个节点
    ListNode node2 = cur.next.next;// 第 2 个节点
    cur.next = node2; // 步骤 1
    node1.next = node2.next;// 步骤 3
    node2.next = node1;// 步骤 2
    cur = cur.next.next;
}
return dummy.next;

19.删除链表的倒数第N个节点

题目链接

做题小结

这道题第一次看到没什么思路去找到倒数第n个节点,经过提示看到可以使用快慢指针进行定位,遂进行代码实现。但在实现过程中,删除head的不同方式处理较为复杂,于是转向随想录。

随想录讲解

文章链接

这里也提到使用虚拟头节点,之前以为虚拟节点是一个二选一的方法,但这里听讲解了解到使用虚拟头可以让对head的处理变得更加简便,遂觉得这是一个必要掌握的点。特别在于删除节点中,如果要删除节点,我们定位到的必须是被删除节点的上一个节点。 Screenshot 2024-06-08 at 21.20.57.png 另外,删除的点的定位就是使用快慢指针。具体实现上,fast指针要走n+1步,如此才能定位到要删除节点的上一个节点。

ListNode dummy = new ListNode(0, head);
ListNode slow = dummy;
ListNode fast = dummy;
for(int i=0;i<=n;i++){
    fast = fast.next;
}
while(fast != null){
    slow = slow.next;
    fast = fast.next;
}

if(slow.next != null){
    slow.next = slow.next.next;
}

return dummy.next;

这道题还让我联想到了一道题就是在单链表中如何定位到中位值,这个也可以使用快慢指针来实现。

面试题 02.07. 链表相交

题目链接

做题小结

这道题主要就是先遍历一遍链表,得到2个链表差多少个节点。让长一点的链表先把多出的节点走掉,然后和另一个链表一起往后走直到找到相同的一个节点。

ListNode A = headA;
int cntA = 0;
ListNode B = headB;
int cntB = 0;
int cnt = 0;
while(A != null){
    A = A.next;
    cntA++;
}
A = headA;

while(B != null){
    B = B.next;
    cntB++;
}
B = headB;


if(cntA > cntB){
    for(int i=0;i < cntA-cntB;i++){
        A = A.next;
    }
    cnt = cntB;
}else if(cntA < cntB){
    for(int i=0;i < cntB-cntA;i++){
        B = B.next;
    }
    cnt = cntA;
}else{
    cnt = cntA;
}

for(int i=0;i<cnt;i++){
    if(A == B){
        return A;
    }

    A = A.next;
    B = B.next;
}

return null;

随想录思路

文章链接 思路相同。 面试题02.07.链表相交_2

(版本一)先行移动长链表实现同步移动
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode curA = headA;
        ListNode curB = headB;
        int lenA = 0, lenB = 0;
        while (curA != null) { // 求链表A的长度
            lenA++;
            curA = curA.next;
        }
        while (curB != null) { // 求链表B的长度
            lenB++;
            curB = curB.next;
        }
        curA = headA;
        curB = headB;
        // 让curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            //1. swap (lenA, lenB);
            int tmpLen = lenA;
            lenA = lenB;
            lenB = tmpLen;
            //2. swap (curA, curB);
            ListNode tmpNode = curA;
            curA = curB;
            curB = tmpNode;
        }
        // 求长度差
        int gap = lenA - lenB;
        // 让curA和curB在同一起点上(末尾位置对齐)
        while (gap-- > 0) {
            curA = curA.next;
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA != null) {
            if (curA == curB) {
                return curA;
            }
            curA = curA.next;
            curB = curB.next;
        }
        return null;
    }

}
(版本二) 合并链表实现同步移动
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
		// p1 指向 A 链表头结点,p2 指向 B 链表头结点
		ListNode p1 = headA, p2 = headB;
		while (p1 != p2) {
			// p1 走一步,如果走到 A 链表末尾,转到 B 链表
			if (p1 == null) p1 = headB;
			else            p1 = p1.next;
			// p2 走一步,如果走到 B 链表末尾,转到 A 链表
			if (p2 == null) p2 = headA;
			else            p2 = p2.next;
		}
		return p1;
    }
}

142.环形链表II

题目链接

做题小结

这里我使用了集合元素的唯一性用于检测链表是否有环。主要思路是使用集合存储出现过的节点,如果有环,那么被指向的节点一定以前存进集合中过。第一个重复的节点就是环的入口。

Set<ListNode> nodes = new HashSet<ListNode>();
ListNode node = new ListNode(0);
node.next = head;
while(node != null){
    if(nodes.add(node.next)){
        node = node.next;
    }else{
        return node.next;
    }
}

return null;

这种方法逻辑比较简单但是在时间复杂度和空间复杂度上略微逊色一些,如果在面试过程中面试官不允许使用pre-built class,那么就会有一定的困难。

随想录思路

文章链接 视频链接 这道题目看视频讲解理解会快一些。

随想录的方法是使用快慢指针以及一定程度的数学计算来解这道题。第一次做题的人会比较难想到这个思路,因此需要巩固。

总体来说分为两步。

判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

  1. 为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

    这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,slow和fast指针之间是按一步的距离在逐步缩短,所以fast一定可以和slow重合。

寻找环的入口

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

图解来自代码随想录

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针,y+z为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。所以要求x ,将x单独放在左面:

x = n (y + z) - y 

因为n一定大于等于1,那么这个公式可以改写成

x = (n - 1) (y + z) + z 

分情况来讨论:

  1. 先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

    当n为1的时候,公式就化解为 x = z,这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

    也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

  2. n > 1的时候,就是在index1和index2逐步靠近的时候,index1多转了几圈才和index2相遇在环的入口。

代码实现

ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow == fast) {// 有环
        ListNode index1 = fast;
        ListNode index2 = head;
        // 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口
        while (index1 != index2) {
            index1 = index1.next;
            index2 = index2.next;
        }
        return index1;
    }
}
return null;

总结

文章链接

总结一下链表里学到的知识和方法:

  1. 虚拟头简化对head的处理
  2. 快慢双指针 -> 定位单链表中倒数n个或者找中位数的问题
  3. 相交节点:需要先让2个链表的指针指在距离交点的同一位上 -> 多的节点需要提前走掉