0
点赞
收藏
分享

微信扫一扫

四、链表————相关算法探讨(持续更新中)

柠檬果然酸 2024-10-08 阅读 13

链表中相关算法探讨


前言

  • 接下来,我们一起学习力扣中跟链表有关的算法题。

一、移除链表元素

  • 力扣算法题目第 203 题:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
  • 前提:
    • 列表中的节点数目在范围 [ 0, 1 0 4 10^4 104] 内
    • 1 <= Node.val <= 50
    • 0 <= val <= 50

1.1 思路分析

  • 将要删除节点的前一个节点的next指向删除节点的下一个节点的地址,就可以删除这个节点。

1.2 解法探讨

1.2.1 直接删除

  • 我们首先检查头节点head是否是要删除的节点,如果是,则不断前进直到找到不是要删除的节点或者到达链表末尾。
  • 如果整个链表都是要删除的节点,则最终head会变成None。
  • 接下来进入常规的遍历过程,从head开始遍历,对于每一个节点current,如果它的下一个节点是需要删除的节点,则跳过这个节点,否则就继续前进。
# 先定义节点类,定义节点的初始化方法
class ListNode:
    def __init__(self, val=0, next=None):
    	# 定义属性 val 存储节点的值  next用来 指向下一个节点的地址
        self.val = val
        self.next = next
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int):

		# 如果第一个节点就是空的,说明链表为空,此时 直接返回空
        if head is None:
            return None
            
    	# 当第一个节点不是空的 并且就是我们要删除的元素
        while head is not None and head.val == val:
         	# 将头节点直接赋予 原链表的第二个元素,这样就删除了第一个节点
            head = head.next
		      
        # 除了上述特殊情况外,我们需要定义一个指针cur 指向原链表中的第一个节点
        cur = head
        # 当cur指向的节点不为空,并且他的下一个节点也不为空,我们就进行循环遍历,
        # 因为我们判断的就是cur指向的节点的下一个节点的状态 这样能保证我们将cur的next赋值的时候能有值
        while cur is not None and cur.next is not None:
        	# 如果cur指向的节点的下一个节点是我们要删除的
            if cur.next.val == val:
            	# 走到这 说明找到要删除的节点,那就将 cur的next指针 指向 下下个元素
                cur.next = cur.next.next
            # 走到这说明,cur指向的节点的下一个节点不是我们要删除的节点, 那就让cur指向原链表的下一个节点
            else:
                cur = cur.next
        # 返回 头节点 也就是 链表的头
        return head
        

1.2.2 创建虚拟头节点来删除

  • 在涉及到 对链表的增删操作的时候,通常设置虚拟头节点会非常便于理解,同时操作也变得非常方便。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        # 创建一个虚拟头结点,指向原链表的第一个节点,便于处理头结点可能需要删除的情况
        dummy_head = ListNode(0, head)
        # 使用 cur 指向虚拟头节点 
        cur = dummy_head

        # 开始遍历链表寻找要删除的节点
        # 当cur 指向的节点的下一个元素不为空。也就是不是尾节点
        while cur.next is not None:
        	# 如果 cur 指针指向的 当前节点的下一个节点就是我们要删除的节点
            if cur.next.val == val:
            	# 那么就让当前cur 指向的节点的 next 指向 cur 的下下个节点,
            	# 这样就跳过了cur的下一个节点,也就是删除了
                cur.next = cur.next.next
            else:
            # 否则,移动到下一个节点
                cur = cur.next

        # 返回新的头结点
        return dummy_head.next

1.2.3 递归版删除

  • 因为每个节点要进行的操作都一样,都是检查是否是要删除的节点,如果是,那么就删除,不是的话就判断下一个节点。

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
        # 如果链表为空,直接返回None
        if head is None:
            return None
    
        # 处理当前节点
        if head.val == val:
        # 如果当前节点需要删除,则递归调用removeElements并跳过当前节点
            return self.removeElements(head.next, val)
        else:
        # 当前节点不需要删除,则处理下一个节点
            head.next = self.removeElements(head.next, val)
            return head

