Python数据结构与算法篇(十)-- 链表的应用与常见题型

        这一部分的内容,前面的大佬总结的挺多,这里进行汇总,方便和大家一起学习和回顾,欢迎大家继续补充。

1 链表和数组

        作为线性表的两种存储方式————链表和数组,这对相爱相杀的好基友有着各自的优缺点。接下来,我们梳理一下这两种方式。

        数组,所有元素都连续的存储于一段内存中,且每个元素占用的内存大小相同。这使得数组具备了通过下标快速访问数据的能力。

        但连续存储的缺点也很明显,增加容量,增删元素的成本很高,时间复杂度均为 O ( n ) O(n) O(n)。增加数组容量需要先申请一块新的内存,然后复制原有的元素。如果需要的话,可能还要删除原先的内存。

        删除元素时需要移动被删除元素之后的所有元素以保证所有元素是连续的。增加元素时需要移动指定位置及之后的所有元素,然后将新增元素插入到指定位置,如果容量不足的话还需要先进行扩容操作。

         总结一下数组的优缺点:

  • 优点:可以根据偏移实现快速的随机读写。
  • 缺点:扩容,增删元素极慢。

        上面对数组增删元素的操作表明使用数组需要注意的东西真的很多很多,这样一来,我们就开始说说链表,链表也是一种数据结构,它弥补了数组带来的诸多不便,让我们可以任意为一些数据进行空间的分配,根据需要进行内存单元的开辟。

        链表是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,由若干个结点组成,在每一个结点里存到下一个结点的指针(Next)。采用动态分配存储单元方式。它能够有效地节省存储空间(同数组比较)。结点结构如下图所示:

        一般来讲,链表中只会有一个结点的指针域为空,该结点为尾结点,其他结点的指针域都会存储一个结点的内存地址。链表中也只会有一个结点的内存地址没有存储在其他结点的指针域,该结点称为头结点。

        对于链表而言,分为静态链表和动态链表,根据处理数据的方向又分为单向链表和双向链表。对于链表的更多操作,请阅读 Python数据结构与算法篇(四)-- 链表的实现

        小结: 从本质上来讲,链表与数组的确有相似之处,他们的相同点是都是线性数据结构,这与树和图不同,而它们的不同之处在于数组是一块连续的内存,而链表可以不是连续内存,链表的节点与节点之间通过指针来联系。


2 常见链表问题解决思路

2.1 单链表的反转

方法一:头插法(迭代法)
        算法思想:逆置链表初始为空,表中节点从原链表中依次“删除”,再逐个插入逆置链表的表头(即“头插”到逆置链表中), 使它成为逆置链表的“新”的第一个结点,如此循环,直至原链表为空。

方法二:递归法:
        算法思想:从后向前改变指向,可以理解成向后的箭头变成了向前的箭头

方法三:三指针法
        算法思想:从前向后改变指向,可以理解成向后的箭头变成了向前的箭头

2.2 单链表的删除某一结点

方法一:遍历
        思路:查找到所要删除的节点,以及其前驱节点,让其前驱节点,指向其后继节点

方法二:置换法(移花接木)
        思路:明确要删除的节点后,把其后继节点复制到该节点上,然后删除那个后继节点,也等于变相的删除节点(注意如果删除的是尾节点 删除的链表只有一个节点)

2.3 在当前节点前插入一个数据

方法一:遍历
        思路:找出当前结点的前驱节点,完成插入;

方法二:置换法
        思路:把插入节点的数据放到新节点上,把新节点的数据放到插入节点的数据上,这样我们就可以实现在当前节点前插入一个节点了。

2.4 查找链表的中间结点

快慢指针法
        思路:给一个快指针,让快指针每次移动两步,给一个慢指针,让慢指针每次移动一步,最后结果就是快指针移动到最后一个节点,慢指针最后移动到了中间的节点上。

        设有两个指针 fast 和 slow,初始时指向头节点。每次移动时,fast 向后走两次,slow 向后走一次,直到 fast 无法向后走两次。这使得在每轮移动之后。fast 和 slow 的距离就会增加一。设链表有 n 个元素,那么最多移动 n/2 轮。当 n 为奇数时,slow 恰好指向中间结点,当 n 为 偶数时,slow 恰好指向中间两个结点的靠后一个。

2.5 单链表的倒数第k个结点

