day7 :链表复杂操作

115 阅读4分钟

上篇学习了链表比较基础的操作,踏进了链表的世界,这篇我们学习相对复杂的链表操作了:反转、指定位置的删除等等

处理链表的本质,是处理链表结点之间的指针关系

快慢指针

删除链表的倒数第n个结点

真题描述:给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例: 给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个结点后,链表变为 1->2->3->5.
说明: 给定的 n 保证是有效的。

复习:dummy 结点——能够处理掉头结点为空的边界问题

const dummy = new ListNode() 
// 这里的 head 是链表原有的第一个结点 
dummy.next = head

思路分析:
难点:“倒数第 N 个”如何定位???因为遍历不可能从后往前走的,所以我们转换为“正数第 len - n + 1"个
噔噔磴~~~~ 快慢指针登场

快慢指针.gif

步骤:
① 快指针先走n步;
② 快慢指针一起前进;
③ 快指针到最后一个结点,两个指针就一起停下来;
④ 此时的满指针就是我们目标结点前一个(倒数第 n 个结点的前一个结点),然后做删除操作

const removeNthFromEnd = function (head, n) {
    // 初始化 dummy 结点
    const dummy = new ListNode()
    // dummy指向头结点
    dummy.next = head
    // 初始化快慢指针,均指向dummy
    let fast = dummy
    let slow = dummy

    // 快指针先走 n 步
    while (n !== 0) {
        fast = fast.next
        n--
    }

    // 快慢指针一起走
    while (fast.next) {
        fast = fast.next
        slow = slow.next
    }

    // 慢指针删除自己的后继结点
    slow.next = slow.next.next
    // 返回头结点
    return dummy.next
};

多指针

完全反转一个链表

真题描述:定义一个函数,输入一个链表的头结点,反转该链表并输出反转后链表的头结点。

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

思路分析:
把每个结点next指针反过来

image.png 怎么做呢???利用多指针

image.png cur.next = pre

image.png 到这时候有个疑问就是:next有什么用呢?
当cur.next改变指针指向后,下一次遍历就没法定位下一个结点了,所以next是保存cur的后续结点,下次遍历从这开始

完全反转链表.gif

const reverseList = function (head) {
    // 初始化前驱结点为 null
    let pre = null;
    // 初始化目标结点为头结点
    let cur = head;
    // 只要目标结点不为 null,遍历就得继续
    while (cur !== null) {
        // 记录一下 next 结点
        let next = cur.next;
        // 反转指针
        cur.next = pre;
        // pre 往前走一步
        pre = cur;
        // cur往前走一步
        cur = next;
    }
    // 反转结束后,pre 就会变成新链表的头结点
    return pre
};

局部反转一个链表

真题描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。

说明: 1 ≤ m ≤ n ≤ 链表长度。

示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL

思路分析:

image.png

问题来了: 如何让1指向4、让2指向5呢?
先来把m-n区域定为被逆序区间,解决这个问题关键就是对被逆序区间前后两个结点做额外处理!

image.png

// 入参是头结点、m、n
const reverseBetween = function (head, m, n) {
    // 定义pre、cur,用leftHead来承接整个区间的前驱结点
    let pre, cur, leftHead
    // 别忘了用 dummy 嗷
    const dummy = new ListNode()
    // dummy后继结点是头结点
    dummy.next = head
    // p是一个游标,用于遍历,最初指向 dummy
    let p = dummy
    // p往前走 m-1 步,走到整个区间的前驱结点处
    for (let i = 0; i < m - 1; i++) {
        p = p.next
    }
    // 缓存这个前驱结点到 leftHead 里
    leftHead = p
    // start 是反转区间的第一个结点
    let start = leftHead.next
    // pre 指向start
    pre = start
    // cur 指向 start 的下一个结点
    cur = pre.next
    // 开始重复反转动作
    for (let i = m; i < n; i++) {
        let next = cur.next
        cur.next = pre
        pre = cur
        cur = next
    }
    //  leftHead 的后继结点此时为反转后的区间的第一个结点
    leftHead.next = pre
    // 将区间内反转后的最后一个结点 next 指向 cur
    start.next = cur
    // dummy.next 永远指向链表头结点
    return dummy.next
};

环形链表

如何判断链表是否成环?

真题描述:给定一个链表,判断链表中是否有环。

示例 1:
输入:输入head=[3,2,0,-4],flag = 1
解释:链表中存在一个环 链表图片

思路视频讲解

思路分析:
从 flag 出发,只要我能够再回到 flag 处,那么就意味着,我正在遍历一个环形链表

image.png

const hasCycle = function (head) {
// 定义快慢指针
    let fast = slow = head
    while (slow && fast && fast.next) {
        // 慢指针走一步
        slow = slow.next;
        // 快指针走两步
        fast = fast.next.next
        // 如果存在环,快慢指针一定会相遇(相等)
        if (slow == fast) {
            return true
        }
    }
    // 如果快指针next指向空,那么就是无环
    if (fast.next == null) {
        return null
    }
}

衍生问题——定位环的起点

真题描述:给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 null。

示例 1:
输入:head = [3,2,0,-4](如下图)
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个结点。 链表成环1

示例 2:
输入:head = [1,2](如下图)
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个结点。 链表成环2

示例 3:
输入:head = [1](如下图)
输出:null
解释:链表中没有环。
链表成环3

视频讲解思路

// 快慢指针
const hasCycle = function (head) {
    // 定义快慢指针
    let fast = slow = head
    while (slow && fast && fast.next) {
        // 慢指针走一步
        slow = slow.next;
        // 快指针走两步
        fast = fast.next.next
        // 如果存在环,快慢指针一定会相遇(相等)
        if (slow == fast) {
            return true
        }
    }
    // 如果快指针next指向空,那么就是无环
    if (fast.next == null) {
        return null
    }
    // 如果有环,找出环入口
    // 定义新的快指针从头开始
    fast = head;
    while (slow != fast) {
        // 慢指针从刚刚相遇地方开始,快指针从head开始
        // 快慢指针同步前进
        slow = slow.next
        fast = fast.next
    }
    // 最后快慢指针相遇的地方就是环入口
    return slow

}