在python中,切片中的-1,除了在[::-1]代表逆序开始,其他都代表序列中的最后一个位置
1. start
(起始索引)
- 定义:切片开始的索引位置(包含)。
- 默认值:如果省略,默认为
0
(即从序列的开头开始)。 - 效果:切片从这个位置开始提取元素。
2. stop
(结束索引)
- 定义:切片结束的索引位置(不包含)。
- 默认值:如果省略,默认为序列的末尾。
- 效果:切片到这个位置之前的元素,但不包括
stop
位置上的元素。
3. step
(步长)
- 定义:切片中元素之间的间隔。
- 默认值:如果省略,默认为
1
(即每次递增 1)。 - 效果:决定了从
start
到stop
之间的元素间隔。步长为负数时,表示反向切片。
而for i in range(3, -1, -1):
这里与切片不同,代表的是起始数字和结束数字,和步长,意义不同。
1. start
(起始值)
- 定义:生成的序列的起始值(包含)。
- 默认值:如果省略,默认为
0
。 - 效果:序列从这个值开始。
2. stop
(结束值)
- 定义:生成的序列的结束值(不包含)。
- 默认值:没有默认值,必须提供。
- 效果:序列到达这个值之前(不包括此值)结束。
3. step
(步长)
- 定义:序列中值之间的差距。
- 默认值:如果省略,默认为
1
。 - 效果:决定了从
start
到stop
之间的数值间隔。步长为负数时,表示倒序生成序列。
一个是数值一个是索引,索引中的-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 列表的灵活性
-
索引访问:像数组一样,Python 列表可以通过索引访问和修改元素。
lista = [0] * n print(lista[2]) # 访问索引为 2 的元素 lista[2] = 5 # 修改索引为 2 的元素
-
切片操作:Python 列表支持切片操作,这也是数组常用的功能。
sub_list = lista[1:4] # 切片操作,提取子列表
-
动态调整大小:Python 列表可以轻松地进行扩展或缩减,类似于动态数组(如 C++ 的
std::vector
)。lista.append(6) # 在列表末尾添加元素 lista.pop() # 从列表末尾移除元素
为什么说 Python 会"把列表当成数组"?
- 简化了内存管理:列表可以动态扩展或收缩,并且隐藏了底层的内存分配和管理逻辑。用户不需要手动管理大小,而这是数组在许多编程语言中需要处理的地方。
- 列表在数值计算中的应用:虽然 Python 提供了专门的数组模块和库(如
array
和numpy
),但在许多简单场景下,列表足以应对基本的数值存储和计算需求。Python 在内部对列表进行了很多优化,使其性能在大多数场景下与数组差距不大。
什么时候需要数组?
- 性能需求:当你需要更高效地处理数值数据时(特别是在科学计算或大规模矩阵运算中),像
numpy
这样的数组库会提供更好的性能,因为它们在底层进行了内存优化和向量化计算。 - 数据类型一致性:数组通常要求数据类型一致,而列表则允许混合类型。如果你需要确保所有元素都是同类型的,使用
numpy
或array
的数组会更加安全。
总结
虽然 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引用的进一步理解
是的,你可以这样理解:
-
可变对象:在 Python 中,可变对象(如列表、字典和集合)可以被多个引用共享。当你通过一个引用修改可变对象的内容时,所有指向该对象的引用都会看到这些修改。因此,这种情况下的引用行为类似于指针,因为它们直接指向相同的内存位置。
a = [1, 2, 3] b = a b.append(4) print(a) # 输出 [1, 2, 3, 4]
-
不可变对象:不可变对象(如整数、字符串和元组)在 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
模块中的一个函数,用于向堆中添加元素。它有两个参数:
参数:
- heap:堆列表。Python 中的堆是通过列表实现的,
heapq
模块会维护这个列表的堆结构。 堆都是通过队列来实现的。 - item:要添加的元素。可以是任意类型的元素,只要该类型支持比较操作。
详细说明:
- heap 是一个列表,Python 中的最小堆是基于这个列表实现的。
- item 是要加入堆的元素。如果堆中的元素是元组类型(比如
(value, index, node)
),堆会根据元组的第一个元素来进行比较
堆元素的比较是依次比较,插入的是这个元组,也按这个元组顺序依次比较
是的,heapq.heappush(heap, (node.val, i, node))
会按照元组中的元素依次比较。
在 Python 的堆中,如果你插入的是元组 (a, b, c)
,heapq
会依次使用元组的各个元素进行比较:
- 先比较第一个元素
a
,决定堆中元素的顺序。 - 如果第一个元素相同(
a1 == a2
),则比较第二个元素b
。 - 如果前两个元素都相同,才会比较第三个元素
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)
参数说明:
-
heap
:- 这是用于表示最小堆的列表。在 Python 中,堆实际上是一个列表,通过
heapq
模块的操作可以维持最小堆的性质(每个父节点都小于或等于其子节点)。 - 例如,如果你传入的是一个空列表,
heapq
会通过后续的操作将其转变为一个最小堆。
- 这是用于表示最小堆的列表。在 Python 中,堆实际上是一个列表,通过
-
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
模块),堆的常用操作heappush
和heappop
,本质上是实现优先队列的一种方式。
结合你的问题:
- 最小堆是数据结构。
- 优先队列是应用场景。
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
通常被称为“堆队列模块”或者直接称作“堆模块”。这个模块提供了一些用于操作堆的函数,比如 heappush
、heappop
、heapify
等