HOT100,28-35题

在python中,切片中的-1,除了在[::-1]代表逆序开始,其他都代表序列中的最后一个位置

1. start(起始索引)

  • 定义:切片开始的索引位置(包含)。
  • 默认值:如果省略,默认为 0(即从序列的开头开始)。
  • 效果:切片从这个位置开始提取元素。

2. stop(结束索引)

  • 定义:切片结束的索引位置(不包含)。
  • 默认值:如果省略,默认为序列的末尾。
  • 效果:切片到这个位置之前的元素,但不包括 stop 位置上的元素。

3. step(步长)

  • 定义:切片中元素之间的间隔。
  • 默认值:如果省略,默认为 1(即每次递增 1)。
  • 效果:决定了从 startstop 之间的元素间隔。步长为负数时,表示反向切片。

for i in range(3, -1, -1):这里与切片不同,代表的是起始数字和结束数字,和步长,意义不同。

1. start(起始值)

  • 定义:生成的序列的起始值(包含)。
  • 默认值:如果省略,默认为 0
  • 效果:序列从这个值开始。

2. stop(结束值)

  • 定义:生成的序列的结束值(不包含)。
  • 默认值:没有默认值,必须提供。
  • 效果:序列到达这个值之前(不包括此值)结束。

3. step(步长)

  • 定义:序列中值之间的差距。
  • 默认值:如果省略,默认为 1
  • 效果:决定了从 startstop 之间的数值间隔。步长为负数时,表示倒序生成序列。

一个是数值一个是索引,索引中的-1,都是代表最后一个位置

28.两数相加

数组法NB!!!

class Solution:
    def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
        num1 = []
        num2 = []
        while l1:
            num1.append(l1.val)
            l1 = l1.next 
        while l2:
            num2.append(l2.val)
            l2 = l2.next 
        x1, x2 = 0, 0 
        for i in range(len(num1)):
            x1 += num1[i] * (10) ** i
        for i in range(len(num2)):
            x2 += num2[i] * (10) ** i 
        x = x1 + x2 
        arr = [int(digit) for digit in str(x)[:: -1]]
        dummy = ListNode(-1)
        p = dummy
        for digit in arr:
            p.next = ListNode(digit)
            p = p.next 
        return dummy.next 

链表法 逆序存储很友好了,直接遍历链表就是从个位开始的,符合我们计算加法的习惯顺序。 这道题主要考察链表指针处理和加法运算过程中对进位的处理。注意这个 carry 变量的处理,在我们手动模拟加法过程的时候会经常用到 carry的中文有进位的意思

class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        # 在两条链表上的指针
        p1, p2 = l1, l2
        # 虚拟头结点(构建新链表时的常用技巧)
        dummy = ListNode(-1)
        # 指针 p 负责构建新链表
        p = dummy
        # 记录进位
        carry = 0
        # 开始执行加法,两条链表走完且没有进位时才能结束循环
        while p1 is not None or p2 is not None or carry > 0:
            # 先加上上次的进位
            val = carry
            if p1 is not None:
                val += p1.val
                p1 = p1.next
            if p2 is not None:
                val += p2.val
                p2 = p2.next
            # 处理进位情况
            carry = val // 10 #进给下一位的
            val = val % 10 #这一位的 
            # 构建新节点
            p.next = ListNode(val) #存下这一位
            p = p.next
        # 返回结果链表的头结点(去除虚拟头结点)
        return dummy.next

变量名选择,

在 Python 编程中,选择合适的变量名可以提高代码的可读性和可维护性。以下是一些常用的命名约定和临时变量的命名惯例:

1. 临时变量

  • temp: 常用来表示一个临时存储值的变量。例如:

    temp = a
    a = b
    b = temp
    
  • t: 有时也用来表示临时变量,尤其是在短小的循环或交换操作中。例如:

    for t in range(10):
        print(t)
    
  • _: 通常用来表示不需要的变量,尤其是在循环中。例如:

    for _ in range(5):
        do_something()
    

2. 索引变量

  • i, j, k: 常用于表示循环的索引,尤其是在多层嵌套循环中。例如:

    for i in range(10):
        for j in range(5):
            print(i, j)
    

3. 临时存储变量

  • result, res: 用于存储计算结果的变量。例如:

    result = calculate_some_value()
    
  • current, next: 用于表示当前值或下一个值的变量。例如:

    current = start_value
    next = current + step
    

