链表算法题


链表算法题

链表题常见解法

  1. 创建一个新链表。let newHead = new ListNode()

需要使用到记录【前一个节点】这种概念时,要用到 newHead 这种虚拟头节点

  1. 记录 被循环的节点 的next。const next = currentNode.next

链表考什么?就是哨兵节点+虚拟节点+链表指针的移动

  1. 链表节点的交换,插入等等,每记录一个节点,下一步就是对其进行替换,如const next = p.next; p.next = xxx

双指针

  1. 要删除一个节点,需要知道这个节点的前一个节点,判断条件得是p.next
    p.next = p.next.next 代表p还指向旧p,抛弃了next,用于要连续判断后继节点时
    p = p.next.next 代表跳过了p,还跳过了next,指向了新p

头部可能插值的问题都需要一个头空结点

2、反转链表( LeetCode 206 )

leetcode

一个一个插入到newRoot节点后面,先记录下一个节点,再 currentNode.next = root.next

var reverseList = function(head) {
    let newHead = new ListNode(-1)
    while(head) {
        const next = head.next
        head.next = newHead.next
        newHead.next = head
        head = next
    }
    return newHead.next
};
// 极简
var reverseList = function(head) {
    let [prev, current] = [null, head]
    while(current) {
        [current.next, prev, current] = [prev, current, current.next]
    }
    return prev
}
// 递归
// 假设列表的其余部分已经被反转,现在我们应该如何反转当前节点?
// 1.如果为null则当前就是头节点,返回
// 2.先反转后续的节点
// 3.再反转当前节点
var reverseList = function(head) {
    if (head === null || head.next === null) { return head; }
    const newHead = reverseList(head.next); // 从最后一个结点开始轮,此处返回最后一个结点
    head.next.next = head; // 倒数第二个 的下一个结点 的next 指向当前结点
    head.next = null; // 当前结点 也就是 倒数第二个结点 next为null
    // 每一层 递归都记录了当前结点,所以直接把next置为null也没关系
    // 外层会继续将next 置为前一个数
    return newHead;
}

3、相交链表( LeetCode 160 )

leetcode

循环条件是pA !== pB

// 相交链表
var getIntersectionNode = function(headA, headB) {
    let p1 = headA,p2 = headB
    while(p1 !== p2) {
        p1 = p1 === null ? headB : p1.next
        p2 = p2 === null ? headA : p2.next
    }
    return p1
};
// 两个指针走的路程必然等长,不会无限循环,在 AB同为null时已经跳出循环了

4、合并两个有序链表 ( LeetCode 21 )

leetcode

// 迭代
var mergeTwoLists = function(l1, l2) {
    const preHead = new ListNode(-1);

    let prev = preHead;
    while (l1 && l2) {
        if (l1.val <= l2.val) {
            prev.next = l1;
            l1 = l1.next;
        } else {
            prev.next = l2;
            l2 = l2.next;
        }
        prev = prev.next;
    }
    prev.next = l1 === null ? l2 : l1;

    return preHead.next;
};
// 递归
var mergeTwoLists = function(l1, l2) {
    if (l1 === null) {
        return l2;
    } else if (l2 === null) {
        return l1;
    } else if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
};

5、分隔链表 ( LeetCode 86 )

var partition = function(head, x) {
    let small = new ListNode(0);
    const smallHead = small;
    let large = new ListNode(0);
    const largeHead = large;
    while (head !== null) {
        if (head.val < x) {
            small.next = head;
            small = small.next;
        } else {
            large.next = head;
            large = large.next;
        }
        head = head.next;
    }
    large.next = null;
    small.next = largeHead.next;
    return smallHead.next;
};

// 空间复杂度0
var partition = function(head, x) {
    let p = root = new ListNode(-1, head)
    while(p.next && p.next.val < x) {
        p = p.next
    }

    let p2 = p.next
    if(!p2) return root.next
    while(p2.next) {
        if(p2.next.val < x) {
            const temp = p2.next
            p2.next = p2.next.next
            temp.next = p.next
            p.next = temp
            p = p.next
        } else {
            p2 = p2.next
        }
    }
    return root.next
};

6、环形链表 II ( LeetCode 142 )

// 我想的是在节点上做标记,显然用set更好,记住这思路
var detectCycle = function(head) {
    const visited = new Set();
    while (head !== null) {
        if (visited.has(head)) {
            return head;
        }
        visited.add(head);
        head = head.next;
    }
    return null;
};
// 要理解 快慢指针 找链的环结构入口,要理解两个重要结论,
// 1.慢指针 走过的的长度 等于 环的长度
// 2.慢指针 从头走到环入口的长度 等于 慢指针 从相遇点走完剩余环的长度

