LeetCode热题100链表题解析

145 阅读7分钟

难度标识:⭐:简单,⭐⭐:中等,⭐⭐⭐:困难

tips:这里的难度不是根据LeetCode难度定义的,而是根据我解题之后体验到题目的复杂度定义的。

1.相交链表

思路

解这题的思路可以使用双指针。首先拿一个指针遍历链表A,当遍历到链表A末尾之后,让指针再遍历B,在遍历链表A的同时,拿另一个指针去遍历链表B,当遍历到链表B的末尾时,再去遍历链表A,当这两个指针相遇时,那这个点就是两个链表相交的节点。具体原理见下图:

链表.png

第1个指针遍历的总长度是:a+b+c

第2指针遍历的总长度是:c+b+a

a+b+c = c+b+a,所有两个指针相遇的点就是两个链表相交。

代码

var getIntersectionNode = function (headA, headB) {
    let pA = headA, pB = headB
    while (pA !== pB) {
        pA = pA ? pA.next : headB
        pB = pB ? pB.next : headA
    }
    return pA
};

2.反转链表

思路

解这题可以使用3个指针,通过迭代调整链表中的指针来实现反转。具体思路步骤:

  1. 初始化指针:

    • prev 指针初始化为 null
    • curr 指针初始化为 head
    • next 指针初始化为 null 或者不需要立即初始化。
  2. 遍历链表:

    • 在每次迭代中,首先保存 curr 的下一个节点到 next(这样我们就可以安全地更改 currnext 指针)。
    • 更改 currnext 指针指向 prev,完成一个节点的反转。
    • 移动 prevcurr 指针到下一个位置。具体来说,prev 移动到 curr 的位置,curr 移动到 next 的位置。
  3. 迭代完成后:

    • currnull 时,遍历结束。此时,prev 将指向新的头节点。

代码

var reverseList = function (head) {
    let prev = null, curr = head, next;
    while (curr) {
        next = curr.next
        curr.next = prev
        prev = curr
        curr = next
    }
    return prev
};

3.回文链表

思路

这题可以把链表的值放到数组里,然后通过数组去判断链表是否是回文链表。

代码

var isPalindrome = function (head) {
    const arr = []
    while (head) {
        arr.push(head.val)
        head = head.next
    }
    let left = 0, right = arr.length - 1;
    while (left < right) {
        if (arr[left] !== arr[right]) {
            return false
        } else {
            left++
            right--
        }
    }
    return true
};

4. 环形链表

思路

解这题也可以通过双指针来解决,而且是快慢双指针。拿一个慢指针,一个快指针,快指针每次走2步,而慢指针每次走1步,如果快指针和慢指针相遇了,那一定是有环的。然后这一题和下一题想要了解的更清楚可以看下这个视频

代码

var hasCycle = function (head) {
    let slow = head, fast = head;
    while (fast && fast.next) {
        slow = slow.next
        fast = fast.next.next
        if (slow === fast) {
            return true
        }
    }
    return false
};

5.环形链表 II

思路

这题是在上面那题的基础上的题目,然后这题就是找到上面那题开始入环的第一个节点。然后解这题的思路就是先确认有没有环,有环的话再找到入环的第一个节点。那怎么找到这个入环的起点呢?上面我们通过快慢指针找环,当找到环时,我们将慢指针指向链表开头,然后快指针和慢指针每次走一步,当它们相遇时,那么这个节点就是入环的第一个节点。具体的数学证明和思路讲解可以看这个视频

代码

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

6.合并两个有序链表

思路

解这题也可以使用双指针的思路。对两个链表都设置一个指针,然后比较它们当前的值,将较小值的节点添加到新的链表中,并移动那个较小值的指针。

代码

var mergeTwoLists = function (list1, list2) {
    let dummy = new ListNode(0)
    let curr = dummy
    while (list1 && list2) {
        if (list1.val < list2.val) {
            curr.next = list1
            list1 = list1.next
        } else {
            curr.next = list2
            list2 = list2.next
        }
        curr = curr.next
    }
    curr.next = (!list1 ? list2 : list1)
    return dummy.next
};

7.两数相加

思路

解这题就是一个模拟的思路,就是模拟加法计算,拿一个变量去记录进位。核心思路就是模拟数字的加法运算过程。考虑到链表表示的数字是逆序存储的,这为我们提供了方便,因为我们可以直接从链表的头部开始模拟加法的过程。

代码

var addTwoNumbers = function (l1, l2) {
    let dummy = new ListNode(0)
    let curr = dummy, count = 0
    while (l1 || l2 || count) {
        const sum = (l1 ? l1.val : 0) + (l2 ? l2.val : 0) + count
        count = sum > 9 ? 1 : 0
        curr.next = new ListNode(sum % 10)
        curr = curr.next
        l1 = l1 && l1.next
        l2 = l2 && l2.next
    }
    return dummy.next
};

8.删除链表的倒数第 N 个结点

思路