4. 函数中的临时变量

  • temp_var: 在函数内部进行计算时,临时变量可以使用有意义的名字以便于理解。例如:

    def process_values(a, b):
        temp_sum = a + b
        temp_diff = a - b
        return temp_sum, temp_diff
    

5. 短变量名

  • x, y, z: 常用于数学计算中。例如:

    x = a + b
    y = x * c
    z = y / d
    

总的来说,虽然临时变量可以用简短的名字,但最好还是根据上下文选择有意义的名字,以提高代码的可读性。

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

之前采用的是将链表转换成列表,而不是数组,python列表非常具有灵活性。但其实这就是python的特性。

解释:

在 Python 中,虽然 [0] * n 生成的严格来说是列表,而不是数组,但由于 Python 的灵活性,很多情况下,列表 可以像数组一样使用。Python 的设计理念是追求简洁和易用,所以它在很多场景下允许使用列表来代替传统的数组,并且提供了丰富的列表操作方法,这使得它足够灵活来应对大多数数组的常见操作。

Python 列表的灵活性

  1. 索引访问:像数组一样,Python 列表可以通过索引访问和修改元素。

    lista = [0] * n
    print(lista[2])  # 访问索引为 2 的元素
    lista[2] = 5     # 修改索引为 2 的元素
    
  2. 切片操作:Python 列表支持切片操作,这也是数组常用的功能。

    sub_list = lista[1:4]  # 切片操作,提取子列表
    
  3. 动态调整大小:Python 列表可以轻松地进行扩展或缩减,类似于动态数组(如 C++ 的 std::vector)。

    lista.append(6)   # 在列表末尾添加元素
    lista.pop()       # 从列表末尾移除元素
    

为什么说 Python 会"把列表当成数组"?

  1. 简化了内存管理:列表可以动态扩展或收缩,并且隐藏了底层的内存分配和管理逻辑。用户不需要手动管理大小,而这是数组在许多编程语言中需要处理的地方。
  2. 列表在数值计算中的应用:虽然 Python 提供了专门的数组模块和库(如 arraynumpy),但在许多简单场景下,列表足以应对基本的数值存储和计算需求。Python 在内部对列表进行了很多优化,使其性能在大多数场景下与数组差距不大。

什么时候需要数组?

  • 性能需求:当你需要更高效地处理数值数据时(特别是在科学计算或大规模矩阵运算中),像 numpy 这样的数组库会提供更好的性能,因为它们在底层进行了内存优化和向量化计算。
  • 数据类型一致性:数组通常要求数据类型一致,而列表则允许混合类型。如果你需要确保所有元素都是同类型的,使用 numpyarray 的数组会更加安全。

总结

虽然 Python 列表和数组是不同的数据结构,但在许多情况下,Python 设计得足够灵活,允许列表像数组一样使用。对于大部分日常编程任务,列表已经可以胜任,但在对性能和数据类型有更高要求时,使用数组(如 numpy)会更为合适。

列表的一些常用操作,灵活使用这些足以灵活运用python解题!!!

Python 列表(list)是一个非常灵活和强大的数据结构,支持多种操作和方法。以下是一些常用的列表方法及其功能介绍:

1. append() - 在列表末尾添加元素

lista = [1, 2, 3]
lista.append(4)
print(lista)  # 输出: [1, 2, 3, 4]

将元素 4 添加到列表 lista 的末尾。

2. extend() - 将另一个序列的元素添加到列表末尾

lista = [1, 2, 3]
lista.extend([4, 5])
print(lista)  # 输出: [1, 2, 3, 4, 5]

[4, 5] 中的每个元素添加到 lista 的末尾。

3. insert() - 在指定位置插入元素

lista = [1, 2, 4]
lista.insert(2, 3)
print(lista)  # 输出: [1, 2, 3, 4]

在索引 2 的位置插入元素 3,原索引及之后的元素向后移动。

4. remove() - 删除列表中第一次出现的指定元素

lista = [1, 2, 3, 2, 4]
lista.remove(2)
print(lista)  # 输出: [1, 3, 2, 4]

删除列表中第一个出现的元素 2

5. pop() - 删除并返回指定索引处的元素

lista = [1, 2, 3]
item = lista.pop(1)
print(item)  # 输出: 2
print(lista)  # 输出: [1, 3]

从列表中删除索引为 1 的元素,并返回它。如果不指定索引,pop() 默认删除并返回列表中的最后一个元素。

