Leetcode刷题笔记—链表篇

Leetcode刷题笔记—链表篇

素材来自网络

链表子串数组题,用双指针别犹豫。
双指针家三兄弟,各个都是万人迷。
快慢指针最神奇,链表操作无压力。
归并排序找中点,链表成环搞判定。

左右指针最常见,左右两端相向行。
反转数组要靠它,二分搜索是弟弟。
滑动窗口老猛男,子串问题全靠它
左右指针滑窗口,一前一后齐头进
自诩十年老司机,怎料农村道路滑。
一不小心滑到了,鼻青脸肿少颗牙。
算法思想很简单,出了bug想升天

前言:文章有点长,这是博主本人的写作风格,大家无需一口气把文章从头到尾看完,只看自己有疑惑的地方即可

力扣上给的是伪代码,对链表的题型无法在自己的本地编辑器上调试,这里给大家一个利用列表创建链表的模板可以方便大家实际调试

def create_list(linklist):
    """
    :param linklist:拿来构建链表的数组
    :return: head-返回链表的头结点
    """
    node_list = []
    for i in range(len(linklist)):
        node = ListNode()
        node.val = linklist[i]
        node_list.append(node)
    head = node_list[0]
    p = head
    for node in node_list[1:]:
        p.next = node
        p = p.next
    return head

一、快慢指针在链表中的应用

第一题:旋转链表

Leetcode61. 旋转链表中等题 详情请点击链接看原题

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置

python代码解法:

class Solution:
    def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        if not head:
            return head
        length = 0
        p = head
        while p:
            length += 1
            p = p.next
        k %= length     # 对长度取模
        if k == 0 or length == 1:
            return head
        slow, fast = head, head
        dummy_head = ListNode()
        dummy_head.next = head
        while fast.next:
            if k <= 0:
                slow = slow.next
            k -= 1
            fast = fast.next
        dummy_head.next = slow.next
        slow.next = fast.next
        fast.next = head
        return dummy_head.next

第二题:删除排序链表中的重复元素

Leetcode83:删除排序链表中的重复元素简单题

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次,返回已排序的链表

细心的同学会发现这道题和 Leetcode26-删除有序数组中的重复项 这道题非常类似,只不过这里是有序链表,解题思路完全一致,只不过对链表和数组的处理稍有不一致,这篇文章中有对删除有序数组中的重复项的题解

python题解代码1

class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head:
            return head
        dummy = ListNode()
        dummy.next = head
        slow, fast = head, head
        while fast:
            if slow.val != fast.val:   # 快慢指针同时移动
                slow.next = fast       # 将中间重复的元素断链
                slow = slow.next       # slow指针后移一位 
            fast = fast.next 
        slow.next = fast     # 防止最后一个元素重复
        return dummy.next

python题解代码2

class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head:
            return head
        dummy_head = ListNode()
        dummy_head.next = head
        slow = fast = head
        while fast:
            while fast and fast.val == slow.val:
                fast = fast.next
            slow.next = fast
            slow = fast
        return dummy_head.next

第三题:删除排序链表中的重复元素 II

Leetcode82:删除排序链表中的重复元素 II中等题

给定一个已排序的链表的头 head, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表

和上一题不同的是这道题中只要存在重复元素,就删除所有包含重复元素的节点,上一题对重复元素的处理是保留一个
先创建一个虚拟头结点 dummy,令dummy.next = head,然后创建指针pre指向dummy,指针cur指向head,开始遍历链表

  1. cur指向的结点值与cur.next指向的结点值相同时,我们就让cur不断向后移动,直到cur指向的结点值与cur.next指向的结点值不相同时,停止移动
  2. 我们判断 pre.next 是否等于 cur
  • 如果相等说明 precur 之间没有重复结点,我们就让pre移动到cur的位置
  • 如果不相等就让pre.next = cur.next,然后让 cur 继续向后移动
  1. 继续上述操作直到 cur 为空,遍历结束

python题解代码

class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head:
            return head
        dummy_head = ListNode()
        dummy_head.next = head
        pre, cur = dummy_head, head
        while cur:
            while cur.next and cur.val == cur.next.val:  # 1.如果cur指向的结点值与cur.next指向的结点值相同时,就让cur不断向后移动
                cur = cur.next
            if pre.next == cur:   # 2.说明pre与cur之间没有重复结点
                pre = cur
            else:      # 3.说明pre与cur之间有重复结点
                pre.next = cur.next
            cur = cur.next   # 4.cur继续向后移动
        return dummy_head.next

第四题:回文链表

Leetcode234. 回文链表简单题

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

解题思路:
我们可以将链表的后半部分反转【修改链表结构】,然后将前半部分和后半部分进行比较,比较完后将链表恢复

  1. 找到前半部分链表的尾结点(慢指针走一步,快指针一次走两步,快慢指针同时出发,快指针走到末尾慢指针走到中间)
  2. 反转后半部分链表
  3. 恢复链表
  4. 返回结果

