2025/3/10 【栈与队列】LeetCode347.前K个高频元素 【🌟】知识点:import heapq ,堆队列(优先队列)

347. 前 K 个高频元素 - 力扣(LeetCode)

代码随想录

我的方法:

1.遍历nums,用字典统计频率,O(n)

2.用sort根据字典中键对应的值对键进行排序,O(n log n)

3.取排序的键列表的前K个元素,O(k)

总的时间复杂度为 O(n) + O(n log n) + O(k)。由于 O(n log n) 是主导项,所以总的时间复杂度为 O(n log n)

from collections import defaultdict
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        mydict = defaultdict(int)
        for num in nums:
            mydict[num] += 1
        
        mydict_key = sorted(mydict, key=mydict.get, reverse=True)
        return mydict_key[:k]
        

这个方法比代码随想录提供的Python的解法二好理解,更加明了。

为什么不是 sorted(mydict.keys(), key=mydict.get, reverse=True)?

sorted(mydict, key=mydict.get, reverse=True) 和 sorted(mydict.keys(), key=mydict.get, reverse=True) 的功能是 完全相同的,它们的返回结果也是一样的。区别在于前者是直接对字典 mydict 进行迭代,而后者是显式地调用 mydict.keys() 获取键后再进行排序。

为什么两者等价?

  1. 字典的默认迭代行为

    • 在 Python 中,直接对字典进行迭代(如 for key in mydict)会默认迭代字典的 键(keys)

    • 因此,sorted(mydict, ...) 实际上是对字典的键进行排序。

  2. mydict.keys()

    • mydict.keys() 显式地返回字典的键视图(dict_keys 对象),它是一个可迭代对象,包含字典的所有键。

    • 因此,sorted(mydict.keys(), ...) 也是对字典的键进行排序。

推荐直接使用 sorted(mydict, ...)

  • sorted(mydict, ...) 更简洁,不需要显式调用 .keys()

  • Python 的设计哲学之一是简洁明了,直接对字典进行迭代是更常见的写法。

  • 直接对字典进行迭代是Python中常见的习惯用法,代码更易读且符合Python的风格。

额外说明

如果你需要对字典的 键值对(key-value pairs) 进行排序,可以使用 mydict.items(),例如:

sorted(mydict.items(), key=lambda item: item[1], reverse=True)

这会返回一个列表,列表中的元素是 (key, value) 元组,按值从大到小排序。例如:

[('a', 3), ('c', 2), ('b', 1)]

方法2:小顶堆

代码使用了最小堆来高效地维护频率最高的 k 个元素,适合 k 远小于 n 的场景

实现1:时间复杂度是 O(n log k)

import heapq
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        mydict = {}
        for num in nums:
            mydict[num] = mydict.get(num, 0) + 1
        
        return heapq.nlargest(k, mydict.keys(), key=mydict.get)

1. 为什么不是 heapq.nlargest(k, mydict.keys(), key=mydict.get())

  • mydict.get():

    • mydict.get 是一个函数,而 mydict.get() 是调用这个函数

    • 调用 mydict.get() 时,如果没有提供默认值,会抛出 TypeError,因为 get 方法需要一个键作为参数。

    • 即使提供了默认值(例如 mydict.get(default=0)),heapq.nlargest 的 key 参数需要的是一个函数,而不是函数的返回值

  • 错误原因:

    • key 参数需要的是一个函数,而 mydict.get() 是一个返回值,不符合要求。

2. 为什么不是 heapq.nlargest(k, mydict, key=mydict.get)

  • mydict:

    • 直接传入 mydict 时,heapq.nlargest 会遍历字典的键(keys),而不是键值对(items)。

    • 例如,如果 mydict = {'a': 3, 'b': 1, 'c': 2}heapq.nlargest 会遍历 'a''b''c'

  • key=mydict.get:

    • mydict.get 会根据键返回对应的值。

    • 例如:

      • mydict.get('a') 返回 3

      • mydict.get('b') 返回 1

      • mydict.get('c') 返回 2

  • 结果:

    • 这种写法与 heapq.nlargest(k, mydict.keys(), key=mydict.get) 是等价的因为 heapq.nlargest 会默认遍历字典的键

    • 但是,直接传入 mydict 的写法不够直观,容易引起误解,因此推荐使用 mydict.keys() 明确表示遍历的是键。

推荐写法

推荐使用以下写法,既直观又明确:

heapq.nlargest(k, mydict.keys(), key=mydict.get)

或者更简洁地使用字典的 items() 方法:

heapq.nlargest(k, mydict.items(), key=lambda x: x[1])
  • 这种写法直接遍历键值对,排序依据是值(x[1]),返回的结果是键值对的列表。

实现2:总的时间复杂度为 O(n log k)

import heapq
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        # 统计频率:O(n)
        mydict = {}
        for num in nums:
            mydict[num] = mydict.get(num, 0) + 1
        
        # 堆操作:每次 heapq.heappush 和 heapq.heappop 的时间复杂度为 O(log k),
        # 总共有 n 次操作,时间复杂度为 O(n log k)
        s_heapq = []
        for num, freq in mydict.items():
            # heapq 默认根据元组的第一个元素(即 num)进行排序
            heapq.heappush(s_heapq, (freq, num))
            if len(s_heapq) > k:
                heapq.heappop(s_heapq)
        
        # 构建结果数组:O(k log k)
        res = []
        for i in range(k):
            res.append(heapq.heappop(s_heapq)[1])
        return res\

知识点:

