Linked List

链表

Tip:

链表是一种线性数据结构,由一系列节点组成,每个节点包含数据和指向下一个节点的指针。

链表的优点是动态大小和高效的插入和删除操作,但缺点是随机访问速度较慢。

对于链表题,我们要勤画图,敢于用变量,善用递归,保持思路清晰。

1. 合并两个有序链表

leetcode 21

本题要求将两个有序链表合并为一个新的有序链表。

这里我们可以使用递归的方法来解决这个问题,递归的思路是最后为 null 时返回上一级, 然后比较两个链表的头节点,较小的节点作为新的链表的头节点,然后递归地合并剩下的部分。

Tip:

递归的思路是:

  1. 如果 list1 为空,返回 list2;
  2. 如果 list2 为空,返回 list1;
  3. 如果 list1 的头节点小于等于 list2 的头节点,将 list1 的头节点作为新的链表的头节点, 然后递归地合并 list1.next 和 list2;
  4. 否则,将 list2 的头节点作为新的链表的头节点, 然后递归地合并 list1 和 list2.next。
var mergeTwoLists = function(list1, list2) {
    if (!list1) return list2;
    if (!list2) return list1;

    if (list1.val <= list2.val) {
        // list1 的头节点较小(或相等),那下面继续合并的就是 list1.next 和 list2
        list1.next = mergeTwoLists(list1.next, list2); // 递归合并 list1.next 和 list2
        return list1; // 返回 list1 作为合并后的头
    } else {
        // list2 的头节点较小(或相等),那下面继续合并的就是 list1 和 list2.next
        list2.next = mergeTwoLists(list1, list2.next); // 递归合并 list1 和 list2.next
        return list2; // 返回 list2 作为合并后的头
    }
};

2. 反转链表

leetcode 206

反转链表是一个经典的链表操作,要求将链表的节点顺序反转。 我们可以递归直到链表的末尾,然后在调用栈中逐层返回时,反转指针的方向。

Tip:

反转链表的思路是:

  1. 如果链表为空或只有一个节点,直接返回链表;
  2. 递归地反转链表的后半部分;
  3. 将当前节点的 next 指针指向前一个节点;
  4. 返回新的头节点。
var reverseList = function(head) {
    if (!head || !head.next) return head;
    const newHead = reverseList(head.next);
    // 将当前节点的下一个节点的 next 指向当前节点
    head.next.next = head;
    head.next = null;
    return newHead;
};

3. 反转链表 II

leetcode 92

反转链表 II 要求反转链表的部分区间,给定一个头节点和两个整数 m 和 n,要求反转从位置 m 到 n 的链表。 为了更好理解,我们可以用迭代的方法来解决这个问题。

Tip:

反转链表 II 的思路是:

  1. 找到反转区间的前一个节点和后一个节点;
  2. 反转区间内的链表;
  3. 将前一个节点的 next 指向反转后的头节点,将反转后的尾节点的 next 指向后一个节点。
  4. 返回新的头节点。
var reverseBetween = function(head, left, right) {
    if (!head || left === right) return head;

    // 创建一个虚拟头节点,方便处理边界情况(链表题常用)
    let dummy = new ListNode(0, head);
    let prev = dummy;

    // 找到反转区间的前一个节点
    for (let i = 1; i < left; i++) {
        prev = prev.next;
    }

    let curr = prev.next;
    let next = null;

    // 反转链表
    for (let i = 0; i < right - left; i++) {
        next = curr.next;
        curr.next = next.next;
        next.next = prev.next;
        prev.next = next;
    }

    return dummy.next;
};

4. 环形链表

leetcode 141

环形链表是一个特殊的链表,要求判断链表中是否存在环。 这种题的思路就是使用快慢指针,快指针每次走两步,慢指针每次走一步, 如果链表中存在环,快指针和慢指针一定会相遇。(可以理解为小学生都会的追及问题 hhh)

var hasCycle = function(head) {
    if (!head || !head.next) return false;

    let slow = head;
    let fast = head.next;

    while (slow !== fast) {
        if (!fast || !fast.next) return false;
        slow = slow.next;
        fast = fast.next.next;
    }

    return true;
};

5. 稍微上难度!K 个一组翻转链表

leetcode 25

本题要求将链表每 k 个节点一组进行翻转,如果最后一组的节点数不足 k,则不翻转。 这个题的思路就是对于每 k 个节点进行翻转,然后将翻转后的链表连接起来。

/**
 * @param {ListNode} head
 * @param {number} k
 * @return {ListNode}
 */
