1 - 138. 随机链表的复制

构造这个链表的 深拷贝。
    #### 题意:

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数。

难度:

中等

分析:

和通常的深拷贝不同的是, 这里的节点多了随机指针

那么在拷贝随机指针之前, 就必须有原链表

因此分为三步

  • 拷贝原链表
  • 拷贝随机指针
  • 分离链表

复制节点并插入原节点后面

while (cur != null) {
    Node newNode = new Node(cur.val);
    newNode.next = cur.next;
    cur.next = newNode;
    cur = newNode.next;
}

比如原链表:A->B->C 变成 A->A’->B->B’->C->C'

复制随机指针

while (cur != null) {
    if (cur.random != null) {
        cur.next.random = cur.random.next;
    }
    cur = cur.next.next;
}

利用第一步创建的节点关系,设置复制节点的random指针:

  • 如果原节点A的random指向节点C
  • 那么A’的random就应该指向C’(即C的next)

分离两个链表


while (cur != null) {
    Node next = cur.next.next;
    Node copy = cur.next;
    copyCur.next = copy;
    copyCur = copy;
    cur.next = next;
    cur = next;
}

将交织在一起的链表分开:

  • 原链表恢复原样:A->B->C
  • 得到复制的链表:A’->B’->C'

整体:

class Solution {

    public Node copyRandomList(Node head) {

        if (head == null) {

            return null;

        }

        // 第一步:在每个原节点后创建一个新节点

        Node cur = head;

        while (cur != null) {

            Node newNode = new Node(cur.val);

            newNode.next = cur.next;

            cur.next = newNode;

            cur = newNode.next;

        }

        // 第二步:处理random指针

        cur = head;

        while (cur != null) {

            if (cur.random != null) {

                cur.next.random = cur.random.next;

            }

            cur = cur.next.next;

        }

        // 第三步:分离两个链表

        Node dummy = new Node(0);

        Node copyCur = dummy;

        cur = head;

        while (cur != null) {

            // 保存下一个原始节点

            Node next = cur.next.next;

            // 复制的节点

            Node copy = cur.next;

            copyCur.next = copy;

            copyCur = copy;

            // 恢复原始链表

            cur.next = next;

            cur = next;

        }

        return dummy.next;

    }
}

2 - 141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。

题意:

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

难度:

简单

示例:

示例 1:

输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。

解析:

使用循环遍历链表查重的方法内存和时间开销很大

这里介绍一种算法, 龟兔赛跑算法, 就是快慢指针

定义两个指针, 二者遍历速度不同, 这样就可以保证快指针在有环的情况下可以追上慢指针

public class Solution {

   public boolean hasCycle(ListNode head) {

    if (head == null || head.next == null) {

        return false;

    }

    ListNode slow = head;

    ListNode fast = head.next;

    while (slow != fast) {

        if (fast == null || fast.next == null) {

            return false;

        }

        slow = slow.next;

        fast = fast.next.next;

    }

    return true;

    }

}

3 - 160. 相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

题意:

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

难度:

简单

示例:

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3

输出:Intersected at ‘8’

分析:

判断相交在链表中是一项很基本, 也很重要的算法

我们可以将两个链表分别遍历并放入哈希表中去重, 但是当数组较长时会浪费许多资源

其实这道题的核心是找出第一个相交节点, 而非全部节点, 所以可以使用双指针同时遍历两个链表

大致思路如下:

  1. 设置两个指针分别指向两个链表的头部
  2. 判断是否重复, 重复即为相交
  3. 首先移动其中一个指针, 判断重复
  4. 移动另一个指针, 判断重复
  5. 直至某个指针为空返回false

public class Solution {

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {// 定义两个指针,初始分别指向两个链表头部
  // 如果任一链表为空,则不可能相交

        if (headA == null || headB == null) {
            return null;
        }

        ListNode ptrA = headA;
        ListNode ptrB = headB;

        // 当两个指针不相等时继续遍历

        while (ptrA != ptrB) {
            // 移动指针A
            ptrA = ptrA == null ? headB : ptrA.next;
            // 移动指针B
            ptrB = ptrB == null ? headA : ptrB.next;
        }
         // 返回相交节点,如果不存在则返回null

        return ptrA;

4 - 19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

题意:

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例:

输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5]

难度:

中等

分析:

题目一共要求做两件事