python题解代码

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next


class Solution:
    def reverse_link(self, head):
        dummy_head = ListNode()
        cur_node = head
        while cur_node:
            next_node = cur_node.next
            cur_node.next = dummy_head.next
            dummy_head.next = cur_node
            cur_node = next_node
        return dummy_head.next

    def find_middle_node(self, head):   # 找到中间结点
        slow, fast = head, head
        while fast.next and fast.next.next:
            fast = fast.next.next
            slow = slow.next
        return slow

    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        if not head:
            return True
        mid_node = self.find_middle_node(head)    # 找到前半部分链表的尾结点(用于还原链表)
        reverse_head = self.reverse_link(mid_node.next)   # 反转后半部分(保存反转后的头结点用于恢复链表)
        i, j = head, reverse_head    # 重新初始化i和j指针指向两个链表的头结点
        flag = False
        while not flag and j:
            if i.val != j.val:
                flag = True
            i, j = i.next, j.next
        mid_node.next = self.reverse_link(reverse_head)  # 还原链表
        if flag:
            return False
        return True

二、链表删除的相关题型

第一题:移除链表元素

Leetcode203:移除链表元素简单题

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

情形1对应解法:不借助于虚拟头结点,直接使用原来的链表来进行移除节点操作,那么删除头结点的操作需要另做考虑
python题解代码

class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        while head and head.val == val:	# [7, 7, 7, 7]对于要移除的节点为链表的头结点需要用另外的逻辑来处理
            head = head.next
        if not head:	# 如果链表为空,直接返回
            return head
        prev = head		# prev指向头结点(用来指向待移除节点的前一个节点)
        while prev.next:	# 注意这里的条件!!!
            if prev.next.val == val:
                prev.next = prev.next.next
            else:
                prev = prev.next
        return head

情形2对应解法:设置一个虚拟头结点在进行删除操作(设置虚拟头结点后,对原链表中头结点则无需做特殊处理)
python题解代码

class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        if not head:
            return head
        dummy = ListNode()	# 创建一个虚拟头结点
        dummy.next = head	# 和原链表连接起来
        prev = dummy		# prev指针指向虚拟头结点
        while prev.next:
            if prev.next.val == val:
                prev.next = prev.next.next
            else:
                prev = prev.next
        return dummy.next

注意上面prev指针为什么不直接指向待删除的节点,第二个while循环的条件中为什么是while prev.next而不是while prev,别急,第二题中会有详细介绍为什么

第二题:删除链表的倒数第 N 个结点

Leetcode19:删除链表的倒数第 N 个结点中等题 详情请点击链接看原题

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

题目提示中:链表中结点的数目为sz1 <= sz <= 301 <= n <= sz,所以我们不用担心n会超出链表的长度

快慢指针大显神威

step1:题目要求返回链表的头结点,所以这里我们创建一个虚拟头结点,让它的next指针指向head【常规操作】用于最后返回链表的头结点

step2:快慢指针同时指向头结点head,快指针先行 n 步,然后快慢指针同时移动,当快指针指向最后一个节点(即 fast.next=None 的时候),慢指针指向节点的下一个节点(slow.next)即我们要删除的倒数第n个节点

step3:快慢指针同时移动之前需要先判断快指针是否指向末尾,如果快指针为None说明,n 正好等于链表的长度即要删除的倒数第 n 个节点正好是正数第1个节点
python题解代码

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy_head = ListNode()		# 创建一个虚拟头结点
        dummy_head.next = head
        slow, fast = head, head	# 快慢指针同时指向链表头结点
        for i in range(n):
            fast = fast.next
        if not fast:  # 如果快指针指到头了,第一个节点就是待删除的节点
            return slow.next
        while fast.next:	# 注意这里的条件 !!!
            fast = fast.next
            slow = slow.next
        slow.next = slow.next.next	# 删除 slow.next 节点操作
        return dummy_head.next

注:请同学们思考,为什么快指针指向最后一个节点的时候,慢指针指向节点的下一个节点才是我们要删除的倒数第n个节点呢?

假设链表的长度为5,我们要删除倒数第2个节点,同学们可以在纸上画一下,我们先让快指针移动2步,快慢指针同时移动,当快指针指向None的时候,慢指针正好指向倒数第2个节点(即我们要删除的节点)

But…

但由于链表的特性,对链表的删除操作而言,我们是无法直接删除某个指针所指向的节点的,但我们可以很轻松的删除该指针指向的节点的下一个节点,所以我们必须要让slow指针停留在待删除节点的前一个位置,所以我们让fast指针指向最后一个节点,slow指向待删除节点的前驱结点的时候就跳出循环

