刷题记录-HOT 100(三)

链表

1、环形链表找环起始结点

  • 使用快慢指针检测环

    • 初始化两个指针 slowfast,都指向链表的头节点。
    • slow 每次移动一步,fast 每次移动两步。
    • 如果 fastslow 相遇(即 fast == slow),说明链表中存在环。否则,如果 fast 遇到链表的末尾(fastNonefast.nextNone),则链表无环。
  • 找到环的起始节点

    • 一旦快慢指针相遇,就说明链表中存在环。
    • 此时,将其中一个指针(例如 slow)重新指向链表的头节点,另一个指针(例如 fast)保持在相遇点。然后两个指针都每次移动一步,它们最终会在环的起始节点相遇。
    • 返回相遇的节点作为环的起始节点。
  • 没有环的情况

    • 如果快慢指针没有相遇,则返回 None,表示链表中没有环。

 第一阶段,soft和fast在b点相遇,soft走过的距离是x+y,fast走过的距离是2(x+y)。fast比soft多走的距离x+y=r+y。可以得到x=r。

第二阶段,找环起点a,soft重定向到头结点head,fast从相遇点b出发。soft走到a的距离为x,fast回到环起点经过的距离为r,x=r,所以两者会在a处相遇。

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        slow=fast=head
        while fast and fast.next:
            slow=slow.next
            fast=fast.next.next
            if fast==slow:
                flag=True
                break
        else:
            return None
        slow=head
        while slow!=fast:
            slow=slow.next
            fast=fast.next
        return slow

2、合并两个有序链表

  • 创建虚拟头节点

    • 为了方便操作和返回结果,创建一个虚拟头节点 prehead,并初始化一个指针 prev 指向 prehead
    • prehead 的作用是简化边界情况的处理,例如当 list1list2 一开始就是 None 的情况。
  • 遍历两个链表

    • 进入 while 循环,只要 list1list2 都不为空,就比较它们的当前节点值。
    • 如果 list1 当前节点的值小于或等于 list2 的当前节点值,则将 list1 的当前节点连接到 prev 后面,并移动 list1 指针到下一个节点。
    • 如果 list2 当前节点的值小于 list1,则将 list2 的当前节点连接到 prev 后面,并移动 list2 指针到下一个节点。
    • 无论哪种情况,prev 都需要前进到下一个位置,即移动 prev = prev.next
  • 处理剩余节点:当 while 循环结束时,说明至少有一个链表已经遍历完毕,但另一个链表可能还有剩余节点。直接将剩余的链表连接到 prev.next,因为剩下的节点一定比已经处理的所有节点都大,所以直接附加到链表末尾。

  • 返回合并后的链表:最后返回 prehead.next,即合并后链表的头节点。prehead 只是一个虚拟头节点,不包含在最终结果中。

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

时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。空间复杂度:O(1)。

3、链表两数相加

  • 创建虚拟头节点

    • 使用 prehead 作为虚拟头节点,它帮助我们简化结果链表的构建过程。
    • pre 指针用于遍历和构建最终的结果链表。
  • 初始化进位

    • carry 用于存储每次相加后的进位(即两数相加超过10的情况)。
  • 遍历两个链表

    • 使用 while 循环,条件为 l1l2carry 不为 0。这意味着即使一个链表已经处理完毕,我们仍然需要处理另一个链表的剩余部分以及进位。
  • 按位相加

    • res 用于存储当前节点的值,首先加上进位 carry
    • 如果 l1 不为空,将 l1 的当前节点值加到 res 中,然后将 l1 移动到下一个节点。
    • 如果 l2 不为空,将 l2 的当前节点值加到 res 中,然后将 l2 移动到下一个节点。
  • 处理进位

    • 计算新的 carry,即 res 除以 10 的结果。
    • 计算当前节点值 res10 取模后的结果,并创建一个新的节点 new,将其连接到 pre 后面。
  • 更新指针:将 pre 移动到新创建的节点 new

  • 返回结果链表

    • 循环结束后,prehead.next 指向的链表就是结果链表的头节点,返回该节点即可。
class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        prehead=ListNode(-1)
        pre=prehead
        carry=0
        while l1 or l2 or carry:
            res=carry
            if l1:
                res+=l1.val
                l1=l1.next
            if l2:
                res+=l2.val
                l2=l2.next
            carry=res//10
            res%=10;new=ListNode(res)
            pre.next=new;pre=pre.next
        return prehead.next

4、删除链表的倒数第N个数

为了统一处理头结点和其他结点的删除操作,创建一个哑结点dummy,它的next指针指向头结点。

这道题主要利用双指针的思想,一个指针first来遍历链表,一个指针second指向需要删除的节点的前一个结点。初始化first指向头结点,second指向dummy结点。

主要分成两步解决问题:

第一步,first遍历链表前移n步,指向第n+1个结点。second此时仍指向dummy结点。因此second和first之间有n个结点。

