K-最大元素算法比较

时间:2018-02-03 07:25:48

标签: python algorithm time-complexity heap

我试图从大数组中提取k个最大元素,并比较以下2个算法

  1. Min Heap方法。 T(n) = k + nlogk
  2. 排序方法。 T(n) = nlogn
  3. 但由于某种原因,算法2的表现更好。有人可以解释原因吗

    import math
    
    
    class MinHeap:
        def __init__(self, arr):
            self.__items = arr
            self.build_heap()
    
        def size(self):
            return len(self.__items)
    def items(self):
        return self.__items
    
    def build_heap(self):
        for node_number in xrange(int(len(self.__items) / 2), 0, -1):
            self.heapify(node_number)
    
    def heapify(self, node_number):
        # return if leave node
        if node_number > int(len(self.__items) / 2):
            return
    
        node = self.__items[node_number-1]
        left_child = self.__items[(2 * node_number)-1] if (((2 * node_number)-1) < len(self.__items)) else None 
    
        right_child = self.__items[(2 * node_number + 1)-1] if (((2 * node_number + 1)-1) < len(self.__items)) else None        
    
        min_node = node
    
        if left_child != None and right_child != None:
            min_node = min(node, left_child, right_child)
        elif left_child != None :
            min_node = min(node, left_child)
        elif right_child != None :
            min_node = min(node, right_child)
    
        if min_node == node:
            return
        elif left_child!=None and min_node == left_child:
    
            self.__items[node_number - 1], self.__items[(2 * node_number)-1] = self.__items[(2 * node_number)-1], self.__items[node_number - 1]
            self.heapify(2 * node_number)
        elif right_child!=None and min_node == right_child:
    
            self.__items[node_number - 1], self.__items[(2 * node_number + 1)-1] = self.__items[(2 * node_number + 1)-1], self.__items[node_number - 1]
            self.heapify(2 * node_number + 1)
    
    def extract_min(self):
        length = len(self.__items)
        if length == 0:
            return
        self.__items[0], self.__items[length-1] = self.__items[length-1], self.__items[0]
        min_element =  self.__items.pop()
        self.heapify(1);
        return min_element
    
    def insert(self, num):
        self.__items.append(num)
        current_node = len(self.__items)
        parent_node = int(current_node / 2)
        while current_node > 1:
            min_node = min(self.__items[current_node-1], self.__items[parent_node-1])
            if min_node == self.__items[parent_node-1]:
                break
            self.__items[current_node-1], self.__items[parent_node-1] = self.__items[parent_node-1], self.__items[current_node-1]
            current_node = parent_node
            parent_node = int(current_node / 2)
    
    # Comparing Algorithms ::::::::::::::::::
    import time
    import random
    from min_heap import *
    
    numbers = random.sample(range(100000), 50000)
    
    k = 3
    n = len(numbers)
    
    # k Largest element using Heap T(n) = k + nlogk
    start = time.time()
    my_heap = MinHeap(numbers[:k+1])
    
    for number in numbers[k+1:]:
        my_heap.extract_min()
        my_heap.insert(number)
    
    data =  sorted(my_heap.items(),reverse=True)
    print data[:len(data)-1]
    end = time.time()
    print "Took {} seconds".format(end-start)
    
    
    # k Largest element using sorting T(n) = nlogn
    start = time.time()
    sorted_arr = sorted(numbers, reverse=True)
    print sorted_arr[:k]
    end = time.time()
    print "Took {} seconds".format(end-start)
    

    这是我得到的输出:

    算法1

    [99999, 99998, 99997]
    Took 0.15064406395 seconds
    

    算法2

    [99999, 99998, 99997]
    Took 0.0120780467987 seconds
    

2 个答案:

答案 0 :(得分:0)

答案分为两部分:

  1. 实践方面:

    调用用户定义的函数通常很慢,不断增长的列表需要重新分配内存等。“sort() + [:k]”方法中没有一个重要。如果作者不注意内存管理的细节,Python代码可能会很慢。因此,您要测量算法中的实施差异和差异。

  2. 理论方面:

    我不知道您是如何为{min}方法提出T(n) = k + nlogk的,但我看到您呼叫my_heap.insert()my_heap.extract_min()(触发heapify())几乎{ {1}}次。两者都是n中的对数,因此它看起来更像n

  3. 以下是探查器运行的输出:

    nlog(n)

答案 1 :(得分:0)

理论界限是:

  • 排序:O(n log n)
  • 堆选择:O(n log k)
  • quick select:O(n)

堆选择方法的一个问题是您要创建整个项目集的堆。你真的只需要创建前k个项目的堆。算法是:

h = create a min heap of first k items // O(k)
for each remaining item
    if item is larger than smallest item on heap
        remove smallest item from heap
        add new item to heap

该算法的最坏情况是O(n log k)。这个想法是,在最坏的情况下,你最终会从大小为k的堆中进行n次插入和n次删除。在一般情况下,插入和删除的数量相当小。它取决于k与n的关系。如果k远小于n,则此方法可以非常快。

在你的代码中,你创建了前k个项目的最小堆,然后你就有了这个循环:

for number in numbers[k+1:]:
    my_heap.extract_min()
    my_heap.insert(number)

该代码无法可靠地生成正确的结果,因为您无条件地删除了最小元素。因此,如果堆已经包含前k个元素,但仍然有数字,则它将用较小的元素替换较大的元素。您需要修改代码:

for number in numbers[k+1:]:
    if (number < my_heap.get_min())  // get_min returns the minimum element, but doesn't remove it
        my_heap.extract_min()
        my_heap.insert(number)

这不仅可以为您提供正确答案,还可以减少您在平均情况下必须进行的插入和删除次数,这将加快您的计划。

您的堆实现也不是最佳的。您将节省大量时间使heapify函数迭代而不是递归,并且您有充分的机会优化该函数。

如上所述,快速选择是一种O(n)算法。通常,当k大于n的约2%时,快速选择更快。因此,如果您从1,000个列表中选择100个项目,快速选择几乎肯定会更快。如果您从1,000,000的列表中选择100,那么堆选择方法应该更快。

排序不应该比快速选择更快,并且它不应该比堆选择快,除非你选择一个非常大的百分比(超过60%,我期望)项目

几年前我对此做了一些相当详细的分析。请参阅我的博客文章When theory meets practice