方法一:正数转换法
        思路:遍历一遍单链表,记录单链表的长度,倒数第k个,即正数 length-k+1 个,在重头遍历一次便能够找到

方法二:快慢指针法
        思路:一个指针先走k步,然后两个指针同时走,当先走的那个指针指向空的时候,后面的指针所指即为倒数第K个节点。

        设有两个指针 p 和 q,初始时均指向头结点。首先,先让 p 沿着 next 移动 k 次。此时,p 指向第 k+1 个结点,q 指向头节点,两个指针的距离为 k 。然后,同时移动 p 和 q,直到 p 指向空,此时 q 即指向倒数第 k 个结点。可以参考下图来理解:

2.6 对称单链表

1. 知道链表的长度
        思路:根据对称来确定两个指针的位置,对所指向的元素进行判断,不断前进指针

2. 链表长度未知
        思路1:将前一半的节点压入栈中,并将当前节点继续遍历,每遍历一个都与栈弹出的节点相比较,若不同则不是。额外空间复杂度 O(N/2)。

        思路2:不使用辅助空间 两个指针,一个指向头 first,指向头的后继节点 last;first 走一步,last走两步;直到 last 为空或 last 的后继节点为空,此时 first 指向(链表长度为奇数,指向中间;为偶数,指向一半);然后 fisrt 向后走,再申请一个节点指向头,不断进行比较,直到 first 指向空。

2.7 单链表是否有环

方法一:map表法
        算法思想:每走一步将走过的节点使用map表存储起来,当遇到第一个在map中存在的节点时,就说明回到了出发点,即链表有环,同时也找到了环的入口。

方法二:快慢指针法
        算法思想:一个指针走两步;一个指针走一步;如果存在环,两个指针最终会指向同一个元素;如果不存在环,走两步的会最终走向空节点。

        当一个链表有环时,快慢指针都会陷入环中进行无限次移动,然后变成了追及问题。想象一下在操场跑步的场景,只要一直跑下去,快的总会追上慢的。当两个指针都进入环后,每轮移动使得慢指针到快指针的距离增加一,同时快指针到慢指针的距离也减少一,只要一直移动下去,快指针总会追上慢指针。

确定有环后求环的长度

        通过公式的推导我们发现 L = k c − n L=kc-n L=kcn(这里的 k k k 是倍数,有可能快指针在环里转了 k k k 圈),即相遇节点到入环点的距离等于链表的头到入环点的距离。写代码的时候只需要找到相遇节点,再让一个指针从头开始走即可。

2.8 判断两个链表是否相交

1. 相交则求交点(链表不带环)
        思路:若两个不带环的链表相交,则他们的尾节点必相同;若要求交点,则需要比较两个链表的长度,让较长的链表先向后遍历至和较短的链表长度相等,然后两个链表同时向后遍历,并比较节点是否相同,当遇到第一个相同的节点时,则为两个链表的交点。

2. 相交则求交点(链表可能带环)
        情况分析:
        若有两个链表,则他们的带环情况有以下三种可能:
        (1)两个链表都不带环
        直接采用上述思路即可;
        (2)一个链表带环一个链表不带环
        必定不想交;
        (3)两个链表都带环
        下面详细讨论:

  • 当出现①情况时,两个链表不相交。
  • 当出现②情况时,两个链表的交点在环外,那么我们可以转化为不带环链表判断相交即可。
  • 当出现③情况时,两个链表的交点在环内,那么我们可以遍历其中一个链表的环,若在环内与另一个链表环的入口点相交,则两个链表相交,相遇点即为两个链表的交点。
  • 要判断为情况②还是情况③,只需判断两个链表环的入口点是否相同即可。

链表的 .next 指向问题

        如果放在左边就表示是自己的指向,如果放在右边就表示是它的下一个节点。类似于代码中的这三行:

node.next = head.next;
head.next = head.next.next;
node.next.next = head;

        这种就代表等号左边指向右边,左边的是指向,右边就代表确切的下一个节点。

        如果类似于后两行代码:

node = node.next.next;
head = head.next;

        像这样,左边不带 .next 的是类似于赋值语句,自己的指针指向右边位置。


3 LeetCode

        俗话说一图胜千言,接下来主要通过图片展示解决思路,通过代码展示实现的细节。

3.1 删除结点

3.1.1 题解方法