解这题也是使用双指针的思路。拿一个快指针先走n+1步,然后再拿一个慢指针,快指针慢指针一起走,当快指针走到末尾时,慢指针的位置就是倒数第n+1个位置,然后将这个位置的指针指向改到下下个结点即可,这样就删掉了倒数第N个结点。

代码

var removeNthFromEnd = function (head, n) {
    let dummy = new ListNode(0, head)
    let slow = dummy, fast = dummy
    for (let i = 0; i < n + 1; i++) {
        fast = fast.next
    }
    while (fast) {
        slow = slow.next
        fast = fast.next
    }
    slow.next = slow.next.next
    return dummy.next
};

9.两两交换链表中的节点 ⭐⭐

思路

解下面这题的思路也是使用指针。这个问题的核心思路是使用指针来帮助我们完成节点之间的交换。你可以考虑使用三个指针:prev, curr, 和 next。这三个指针将帮助我们迭代链表,并在每一步中完成节点的交换。如果想要了解这题思路的具体细节,可以看下面这个视频,讲的很清晰。

代码

var swapPairs = function (head) {
    let dummy = new ListNode(0, head)
    let curr = dummy
    while (curr.next && curr.next.next) {
        let first = curr.next, third = curr.next.next.next
        curr.next = curr.next.next
        curr.next.next = first
        first.next = third
        curr = curr.next.next
    }
    return dummy.next
};

10.K 个一组翻转链表 ⭐⭐

思路

解决这题的思路同样是使用指针。首先上面的第2题是反转链表,既然我们会了反转链表,那就根据k分组即可,然后分组反转,前后连接起来即可。这题如果你会上面的反转链表,那就会这一题。想详细了解反转过程的可以看下这个视频,讲的很清楚。

代码

var reverseKGroup = function (head, k) {
    let dummy = new ListNode(0, head)
    let left = dummy, right = dummy
    while (right.next) {
        for (let i = 0; i < k && right; i++) {
            right = right.next
        }
        if (!right) break
        let start = left.next, end = right.next
        right.next = null
        left.next = reverseList(left.next)
        start.next = end
        left = right = start
    }
    return dummy.next
};
function reverseList(head) {
    let prev = null, curr = head
    while (curr) {
        let next = curr.next
        curr.next = prev
        prev = curr
        curr = next
    }
    return prev
}

11.随机链表的复制

思路

解这题的思路是用一个哈希表维护一个原始节点到复制节点的映射,这样,当遍历原始链表时,可以在常数时间内找到其对应的复制节点,并建立正确的nextrandom关系。

代码

var copyRandomList = function (head) {
    const map = new Map()
    let curr = head
    while (curr) {
        map.set(curr, new Node(curr.val))
        curr = curr.next
    }
    curr = head
    while (curr) {
        if (curr.next) {
            map.get(curr).next = map.get(curr.next)
        }
        if (curr.random) {
            map.get(curr).random = map.get(curr.random)
        }
        curr = curr.next
    }
    return map.get(head)
};

12.排序链表

思路

这题一个暴力的方法就是将链表转成数组进行排序,然后再构建成链表。

代码

var sortList = function (head) {
    const arr = []
    while (head) {
        arr.push(head.val)
        head = head.next
    }
    arr.sort((a, b) => a - b)
    let dummy = new ListNode(0)
    let curr = dummy
    for (let i = 0; i < arr.length; i++) {
        curr.next = new ListNode(arr[i])
        curr = curr.next
    }
    return dummy.next
};

13.合并 K 个升序链表

思路

这题也可以跟上面一题一样,转成数组排序,然后构造链表。当然这也是暴力解法。

代码

var mergeKLists = function (lists) {
    const arr = []
    for (let list of lists) {
        while (list) {
            arr.push(list.val)
            list = list.next
        }
    }
    arr.sort((a, b) => a - b)
    let dummy = new ListNode(0)
    let curr = dummy
    for (let i = 0; i < arr.length; i++) {
        curr.next = new ListNode(arr[i])
        curr = curr.next
    }
    return dummy.next
};

14.LRU 缓存

思路

解这题可以使用JS中的Map,因为在JavaScript中,Map会根据插入的顺序保存键值对。所以在此场景中,它特别适合实现LRU缓存。

代码

var LRUCache = function (capacity) {
    this.map = new Map()
    this.size = capacity
};
LRUCache.prototype.get = function (key) {
    const val = this.map.get(key)
    if (this.map.has(key)) {
        this.map.delete(key)
        this.map.set(key, val)
        return val
    } else {
        return -1
    }
};
LRUCache.prototype.put = function (key, value) {
    if (this.map.has(key)) {
        this.map.delete(key)
    }
    this.map.set(key, value)
    if (this.map.size > this.size) {
        this.map.delete(this.map.keys().next().value)
    }
};

热题100的链表部分的题目就全部完结了,我们发现解链表的题目大部分都是使用指针去解决,然后上面我有把链表转成数组解决,这种方法是不推荐的,我只是图个简单才用这种方法的,其实链表的题目只要理解了思路还是很简单的,真不行就转成数组解,然后再构造成链表,当然这并不是好的解法,只是没办法之下或者偷懒解法。