第二步,first遍历链表直到指向链表尾NULL,second随着first遍历直到停止,两者之间始终有n个结点,second指向的是倒数第n个结点的前一个结点。

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy=ListNode(0,head)
        first=head
        second=dummy
        for _ in range(n):
            first=first.next
        while first:
            first=first.next
            second=second.next
        second.next=second.next.next
        return dummy.next

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

两两结对交换节点,1和2交换,3和4交换。

设置一个哑结点dummy方便后面处理头部结点。

在交换过程中主要有三个结点比较关键:

  • 要交换的对子之前的节点node0。
  • 要交换的对子中靠前的节点node1。
  • 要交换的对子中靠后的节点node2。

实现交换要建立的指针关系分别有以下三种:

  • node0->node2
  • node1->node2.next
  • node2.next=node1

要遍历更新的结点:node0=node1

  • 创建虚拟头节点

    • 使用 dummy 作为虚拟头节点,它指向链表的头节点 head。这样做的目的是处理链表头部交换时的边界情况,并方便返回结果。
  • 初始化指针

    • node0 指针初始化为 dummy,用于遍历链表,并进行节点交换操作。
  • 遍历链表并交换节点

    • 进入 while 循环,条件是 node0.nextnode0.next.next 都不为空。这保证了至少有两个节点可以进行交换。
    • node1 指向 node0.next,即待交换的第一个节点。
    • node2 指向 node0.next.next,即待交换的第二个节点。
    • 进行节点交换:
      • node0.next 指向 node2,使 node0 直接指向 node2
      • node1.next 指向 node2.next,使 node1 连接到交换后的下一个节点。
      • node2.next 指向 node1,完成交换。
    • node0 移动到 node1,准备处理下一对节点。
  • 返回结果链表

    • 最后返回 dummy.next,即交换后的链表头节点。
class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy=ListNode(0,head)
        node0=dummy
        while node0.next and node0.next.next:
            node1=node0.next
            node2=node0.next.next
            node0.next=node2
            node1.next=node2.next
            node2.next=node1
            node0=node1
        return dummy.next

6、K个一组翻转链表

主要思路如下:可以将链表划分为三部分,已翻转部分+待翻转部分+未翻转部分

  • 首先,为了统一处理操作,为链表设置一个哑结点,next指向头结点。
  • 其次,每次翻转链表,都是以的形式进行操作的,一组节点的数量为k。使用pre指向一组节点的前序节点,使用start指向一组的起始节点,使用end指向一组的尾部节点,使用next_group指向未翻转部分的第一个节点。其中,三者的关系可以描述为:start=pre.next,而end的位置是从pre.next开始遍历k个节点获取的。需要注意的是,当一个组内节点个数不足k个的时候(也就是end没遍历k次就到链表尽头了),直接返回处理过的链表即可(return dummy.next)。
  • 因此根据上面的信息,为了处理第一组节点,pre设置为dummy,end也设置为dummy。
  • 再每次翻转完链表后,需要将链表与前后两部分衔接起来,因此pre.next=当前翻转后的部分。当前翻转后的部分中,start指向的是该部分的最后一个节点,因此start.next=next_group。
  • 更新pre和end,pre更新为start,end也更新为start。

class Solution:
    def reverseList(self,head):
            pre=None
            curr=head
            while curr:
                nex=curr.next
                curr.next=pre
                pre=curr
                curr=nex
            return pre
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        dummy=ListNode(0,head)
        pre=end=dummy
        while end.next:
            for _ in range(k):
                end=end.next
                if not end:
                    return dummy.next

            start=pre.next
            next_group=end.next
            end.next=None

            pre.next=self.reverseList(start)
            start.next=next_group

            pre=start
            end=pre
        return dummy.next

7、随机链表的复制

主要分两步走:第一步,遍历链表,创建每个节点对应的新节点,此时创建的新节点与原结点相同,但next和random关系没有建立。第二步,遍历链表,构建新节点的random和next关系

  • 创建一个哈希表:用来存储原链表节点与新链表节点之间的映射关系。
  • 复制所有节点:遍历原链表,为每个节点创建一个新节点,这些新节点具有相同的值但nextrandom指针先不处理。
  • 设置指针:再次遍历原链表,根据哈希表中的映射关系,设置新链表的nextrandom指针。
  • 返回新链表的头节点:使用哈希表找到与原链表头节点对应的新链表头节点,返回此节点作为结果。
"""
# Definition for a Node.
class Node:
    def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
        self.val = int(x)
        self.next = next
        self.random = random
"""

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        if not head:
            return None
        d=dict()
        pre=head
        while pre:
            new=Node(pre.val,None,None)
            d[pre]=new
            pre=pre.next
        pre=head
        while pre:
            if pre.next:
                d[pre].next=d[pre.next]
            if pre.random:
                d[pre].random=d[pre.random]
            pre=pre.next
        return d[head]
        

8、排序链表

主要是利用归并排序的思想来实现的,在链表的迭代归并排序中,逐渐增加排序的子段长度(从1开始,然后是2、4、8等),每次迭代合并相邻的两个已排序的子链表。