1. 画草图: 理解指针的变动与思考逻辑!!(重要!实用!)
2. 边界条件: 怎么处理不会有空指针异常?在循环里放什么停止条件

  • 如果是遍历链表元素,while(node!=null)
  • 如果是删除某个元素,需要,while(node.next!=null)
  • 需要考虑的仅仅是被改变 next 指针的部分,并且循环之后哪个指针在最后的节点处,就判断谁
# 比如快慢指针,输出中间节点,slow 和 fast 的指针都在变,但是 fast 先指向链表尾巴,所以判断 fast
# 同时每个判断 next.next 的都必须先判断,next,才能保证 奇偶链长 中不会出现空指针异常
while(fast.next!=null && fast.next.next!=null){
    
    
            slow = slow.next;
            fast = fast.next.next;
        }

3. 只要会删除头结点,都要进行 dummy虚指针,有了 dummy 节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性
4. 特殊的需求可以考虑结合各种工具类,比如删除重复里面,利用HashSet,删除倒数第k个,利用栈LinkedList

1.3.2 可能出现的问题

① NullPointerException,就是当前节点为空,我们还去操作它的 next;
② 输出不了结果,一定是指针移动出了问题

1.3.3 题库列表

237. 删除链表中的节点 ====面试题 02.03. 删除中间节点

203. 移除链表元素(虚拟头结点)

19. 删除链表的倒数第 N 个结点(双指针经典类型)

237. 删除链表中的节点

        题目描述:给你一个需要删除的节点 node,但无法访问 第一个节点 head。链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

# 题目说 node 不是链表中最后一个结点,直接将当前节点的值改为next的值,next指向next.next,实现原地更新
class Solution:
    def deleteNode(self, node):
        node.val = node.next.val
        node.next = node.next.next

203. 移除链表元素

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

① 如果删除的节点是中间的节点,则问题似乎非常简单:

  • 选择要删除节点的前一个结点 prev。
  • 将 prev 的 next 设置为要删除结点的 next。

② 当要删除的一个或多个节点位于链表的头部时,要另外处理

三种方法:

  • 删除头结点时另做考虑(由于头结点没有前一个结点)
  • 添加一个虚拟头结点,删除头结点就不用另做考虑
  • 递归
  • 双指针法

1. 对头结点单独考虑

class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        prev, cur = None, head
        while cur:
            if cur.val == val:					# 找到指定元素
                if prev:						# 不是头结点	
                    prev.next = cur.next		# 将删除位置的前一个节点的next指向删除位置的后一个结点
                else:							# 如果第一个就是删除结点
                    head = cur.next				# 将头指针指向头节点的后一个结点
            else:
                prev = cur
            cur = cur.next
        return head

2. 添加一个虚拟头结点

class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        dummy = ListNode(-1, head)			# 创建虚结点
        prev = dummy
        while prev and prev.next:
            if prev.next.val == val:
                prev.next = prev.next.next
            else:
                prev = prev.next
        return dummy.next

3. 递归

class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        if head == None:
            return head
        # 因为递归函数返回的是已经删除节点之后的头结点
        # 所以直接接上在 head.next,最后就只剩下判断头结点是否与需要删除的值一致了
        head.next = self.removeElements(head.next, val)
        if head.val == val:
            return head.next
        else:
            return head

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

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

class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        curr = head     						# 指针节点,这里不会删除头结点
        while curr and curr.next:
            if curr.val == curr.next.val:		# 如果两个结点元素值相同,则执行删除
                curr.next = curr.next.next
            else:
                curr = curr.next
        return head

        题目解法并不唯一,可以使用递归、双指针、虚拟头结点、栈的方法,详细了解可以阅读:删除排序链表中的重复元素(五种方法)

剑指 Offer 18. 删除链表的节点

        题目描述:给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点

class Solution:
    def deleteNode(self, head: ListNode, val: int) -> ListNode:
        if head.val == val:             # 如果头指针相等,直接返回
            return head.next
        prev, cur = head, head.next     # 双指针
        while (cur and cur.val != val): # 找元素
            prev, cur = cur, cur.next
        if cur:                         # 找到了,进行删除
            prev.next = cur.next        
        return head

        温馨提示: 这里既可以添加虚拟头结点,也可以先判断第一个结点是否满足条件,第二种方法更快,这里就采用先判断再循环的方式。

面试题 02.01. 移除重复节点

        题目描述:移除未排序链表中的重复节点。保留最开始出现的节点,由于未排序,重复的元素不一定连续。