6. clear() - 清空列表

lista = [1, 2, 3]
lista.clear()
print(lista)  # 输出: []

将列表中的所有元素删除,列表变为空列表。

7. index() - 查找元素的索引

lista = [1, 2, 3, 4]
index = lista.index(3)
print(index)  # 输出: 2

返回元素 3 在列表中的索引。如果元素不在列表中,会抛出 ValueError 异常。

8. count() - 统计元素出现的次数

lista = [1, 2, 2, 3, 4]
count = lista.count(2)
print(count)  # 输出: 2

统计元素 2 在列表中出现的次数。

9. sort() - 对列表进行原地排序

lista = [3, 1, 4, 2]
lista.sort()
print(lista)  # 输出: [1, 2, 3, 4]

对列表进行升序排序。如果需要降序排序,可以传递参数 reverse=True

lista.sort(reverse=True)
print(lista)  # 输出: [4, 3, 2, 1]

10. reverse() - 反转列表中的元素顺序

lista = [1, 2, 3, 4]
lista.reverse()
print(lista)  # 输出: [4, 3, 2, 1]

将列表中的元素顺序颠倒。

11. copy() - 浅复制列表

lista = [1, 2, 3]
lista_copy = lista.copy()
print(lista_copy)  # 输出: [1, 2, 3]

创建列表的浅拷贝。对于多层嵌套的列表,子列表仍然是引用。

12. len() - 获取列表长度

lista = [1, 2, 3, 4]
length = len(lista)
print(length)  # 输出: 4

返回列表中元素的个数。

13. in 运算符 - 检查元素是否在列表中

lista = [1, 2, 3, 4]
print(3 in lista)  # 输出: True
print(5 in lista)  # 输出: False

检查元素是否在列表中。

14. 列表切片 - 获取子列表

lista = [1, 2, 3, 4, 5]
print(lista[1:4])  # 输出: [2, 3, 4]

使用切片来获取列表中的子列表。

15. enumerate() - 获取元素及其索引

lista = ['a', 'b', 'c']
for index, value in enumerate(lista):
    print(f"Index: {index}, Value: {value}")
# 输出:
# Index: 0, Value: a
# Index: 1, Value: b
# Index: 2, Value: c

enumerate() 返回的是元素和索引的元组。


这些方法和操作使得 Python 列表成为处理动态数据的一种强大工具。根据需求,您可以灵活选择合适的方法来操作列表。

这一题,列表解法,或者叫数组解法运用到列表的pop方法,弹出第i个位置的元素

在 Python 中,如果使用列表的 pop() 方法时不写入参数,它会弹出并返回列表的最后一个元素。

例子:

lista = [1, 2, 3, 4]
popped_element = lista.pop()  # 不指定参数
print(popped_element)  # 输出: 4
print(lista)  # 输出: [1, 2, 3]

在这个例子中,pop() 方法弹出了列表中最后一个元素 4,并从列表中删除了它。

说明:

  • 如果不提供索引,pop() 默认删除并返回最后一个元素。

  • 如果列表是空的,调用 pop() 会抛出 IndexError 异常:

    empty_list = []
    empty_list.pop()  # 抛出 IndexError: pop from empty list
    

如果你想弹出特定位置的元素,可以在 pop() 中传递索引参数。

class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        nums = []
        while head:
            nums.append(head.val)
            head = head.next 
        #倒数第n个结点,在数组中的位置是-i
        nums.pop(len(nums) - n) #这一句直接绝杀
        #已经成功删除倒数第n个结点
        #这里是新建一个链表,所以采用虚拟头结点的方法
        dummy = ListNode(-1)
        p = dummy 
        for num in nums:
            p.next = ListNode(num)
            p = p.next 
        return dummy.next 

想清楚倒数第i个位置,如何表达, 倒数第i个数,也就是整数第len(nums) + 1 - i 个数, 用索引表达就是nums[len(Nums) - i] 表达的就是倒数第i个数字

双指针法,比列表法麻烦太多,要思考的点更多。