// 假设 快指针 一次走 2个节点,慢指针 一次走 1个节点
// 假设 头到环入口长度 a, 环长度 b.
// 第 1 次相遇时, 快指针总路程 f,慢指针总路程 s,此时可得到两个等式
// (注意是第1次相遇,如果继续跑,第1次相遇后,s每跑1圈,f会跑2圈,会再次在这相遇,算法只需要考虑第1次相遇就行)
// f(快指针路程) = 2s(慢指针走的路程) (快指针是慢指针两倍速)
// f(快指针路程) = s(慢指针走的路程) + b(环的长度)
// 得到 s = b (慢指针走的路程 就是 环的长度b)
// 可理解为,第一次相遇的时候,s还刚进 第1圈,f已经多走了1圈环,所以 s=b
// 所以得到 重要结论 (1) 慢指针路程 s = b 环的节点数

// 接下来要思考,直觉上来说,为什么再次从头开始走一个 慢指针2
// 同时从第一次相遇处 走 慢指针1,两个慢指针必然会在 环入口处相遇?
// 换句话说 为什么 慢指针1 走完 剩余环的长度(y),会等于 从头到入环 的长度?

// 此时,我们整理下, 头到环入口a, 环入口到相遇处(x), 环长度b
// 很显然 环的长度(b) = x(入口到相遇处) + y(相遇处到环入口)
// 同时由结论(1) 环的长度(b) = s(慢指针路径) = a(头到环入口) + x(入口到相遇处)
// x(入口到相遇处) + y(相遇处到环入口) = a(头到环入口) + x(入口到相遇处)

// 语言解释就是, 头到相遇处, 相遇处再到环入口,是等长的,重合部分就是 入口到相遇处
// 路径上来说,都减去重合部分(入口到相遇处x),剩余部分路径相等
// 如果 假设两个慢指针,都不走重合部分,一个从头开始走,一个跳过重合部分从相遇处开始走
// 相同速度,走过相同路径时,便会相遇在入口

var detectCycle = function (head) {
    let fast = slow = head
    while (fast && fast.next) {
        slow = slow.next
        fast = fast.next.next
        if (fast === slow) {
            let p = head
            while(p !== slow) {
                p = p.next
                slow = slow.next
            }
            return p
        }
    }
    return null
};

7、反转链表 II ( LeetCode 92 )

// 要点,拆分子列,标记 左前与右 结点,左与右后 结点
var reverseBetween = function(head, left, right) {
    const pointH = new ListNode(-1,head) // 这类头部可能插值的问题都需要一个头空结点

    let pointL = pointH // 指向子链表left前一个结点
    for (let i = 0; i < left - 1; i++) {// 右移到left位置-1
        pointL = pointL.next
    }

    let pointR = pointL // 指向子链表right结点
    for (let i = 0; i < right - left + 1; i++) { // 右移差值+1个位置
        pointR = pointR.next
    }

    // 切出子链表,记录子链表头部 left,尾部 right的后一个结点
    let plr = pointL.next // 子链表的头部 left结点
    let prr = pointR.next // 子链表尾部 right的后一个结点

    // 切断
    pointL.next = null
    pointR.next = null

    pointL.next = reverseLinkedList(plr) // 左前结点拼上反转后的子链
    plr.next = prr // plr结点反转后已经是子链的right尾结点

    return pointH.next
};
var reverseLinkedList = function(head) {
    let root = null
    while(head) {
        const nextNode = head.next
        head.next = root
        root = head
        head = nextNode
    }
    return root
}

更优的解法,头插法

把下一个标记出来,再插入头部之后。

var reverseBetween = function(head, left, right) {
    // 设置 dummyNode 是这一类问题的一般做法
    const dummy_node = new ListNode(-1);
    dummy_node.next = head;
    let pre = dummy_node;
    for (let i = 0; i < left - 1; ++i) {
        pre = pre.next;
    }

    let cur = pre.next;
    for (let i = 0; i < right - left; ++i) {
        const next = cur.next;
        cur.next = next.next;
        next.next = pre.next;
        pre.next = next;
    }
    return dummy_node.next;
};

8、复制带随机指针的链表( LeetCode 138 )

先创建一批具有val的新节点,制作新旧node的映射,再循环原链表,通过原node,拿到新node,并赋值next,random等。

var copyRandomList = function (head) {
    if(head == null) { return null }
    let map = new Map() // map记录结点
    let cur = head;
    while(cur){
        map.set(cur,new Node(cur.val)) // 先创建一个个新节点,并储存
        cur = cur.next;
    }
    cur = head;
    while(cur){
        const curNode = map.get(cur)
        curNode.next = cur.next? map.get(cur.next):null // 根据旧节点,从hashMap拿到新节点
        curNode.random = map.get(cur.random)
        cur = cur.next;
    }
    return map.get(head);
}

9、移除链表元素( LeetCode 203 )

leetcode