第三题:删除链表中的节点

Leetcode237:删除链表中的节点中等题 详情请点击链接看原题

有一个单链表的 head,我们想删除它其中的一个节点 node,给你一个需要删除的节点 node 。在本题中你将无法访问头结点 head
题目的限制条件:链表的所有值都是唯一的,并且保证给定的节点 node 不是链表中的最后一个节点(这里不做过多赘述,请看原题)

题目分析
我们删除链表中节点的操作一般是修改要删除节点的上一个节点的指针,将该指针指向要删除节点的下一个节点
相信同学们看到这道题的第一反应肯定很懵,要你删除链表中的节点却没有给出链表头结点head,我们如何从头结点遍历到待删除节点的上一个节点

其实不然,这道题无需你从头结点遍历到指定的节点,题目所给的node参数已经指向了要删除的节点,but 我们也无法直接删除node指针指向的节点

狸猫换太子…
第二题我们可以知道,既然我们无法直接删除给定的节点,那我们可以将所给节点的下一个节点的val拿过来覆盖自己本来的val,然后删除所给节点的下一个节点就可以达到同样的目的
python题解代码【一共就两行代码】

class Solution:
    def deleteNode(self, node):
        """
        :type node: ListNode
        :rtype: void Do not return anything, modify node in-place instead.
        """
        node.val = node.next.val
        node.next = node.next.next

三、链表合并

第一题:合并两个有序链表

Leetcode21:合并两个有序链表简单题

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

python题解代码

class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()  # 创建一个虚拟头结点
        cur = dummy  # 创建一个指针指向虚拟头结点
        l1, l2 = list1, list2
        while l1 and l2:
            if l1.val < l2.val:
                cur.next = l1
                l1 = l1.next
            else:
                cur.next = l2
                l2 = l2.next
            cur = cur.next
        cur.next = l1 if l1 else l2
        return dummy.next

第二题:合并K个升序链表

Leetcode23:合并K个升序链表困难题

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

方法1:最小堆(需要具备堆的基础概念)

方法1需要大家对堆的基础知识有一定的了解,如果不熟悉的同学请先去了解一下最大堆最小堆的概念以及如何进行堆调整,堆中节点的下沉上浮,没搞清楚这些概念之前这个方法会很难理解

分析

  • 合并后的第一个节点first一定是某个链表的头结点(因为链表已经升序排列),
  • 合并后的第二个节点可能是某个链表的头结点,也可能是first的下一个节点,那么每当我们找到一个节点值最小的节点x,我们就把节点 x.next 加入【可能是最小节点的集合】中因此我们可以利用最小堆来充当这个可能是最小节点的集合

在了解了上述概念之后其实并不用大家动手写关于堆的代码,python中有个标准库heapq里面封装好了关于堆的操作方法,现在让我来给大家介绍一下

heapq.heappush(heap, num):先创建一个空堆,然后将数据一个一个地添加到堆中。每添加一个数据后,heap都满足小顶堆的特性
heapq.heappop(heap):将堆顶的数据出堆,并将堆中剩余的数据构造成新的小顶堆

python题解代码

import heapq


class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next


class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:

        dummy = ListNode(0)
        p = dummy
        heap = []
        for i in range(len(lists)):
            if lists[i]:
                heapq.heappush(heap, (lists[i].val, i))  # 将所有链表的头结点入堆(这里根据lists[i].val构建最小堆)
                lists[i] = lists[i].next  # 指针后移
        while heap:
            val, idx = heapq.heappop(heap)  # 将堆顶的最小元素弹出
            p.next = ListNode(val)
            p = p.next
            if lists[idx]:  # 如果弹出的最小节点的下个节点不为空(则有可能为最小)
                heapq.heappush(heap, (lists[idx].val, idx))  # 继续入堆(重新自动构建最小堆)
                lists[idx] = lists[idx].next
        return dummy.next


def create_list(linklist):
    """
    :param linklist:拿来构建链表的数组
    :return: head-返回链表的头结点
    """
    node_list = []
    for i in range(len(linklist)):
        node = ListNode()
        node.val = linklist[i]
        node_list.append(node)
    head = node_list[0]
    p = head
    for node in node_list[1:]:
        p.next = node
        p = p.next
    return head


if __name__ == '__main__':
    s = Solution()
    res = []
    lists = [[1, 4, 5], [1, 3, 4], [2, 6]]
    for lis in lists:
        res.append(create_list(lis))
    after_head = s.mergeKLists(res)
    while after_head:
        print(after_head.val)
        after_head = after_head.next

对了堆也是大厂面试中的高频考点,对于这道题而言,主要考察的是你对于链表的操作,在面试中你直接使用pythonheapq封好好的功能也是没有问题的

方法2:未完待续

四、链表相关的其他题型