二、反转列表

  • 力扣算法题目第 203 题:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
  • 前提:
    • 链表中节点的数目范围是 [ 0, 5000 ]
    • -5000 <= Node.val <= 5000

2.1 思路分析

  • 如果再定义一个新的链表,然后遍历原来的链表,再依次将原来链表的值插进新链表,这会浪费内存空间,不过我们下边也会介绍这种做法,其实只要改变头节点跟next的指针指向即可。

2.2 做法

2.2.1 创建新链表方式

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
         # 初始化一个空的新链表
        reversed_head = None
        
        # 遍历原链表
        current = head
        while current:
            # 创建一个新节点,值为当前节点的值
            new_node = ListNode(current.val)
            
            # 将新节点连接到新链表的头部
            new_node.next = reversed_head
            reversed_head = new_node
            
            # 移动到原链表的下一个节点
            current = current.next
        
        return reversed_head

2.2.2 双指针法

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 设置 双指针 cur指向头节点 pre 先指向 头节点前面 也就是None
        cur = head
        pre = None
        # 从头节点开始遍历链表
        while cur:
            # 设置tmp存储 原链表的头节点的下一个节点的,因为下边改变指向方向后,会丢失原链表
            tmp = cur.next
            # 这一步就是改变链表指向方向的
            cur.next = pre

            # 改变指示方向后要更新 cur跟pre的值
            pre = cur
            cur = tmp
        # 返回的是pre 因为遍历完pre刚好指向原链表的最后一个节点, 也就是新链表的头节点
        return pre

2.2.3 递归法

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 调用翻转函数, 传入头结点跟None 
        return self.reverse(head, None)
    
    # 定义 翻转函数
    def reverse(self, cur: ListNode, pre: ListNode) ->ListNode:
        # 如果cur空的话,也就是头结点是空的,那么就返回None
        if cur == None:
        	# 递归出口
            return pre

        # 用 tmp 用来保存 cur 后边的那个节点
        tmp = cur.next
        # 翻转 链表中的指针方向
        cur.next = pre

        # 递归调用 reverse 传入改变 后的参数
        return self.reverse(tmp, cur)

三、两两交换链表中的节点

  • 力扣算法题目第 24 题:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
  • 前提:
    • 链表中节点的数目在范围 [ 0, 100 ] 内
    • 0 <= Node.val <= 100

3.1 思路分析

  • 两两交换链表中的节点,我们就需要两个指针指向要交换的两个节点,还需要保存第二个节点的next 防止丢失,循环遍历链表中的节点,依次完成这个动作。
  • 也可以使用虚拟头节点,这样更好理解一点

3.2 解法探讨

3.2.1 不使用虚拟头节点(递归法)

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head is None or head.next is None:
            return head
        # 先定义指针指向前两个节点
        pre = head
        cur = head.next
        
        # 重置头节点
        head = head.next.next
        
        # 交换两个指针的指向
        cur.next = pre

        # 因为 swapPairs 返回值就是上一次的头节点的指向
        pre.next = self.swapPairs(head)

        # 返回cur 因为cur指向的就是交换后的第一个节点
        return cur

3.2.2 使用虚拟头节点

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 创建一个哑节点并将其next指向head
        dummy = ListNode(0)
        dummy.next = head
        # 初始化一个当前节点指针
        current = dummy
    
        while current.next and current.next.next:
            # 初始化A、B两个节点
            A = current.next
            B = current.next.next
        
            # 交换A和B
            current.next = B
            A.next = B.next
            B.next = A
        
            # 移动current指针到交换后的B节点后面
            current = A
    
        # 返回哑节点之后的节点
        return dummy.next

四、删除链表的倒数第N个节点

  • 力扣算法题目第 19 题:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
  • 前提:
    • 链表中结点的数目为 sz
    • 1 <= sz <= 30
    • 0 <= Node.val <= 100
    • 1 <= n <= sz