var removeElements = function(head, val) {
    if(head === null) return null
    let tmpHead = new ListNode(-1) // 可能删除头节点的都得先设置伪头部
    tmpHead.next = head

    let cur = tmpHead
    while(cur.next) {
        if(cur.next.val === val) { // 删除了下一个就不用跳
            cur.next = cur.next.next || null
        } else {
            cur = cur.next
        }
    }

    return tmpHead.next
};

10、删除有序数组中的重复项( LeetCode 26 )

leetcode

慢指针 左侧为已经去重的数组,右侧为待去重的数组

循环快指针,如果 当前节点 与上一节点不同,

代表当前节点是新节点(非重复节点),放入慢指针位置,慢指针+1,快指针+1

如果 当前节点 与上一节点相同,代表为重复节点,不做操作,快指针+1并再次判断。

var removeDuplicates = function(nums) {
    const length = nums.length
    if(length === 0) return 0
    let f = s = 1
    while(f < length) {
        if(nums[f] !== nums[f-1]) {
            nums[s] = nums[f]
            s++
        }
        f++
    }
    return s
};

11、移动零( LeetCode 283 )

将数组内所有0移动到数组末尾

leetcode

慢指针 左侧为已经去0的数组,右侧为待去0的数组

循环快指针,如果 当前节点 不是0,

放入慢指针位置,慢指针+1,快指针+1,保持慢指针左侧去0

如果 当前节点 是0,不做操作,快指针+1并再次判断。

var moveZeroes = function(nums) {
    const length = nums.length
    let f = s = 0
    while(f < length) {
        if(nums[f] !== 0) {
            nums[s] = nums[f]
            s++
        }
        f++
    }
    while(s < length) {
        nums[s] = 0
        s++
    }
    return nums
};

12、合并两个有序数组( LeetCode 88 )

nums1 的长度为 m + n
nums1 的元素个数为 m
nums2 的元素个数为 n
leetcode

为避免消除了nums1中的待比对元素,采取从后向前 比对 和 放置的方式

cur指针右侧为 已比对元素, 左侧为待比对元素

循环i2,当i2小于0,代表nums2中的所有元素已放入nums1中,代表合并结束

i1 i2 >= 0,代表其数组还有未对比的数。

如果 i1>=0,并且i1数更大,cur处放i1,i1–,左移进行下一轮判断

如果 i2 数更大或相等,cur处放i2,保持从左往右从小到大。

(注意,为什么要加上 i1>=0 这个条件,因为

如果 i1<0,代表nums1数组所有数已排完,剩下cur左侧位置全放剩下的nums2就行,无需比较i1,i2大小)

var merge = function (nums1, m, nums2, n) {
    let cur = m + n - 1
    let p1 = m - 1
    let p2 = n - 1
    while (p2 >= 0) {
        nums1[cur] = p1 >= 0 && nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--]
        cur--
    }
    return nums1
};

13、K 个一组翻转链表( LeetCode 25 )

leetcode

需要使用到记录【前一个节点】这种概念时,要用到 newHead 这种虚拟头节点

标记好 要翻转的链表 头(start) 尾(fast),断开处的 头的前一(slow) 尾的后一(next) 四个节点

并将 要翻转的链表 与主体断开,准备进行反转。

然后 反转链表,反转后,(start)成为了 新尾,(reverse)返回的是 新头

最后 旧头的前一(slow) 拼接 (reverse)返回值, 新尾(start) 拼接 尾的后一(next)

(fast)(slow)均指向 新尾(start),继续进行后续翻转。

直到fast无结果

var reverseKGroup = function (head, k) {
    const newHead = new ListNode(-1, head)
    let slow = fast = newHead
    while (fast.next) {
        for (let i = 0; i < k && fast; i++) {
            fast = fast.next
        }
        if (!fast) break

        const next = fast.next
        fast.next = null
        const start = slow.next
        slow.next = null
        

        slow.next = reverse(start)

        start.next = next
        slow = start
        fast = start
    }
    return newHead.next
};

var reverse = function (head) {
    const newHead = new ListNode(-1)
    while (head) {
        const next = newHead.next
        newHead.next = head
        head = head.next
        newHead.next.next = next
    }
    return newHead.next
}

14、加一( LeetCode 66 )

用一个数组模拟一个非负整数,如 [1,2,3] 代表 123,

给这个数+1,并返回,如 [1,2,4]

leetcode

先将末位数 +1,然后循环从后往前判断进位

var plusOne = function (digits) {
    const index = digits.length - 1

    digits[index] = digits[index] + 1

    for (let i = index; i >= 0; i--) {
        if (digits[i] !== 10) { break; }

        digits[i] = 0
        if (i - 1 >= 0) {
            digits[i - 1] = digits[i - 1] + 1
        } else {
            digits.unshift(1)
        }
    }
    return digits
};

文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录