leetcode-python-优先级队列与时间复杂度

leetcode-python-专栏目录

专题概述

目录

代码相关

  1. 所有代码在leetcode英文网站上都通过了测试。
  2. github如下,本专题代码在code/priority-in-queue中。
dyq666/leetcode-pythongithub.comleetcode-python-优先级队列与时间复杂度_第1张图片

最后

如果觉得本文对你有帮助,为我收藏点赞,若文中有任何问题(哪步算法没看懂,或者涉及到的python语法不了解,或者哪里出错了)可在评论区留言。



本文的内容

本文围绕leetcode-347展开,先用最少量的代码(collections.Counter)解决问题,然后根据Counter的源码找到该算法的时间复杂度。由此引入优先级队列,介绍python中优先级队列的用法,最后用两个不同的优先级队列解决本题。

熟悉了优先队列之后,通过leetcode-23来实战。


Leetcode347号问题-前K个高频元素

题目链接


代码思路:

  1. 初始化Counter
  2. 使用most_common返回前k个元素


代码实现:

def topKFrequent(self, nums, k):
    from collections import Counter

    return [c[0] for c in Counter(nums).most_common(k)]

虽然本道题目完成了,但是我们想深究下内部的时间复杂度。


most_common()源码分析

我们发现了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中的优先级队列

在python中优先级队列也是在堆的基础上进行了一层封装。(from queue import PriorityQueue)

详情请看:

dyq666:日常编程中的python-优先级队列zhuanlan.zhihu.com


使用优先级队列

在使用前需要先搞清楚优先级队列的时间复杂度,往一个长度为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]

(二)优先级队列保存频率小的元素

区别分析:

  1. 队列最大长度为len_counter - k
  2. 优先级的定义不同,这里我们希望让频率高的元素先从队列中出队,也就是需要频率高的元素优先级低,所以频率和优先级是负相关的,优先级=-频率
  3. 返回值我们需要取counter中所有数与队列的数的差集(整个集合-频率小的内部分)
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))

Leetcode23号问题-合并K个排序链表

题目链接


题目分析:

总共有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


总结

  1. 优先级队列的长度会影响时间复杂度
  2. 使用优先级队列要明确设置哪个值为优先级

你可能感兴趣的:(面试题)