class Solution:
    def removeDuplicateNodes(self, head: ListNode) -> ListNode:
        pre, cur = None, head			# 初始化 pre, cur 节点引用(指针)
        visited = set()					# 初始化 set 用于保存节点值
        while cur:						# 遍历链表 
            if cur.val in visited:		# 若节点值 cur.val 在 set 中
                pre.next = cur.next		# 删除节点 cur
            else:						# 若节点值 cur.val 不在 set 中
                visited.add(cur.val)	# 将 cur.val 添加进 set
                pre = cur				# 令 pre 指向 cur ,作为下一轮的前驱节点
            cur = cur.next				# 遍历下一节点
        return head						# 删除完成,返回链表头节点 head

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

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

class Solution:
    def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
        '''双指针记录 pre 用 cur 记录相同的数,加虚头节点'''
        dummy = ListNode(-1, head)
        prev, curr = dummy, dummy.next
        while curr and curr.next:
            if curr.val == curr.next.val:
                while curr and curr.next and curr.val == curr.next.val:		# 如果有奇数个相同的值,就删不完,所以必须用 while 循环
                    curr = curr.next        							# 找到最后一个相等的数
                curr = curr.next
                prev.next = curr
            else:
                prev = curr
                curr = curr.next
        return dummy.next

19、删除链表的倒数第 N 个结点

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

1. 快慢指针

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        if head == None:
            return head.next
        dummy = ListNode(-1, head)
        fast = dummy.next
        for _ in range(n):			 # 快指针先走n步
            fast = fast.next
        slow = dummy
        while fast:					# 快慢指针同时走,直到 fast 指针到达尾部节点,此时 slow 到达倒数第 N 个节点的前一个节点
            fast, slow = fast.next, slow.next
        slow.next = slow.next.next	 # 删除节点,并重新连接
        return dummy.next

2. 循环迭代 – 找到 length -n 个节点

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        dummy = ListNode(0, head)
        cur, length = head, 0 		# step1: 获取链表长度
        while cur:
            length += 1
            cur = cur.next 
        cur = dummy				 	# step2: 找到倒数第N个节点的前面一个节点
        for _ in range(length - n):
            cur = cur.next
        cur.next = cur.next.next	 # step3: 删除节点,并重新连接
        return dummy.next 

3. 递归迭代 – 回溯时,进行节点计数

class Solution:  
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        if not head: 
            self.count = 0
            return head  
        head.next = self.removeNthFromEnd(head.next, n) # 递归调用
        self.count += 1 								# 回溯时进行节点计数
        return head.next if self.count == n else head 

876、链表的中间结点

        题目描述:给你单链表的头结点 head ,请你找出并返回链表的中间结点。

(1)若为奇数,指向中间的结点,若为偶数,指向中间靠后的结点

class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow, fast = head, head
        while fast and fast.next:		# 如果不加 fast,链表元素个数为偶数时会报空指针异常;
            slow = slow.next
            fast = fast.next.next
        return slow

(2)若为奇数,指向中间的结点,若为偶数,指向中间靠前的结点

class Solution:
    def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow, fast = head, head
        while fast.next is not None and fast.next.next is not None:
            fast = fast.next.next
            slow = slow.next
        return slow

86、分隔链表(两个临时链表)

        题目描述:给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。你应当 保留 两个分区中每个节点的初始相对位置。

class Solution:
    def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
        dummy1, dummy2 = ListNode(-1), ListNode(-1)	# dummy1 存放小于x链表的虚拟头结点,dummy2存放不小于x的虚拟头结点
        p, p1, p2 = head, dummy1, dummy2			#  p 负责遍历链表,类似合并两个有序链表的逻辑;p1, p2 指针负责生成结果链表
        while p:
            if p.val < x:
                p1.next = p
                p1 = p1.next
            else:
                p2.next = p 
                p2 = p2.next
            temp = p.next			# 断开原链表中的每个结点的 next 指针
            p.next = None
            p = temp
        p1.next = dummy2.next		# 合并两个链表
        return dummy1.next

        这里为什么需要断开 next?

        如果不断开直接 p = p.next,p 是正常往下走了,但是其中有个问题就是会陷入无限循环。以本题为例,由于14325的5在被p2连起来之后,其由于指向 p,因此 p.next 也是被继承过来的,所以后面是2,而 p 直接等于 p.next 后判断为2后,就直接加到 p1 后面了。现在问题就是 p1 中的 第一个2 指向的 第二个2 和 p2 中的 5 指向的 2 是一个指针(地址),不把他们俩合并还好说,一旦合并,就成了 下图所示.的1-->2-->2-->4-->3-->5-->2|-->4-->3-->5-->2|-->4-->...,看出来了吗?形成了一个环了!

        因此在每步的赋值结束后,应当对next清除,以防止在最后的时候陷入这种无限循环。

