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


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

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

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

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

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

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

    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());
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    测试代码:

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

    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(http://www.my516.com));
    }
    return res;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    测试代码:

    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);
    1
    2
    3

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

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

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

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

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

    如果一个数据集不能放入一个程序的内存,那么运行时间通常会大大增加,或者根本不能运行。如果你选择一个需要较少空间的算法,并且这样可以将计算放入内存中,则可能会运行得更快。同样,使用较少空间的程序,可能会更好地利用 CPU 缓存并运行速度更快。
    在同时运行多个程序的服务器上,如果可以减少每个程序所需的空间,则可以在同一台服务器上运行更多程序,从而降低硬件和能源成本。
    ---------------------

  • 相关阅读:
    利用Trace.WriteLine定位难以重现的问题
    技术经验分享
    辞职小记
    残阳如血--读《忆秦娥·娄山关》 有感
    一个简单多线程等待窗口
    [转]Control的Invoke和BeginInvoke
    elk 改为使用 ik 中文分词器
    在 jenkins 的 pipeline 中使用分支参数
    centos8 下删除网桥 docker0
    vscode 实现组件之间的跳转
  • 原文地址:https://www.cnblogs.com/ly570/p/11106210.html
Copyright © 2020-2023  润新知