第一题:两两交换链表中的节点

Leetcode24:两两交换链表中的节点

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

方法一:分析
初始时,cur指向虚拟头结点
这里建议大家画图,能用纸笔画就用纸笔画,身边没纸笔用你的画图工具
在这里插入图片描述
在这里插入图片描述
注意,链表的索引只能根据上一个节点的next来找到下一个节点的位置

cur.next = cur.next.next	# 步骤一

所以执行完骤一(将头结点指向原本的第一个节点改为指向第二个节点)之后我们无法找到第一个节点

temp1 = cur.next	# 先将第一个节点的位置暂存起来

同理执行完步骤二(将第二个节点和链表断开)后我们也无法找到第三个节点

temp2 = cur.next.next.next		# 将第三个节点的位置暂存起来

方法1总结

因为要通过改变 cur节点nextcur.next.next节点next 的指向来交换cur.nextcur.next.next 节点
所以我们需要先将cur节点cur.next.next节点 这两个节点原来的指向暂存起来,即temp1 = cur.next, temp2 = cur.next.next.next

python题解代码

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()
        dummy.next = head
        cur = dummy
        while cur.next and cur.next.next:
            temp1 = cur.next
            temp2 = cur.next.next.next
            cur.next = cur.next.next
            cur.next.next = temp1
            cur.next.next.next = temp2
            cur = cur.next.next     # cur移动两位,准备下一轮交换
        return dummy.next

方法2分析:递归法

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head or not head.next:
            return head

        # 待翻转的两个node分别是pre和cur
        pre = head
        cur = head.next
        next_node = head.next.next

        cur.next = pre
        pre.next = self.swapPairs(next_node)

        return cur

第二题:相交链表

Leetcode160:相交链表简单题 详情请点击链接看原题

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

分析
严格证明取自力扣K神大佬题解
设【第一个公共节点为】node,【链表 headA】的节点数量为 a ,【链表 headB】的节点数量为 b ,【两链表的公共尾部】的节点数量为 c ,则有:

  • 头节点headAnode 前共有 a - c 个节点
  • 头节点headBnode 前共有 b - c 个节点
  • 指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node时,共走步数为:a + (b − c)
  • 指针 B 先遍历完链表 headB ,再开始遍历链表 headA,当走到 node 时,共走步数为:b + (a − c)

因为a + (b - c) = b + (a - c),此时指针A, B重合(两指针相遇即链表相交),并有两种情况:

case1:若两链表有公共尾部(即c > 0):指针AB同时指向第一个公共节点node
case2:若两链表无公共尾部(即c=0),指针A,B同时指向None

python题解代码

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        A, B = headA, headB
        while A != B:	# 当A==B时推出循环,两指针同时扫描到交点或同时指向 None
            A = A.next if A else headB
            B = B.next if B else headA
        return A

换种更容易理解的思路,若两链表相交,交点为c,链表A的长度为a + c,链表B的长度为b + c,两个头节点指针分别走过对方来时的路则有a + c + b + c = b + c + a + c,则证明两链表相交与一点,若不相交,两头节点互相走过对方来时的路有a + b = b + a,两指针刚好同时指向None
两个人走的路一样,走路的速度一样,两条路有交点必在交点相遇,没交点则在在终点(None)相遇

第三题:交换链表中的节点

Leetcode1721:交换链表中的节点中等题 详情请点击链接看原题