# 断开原链表中的每个结点的 next 指针
temp = p.next
p.next = None   
p = temp

        或者,如果不想每个都断开,其实在 p 往下走的时候,每个 p1 和 p2 的 next 都在同时进行着更新,因此只有 p2 的最后一个是存在问题的,因此也可以加一句 p2.next = None 来解决。

class Solution:
    def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
        # dummy1 存放小于x链表的虚拟头结点, 度没有 存放不小于x的虚拟头结点
        dummy1, dummy2 = ListNode(-1), ListNode(-1)
        # p1, p2 指针负责生成结果链表
        p1, p2 = dummy1, dummy2
        # p 负责遍历链表,类似合并两个有序链表的逻辑
        # 这里是将两个链表分解成两个链表
        p = head
        while p:
            if p.val < x:
                p1.next = p
                p1 = p1.next
            else:
                p2.next = p 
                p2 = p2.next
            p = p.next
        p2.next = None
        # 合并两个链表
        p1.next = dummy2.next
        return dummy1.next

328. 奇偶链表

        题目描述:给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。第一个节点的索引被认为是 奇数 , 第二个节点的索引为 偶数 ,以此类推。请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

class Solution:
    def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy1, dummy2 = ListNode(-1), ListNode(-1)
        p, p1, p2 = head, dummy1, dummy2
        count = 0
        while p:
            if count % 2 == 0:
                p1.next = p
                p1 = p1.next
            else:
                p2.next = p
                p2 = p2.next
            count += 1
            temp = p.next
            p.next = None
            p = temp
        p1.next = dummy2.next
        return dummy1.next

3.2 反转链表

3.2.1 题库列表

206. 反转链表====剑指 Offer 24. 反转链表

92. 反转链表 II

234. 回文链表====面试题 02.06. 回文链表

25. K 个一组翻转链表

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

1. 双指针法迭代

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        prev, cur = None, head		# 申请两个节点,pre和 cur,pre指向None
        while cur:					# 遍历链表
            temp = cur.next			# 记录当前节点的下一个节点
            cur.next = prev			# 然后将当前节点指向pre
            prev = cur				# pre和cur节点都前进一位
            cur = temp
        return prev

2. 尾递归法

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head == None or head.next == None:
            return head
        # reverse_head 表示 head.next 后面一整段反转之后的头结点,所以最后return reverse_head
        reverse_head = self.reverseList(head.next)
        head.next.next = head		# 此时 head.next 指向的已经是反转部分的尾巴
        head.next = None			# head 指向 None,表示此时 head 已经是尾巴了
        return reverse_head

92. 反转链表 II

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

class Solution:
    def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
        if  head.next == None:
            return head
        dummy = ListNode(-1, head)
        prev = dummy
        for _ in range(left-1):     # 迭代法,先找到起点
            prev= prev.next         # 来到 left 节点的前一个节点
        curr = prev.next            # cur 是真正反转的指针
        tail = curr
        for _ in range(right-left+1):
            node = curr.next        # node 保存 curr.next 的临时指针,保存后面的顺序
            curr.next = prev.next   # 将要反转的节点,接入到 left 节点
            prev.next = curr        
            tail.next = node
            curr = node
        return dummy.next

234. 回文链表

        题目描述:给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
1. 数组模拟

class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        prev, val_list = head, []
        while prev:
            val_list.append(prev.val)
            prev = prev.next
        return val_list == val_list[::-1]

2. 维持半条翻转链表(双指针)