var reverseKGroup = function(head, k) {
    if (!head || k <= 1) {
        return head;
    }

    // 创建一个哑节点,方便处理头节点的变化
    let dummy = new ListNode(0, head);

    // groupPrev 指向当前 k-group 的前一个节点
    // 初始时,它是哑节点
    let groupPrev = dummy;

    while (true) {
        // 1. 检查是否有足够的节点(k 个)来进行反转
        let kthNode = groupPrev;
        for (let i = 0; i < k; i++) {
            kthNode = kthNode.next;
            if (!kthNode) {
                // 剩余节点不足 k 个,不需要反转,直接返回 dummy.next
                // groupPrev.next 已经正确指向剩余部分的头部
                return dummy.next;
            }
        }
        // 此时 kthNode 是当前 k-group 的尾节点

        // 2. 记录下一组的头节点
        let groupNext = kthNode.next;

        // 3. 反转当前的 k-group
        // k-group 的头节点是 groupPrev.next
        // k-group 的尾节点是 kthNode
        // 反转后,原来的头变成尾,原来的尾变成头

        let prevInSub = groupNext; // 反转后,当前组的(新)尾节点的 next 指针将指向 groupNext
        let currInSub = groupPrev.next; // 当前 k-group 的头节点,也是反转过程中的当前节点

        // 标准的链表反转过程,反转从 currInSub 到 kthNode (包含 kthNode) 的部分
        // 或者说,反转的节点直到 currInSub 变成 groupNext
        while (currInSub !== groupNext) {
            let tempNext = currInSub.next;
            currInSub.next = prevInSub;
            prevInSub = currInSub;
            currInSub = tempNext;
        }
        // 反转完成后, prevInSub 是当前 k-group 反转后的新头节点

        // 4. 将反转后的 k-group 重新连接回主链表
        let originalGroupHead = groupPrev.next;
        groupPrev.next = prevInSub;

        groupPrev = originalGroupHead;
    }

    return head;
};

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

leetcode 19

本题要求删除链表的倒数第 N 个节点。 我们可以使用快慢指针的方法,快指针先走 n 步,然后慢指针和快指针一起走, 那么当快指针走到链表的末尾时,慢指针正好在要删除的节点的前一个节点。

Tip:

删除链表的倒数第 N 个节点的思路是:

  1. 快指针先走 n 步;
  2. 快慢指针一起走,直到快指针到达链表的末尾;
  3. 慢指针的下一个节点就是要删除的节点,删除它。
  4. 返回链表的头节点。
var removeNthFromEnd = function(head, n) {
    // 创建一个虚拟头节点,方便处理边界情况(哨兵节点)
    let dummy = new ListNode(0, head);
    let fast = dummy;
    let slow = dummy;

    // 快指针先走 n 步
    for (let i = 0; i < n + 1; i++) {
        fast = fast.next;
    }

    // 快慢指针一起走,直到快指针到达链表的末尾
    while (fast) {
        fast = fast.next;
        slow = slow.next;
    }

    // 慢指针的下一个节点就是要删除的节点
    slow.next = slow.next.next;

    return dummy.next;
};

7. 删除排序链表中的重复元素 II

leetcode 82

本题要求删除排序链表中的重复元素。

思路就是一次遍历链表,使用一个指针来记录当前节点的下一个节点,

如果下一个节点的值和下下个节点的值相同,就将当前节点的 next 指针指向下下个节点,

否则就将当前节点的 next 指针指向下一个节点。

Tip:

删除排序链表中的重复元素 II 的思路是:

  1. 创建一个虚拟头节点,方便处理边界情况;
  2. 使用一个指针来记录当前节点的下一个节点;
  3. 如果下一个节点的值和下下个节点的值相同,就将当前节点的 next 指针指向下下个节点;
  4. 否则就将当前节点的 next 指针指向下一个节点。
  5. 返回链表的头节点。
var deleteDuplicates = function(head) {
    // 如果链表为空或只有一个节点,则不可能有重复,直接返回
    if (!head || !head.next) {
        return head;
    }

    // 创建一个哨兵节点,指向原始头节点,方便处理头节点可能被删除的情况
    const dummy = new ListNode(0, head); // 可以用任何与节点值不同的值

    let cur = dummy;
    while (cur.next && cur.next.next) {
        if (cur.next.val === cur.next.next.val) {
            const x = cur.next.val;
            while (cur.next && cur.next.val === x) {
                cur.next = cur.next.next;
            }
        } else {
            cur = cur.next;
        }
    }
    return dummy.next;
};

8. 旋转链表

leetcode 61

这题说是循环旋转,但其实本质上是将尾部向前数第 K 个元素作为头,原来的头接到原来的尾上

Tip:

旋转链表的思路是:

  1. 计算链表的长度;
  2. 将 k 对链表长度取模,得到实际需要旋转的步数;
  3. 找到新的头节点和尾节点;
  4. 将尾节点的 next 指向原来的头节点;
  5. 返回新的头节点。
var rotateRight = function(head, k) {
    if (!head || !head.next || k === 0) return head;

    // 计算链表的长度
    let length = 1;
    let tail = head;
    while (tail.next) {
        tail = tail.next;
        length++;
    }

    // 将 k 对链表长度取模,得到实际需要旋转的步数
    k %= length;

    // 如果 k 为 0,说明不需要旋转,直接返回头节点
    if (k === 0) return head;

    // 找到新的头节点和尾节点
    let newTail = head;
    for (let i = 1; i < length - k; i++) {
        newTail = newTail.next;
    }
    let newHead = newTail.next;

    // 将尾节点的 next 指向原来的头节点
    tail.next = head;
    // 将新的尾节点的 next 指向 null
    newTail.next = null;
    return newHead;
};