排序算法(三)堆排序及有界堆排序Java实现及分析


1.堆排序

基数排序适用于大小有界的东西,除了他之外,还有一种你可能遇到的其它专用排序算法:有界堆排序。如果你在处理非常大的数据集,你想要得到前 10 个或者前k个元素,其中k远小于n,它是很有用的。

例如,假设你正在监视一 个Web 服务,它每天处理十亿次事务。在每一天结束时,你要汇报最大的k个事务(或最慢的,或者其它最 xx 的)。一个选项是存储所有事务,在一天结束时对它们进行排序,然后选择最大的k个。需要的时间与nlogn成正比,这非常慢,因为我们可能无法将十亿次交易记录在单个程序的内存中。我们必须使用“外部”排序算法。

我们首先了解一下堆,这是一个类似于二叉搜索树(BST)的数据结构。有一些区别:

  • 在 BST 中,每个节点x都有“BST 特性”:x左子树中的所有节点都小于x,右子树中的所有节点都大于x
  • 在堆中,每个节点x都有“堆特性”:两个子树中的所有节点都大于x
  • 堆就像平衡的 BST;当你添加或删除元素时,他们会做一些额外的工作来重新使树平衡。因此,可以使用元素的数组来有效地实现它们。

现在讨论的是小根堆。如果子树中的节点都小于根节点,则为大根堆。

堆中最小的元素总是在根节点,所以我们可以在常数时间内找到它。在堆中添加和删除元素需要的时间与树的高度h成正比。而且由于堆总是平衡的,所以hlog n成正比。

JavaPriorityQueue使用堆实现。PriorityQueue提供Queue接口中指定的方法,包括offerpoll

  • offer:将一个元素添加到队列中,更新堆,使每个节点都具有“堆特性”。需要logn的时间。
  • poll:从根节点中删除队列中的最小元素,并更新堆。需要logn的时间。

给定一个PriorityQueue,你可以像这样轻松地排序的n个元素的集合 :

  • 使用offer,将集合的所有元素添加到PriorityQueue
  • 使用poll从队列中删除元素并将其添加到List

因为poll返回队列中剩余的最小元素,所以元素按升序添加到List。这种排序方式称为堆排序 。

向队列中添加n个元素需要nlogn的时间。删除n个元素也是如此。所以堆排序的运行时间是O(n logn)



2.代码实现:
	/**
     * @Author Ragty
     * @Description 堆排序
     * @Date 19:15 2019/6/12
     **/
    public void heapSort(List<T> list,Comparator<T> comparator) {
        PriorityQueue<T> heap = new PriorityQueue<T>(list.size(),comparator);
        heap.addAll(list);
        list.clear();
        while(!heap.isEmpty()) {
            list.add(heap.poll());
        }
    }

测试代码:

list = new ArrayList<Integer>(Arrays.asList(3, 5, 1, 4, 2));
sorter.heapSort(list, comparator);
System.out.println(list);



3.有界堆排序

有界堆是一个限制为最多包含k个元素的堆。如果你有n个元素,你可以跟踪这个最大的k个元素:

最初堆是空的。对于每个元素x

  • 分支 1:如果堆不满,请添加x到堆中。
  • 分支 2:如果堆满了,请与堆中x的最小元素进行比较。如果x较小,它不能是最大的k个元素之一,所以你可以丢弃它。
  • 分支 3:如果堆满了,并且x大于堆中的最小元素,请从堆中删除最小的元素并添加x

使用顶部为最小元素的堆,我们可以跟踪最大的k个元素。我们来分析这个算法的性能。对于每个元素,我们执行以下操作之一:

  • 分支 1:将元素添加到堆是O(log k)
  • 分支 2:找到堆中最小的元素是O(1)
  • 分支 3:删除最小元素是O(log k)。添加x也是O(log k)

在最坏的情况下,如果元素按升序出现,我们总是执行分支 3。在这种情况下,处理n个元素的总时间是O(n log k),对于n是线性的。


4.代码实现:
	/**
     * @Author Ragty
     * @Description 有界堆排序
     * @Date 19:49 2019/6/12
     **/
    public List<T> topK(int k,List<T> list,Comparator<T> comparator) {
        PriorityQueue<T> heap = new PriorityQueue<T>(list.size(),comparator);
        for (T element : list) {
            if (heap.size() < k) {
                heap.offer(element);
                continue;
            }
            int cmp = comparator.compare(element,heap.peek());
            if (cmp>0) {
                heap.poll();
                heap.offer(element);
            }
        }
        List<T> res = new LinkedList<T>();
        while (!heap.isEmpty()) {
            res.add(heap.poll());
        }
        return res;
    }

测试代码:

list = new ArrayList<Integer>(Arrays.asList(6, 3, 5, 8, 1, 4, 2, 7));
List<Integer> queue = sorter.topK(4, list, comparator);
System.out.println(queue);



5.空间复杂性

到目前为止,我们已经谈到了很多运行时间的分析,但是对于许多算法,我们也关心空间。例如,归并排序的一个缺点是它会复制数据。在我们的实现中,它分配的空间总量是O(n log n)。通过优化,可以将空间降至O(n)

相比之下,插入排序不会复制数据,因为它会原地排序元素。它使用临时变量来一次性比较两个元素,并使用一些其它局部变量。但它的空间使用不取决于n

我们的堆排序实现创建了新PriorityQueue,来存储元素,所以空间是O(n); 但是如果你能够原地对列表排序,则可以使用O(1)的空间执行堆排序 。

刚刚实现的有界堆栈算法的一个好处是,它只需要与k成正比的空间(我们要保留的元素的数量),而k通常比n小得多 。

软件开发人员往往比空间更加注重运行时间,对于许多应用程序来说,这是适当的。但是对于大型数据集,空间可能同等或更加重要。例如:

  • 如果一个数据集不能放入一个程序的内存,那么运行时间通常会大大增加,或者根本不能运行。如果你选择一个需要较少空间的算法,并且这样可以将计算放入内存中,则可能会运行得更快。同样,使用较少空间的程序,可能会更好地利用 CPU 缓存并运行速度更快。
  • 在同时运行多个程序的服务器上,如果可以减少每个程序所需的空间,则可以在同一台服务器上运行更多程序,从而降低硬件和能源成本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章