class Solution:
    def isPalindrome(self, head: Optional[ListNode]) -> bool:
        p, slow, fast = head, head, head				# p 存储前半段的尾结点
        while fast and fast.next:   					# 快慢指针找到中间节点
            p = slow
            slow = slow.next
            fast = fast.next.next
        left, right = head, self.reverse_list(slow)     # 额外维持的半条链表;反转 slow 后面的元素
        q = right										# 存储末尾的断点用于恢复原来链表的顺序
        while right:                                    # 两个半长链表的比较 遍历两个 半长链表
            if left.val != right.val:
                return False
            left = left.next
            right = right.next
        p.next = self.reverse_list(q)					# 还原链表
        return True
    
    def reverse_list(self, head: Optional[ListNode]):
        prev, curr = None, head
        while curr:
            node = curr.next
            curr.next = prev
            prev = curr
            curr = node
        return prev

         温馨提示: 比较完成后我们应该将链表恢复原样。虽然不需要恢复也能通过测试用例,但是使用该函数的人通常不希望链表结构被更改。可以记录前半段的尾结点,将后半部分翻转之后在比较完成之后再次翻转,再让前半段的尾结点指向两次翻转的后半部分即可还原链表。

25. K 个一组翻转链表

        题目描述:给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
1. 模拟法

class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        if k== 1 or not head or not head.next:
            return head
        dummy = ListNode(0, head)
        prev1, prev2, curr = dummy, dummy, dummy.next
        count = -1
        while prev1:        				# 查找节点个数
            count += 1
            prev1 = prev1.next
        tail = curr
        for i in range(count//k):			# K个一组反转
            if i != 0:
                prev2 = tail
                tail = curr
            for _ in range(k):
                temp = curr.next
                curr.next = prev2.next
                prev2.next = curr
                tail.next = temp
                curr = temp
        return dummy.next

3.3 合并链表

3.3.1 题库列表

21. 合并两个有序链表

23. 合并K个升序链表

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

class Solution:
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        prehead = ListNode(-1)  # 哨兵节点
        prev = prehead          # 指针节点
        while list1 and list2:
            if list1.val <= list2.val:
                prev.next = list1
                list1 = list1.next
            else:
                prev.next = list2
                list2 = list2.next
            prev = prev.next
        prev.next = list1 if list1 else list2
        return prehead.next

23. 合并K个升序链表

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

1. 顺序合并

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        if not lists:
            return None
        head = lists[0]
        for item in lists[1:]:
            head = self.mergeTwoLists(head, item)
        return head
    
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        prehead = ListNode(-1)  # 哨兵节点
        prev = prehead          # 指针节点
        while list1 and list2:
            if list1.val <= list2.val:
                prev.next = list1
                list1 = list1.next
            else:
                prev.next = list2
                list2 = list2.next
            prev = prev.next
        prev.next = list1 if list1 else list2
        return prehead.next

2. 分治合并

class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        if not lists: return None
        n = len(lists)                          # 记录子链表数量
        return self.mergeSort(lists, 0, n - 1)  # 调用归并排序函数
    
    def mergeSort(self, lists: List[ListNode], l: int, r: int) -> ListNode:
        if l == r:
            return lists[l]
        m = (l + r) // 2
        L = self.mergeSort(lists, l, m)         # 循环的递归部分
        R = self.mergeSort(lists, m + 1, r)
        return self.mergeTwoLists(L, R)         # 调用两链表合并函数
    
    def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
        prehead = ListNode(-1)  # 哨兵节点
        prev = prehead          # 指针节点
        while list1 and list2:
            if list1.val <= list2.val:
                prev.next = list1
                list1 = list1.next
            else:
                prev.next = list2
                list2 = list2.next
            prev = prev.next
        prev.next = list1 if list1 else list2
        return prehead.next

3. 优先队列

        priority_queue = []
        for llist in lists: 
            while llist:
                heapq.heappush(priority_queue, llist.val)       # 把llist中的数据逐个加到堆中
                llist = llist.next
        dummy = ListNode(0)                                     # 构造虚节点
        p = dummy
        while priority_queue:
            p.next = ListNode(heapq.heappop(priority_queue))    # 依次弹出最小堆的数据
            p = p.next
        return dummy.next 

3.4 排序链表

3.4.1 解题方法

        在数组排序中,常见的排序算法有:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序等。

        而对于链表排序而言,因为链表不支持随机访问,访问链表后面的节点只能依靠 next 指针从头部顺序遍历,所以相对于数组排序问题来说,链表排序问题会更加复杂一点。

        下面先来总结一下 适合链表排序与不适合链表排序的算法:

  • 适合链表的排序算法:冒泡排序、选择排序、插入排序、归并排序、快速排序、计数排序、桶排序、基数排序。
  • 不适合链表的排序算法:希尔排序。
  • 可以用于链表排序但不建议使用的排序算法:堆排序。

         希尔排序为什么不适合链表排序?
        希尔排序:希尔排序中经常涉及到对序列中第 i + gap 的元素进行操作,其中gap 是希尔排序中当前的步长。而链表不支持随机访问的特性,导致这种操作不适合链表,因而希尔排序算法不适合进行链表排序。

         为什么不建议使用堆排序?

        堆排序:堆排序所使用的最大堆 / 最小堆结构本质上是一棵完全二叉树。而完全二叉树适合采用顺序存储结构(数组)。因为数组存储的完全二叉树可以很方便的通过下标序号来确定父亲节点和孩子节点,并且可以极大限度的节省存储空间。

        而链表用在存储完全二叉树的时候,因为不支持随机访问的特性,导致其寻找子节点和父亲节点会比较耗时,如果增加指向父亲节点的变量,又会浪费大量存储空间。所以堆排序算法不适合进行链表排序。

        如果一定要对链表进行堆排序,则可以使用额外的数组空间表示堆结构。然后将链表中各个节点的值依次添加入堆结构中,对数组进行堆排序。排序后,再按照堆中元素顺序,依次建立链表节点,构建新的链表并返回新链表头节点。

        需要用到额外的辅助空间进行排序的算法

        刚才我们说到如果一定要对链表进行堆排序,则需要使用额外的数组空间。除此之外,计数排序、桶排序、基数排序都需要用到额外的数组空间。

3.4.2 题库列表:

147. 对链表进行插入排序

148. 排序链表

147. 对链表进行插入排序
        题目描述:给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。

class Solution:
    def insertionSortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(-1, head)                 # 会移动头结点,这里虚拟头结点
        last_sorted, curr = head, head.next        # last_sorted 维护已排序部分的最后一个位置;curr 为遍历的待插入元素
        while curr:           # 外层循环遍历完链表所有数;内层循环遍历[head, lastSort]这段位置找插入
            if curr.val >= last_sorted.val:
                last_sorted = last_sorted.next      # 大,直接后移,或者直接 last_sorted = cur
            else:
                prev = dummy                        # 用来遍历已经排序的部分
                while prev.next.val <= curr.val:    # 从前往后比较,找插入的位置
                    prev = prev.next
                last_sorted.next = curr.next        # 找到位置进行插入操作
                curr.next = prev.next
                prev.next = curr
            curr = last_sorted.next                 # 指针后移
        return dummy.next

148. 排序链表
        题目描述:给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

        插入排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2),其中 n n n 是链表的长度。这里考虑时间复杂度更低的排序算法。题目的进阶问题要求达到 O ( n l o g n ) O(nlogn) O(nlogn) 的时间复杂度和 O ( 1 ) O(1) O(1) 的空间复杂度,时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn) 的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O ( n 2 ) O(n^2) O(n2)),其中最适合链表的排序算法是归并排序。