  1. 找到链表的倒数第n个节点
  2. 删除该节点

对于单项链表, 如何找到节点的位置?

可以两次遍历, 第一次遍历拿链表长度

第二次才获取具体的节点

如何删除该节点?

删除节点涉及到三个部分

前驱节点, 该节点, 后驱节点

所以我们在第一步拿到前驱节点, 并保存后驱节点, 修改引用就算删除完毕

遍历链表

 while (current != null) { length++; current = current.next; }

注意是倒数第 n 个节点,所以要用链表的长度减去 n。倒数第 4 个节点的前一个节点就是 5-4=1,也就是第一个节点。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 创建一个虚拟头节点,简化边界条件处理
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        
        // 第一次遍历,计算链表的总长度
        int length = 0;
        ListNode current = head;
        while (current != null) {
            length++;
            current = current.next;
        }
        
        // 设置长度为到达要删除的节点的前一个节点
        int index = length - n;
        current = dummy;
        // 第二次遍历,找到要删除的节点的前一个节点
        for (int i = 0; i < index; i++) {
            current = current.next;
        }
        
        // 删除节点,即跳过要删除的节点
        current.next = current.next.next;
        
        return dummy.next;
    }
}

5 - 206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

题意:

翻转给定的链表

示例:

输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]

难度:

简单

分析:

链表翻转有两种方法

  1. 迭代
  2. 递归

这里我们用迭代法

从图中可以看出, 对链表的翻转可以理解为箭头方向变化

这样我们可以讲链表转为两两一组的节点对, 从头开始迭代

对于节点对, 我们分为首节点和次节点, 在单向链表, 次节点指向其他节点, 一旦断开次节点的引用, 其他节点会丢失

所以需要提前存储其他节点

改变引用关系大致流程如下:

  1. 获得首节点
  2. 临时存储其他节点
  3. 次节点指向首节点
  4. 迭代, 次节点为下一代的首节点, 临时存储节点为次节点
class Solution {

    public ListNode reverseList(ListNode head) {

        //迭代法

        ListNode current = head;

        ListNode previous = null;

        while(current != null){

            ListNode temp = current.next;

            current.next = previous;

            previous = current;

            current = temp;

        }

        return previous;

    }

}

6 - 21. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

题意:

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

难度:

简单

示例:

输入:l1 = [1,2,4], l2 = [1,3,4] 输出:[1,1,2,3,4,4]

分析:

提供的链表有序, 所以可以同时遍历. 连接较小的节点即可

  • 创建哑结点, 作为链表头, 当前节点为哑结点
  • 同时比较两个链表, 当前节点连接较小节点, 当前节点后驱, 当前节点为新节点
  • 直至某个链表遍历完成, 拼接其剩余节点
class Solution {

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {

        // 创建哑节点作为合并后链表的头部

        ListNode dummy = new ListNode(0);

        ListNode current = dummy;

        // 同时遍历两个链表,比较节点值

        while (l1 != null && l2 != null) {

            if (l1.val <= l2.val) {

                current.next = l1;

                l1 = l1.next;

            } else {

                current.next = l2;

                l2 = l2.next;

            }

            current = current.next;

        }

        // 将剩余节点接到合并后的链表末尾

        current.next = (l1 != null) ? l1 : l2;

        return dummy.next;

    }

}

7 - 23. 合并 K 个升序链表

给你一个链表数组,每个链表都已经按升序排列。 请你将所有链表合并到一个升序链表中,返回合并后的链表

题意:

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例:

image.png

合并链表. 最容易想到的就是依次合并

首先合并 1,2 之后合并3 这样依次合并

如何合并?