4.1 思路分析

  • 因为我们不知道倒数的第 n 个节点是正着数的第几个节点, 所以我们需要遍历两次,第一次拿到链表长度,第二次根据长度跟倒数的 n 个节点,就能推出来要删除正数第几个节点
  • 还可以用双指针,双指针之间有n个节点, 那么当其中一个指针指到末尾的时候,另一个指针就是倒数的第 n 个节点
  • 还可以使用递归来做

4.2 解法探讨

4.2.1 两次遍历

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        # 定义一个length 用来存放 链表长度
        length = 0
        # 定义指针指向头节点
        tmp = head
        # 循环链表 
        while tmp:
            # 获取链表长度
            length += 1
            tmp = tmp.next
        
        # 获取到链表长度就要处理 特殊值, 如果倒数第 n 个节点刚好是头节点    
        if length == n:
            # 那就返回头节点的 下一个节点当头节点
            return head.next
        
        # 获取到链表长度后 再次进行遍历 这次根据链表长度以及 n 
        # 我们能推断出要删除的节点是正着数的第几个节点
        i = 1
        pre = head
        # 遍历找到要删除的节点的前一个节点 
        while i < length - n:
            pre = pre.next
            i = i +1

        # 使用要删除节点的前一个节点 链接到 要删除节点的后一个节点
        pre.next = pre.next.next

        # 返回 头节点
        return head        

4.2.2 双指针法

4.2.2.1 一次遍历(不使用虚拟头节点)
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        # 定义 pre 指向 头节点 定义 cur指向 头节点的后边那个节点
        pre = head
        cur = head.next

        # 先让cur走 n - 1 步 这样 当pre跟cur一起移动的时候
        # 当 cur走到最后一个节点的时候,这样 pre 刚好就是要删除的节点的前一个节点
        i = 1
        while i < n:
            cur = cur.next
            i += 1
        
        # 处理特殊值,不然cur越界 会报错
        # 如果 移动了 n 步以后 cur 刚好为None 那么说明链表长度为 n
        # 此时需要删除头节点, 头节点的下一个节点充当头节点 
        if cur is None:
            return head.next
        
        # 此时 pre 跟 cur 一起往后移动 当 cur指向最后一个节点的时候停止
        while cur.next:
            pre = pre.next

            cur = cur.next

        # 使用 要删除节点的前一个节点 指向 删除节点的后一个节点
        pre.next = pre.next.next
        
        # 返回 头结点
        return head
4.2.2.2 一次遍历(使用虚拟头节点)
Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        
        # 创建虚拟头节点,用dummy 来指向 头节点 
        dummy = ListNode(0, head)
    
        # 初始化两个指针 刚开始都指向 虚拟头节点
        first = second = dummy
    
        # 拉开距离, 让 first指针移动 n 个位置 这样 first 跟 second 就会相差n个位置
        for _ in range(n + 1):
            first = first.next
    
        # 同步移动, 当first指针移动到最后的时候,此时 second指向的就是 倒数第 n 个节点
        while first:
            first = first.next
            second = second.next
    
        # 删除节点
        second.next = second.next.next
    
        # 返回头节点
        return dummy.next

4.2.3 递归法

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        
        def helper(node):
            if not node:
            # 如果到达链表末尾,则返回当前节点的位置(距离末尾的距离)
                return 0
        
            # 递归调用,返回下一个节点的位置
            pos = helper(node.next)
        
            # 如果当前位置正好是要删除的节点的前一个位置
            if pos == n :
                node.next = node.next.next
        
            return pos  + 1 
    
        # 创建虚拟头节点
        dummy = ListNode(0, head)
    
         # 调用递归函数
        helper(dummy)
    
        # 返回新的头节点
        return dummy.next
    



五、链表相交

5.1 思路分析

5.2 解法探讨

5.2.1




六、环形链表II

6.1 思路分析

6.2 解法探讨

6.2.1





总结

  • 以上就是力扣中有关链表的题目的解题思路跟代码,我只是列举出来 我能想到的几种办法,如有其他解法,可以后台私信我。
举报

相关推荐

0 条评论