def removeNthFromEnd(head: ListNode, n: int) -> ListNode:
    # 创建一个哑节点(dummy),方便处理边界情况
    dummy = ListNode(0)
    dummy.next = head
    #明确要删除倒数第n个结点,是删除整数第len(nums) + 1 - n个结点,由于链表指针,所以要找到整数第len(nums) - n个结点,让它跨指
    # 初始化两个指针:fast 和 slow  #都放在头结点,这样走多少步到的就是正数第多少个结点 
    fast = slow = dummy
    
    # 让 fast 指针先走 n 步
    for _ in range(n):
        fast = fast.next
    
    # 让 fast 和 slow 同时移动,直到 fast 到达链表末尾
    while fast.next:#停住的时候slow走了len(nums) - n步,到达整数第len(nums) - n结点,让它跨指就可以
        fast = fast.next
        slow = slow.next
    
    # 删除 slow 指针指向的节点
    slow.next = slow.next.next
    
    # 返回头节点(忽略哑节点)
    return dummy.next

30.两两交换链表中的节点

常规方法就是维护多个指针,遍历一遍链表顺便把每两个节点翻转。不过迭代的思路虽然直接,但细节问题会比较多,写起来麻烦。 直接用递归的方式来写,只要搞明白递归函数的定义,然后利用这个定义就可以完成这道题。 之后链表的题目,如果数组方法想不通,考虑递归,最后再考虑迭代解法,因为迭代解法细节实在是太多了,一步错步步错

对python引用的进一步理解

是的,你可以这样理解:

  1. 可变对象:在 Python 中,可变对象(如列表、字典和集合)可以被多个引用共享。当你通过一个引用修改可变对象的内容时,所有指向该对象的引用都会看到这些修改。因此,这种情况下的引用行为类似于指针,因为它们直接指向相同的内存位置。

    a = [1, 2, 3]
    b = a
    b.append(4)
    print(a)  # 输出 [1, 2, 3, 4]
    
  2. 不可变对象:不可变对象(如整数、字符串和元组)在 Python 中不能被修改。当你对一个不可变对象进行操作时,实际上是创建了一个新的对象。如果你将这个新的对象赋值给一个引用,原来的引用仍然指向原来的对象。这种情况下,引用更像是对对象的赋值,因为你不能通过引用修改对象的内容。

    x = 10
    y = x
    x = 20
    print(y)  # 输出 10
    

总结:

  • 对于可变对象,引用类似于指针,因为它们指向相同的内存位置,并且通过一个引用的修改会影响到所有指向该对象的引用。
  • 对于不可变对象,引用行为更像是赋值操作,因为修改不可变对象实际上是创建了一个新的对象,而原来的引用仍然指向原来的对象。

这种理解方式有助于把握 Python 中引用的行为和不可变对象的特性。 Python 主要通过高层次的对象引用来管理内存,而不是直接操作内存地址。这种设计使得 Python 更加安全和易于使用。


递归法,抓住函数的定义, 将链表中元素每两个进行翻转,返回翻转后链表的头结点

这一题,在链表中进行两两交换节点,特别是当链表节点数量为奇数时,最后一个节点无法与另一个节点交换,它会保持在原位置。

# 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转,
# 返回翻转后的链表头结点
class Solution:
    def swapPairs(self, head: ListNode) -> ListNode:
        if head is None or head.next is None:
            return head
        first = head
        second = head.next
        others = head.next.next
        # 先把前两个元素翻转
        second.next = first
        # 利用递归定义,将剩下的链表节点两两翻转,接到后面
        first.next = self.swapPairs(others)
        # 现在整个链表都成功翻转了,返回新的头结点
        return second

在这里,first, second, others相当于指向某个地址的指针。 因为链表是可变变量

31 K 个一组翻转链表

翻转链表的题目尤其适合用递归的方法来做,递归。先回顾如何用递归翻转链表

class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        #明确递归函数的定义,输入链表头结点,将这个链表反转后再返回新链表的头结点
        ##base case 
        if head is None or head.next is None:
            return head 
        #画图理解,  不要进入递归,而是根据递归函数的定义把之看为整体
        last = self.reverseList(head.next)
        head.next.next = head 
        head.next = None
        return last 

代码简洁明了

class Solution:
    def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
        # 检查链表中是否有足够的节点用于反转
        node = head
        count = 0
        while node and count < k:
            node = node.next 
            count += 1
        # 如果节点不足 k 个,则不反转,直接返回头节点
        if count < k:
            return head

        # 递归反转前 k 个节点,这样每做一次是翻转了一个结点,翻转完后head.next = None,头结点也做一次翻转
        prev, curr = None, head
        for _ in range(k):
            next_node = curr.next 
            curr.next = prev #每转一个,转完之后都往后移动
            prev = curr
            curr = next_node  #翻转结束之后prev对应第k个结点,cur对应下一个结点,  prev是翻转之后的起始结点
        #先翻转前K个,然后
        # 递归处理剩下的链表,并将其连接到反转后的部分
        head.next = self.reverseKGroup(curr, k)
        
        # 返回反转后的新头节点
        return prev