因为所提供的链表都是有顺序的 所以采用双指针方式

  • 比较两个节点的值
  • 将较小的节点连接到结果链表
  • 移动较小节点所在链表的指针
  • 移动结果链表的指针
  • 遍历完成后, 剩余节点直接拼接

但很遗憾,这样做的话,每一次合并后的新链表就会非常臃肿,并且在与第 K 个链表合并时,之前链表的节点会多次被访问。

我们可以使用分治的思想解决

为了解决臃肿和重复遍历的问题, 把链表的整体合并转为两两合并链表的子问题

image.png

首先判空

if (lists == null || lists.length == 0) {
    return null;
}

设置间隔, 每次只对间隔的头结点做合并操作

int interval = 1;  // 初始间隔为1
while (interval < n) {  // 当间隔小于链表总数时继续循环

这里使用interval来控制合并的步长,比如:

  • 第一轮:interval = 1,两两合并
  • 第二轮:interval = 2,每次合并相隔2个位置的链表
  • 第三轮:interval = 4,每次合并相隔4个位置的链表

合并

for (int i = 0; i < n - interval; i += interval * 2) {
    lists[i] = merge2Lists(lists[i], lists[i + interval]);
}

合并逻辑

while (l1 != null && l2 != null) {
    if (l1.val <= l2.val) {
        current.next = l1;
        l1 = l1.next;
    } else {
        current.next = l2;
        l2 = l2.next;
    }
    current = current.next;
}

//处理剩余节点
current.next = (l1 != null) ? l1 : l2;

整体如下:

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }

        int n = lists.length;
        int interval = 1;

        while (interval < n) {
            for (int i = 0; i < n - interval; i += interval * 2) {
                lists[i] = merge2Lists(lists[i], lists[i + interval]);
            }
            interval *= 2;
        }

        return lists[0];
    }

    private ListNode merge2Lists(ListNode l1, ListNode l2) {
        // 创建哑节点作为合并后链表的头部
        ListNode dummy = new ListNode(0);
        ListNode current = dummy;

        // 同时遍历两个链表,比较节点值
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                current.next = l1;
                l1 = l1.next;
            } else {
                current.next = l2;
                l2 = l2.next;
            }
            current = current.next;
        }

        // 将剩余节点接到合并后的链表末尾
        current.next = (l1 != null) ? l1 : l2;

        return dummy.next;
    }
}

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

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)

题意:

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。 你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

难度:

中等

示例:

输入:head = [1,2,3,4] 输出:[2,1,4,3]

分析:

看到这道题,我们要先搞清楚什么是两两交换,比如 1->2->3->4,交换后就是 2->1->4->3。

第一个和第二个交换,第三个和第四个交换,以此类推。

那么就可以把整个链转为子链, 通过递归处理

/**
 * Definition for singly-linked list.
 * public 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 swapPairs(ListNode head) {
        return swapAndConnect(head);
    }

    public ListNode swapAndConnect(ListNode node) {
        if (node == null || node.next == null) {
            // 如果当前节点或下一个节点为空,直接返回当前节点
            return node;
        }

        // 递归:获取后面的交换结果
        ListNode partner = node.next;
        ListNode next = swapAndConnect(partner.next);

        // 交换当前节点和下一个节点
        node.next = next;
        partner.next = node;

        // 返回交换后的新头节点
        return partner;
    }
}

9 - 25. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。 k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
/**

 * Definition for singly-linked list.

 * public 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 reverseKGroup(ListNode head, int k) {

        // 基本判断

        if (head == null || k == 1) return head;

        // 计算当前组的长度是否够k个节点

        ListNode curr = head;

        int count = 0;

        while (curr != null && count < k) {

            curr = curr.next;

            count++;

        }

        // 如果不够k个节点,保持原有顺序

        if (count < k) return head;

        // 递归处理后续节点组

        curr = reverseKGroup(curr, k);

        // 翻转当前组的k个节点

        ListNode prev = curr;

        ListNode now = head;

        while (count > 0) {

            ListNode next = now.next;

            now.next = prev;

            prev = now;

            now = next;

            count--;

        }

        return prev;

    }

}