leetcode-python-专栏目录
无
无
如果觉得本文对你有帮助,为我收藏点赞,若文中有任何问题(哪步算法没看懂,或者涉及到的python语法不了解,或者哪里出错了)可在评论区留言。
本文围绕leetcode-347展开,先用最少量的代码(collections.Counter)解决问题,然后根据Counter的源码找到该算法的时间复杂度。由此引入优先级队列,介绍python中优先级队列的用法,最后用两个不同的优先级队列解决本题。
熟悉了优先队列之后,通过leetcode-23来实战。
题目链接
代码思路:
代码实现:
def topKFrequent(self, nums, k):
from collections import Counter
return [c[0] for c in Counter(nums).most_common(k)]
虽然本道题目完成了,但是我们想深究下内部的时间复杂度。
我们发现了heapq.nlargest这个python内置的堆的方法。
def most_common(self, n=None):
if n is None:
return sorted(self.items(), key=_itemgetter(1), reverse=True)
# 堆的使用,也就是优先级
return _heapq.nlargest(n, self.items(), key=_itemgetter(1))
再继续看heapq.nlargest源码。
def nlargest(n, iterable, key=None):
"""Find the n largest elements in a dataset.
Equivalent to: sorted(iterable, key=key, reverse=True)[:n]
"""
注释中告诉我们这个方法约等于sorted方法,我们假设python内置的sorted就是普通的快排(不考虑内部的优化),那么他的时间复杂度就是O(nlogn)
使用most_common时,即使是我们只想获得频率最大的一个,也需要排序整个数组。消耗O(nlogn)的时间复杂度
这时为了降低时间复杂度我们就需要优先级队列了。
在python中优先级队列也是在堆的基础上进行了一层封装。(from queue import PriorityQueue)
详情请看:
dyq666:日常编程中的python-优先级队列在使用前需要先搞清楚优先级队列的时间复杂度,往一个长度为n的优先级队列中插入一个元素时间复杂度是O(logn)。那么把n个元素插入一个空的优先级队列中时间复杂度就为O(nlogn)。
如果把n个元素插入一个空的优先级队列中,但是优先级队列的最大长度为k,那么时间复杂度就为O(nlogk)。
基于这个时间复杂度的计算方式,在这道题中可以构造出两种优先级队列。队列一保存所有频率最大的元素,也就是长度为k。队列二保存所有频率最小的元素,也就是长度为(n-k)。这两种方式哪种更好,也就取决于n与k大小之间的差距了。
题目分析:
先使用Counter统计频率,然后维护一个优先级队列,这个队列总共只能存k个元素( 当前频率最高的k各元素 ),也就是优先级越高代表他的频率越高。
遍历整个Counter,入队到k个元素。队满之后,每次都把队列中优先级最小的元素与当前元素对比,选择优先级大的一个。
遍历结束后,整个队列就是结果了。
在这里最重要的就是想明白你把什么当做优先级,因为每次出队的都是优先级最小的,我们不想要频率小的,所以我们让频率越小的优先级越低。
代码思路:
时间复杂度:O(nlogk)
1. 统计频率,counter算是一个dict,key是数,value是对应的频率
2. 如果k跟所有数相同就直接返回所有数
3. 创建优先队列,设置优先队列的最大长度(这里最大长度是k)。
4. 遍历所有数和它的频率。这里的优先级与频率相同。
5. 如果当前队列长度小于最大长度,直接入队
6. 如果等于最大长度了,并且当前的优先级(-freq)比队列中优先级最低的元素优先级高(第一个元素)。
那么把优先级最低的出队列,当前的入队列
7. 返回优先级队列中所有的数
代码实现:(注释中标明了代码思路中步骤的序号)
def topKFrequent02(self, nums, k):
# 步骤一
from collections import Counter
counter = Counter(nums)
len_counter = len(counter)
# 步骤二
if len_counter == k:
return list(counter.keys())
# 步骤三
from queue import PriorityQueue as PQ
pq, max_len = PQ(), k
# 步骤四
for num, freq in counter.items():
# 步骤五
if len(pq.queue) < max_len:
pq.put((freq, num))
# 步骤六
elif freq > pq.queue[0][0]:
pq.get()
pq.put((freq, num))
# 步骤七
return [p[-1] for p in pq.queue]
区别分析:
def topKFrequent03(self, nums, k):
from collections import Counter
counter = Counter(nums)
len_counter = len(counter)
if len_counter == k:
return list(counter.keys())
from queue import PriorityQueue as PQ
# 区别一
pq, max_len = PQ(), len_counter - k
# 区别二 优先级是-freq
for num, freq in counter.items():
if len(pq.queue) < max_len:
pq.put((-freq, num))
elif -freq > pq.queue[0][0]:
pq.get()
pq.put((-freq, num))
# 区别三 返回差集
return list(set(counter.keys()) - set(q[-1] for q in pq.queue))
题目链接
题目分析:
总共有n个降序的链表,意味着每个链表当前的头结点都是最小的,所以只需要维护一个大小为n的优先级队列,每次出队后,出队的内条链表的头结点,如果不是最后一个节点,就将它的下一个节点入队。
举个例子:[1->2->6, 2->3, 3->4->5]。第一步1,2,和3入队,然后1出队,1的下一个节点2入队。这种方式保证了最终合成的链表一定是排好序的,因为一条链表不能有两个节点同时在队列中,这是没有意义的,因为一条链表上前面节点的一定是更小的或者是相等。
基于上述的讲解,我们只需要把整个优先级队列都出完就结束了
代码思路:
1. queue_index的作用:假设两个元素的优先级相同,那么将会去比较元组中第二个值。
但是第二个值如果是一个类的实例,可能没重写比较的方法,所以需要一个可以比较的辅助变量来区分。(这部分一定要仔细阅读,或者尝试几个例子,或者去文章提到的优先级队列详情的文章中去看看)
2. 初始化队列,将所有链表的头结点入队
3. 创建最终结果链表的虚拟头结点
4. 每次从队列中获取优先级最小的元素,用needle去穿针引线
5. 如果该节点后面还有节点,就将后面的一个节点入队
代码实现:
def mergeKLists(self, lists):
from queue import PriorityQueue as PQ
pq = PQ()
# 步骤一
queue_index = 0
# 步骤二
for node in lists:
if node:
pq.put((node.val, queue_index, node))
queue_index += 1
# 步骤三
dummy = ListNode(None)
needle = dummy
# 步骤四
while pq.queue:
node = pq.get()[-1]
needle.next = node
needle = needle.next
# 步骤五
if node.next:
pq.put((node.next.val, queue_index, node.next))
queue_index += 1
return dummy.next