链表题目举例子,采用1->2->3,这种简单例子,动手操作一遍就能明白

33. 随机链表的复制

Python 的字典(dict)提供了 get 方法,用于安全地获取字典中的值。与直接使用索引 (dict[key]) 不同,get 方法可以避免在键不存在时引发 KeyError 异常。它允许你指定一个默认值,如果键不存在,则返回该默认值。

难者不会,会者不难。

对于数据结构复制,甭管他怎么变,你就记住最简单的方式:一个哈希表 + 两次遍历

第一次遍历专门克隆节点,借助哈希表把原始节点和克隆节点的映射存储起来;键是原来的结点,值是新造的结点 第二次专门组装节点,照着原数据结构的样子,把克隆节点的指针组装起来。

两次遍历

class Solution:
    def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
        origin2clone = {}
        p = head 
        while p:
            origin2clone[p] = Node(p.val)#原结点作为键, 值为对应新结点的指针
            p = p.next 
        p = head
        while p :
            if p.next:
                origin2clone[p].next = origin2clone[p.next]  #找到p.next对应的克隆位置
            if p.random:
                origin2clone[p].random = origin2clone[p.random]
            p = p.next 
        return origin2clone.get(head)
        

34. 排序链表

数组列表法nb

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        nums = []
        while head:
            nums.append(head.val)
            head = head.next 
        nums.sort()
        dummy = ListNode(-1)
        p = dummy 
        for num in nums:
            p.next = ListNode(num)
            p = p.next 
        return dummy.next 

35.合并 K 个升序链表

列表的加法操作是将两个列表拼接在一起,生成一个新列表。你可以使用 + 操作符进行列表拼接,使用 extend 方法将一个列表的元素添加到另一个列表中,或者使用 append 方法将整个列表作为一个元素添加到另一个列表中。列表的加法不是原地操作,若想进行原地操作,用append或者用extend

数组法,链表列表的处理,第一次用

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:#暴力法
        nums = []
        for l in lists: #这是一个链表列表,每个l都是一个链表
            while l :
                nums.append(l.val)
                l = l.next 
        nums.sort()
        dummy = ListNode(-1)
        p = dummy 
        for num in nums:
            p.next = ListNode(num)
            p = p.next 
        return dummy.next 
            

堆,最小堆, heapq 堆队列 heapq默认是最小堆

heapq.heappush 是 Python 标准库 heapq 模块中的一个函数,用于向堆中添加元素。它有两个参数:

参数:

  1. heap:堆列表。Python 中的堆是通过列表实现的heapq 模块会维护这个列表的堆结构。 堆都是通过队列来实现的。
  2. item:要添加的元素。可以是任意类型的元素,只要该类型支持比较操作。

详细说明:

  • heap 是一个列表,Python 中的最小堆是基于这个列表实现的。
  • item 是要加入堆的元素。如果堆中的元素是元组类型(比如 (value, index, node)),堆会根据元组的第一个元素来进行比较

堆元素的比较是依次比较,插入的是这个元组,也按这个元组顺序依次比较

是的,heapq.heappush(heap, (node.val, i, node)) 会按照元组中的元素依次比较。

在 Python 的堆中,如果你插入的是元组 (a, b, c)heapq依次使用元组的各个元素进行比较:

  1. 先比较第一个元素 a,决定堆中元素的顺序。
  2. 如果第一个元素相同(a1 == a2),则比较第二个元素 b
  3. 如果前两个元素都相同,才会比较第三个元素 c,依此类推。

对于 (node.val, i, node)

  • node.val 是链表节点的值,优先用于比较。
  • i 是链表的索引,用来区分不同链表中具有相同值的节点(因为链表的值可能会相同)。
  • node 是节点本身,通常不会用于比较,但在极少数情况下,前两个元素相等时才会用它进行比较。

代码示例:

import heapq

# 假设我们有几个节点,节点的值分别为 2, 1, 1
heap = []
heapq.heappush(heap, (2, 0, "Node2"))
heapq.heappush(heap, (1, 1, "Node1"))
heapq.heappush(heap, (1, 0, "Node0"))