给你链表的头节点 head 和一个整数 k
交换 链表正数第 k 个节点和倒数第 k 个节点的值后,返回链表的头节点(链表 从 1 开始索引

分析
本题有两种解法,值交换(偷天换日)和节点交换,力扣上博主的题解总结的很好,这里只介绍第一种解法,节点交换需要考虑的边界条件太复杂,不想写了
方法1:值交换-找到倒数第 k 个节点和第 k 个节点后进行值交换

找第k个节点非常简单,找倒数第k个节点和前文中的删除倒数第k个节点的方法是一样的

python题解代码

class Solution:
    def swapNodes(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        slow, fast = head, head
        # 因为从头节点(第一个节点)开始遍历,循环k-1次即指向第k个节点
        for _ in range(k - 1):
            fast = fast.next
        cur = fast
        # 同理当cur指向最后一个节点(cur.next为空)的时候,slow正好指向倒数第k个节点
        while cur.next:
            slow = slow.next
            cur = cur.next
        fast.val, slow.val = slow.val, fast.val		# 第k个节点和倒数第k个节点做值交换
        return head

第四题:环形链表I

Leetcode141:环形链表I简单题 详情请点击链接看原题

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

分析
设有两个指针 slowfast 初始时均指向头结点,slow向后走一次,fast 向后走两次,在每轮移动之后,fastslow 的距离就会增加 1,如果链表有环,fast指针一定先进入环中(继续移动),等slow指针进入环中后,二者继续移动,fast指针和slow指针的距离+1,可是在环中,距离+1也意味着距离-1(距离拉开的同时距离也正在缩小)

根据物理学上的相对运动来说,其实就相当于slow指针静止,fast指针每次移动一个距离向slow指针靠近,所以二者是一定会在环中相遇的

python题解代码

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        slow, fast = head, head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if fast == slow:
                return True
        return False

第五题:环形链表II

Leetcode142:环形链表II中等题 详情请点击链接看原题

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

分析
这道题的题解分析很多博主都给出非常严格的数学证明,在本文中,作者尽量用最少的文字简化分析让大家能够快速理解
在这里插入图片描述

从上一题可以知道,slow走一步,fast走两步,链表有环则二者一定会相遇,假设从头结点到环形入口节点 的节点数为x。 环形入口节点到相遇节点节点数为 y 。 从相遇节点再到环形入口节点节点数为 z

从上一题的分析中我们可以知道,fast指针不可能跨过slow指针而不与slow指针相遇,所以slow指针不存在绕环一圈的情况
则相遇时slow指针走过的节点数为 x + y, fast 指针走过的节点数:x + y + n (y + z)x可能很长,环很短,fast指针在环中走了n圈才遇到slow指针

fast指针走过的节点数 = slow指针走过的节点数 * 2

(x + y) * 2 = x + y + n (y + z),利用初中所学的数学知识进行化简,x = (n - 1) (y + z) + z 注意这里 n 一定是大于等于 1 的,因为 fast 指针至少要多走一圈才能相遇 slow 指针,当 n1 的时候,公式就化解为 x = z

x = z:意味着从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是环形入口的节点

到此,我们可以在相遇节点处定义一个指针index1,在头结点处定义一个指针index2index1index2 同时移动,每次移动一个节点,那么它们相遇的地方就是环形的入口节点,如果 n > 1 则说明 index1 指针在环里多转了 (n - 1) 圈再遇到 index2,相遇节点依然是环形的入口节点

python题解代码

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow, fast = head, head
        while fast and fast.next:
            fast = fast.next.next
            slow = slow.next
            if fast == slow:
                index1 = fast	# 定义index1指向相遇节点
                index2 = head	# 定义index2指向头结点
                while index2 != index1:	# 同时移动index1和index2直到二者相遇
                    index1 = index1.next
                    index2 = index2.next
                return index1
        return None

第六题:反转链表

Leetcode206:反转链表简单题 详情请点击链接看原题

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

方法1: 头插法反转单链表
python题解代码

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()
        cur_node = head
        while cur_node:
            next_node = cur_node.next      # 将头结点断链,保存剩余链表到 next_node
            cur_node.next = dummy.next
            dummy.next = cur_node
            cur_node = next_node       # cur_node 重新指向剩余链表的头结点
        return dummy.next

方法2: 递归解法
使用递归法遍历链表,当越过尾节点后终止递归,在回溯时修改各节点的next 引用指向
recur(cur, pre) 递归函数

  1. 终止条件:当 cur 为空,则返回尾节点 pre(即反转链表的头结点)
  2. 递归后继节点,记录返回值为res
  3. 修改当前节点cur引用指向前驱节点pre
  4. 修改反转链表的头结点res

第七题:反转链表 II

Leetcode92. 反转链表 II:中等题 详情请点击链接看原题

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表

比反转链表多了一步是我们要把抽取出来反转后的区间拼接回去,故我们需要知道四个定位节点:
reverseHead:反转区间的前一个节点
reverseHead:反转区间的头结点
reverseTail:反转区间的尾结点
reverseNext:反转区间的下一个节点

python题解代码

class Solution:
    def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
        dummy_head = ListNode()
        dummy_head.next = head
        prev_head = dummy_head
        count = 1
        while count < left:
            prev_head = prev_head.next
            count += 1
        reverse_head = prev_head.next
        cur_node = reverse_head
        next_node = None
        while count <= right:
            next_node = cur_node.next
            cur_node.next = prev_head.next
            prev_head.next = cur_node
            cur_node = next_node
            count += 1
        p = head
        i = 1
        while i <= right:
            p = p.next
            i += 1
        p.next = next_node
        return dummy_head.next

第八题:分隔链表

Leetcode725. 分隔链表:中等题 详情请点击链接看原题

给你一个头结点为 head 的单链表和一个整数 k ,请你设计一个算法将链表分隔为 k 个连续的部分。
每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 1 。这可能会导致有些部分为 null

解题思路
尽可能将链表平均分为 k 份, 无法均分时,应当使前面比后面多,我们可以先对链表进行一次扫描,得到总长度 cnt,再结合需要将链表划分为 k 份,可知每一份的最小分配单位为 per = cnt // k
从前往后切分出 k 份链表,由于是在原链表的基础上进行,因此这里的切分只需要在合适的位置将结点的 next 指针置空即可

python题解代码

class Solution:
    def splitListToParts(self, head: Optional[ListNode], k: int) -> List[Optional[ListNode]]:
        if not head:
            return [None] * k
        p, length = head, 0
        while p:
            length, p = length + 1, p.next
        res, cur = [], head
        a, b = divmod(length, k)
        while k:
            if not cur:
                break
            res.append(cur)
            cur_len = a + (1 if b > 0 else 0)
            b -= 1
            for _ in range(cur_len - 1):
                cur = cur.next
            cur.next, cur = None, cur.next
            k -= 1

        res += [None] * k
        return res

第九题:分隔链表

Leetcode86. 分隔链表:中等题 详情请点击链接看原题

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前

解题思路

  1. 新建两个链表 small_dummy , big_dummy ,分别用于添加所有「节点值 < x」和「节点值 x≥x」的节点
  2. 遍历链表 head 并依次比较各节点值 head.valx 的大小,比 x小的放在 small_dummy后面,比 x 大的放在 big_dummy 后面
  3. 遍历完成后,拼接 small_dummybig_dummy 链表,最后返回 small_dummy.next 即可

python题解代码

class Solution:
    def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
        small_dummy = ListNode()
        big_dummy = ListNode()
        small, big = small_dummy, big_dummy
        p = head
        while p:
            if p.val < x:
                small.next = ListNode(p.val)
                small = small.next
            else:
                big.next = ListNode(p.val)
                big = big.next
            p = p.next
        small.next = big_dummy.next
        return small_dummy.next

第十题:奇偶链表

Leetcode328. 奇偶链表:中等题 详情请点击链接看原题

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表

注意看:该题的解题思路与上题几乎如初一致,只是换了个壳子而已
python题解代码

class Solution:
    def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        odd_dummy = ListNode()
        even_dummy = ListNode()
        odd, even = odd_dummy, even_dummy
        p = head
        i = 0
        while p:
            if i % 2 == 0:
                odd.next = ListNode(p.val)
                odd = odd.next
            else:
                even.next = ListNode(p.val)
                even = even.next
            p = p.next
            i += 1
            odd.next = even_dummy.next
        return odd_dummy.next

第十一题:K 个一组翻转链表

Leetcode25. K 个一组翻转链表:困难题 (详情点击链接见原题)

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换

解题步骤

  1. 链表分区为已翻转部分+待翻转部分+未翻转部分
  2. 每次翻转前,要确定翻转链表的范围,这个必须通过 k 此循环来定
  3. 需记录翻转链表的前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来
  4. 初始需要两个变量 preendpre 代表待翻转链表的前驱,end 代表待翻转链表的末尾
  5. 经过 k 次循环, end 到达末尾,记录待翻转链表的后继 next = end.next
  6. 翻转链表,然后将三部分连接起来,然后重置 preend 指针,然后进入下一次循环【特殊情况:当翻转部分长度不足 k 时,在定位 end 完成后,直接返回即可】

python题解代码(头插)

class Solution:
    def reverse(self, head):   # 头插法反转链表
        dummy = ListNode()
        cur_node = head
        while cur_node:
            next_node = cur_node.next
            cur_node.next = dummy.next
            dummy.next = cur_node
            cur_node = next_node
        return dummy.next

    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        dummy_head = ListNode()
        dummy_head.next = head
        pre, end = dummy_head, dummy_head
        while end.next:
            i = 0
            while i < k and end:
                end = end.next
                i += 1
            if not end:   # 说明结点总数不是 k 的整数倍,将最后剩余的结点保持原有顺序
                break
            start = pre.next
            next = end.next   # 保留剩余的结点用于后面的赋值
            end.next = None   # 断链
            pre.next = self.reverse(start)  # 连接反转后的链表

            start.next = next  # start指向下一个范围的起始结点
            pre = end = start
        return dummy_head.next

python题解代码(尾插)

class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        dummy_head = ListNode()
        dummy_head.next = head
        pre = tail = dummy_head
        while True:
            count = k
            while count and tail:
                count -= 1
                tail = tail.next
            if not tail:
                break
            next_dummy = pre.next   # 保存用来当作下一个虚拟头结点
            while pre.next != tail:  # 尾插法反转
                cur = pre.next  # 获取下一个元素
                pre.next = cur.next  # 连接当前结点的下一个结点
                cur.next = tail.next  # 和剩余的链表连接起来
                tail.next = cur   # 插在 tail 后面
            pre = tail = next_dummy
        return dummy_head.next

五、链表在位运算中的应用

第一题:两数相加

Leetcode2:两数相加中等题 详情请点击链接看原题

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头

分析(使用链表实现大数加法

  1. 将两个链表看成是相同长度进行遍历,如果一个数较短则在前面补0987 + 23 = 987 + 023

数不以0开头,但是当某个数较短的时候需要在前面补0数字在链表中是按照 逆序 的方式存储的,所以当我们遍历到链表末尾的时候若其中一个链表不为空另一个链表为空,则需要在链表的后面补0为了达到前面说的在数字前面补 0 保证数字对齐
7——>8——>9
3——>2——>0

  1. 对每一位计算的同时需要考虑上一位的进位问题,并当前位计算结束后同样需要更新进位值
  2. 如果两个链表遍历完毕后进位值为 1,则在新链表最前方添加结点 1

python题解代码

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode()
        cur_node = dummy
        carry = 0       # 进位
        while l1 or l2:
            x = 0 if not l1 else l1.val   # 如果一个链表为空另一个不为空则需要在后面补0
            y = 0 if not l2 else l2.val

            total = x + y + carry	# 从两个数字的末尾(两个链表的头)开始计算

            carry = total / 10		# 记录当前两个数之和的进位
            total %= 10				# 取个位
            cur_node.next = ListNode(total)	# 将实例化的结点放在头结点dummy之后
            cur_node = cur_node.next		# cur_node指针右移准备计算下一位
            if l1:
                l1 = l1.next	# l1不为空则l1右移
            if l2:
                l2 = l2.next   # l2不为空则l2右移
        if carry == 1:   # 如果两个链表遍历完毕后进位值为 1,则在新链表最末尾(即数的最高位)添加结点 1
            cur_node.next = ListNode(carry)

        return dummy.next

第二题:两数相加 II

Leetcode445. 两数相加 II中等题 详情请点击链接看原题

给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表

解题思路:两数相加+反转链表
python题解代码

class Solution:
    def reverse_link(self, head):
        dummy_head = ListNode()
        cur = head
        while cur:
            next_node = cur.next
            cur.next = dummy_head.next
            dummy_head.next = cur
            cur = next_node
        return dummy_head.next

    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        p1 = self.reverse_link(l1)
        p2 = self.reverse_link(l2)

        dummy_head = ListNode()
        p = dummy_head
        carry = 0
        while p1 or p2:
            x = p1.val if p1 else 0
            y = p2.val if p2 else 0

            cur = x + y + carry
            if cur > 9:
                cur -= 10
                carry = 1
            else:
                carry = 0
            p.next = ListNode(cur)
            p = p.next
            if p1:
                p1 = p1.next
            if p2:
                p2 = p2.next
        if carry == 1:
            p.next = ListNode(1)
        ans = self.reverse_link(dummy_head.next)
        return ans

六、链表在排序中的应用

第一题:排序链表

Leetcode148. 排序链表中等题 详情请点击链接看原题

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

python题解代码

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head or not head.next:  # 1.递归终止条件:空链表或者只有一个结点的链表,递归结束
            return head

        # 2.分割环节
        slow, fast = head, head
        while fast.next and fast.next.next:
            fast, slow = fast.next.next, slow.next  # 快指针走两步慢指针走一步,当快指针走到末尾,慢指针指向链表中点
        mid, slow.next = slow.next, None  # 断开
        left = self.sortList(head)
        right = self.sortList(mid)

        # 3. 合并环节
        p = dummy_head = ListNode()
        while left and right:
            if left.val < right.val:
                p.next = left
                left = left.next
            else:
                p.next = right
                right = right.next
            p = p.next
        p.next = left if left else right
        return dummy_head.next

七、链表与其他数据结构的综合应用

第一题:LRU缓存

Leetcode146:LRU缓存中等题 详情请点击链接看原题

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构

博主水平有限,算法设计思路来源于力扣大佬题解,博主只在这里做个总结,方便大家和本人理解
要让putget方法的时间复杂度为O(1)cache这个数据结构的必要条件

1.cache中的元素必须有时序以区分最近使用和久未使用的数据,容量满了以后要删除最久未使用的那个元素
2.我们要在cache中快速找某个 key 是否已存在并得到对应的 val
3.每次访问cache中的某个key,需要将这个元素变为最近使用的,也就是说 cache要支持在任意位置快速插入和删除元素

这个数据接口应具备如下节点:
1.支持快速查找
2.保存访问的先后顺序,在末尾加入一项,去除最前端一项
3.将队列中的某一项移到末尾

哈希表查找快,但数据无固定顺序,链表有顺序之分但是查找慢,所以二者结合一下形成一种新的数据结构,这就是大名鼎鼎的哈希链表 LinkedHashMap

图片素材来源于网络
在这里插入图片描述

1.默认put操作从链表尾部添加元素,那么显然越靠近尾部的元素就是最近使用的,越靠头部的元素就是最久未使用的(因为是双链表,你也可以在链表头部添加元素,把头部元素当成是最近使用的元素)
2.对于某一个key,我们可以通过哈希表快速定位到链表中的结点从而取得对应的val
3.链表虽然支持通过修改指针在任意位置插入和删除,只不过传统的链表无法按照索引快速访问某一个位置的元素,而这里借助于哈希表,通过key快速映射到任意一个链表结点,然后进行插入和删除

这里为啥用双链表而不是单链表?
从前几题的分析我们可以知道删除一个节点不光要得到该节点本身的指针,也需要操作其前驱结点的指针,而我们用哈希表只能定位到需删除的节点,无法定位到其前驱结点,而双向链表就支持直接查找前驱,保证操作的时间复杂度为O(1)

python题解代码

class ListNode:
    def __init__(self, key=None, value=None):
        self.key = key
        self.value = value
        self.prev = None
        self.next = None


class LRUCache:

    def __init__(self, capacity: int):
        self.capacity = capacity
        self.hash_map = {}
        self.head, self.tail = ListNode(), ListNode()
        self.head.next = self.tail
        self.tail.prev = self.head

    def move_to_head(self, key):
        """
        即双链表的一个查找和插入操作
        :param key:
        :return:
        """
        node = self.hash_map[key]   # 找到 key 对应的结点

		# 将该节点与原链表断链
        node.prev.next = node.next
        node.next.prev = node.prev

        # 将最近访问的节点放到头结点后
        node.next = self.head.next
        node.prev = self.head.next.prev
        self.head.next.prev = node
        self.head.next = node

    def get(self, key: int) -> int:
        if key in self.hash_map:   # 如果关键字 key 已经存在
            self.move_to_head(key)   # 将该 key 对应的结点插入到头结点后更新为最近访问
        return -1 if self.hash_map.get(key, -1) == -1 else self.hash_map[key].value   # 返回关键字对应的值

    def put(self, key: int, value: int) -> None:
        if key in self.hash_map:   # 如果要插入结点的 key 已存在
            self.hash_map[key].value = value   # 变更其数据值
            self.move_to_head(key)     # 将该 key 对应的结点插入到头结点后更新为最近访问
        else:
            if len(self.hash_map) == self.capacity:  # 如果容量已满,去掉最久没有被访问的节点
                self.hash_map.pop(self.tail.prev.key)  # 去掉尾节点的前一个节点(最久未使用)
                self.tail.prev.prev.next = self.tail
                self.tail.prev = self.tail.prev.prev
            new_node = ListNode(key, value)   # 申请一个新结点
            self.hash_map[key] = new_node     # 将该结点与哈希表中的key建立映射关系
			
			# 将新结点插入到头结点之后
            new_node.next = self.head.next
            new_node.prev = self.head.next.prev
            self.head.next.prev = new_node
            self.head.next = new_node

其实对于本题来说核心考察点就是双链表的插入和删除操作,需要注意细节和前后顺序

第二题:随机链表的复制

Leetcode138. 随机链表的复制:中等题 详情请点击链接看原题

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

给定链表的头结点 head,复制普通链表很简单,只需遍历链表,每轮建立新结点 + 构建前驱结点 pre 和当前结点 node 的引用指向即可,本题链表的结点新增了 random 指针指向链表中的任意结点或者 NULL,这个random指针意味着在复制过程中,除了构建前驱节点和当前结点的引用指向 pre.next,还要构建前驱结点和其随机结点的引用指向

  1. 若头结点 head 为空结点,直接返回 None
  2. 初始化哈希表 hash_map,结点 cur 指向原链表的头结点
  3. 复制链表:建立新结点,并向 hash_map 中添加键值对 (原 cur 结点:利用原cur结点的val创建新结点)cur 遍历至原链表下一结点
  4. 构建新链表的引用指向:构建新结点的 nextrandom 引用指向
  5. 返回值:返回新链表的头结点 hash_map[cur]

python题解代码

class Solution:
    def copyRandomList(self, head: 'Node') -> 'Node':
        if not head:
            return head
        hash_map = {}
        cur = head
        while cur:
            hash_map[cur] = Node(cur.val)
            cur = cur.next
        cur = head
        while cur:
            hash_map[cur].next = hash_map.get(cur.next)
            hash_map[cur].random = hash_map.get(cur.random)
            cur = cur.next
        return hash_map[head]

总结

本文给大家总结了大厂面试中喜欢考察的关于链表的相关题型,对于链表题,双指针的快慢指针通常是最常用的,注意一下链表的插入和删除操作中的顺序要求,顺序乱了整个链表就断裂了,主要帮助大家总结了考点以及清晰明了的题解方便大家理解,如果你觉得本文对你有帮助的话,还请点赞收藏转发,最后,祝大家都能找到心仪的工作呀~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_lover_forever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值