`heapq` 是 Python 标准库中的一个模块,提供了堆队列算法的实现,也称为 优先队列。堆是一种特殊的二叉树结构,满足以下性质:
- 在最小堆中,父节点的值总是小于或等于其子节点的值。
- 在最大堆中,父节点的值总是大于或等于其子节点的值。

`heapq` 模块默认实现的是 **最小堆**,即堆顶元素是最小的。通过堆,可以高效地实现插入、删除和获取最小(或最大)元素的操作。

---

 1. `heapq` 模块的基本功能

 (1) 

heapq.heappush(heap, item)

- 功能:将元素 `item` 插入堆 `heap` 中,并保持堆的性质。
- 时间复杂度:`O(log n)`,其中 `n` 是堆的大小。

(2) 

heapq.heappop(heap)

- 功能:从堆 `heap` 中弹出并返回最小的元素(堆顶元素),并保持堆的性质。
- 时间复杂度:`O(log n)`,其中 `n` 是堆的大小。
 (3) 

heapq.nlargest(k, iterable, key=None)

- 功能:从可迭代对象 `iterable` 中返回最大的 `k` 个元素。
- 参数:
- `k`:需要返回的元素个数。
- `iterable`:可迭代对象(如列表、字典等)。
- `key`:可选参数,用于指定排序的依据(类似于 `sorted` 的 `key` 参数)。
-  时间复杂度:`O(n log k)`,其中 `n` 是可迭代对象的大小。
 (4) 

heapq.nsmallest(k, iterable, key=None)

- 功能:从可迭代对象 `iterable` 中返回最小的 `k` 个元素。
- 参数:与 `heapq.nlargest` 相同。
- 时间复杂度:`O(n log k)`,其中 `n` 是可迭代对象的大小。

 2. 堆的性质

- 最小堆:堆顶元素是最小的。
- 最大堆:可以通过将元素取反来实现。例如,将 `[3, 1, 2]` 存储为 `[-3, -1, -2]`,这样堆顶元素是 `-3`,取反后就是最大的元素。

---

3. 使用 `heapq` 实现最大堆

由于 `heapq` 默认是最小堆,可以通过将元素取反来实现最大堆:

import heapq
heap = []
heapq.heappush(heap, -3)
heapq.heappush(heap, -1)
heapq.heappush(heap, -2)
print(heap) # 输出: [-3, -1, -2]
largest = -heapq.heappop(heap)
print(largest) # 输出: 3

---

4. 使用 `heapq` 的典型场景

(1) **Top K 问题**
- 使用 `heapq.nlargest` 或 `heapq.nsmallest` 可以高效地解决 Top K 问题。
- 示例:

import heapq
nums = [3, 1, 4, 1, 5, 9, 2, 6]
top_3 = heapq.nlargest(3, nums)
print(top_3) # 输出: [9, 6, 5]

(2) 合并有序列表
- 使用 `heapq.merge` 可以高效地合并多个有序列表。
- 示例:

import heapq
list1 = [1, 3, 5]
list2 = [2, 4, 6]
merged = list(heapq.merge(list1, list2))
print(merged) # 输出: [1, 2, 3, 4, 5, 6]

(3) 优先队列
- 使用 `heapq` 可以实现优先队列,优先级高的元素先出队。
- 示例:

import heapq
heap = []
heapq.heappush(heap, (2, 'code'))
heapq.heappush(heap, (1, 'eat'))
heapq.heappush(heap, (3, 'sleep'))
while heap:
priority, task = heapq.heappop(heap)
print(task) # 输出: eat, code, sleep

---

优先队列(Priority Queue) 是一种抽象数据类型,它类似于普通的队列(Queue),但每个元素都有一个优先级(Priority)。

在优先队列中,元素按照优先级顺序出队,而不是按照入队的顺序。优先级高的元素会先出队。

---

1. 优先队列的特点

- 优先级决定出队顺序:
- 在普通队列中,元素遵循“先进先出”(FIFO)的原则。
- 在优先队列中,元素遵循“优先级高者先出”的原则。

- 动态插入和删除:
- 优先队列支持动态插入元素和删除优先级最高的元素。

- 实现方式:
- 优先队列通常通过 堆(Heap) 来实现,因为堆可以高效地维护元素的优先级顺序。

---

2. 优先队列的操作

优先队列通常支持以下基本操作:

 (1) 插入(Insert / Push)
- 将一个元素插入优先队列中。
- 时间复杂度:`O(log n)`,其中 `n` 是队列中的元素个数。

 (2) 删除最高优先级元素(Delete-Max / Delete-Min / Pop)
- 删除并返回优先队列中优先级最高(或最低)的元素。
- 时间复杂度:`O(log n)`。

 (3)查看最高优先级元素(Peek-Max / Peek-Min)
- 返回优先队列中优先级最高(或最低)的元素,但不删除它。
- 时间复杂度:`O(1)`。

 (4) 判断是否为空(Is-Empty)
- 检查优先队列是否为空。
- 时间复杂度:`O(1)`。

---

 3. 优先队列的实现方式

优先队列可以通过多种数据结构实现,常见的实现方式包括:

 (1) **堆(Heap)**
- **最小堆**:优先级最低的元素在堆顶。
- **最大堆**:优先级最高的元素在堆顶。
- 时间复杂度:
- 插入:`O(log n)`。
- 删除:`O(log n)`。
- 查看堆顶元素:`O(1)`。
- 优点:高效且易于实现。

 (2) 平衡二叉搜索树(Balanced Binary Search Tree)

(3) 无序数组或链表

总结:我靠,花了好多时间总算吃透这题!

posted on 2025-03-10 21:52  axuu  阅读(33)  评论(0)    收藏  举报