# 依次取出堆中最小的元素
while heap:
    print(heapq.heappop(heap))

# 输出:
# (1, 0, 'Node0')  --> 先比较 node.val,两个都是 1,比较 i 得到 0 比 1 小
# (1, 1, 'Node1')  --> val 相同,比较 i,取出 'Node1'
# (2, 0, 'Node2')  --> val 为 2,最大

总结:

  • heapq 在处理元组时会按照元组的顺序逐个比较。对于 (node.val, i, node),优先比较 node.val,如果 node.val 相同,再比较 i,以此类推。

heapq.heappush 是 Python 内置模块 heapq 中用于将元素推入最小堆的函数。它有两个主要参数:

heapq.heappush(heap, item)

参数说明:

  1. heap:

    • 这是用于表示最小堆的列表。在 Python 中,堆实际上是一个列表,通过 heapq 模块的操作可以维持最小堆的性质(每个父节点都小于或等于其子节点)。
    • 例如,如果你传入的是一个空列表,heapq 会通过后续的操作将其转变为一个最小堆。
  2. item:

    • 这是你想要插入堆中的元素。插入的元素会被放置在适当的位置,保持最小堆的性质。
    • item 可以是任何可比较的对象。对于基础类型(如整数、浮点数等),heapq 会根据它们的值自动排序。
    • 如果要插入复杂的对象(如元组),则 heapq 会根据元组的第一个元素进行排序;如果第一个元素相同,依次比较后续元素。

堆都是用列表来实习的,维持最小堆的性质。维持的是heap,这个堆列表的最小堆性质。 按照item依次比较,item也是入堆元素。

node = heapq.heappop(pq)[2]从当前堆中弹出当前的最小元素,获取第3个值

最小堆实际上是一种(Heap),但在某些场景中也可以用作优先队列(Priority Queue)。尽管堆和队列在概念上有所不同,它们在特定用法上可以结合在一起。具体来说:

1. 堆(Heap)

  • 堆是一种特殊的树形数据结构,满足堆的性质:
    • 最小堆:任意节点的值都小于或等于其子节点的值。
    • 最大堆:任意节点的值都大于或等于其子节点的值。
  • 在最小堆中,堆顶(即树的根节点)总是最小的元素。因此,它适合用来快速找到最小值

2. 优先队列(Priority Queue)

  • 优先队列是一种特殊的队列,元素按照优先级排列,不一定是先入先出(FIFO)。每次从队列中取出元素时,总是取出优先级最高的(即最小或最大的元素)。
  • 当优先级是基于数值大小时,最小堆非常适合作为优先队列,因为堆的性质保证了最小的元素总是在堆顶,可以在常数时间内取出。按照某种优先级

堆与优先队列的关系

  • 最小堆常用来实现优先队列,这是因为堆可以高效地保持元素的顺序,每次插入或删除操作的时间复杂度为 O(log n),而取出最小元素的时间复杂度为 O(1)
  • 在许多编程语言中(包括 Python 的 heapq 模块),堆的常用操作 heappushheappop,本质上是实现优先队列的一种方式。

结合你的问题

  • 最小堆是数据结构。
  • 优先队列是应用场景。

Python 的 heapq 模块实际上提供了对最小堆的操作,但可以用它来模拟和实现优先队列

总结:

  • **最小堆是一种特殊的树形结构,**它总是保证堆顶的元素最小。
  • 优先队列是一种数据结构,在插入和弹出时优先级最高的元素会被优先处理。
  • 最小堆经常被用来实现优先队列,因此你会在许多编程场景中看到堆被当作优先队列使用。

堆(Heap)通常是二叉树,即每个节点最多有两个子节点。这种堆结构被称为二叉堆(Binary Heap),它是最常见的堆实现方式。二叉堆分为最小堆最大堆,分别保证根节点是整个树中最小或最大的元素。 通常都是二叉堆。

1. 堆(Heap)

  • Python 的堆:Python 的 heapq 模块实现了一个最小堆,它能快速找到最小元素。

  • 堆的特性

    • 是一种特殊的树结构,最常用的形式是二叉堆,即每个父节点的值都比它的子节点小(最小堆)。
    • 堆中的元素是无序的,但堆顶元素是最小的(最小堆)。
    • 堆常用于实现优先队列,但它本身并不是传统意义上的队列。