主要通过以下两个步骤实现:

  • 提取子链表:对于每个子链表,遍历链表直到达到所需长度或链表结束。
  • 合并子链表:比较 h1 和 h2 的节点值,将较小的节点链接到 pre,并更新 pre 和选中的子链表的头节点。如果一个子链表先遍历完毕,将剩余的另一个子链表直接链接到已排序部分。

  • 初始化

    • 创建一个哑节点dummy,其 next 指向链表头部。
    • 使用变量 h 遍历整个链表以计算其长度 length
    • 设置 intv作为每次需要排序和合并的子链表的长度。
  • 外层循环:当 intv 小于 length 时,进行合并操作,每次循环结束后将 intv 的值翻倍。

  • 内层循环

    • 每次循环开始前设置 preh 指向dummy.next,不能直接使用head,因为在排序的过程中head可能已经发生变化。
    • 提取两个长度为 intv 的子链表 h1h2 进行合并。
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def splitList(self,head,size):
        prev=None
        cur=head
        count=0
        while cur and count<size:
            prev=cur
            cur=cur.next
            count+=1
        if prev:
            prev.next=None
        return head,cur
    def mergeList(self,h1,h2,pre):
        while h1 and h2:
            if h1.val<h2.val:
                pre.next=h1
                h1=h1.next
            else:
                pre.next=h2
                h2=h2.next
            pre=pre.next
        pre.next=h1 if h1 else h2
        while pre.next:
            pre=pre.next
        return pre
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        p=head;length=0
        dummy=ListNode(0,head)
        while p:
            length+=1
            p=p.next
        intv=1
        while intv<length:
            pre=dummy
            h=dummy.next
            while h:
                h1,h=self.splitList(h,intv)
                h2,h=self.splitList(h,intv)
                if not h2:
                    pre.next=h1
                    break
                pre=self.mergeList(h1,h2,pre)
            intv*=2
        return dummy.next

9、合并K个升序链表

从题意可以得到,一个list里面存放着若干链表,要想合并K个升序链表,可以使用小根堆来存放结点(利用了小根堆的堆顶一定是val最小的结点)。

首先,遍历列表中的每个链表,将每个链表的头结点存放到小根堆中。为了避免后面多个相同最小值pop报错的情况(假如只存放(val,Node)元组,当val相同时,小根堆去比较Node,会发生type error),使用index作为索引来标识不同链表,小根堆存放的元素就变成了(val,index,node)。heappop时比较完val,就会比较列表索引index,然后弹出一个节点。

接下来,在小根堆非空的情况下,每次循环pop出最小结点,链接到当前链表的尾部。同时,如果当前pop出的最小结点所在的链表还没有遍历结束,就将该结点在原链表中next指针指向的结点放入小根堆中。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
import heapq
class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        min_heap=[];index=0
        for l in lists:
            if l:
                heapq.heappush(min_heap,(l.val,index,l))
                index+=1
        dummy=ListNode(0)
        curr=dummy
        while min_heap:
            val,index,node=heapq.heappop(min_heap)
            curr.next=node
            curr=curr.next
            if node.next:
                heapq.heappush(min_heap,(node.next.val,index,node.next))
        return dummy.next

10、LRU缓存

首先,解读题意:

  • LRU(最近最少使用)的规则是:就当下而言,最久没有访问过的被淘汰。所以可以用双向链表这一数据结构来实现,最新访问过的被放在链表的头部位置,最久没有访问过的处于链表的尾部,淘汰时只需要删除尾部节点。
  • get是获取指定Key所对应的value,对于LRU缓存来说相当于访问一次key-value。每访问一次,被访问的结点就被移动到链表的头部。
  • put是在缓存中没有相应的key时,创建新节点插入链表头部;当缓存中有相应的key时,更改它对应的value,并将其移动到链表头部。需要注意的是,当创建新节点插入链表头部时,可能超出了LRU指定的capacity,这个时候就需要移除链表尾部的节点。
class DNode:
    def __init__(self,key=0,value=0,prev=None,next=None):
        self.key=key
        self.value=value
        self.prev=prev
        self.next=next
class LRUCache:
    def __init__(self, capacity: int):
        self.cache=dict()
        self.head=DNode()
        self.tail=DNode()
        self.head.next=self.tail
        self.tail.prev=self.head
        self.capacity=capacity
        self.size=0

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        node=self.cache[key]
        self.moveToHead(node)
        return node.value
 
    def put(self, key: int, value: int) -> None:
        if key not in self.cache:
            if self.capacity==self.size:
                removed=self.removeTail()
                self.cache.pop(removed.key)
                self.size-=1
            node=DNode(key,value)
            self.cache[key]=node
            self.addToHead(node)
            self.size+=1
        else:
            node=self.cache[key]
            node.value=value
            self.moveToHead(node)
    
    def addToHead(self,node):
        node.prev=self.head
        node.next=self.head.next
        self.head.next.prev=node
        self.head.next=node

    def removeNode(self,node):
        node.next.prev=node.prev
        node.prev.next=node.next

    def moveToHead(self,node):
        self.removeNode(node)
        self.addToHead(node)

    def removeTail(self):
        node=self.tail.prev
        self.removeNode(node)
        return node




# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值