1. 归并排序-迭代

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]: 
        if not head:        # 空链表直接返回
            return head
        
        length = 0          # 获取链表的长度
        node = head
        while node: 
            length += 1
            node = node.next
        
        dummy_head = ListNode(0, head)
        sub_length = 1                                # 归并的有效处理长度,最小为 1
        while sub_length < length:
            prev, curr = dummy_head, dummy_head.next  # 为了更好的实现迭代,定义 prev, curr 分别指向 dummy_head, dummy_head.next
            while curr:                               # 当 cur 不为空时
                head1 = curr                          # 定义当前链表,head1 指向 cur
                for i in range(1, sub_length):
                    '''
                    当 i 等于 1 时, cur 不需要指向后下一个链表其余情况,
                    cur 在链表 cur 不为空的情况, 向后移动 sub_length - 1 个位置
                    '''
                    if curr.next:
                        curr = curr.next
                    else:
                        break
                head2 = curr.next   # 切断链表,第一部分长度为 sub_length
                curr.next = None    # 第二部分为 head 除 head1 以外的部分
                curr = head2
                for i in range(1, sub_length):  # 将 cur 在满足 cur.next 不为空的情况下, 又往后移动 sub_length 长度
                    if curr and curr.next:
                        curr = curr.next
                    else:
                        break
                
                remain = None         	# 定义 remain 链表,指向 null
                if curr:              	# 当移动完 sub_length 后,仍不为空,remain 等于 cur.next;
                    remain = curr.next
                    curr.next = None  	# 切断链表, 这时候 head2 的长度与 head1 一致
                
                merged = self.merge(head1, head2)   # merge , 然后 prev 的 next 指针指向将 merge 后的子链表
                prev.next = merged
                while prev.next:        # 然后将 prev 指向与 merge 完成后的链表位置
                    prev = prev.next
                curr = remain           # 然后当前节点位置指向 remain 部分链表
            '''
            进入下一批次的归并排序操作,直到将相同 sub_length 的全部处理完,才会退出此处 while
            再进入到下轮 sub_length, sub_length 以 1 -> 2 -> 4 -> 8 的方法进行,符合自低向上不断迭代,
            直到找到最终答案。其实递归底层本质也是一样的,到最短的1,才开始合并,不断合并,到最终结果
            '''
            sub_length <<= 1
            
        return dummy_head.next
    def merge(self, head1: ListNode, head2: ListNode) -> ListNode:
        dummy_head = ListNode(0)            # 构建虚拟头结点
        temp, temp1, temp2 = dummy_head, head1, head2
        while temp1 and temp2:              # 开始合并操作
            if temp1.val <= temp2.val:
                temp.next = temp1
                temp1 = temp1.next
            else:
                temp.next = temp2
                temp2 = temp2.next
            temp = temp.next
        if temp1:                #  如 temp1, temp2 还存在不为空的链表,将剩余部分赋值给 temp.next
            temp.next = temp1
        elif temp2:
            temp.next = temp2
        return dummy_head.next