堆的操作:
  • heapq.heappush(heap, item):向堆中插入元素,并保持堆的性质。
  • heapq.heappop(heap):从堆中弹出并返回最小元素。
  • heapq.heapify(list):将一个列表转化为最小堆。

在使用堆时,插入和删除操作的时间复杂度是 O(log n),获取最小值的时间复杂度是 O(1)

列表和双端队列的差异

在 Python 中,列表(list) 可以在某些情况下用于模拟 队列(queue),但由于性能上的差异,列表并不总是最佳选择。具体来说,列表可以用于实现队列的基本功能,但如果操作需要频繁在列表的头部进行插入或删除,效率会变得很低。

什么时候可以使用列表代替队列?

  • 列表代替队列 在以下情况下是可行的:

    • 只在列表的尾部进行插入和删除操作时(例如栈结构),列表的效率是很高的,因为这些操作是 O(1)
    • 队列中的元素较少,且性能不是主要问题时,使用列表不会明显影响效率。

    如果你需要高效地在两端插入或删除元素,collections.deque 是更好的选择。它是一个双端队列,底层是双向链表,因此在两端插入和删除的时间复杂度都是 O(1)

    列表缺陷:性能问题:虽然列表支持上述操作,但 Python 的列表是动态数组实现的。列表末尾的添加和删除操作是O(1),但在列表头部进行删除或插入(如 pop(0)insert(0, item))时,需要将列表中的所有元素向前或向后移动,操作的时间复杂度为 O(n),这在长列表中会导致性能瓶颈。

在 Python 中,列表(list) 可以通过以下方式在头部添加和删除元素:

1. 在列表头部添加元素

使用 insert(0, element) 方法可以在列表的头部添加元素。这个方法会在指定索引位置插入元素,0 表示列表的第一个位置。

示例:
my_list = [1, 2, 3]
my_list.insert(0, 0)  # 在头部插入元素 0
print(my_list)  # 输出: [0, 1, 2, 3]
  • 时间复杂度O(n),因为需要移动所有元素以为新元素腾出空间。

2. 在列表头部删除元素 pop默认是在右侧弹出元素

使用 pop(0) 方法可以删除列表的第一个元素。pop() 默认删除最后一个元素,但如果指定索引为 0,则会删除头部元素。

示例:
my_list = [0, 1, 2, 3]
my_list.pop(0)  # 删除头部的第一个元素
print(my_list)  # 输出: [1, 2, 3]
  • 时间复杂度O(n),因为删除第一个元素后,需要移动其他所有元素。

3. 更高效的方式:使用 collections.deque

如前面提到的,使用 collections.deque 可以在头部高效地添加和删除元素,时间复杂度为 O(1)。如果你频繁在列表头部操作,deque 是更好的选择。

示例:
from collections import deque

my_deque = deque([1, 2, 3])
my_deque.appendleft(0)  # 在头部添加元素 0
print(my_deque)  # 输出: deque([0, 1, 2, 3])

my_deque.popleft()  # 删除头部的元素
print(my_deque)  # 输出: deque([1, 2, 3])

总结:

  • 在列表头部添加:使用 insert(0, element),但效率较低(O(n))。
  • 在列表头部删除:使用 pop(0),但同样效率较低(O(n))。
  • 如果你需要频繁操作头部,建议使用 collections.deque,它在头部操作的时间复杂度是 O(1)

最小堆法

class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        if not lists:
            return None
        # 虚拟头结点
        dummy = ListNode(-1)
        p = dummy
        # 优先级队列,最小堆
        pq = []
        for head in lists:#这里的head其实也就是多个链表的头结点指针
            if head:
                heapq.heappush(pq, (head.val, id(head), head)) #这个结点也要入堆,方便后序建列表  值相同的时候比地址
        # 将 k 个链表的头结点加入最小堆
        while pq:
            # 获取最小节点,接到结果链表中
            node = heapq.heappop(pq)[2]
            p.next = node
            if node.next:
                heapq.heappush(pq, (node.next.val, id(node.next), node.next))#如果还有,接着入堆
            # p 指针不断前进
            p = p.next
        return dummy.next

heapq 是 Python 标准库中的一个模块,主要用于实现堆队列算法(堆排序)以及堆操作。在中文中,heapq 通常被称为“堆队列模块”或者直接称作“堆模块”。这个模块提供了一些用于操作堆的函数,比如 heappushheappopheapify

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值