2. 归并排序-递归

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        return self.split_list(head) 
    
    def split_list(self, head: ListNode):   
        '''处理归并排序的边界问题,下面两种场景都不需要进行分合操作'''
        if not head or not head.next:
            return head
        prev, slow, fast = head, head, head
        while fast and fast.next:   # 找中点,偶数找的后面那个中点的位置,奇数找到中点
            prev = slow
            slow = slow.next
            fast = fast.next.next
        prev.next = None
        left, right = head, slow     # 将链表分割成两个子链表
        return self.merge_sort(self.split_list(left), self.split_list(right))    # 分割完后,进行合并部分操作

    def merge_sort(self, head1: ListNode, head2: ListNode) -> ListNode:
        '''归并环节'''
        dummy_head = ListNode(0)            # 构建虚拟头结点
        temp, temp1, temp2 = dummy_head, head1, head2
        while temp1 and temp2:              # 开始合并操作
            if temp1.val <= temp2.val:
                temp.next = temp1
                temp1 = temp1.next
            else:
                temp.next = temp2
                temp2 = temp2.next
            temp = temp.next
        if temp1:                #  如 temp1, temp2 还存在不为空的链表,将剩余部分赋值给 temp.next
            temp.next = temp1
        elif temp2:
            temp.next = temp2
        return dummy_head.next

        了解更多,请阅读:排序链表「八大排序算法」「Python 版」「链表、冒泡、选择、插入、归并、桶排序、计数排序、基数排序

3.5 环形链表

3.5.1 题库列表

160. 相交链表
        题目描述:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null。

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
        p1, p2 = headA, headB
        while p1 != p2:			# 退出的关键是:指向同一个指针(不是值相等),或者都指 None
            if not p1:
                p1 = headB
            else:
                p1 = p1.next
            if not p2:
                p2 = headA
            else:
                p2 = p2.next
        return p1				# 如果没有相等的那么nodeA==nodeB==null

141. 环形链表
        题目描述:给你一个链表的头节点 head ,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。如果链表中存在环 ,则返回 true 。 否则,返回 false 。

1. 双指针

class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        if not head or not head.next:
            return False
        slow, fast = head, head
        while fast and fast.next:			# 快指针在前面,所以只要判断快指针是否达到了队尾就可以
            fast = fast.next.next
            slow = slow.next
            if slow == fast:
                return True
        return False

142. 环形链表 II
        题目描述:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if not head or not head.next:
            return None
        slow, fast = head, head
        while fast and fast.next:		# 快慢指针找重合点
            fast = fast.next.next
            slow = slow.next
            if slow == fast:			# 重合了,这个时候,从头来一个指针遍历
                p1 = slow
                p2 = head
                while p1 != p2:
                    p1 = p1.next
                    p2 = p2.next
                return p2
        return None						# 没有环,返回 None


终于小结完了,期待下一专题——二叉树,也期待各位小伙伴们一起来学习与交流!


参考

猜你喜欢

转载自blog.csdn.net/xq151750111/article/details/129303127
今日推荐