详解Java 堆排序

本文我们学习Java中如何实现堆排序。堆排序基于堆数据结构,为了更好地理解堆排序,我们首先深入堆数据结构及其实现。

1. 堆数据结构

堆是特殊的基于树的数据结构。因此其由节点组成,给节点赋值元素,每个节点恰好包括一个元素。节点能有子节点,没有任何子节点的节点为叶子节点。堆的特殊之处有两点规则:

  1. 每个节点值必须小于等于所有子节点的值
  2. 堆是完全树,即其高度最小

有第一条规则得到最小元素总是树的根节点。如何实现这些规则取决于实现。
堆通常用于实现优先队列,因为抽取最小或最多元素的实现非常有效。

1.1. 堆变体

堆有很多变体,主要差异体现在实现细节上。例如上面描述的最小堆,父节点总是小于其所有子节点。同样,我们也能定义最大堆,即父节点总是大于其子节点,因此最大元素是根节点。

可以选择多种树的实现,最直接是二叉树。二叉树每个节点最多有两个子节点,称为左节点和右节点。

执行第二条规则最简单方式是使用完全二叉树。完全二叉树符合下面简单规则:

  1. 如果节点有一个子节点,应该为左子节点
  2. 只有最底层最右边的节点可以有一个子节点
  3. 叶子节点只能在最深级

下面看一些示例检验这些规则:


1        2      3        4        5        6         7         8        9       10
()       ()     ()       ()       ()       ()        ()        ()       ()       ()/         \     /  \     /  \     /  \      /  \      /        /        /  \()         ()   ()  ()   ()  ()   ()  ()    ()  ()    ()       ()       ()  ()/          \       /  \      /  \     /        /  \()          ()     ()  ()    ()  ()   ()       ()  ()/()

1、2、4、5、7符合以上规则。
3和6违反第一条规则,8和9违反第二条规则,10不符合第三条规则。接下来主要聚集基于二叉树实现最小堆。

1.2. 插入元素

应该基于一种方式实现插入操作,始终保持堆不变。基于此通过重复执行插入操作构件堆。所以我们看单个插入操作。插入一个元素的步骤为:

  1. 创建一个叶子节点作为树最深级上最右边节点并存储元素至节点中
  2. 如何元素小于父节点,则交换两个节点值
  3. 继续第二步,直到元素小于其父节点或该节点称为新的根节点

注意,第二步不违反堆规则,因为如果使用最小值交换,直到父节点成为所有子节点的最小值。请看示例,插入4至堆中:

2/ \/   \3     6/ \
5   7

第一步创建叶子节点存储4:

     2/ \/   \3     6/ \   /
5   7 4

4小于父节点6,需要交换:

     2/ \/   \3     4/ \   /
5   7 6

现在检测4是否小于其父节点,父节点为2,停止交换插入完毕,为有效堆。下面插入1:

     2/ \/   \3     4/ \   / \
5   7 6   1

交换1和4:

     2/ \/   \3     1/ \   / \
5   7 6   4

继续交换1和2:

     1/ \/   \3     2/ \   / \
5   7 6   4

1为新的根节点,插入完成。

2. Java实现堆

我们使用完全二叉树,可以使用数组实现。数组中的元素作为树节点。使用数组索引从左到右标记每个节点,从顶到底遵循下列方式:

     0/ \/   \1     2/ \   /
3   4 5

我们唯一需要做的是跟踪我们在树中存储了多少元素。这样,我们要插入的下一个元素的索引将是数组的大小。使用索引可以计算父节点和子节点的索引:

  • 父节点: (index – 1) / 2
  • 左子节点: 2 * index + 1
  • 右子节点: 2 * index + 2

如果不想总是重建数组,可以简化实现直接使用ArrayList。基于二叉树实现如下:

class BinaryTree<E> {List<E> elements = new ArrayList<>();void add(E e) {elements.add(e);}boolean isEmpty() {return elements.isEmpty();}E elementAt(int index) {return elements.get(index);}int parentIndex(int index) {return (index - 1) / 2;}int leftChildIndex(int index) {return 2 * index + 1;}int rightChildIndex(int index) {return 2 * index + 2;}}

上面代码仅增加元素至树末尾。因此如果需要向上遍历新元素,使用下面代码实现:

class Heap<E extends Comparable<E>> {// ...void add(E e) {elements.add(e);int elementIndex = elements.size() - 1;while (!isRoot(elementIndex) && !isCorrectChild(elementIndex)) {int parentIndex = parentIndex(elementIndex);swap(elementIndex, parentIndex);elementIndex = parentIndex;}}boolean isRoot(int index) {return index == 0;}boolean isCorrectChild(int index) {return isCorrect(parentIndex(index), index);}boolean isCorrect(int parentIndex, int childIndex) {if (!isValidIndex(parentIndex) || !isValidIndex(childIndex)) {return true;}return elementAt(parentIndex).compareTo(elementAt(childIndex)) < 0;}boolean isValidIndex(int index) {return index < elements.size();}void swap(int index1, int index2) {E element1 = elementAt(index1);E element2 = elementAt(index2);elements.set(index1, element2);elements.set(index2, element1);}// ...}

需要对元素进行比较,故实现java.util.Comparable接口。

3. 堆排序

因为堆根元素总是最小元素,堆排序实现思路非常简单:删除根节点直到堆为空。

我们唯一需要实现的是删除操作,需始终保持堆的状态一致,确保不违反二叉树的结构,即堆的特性。

为了保持结构,每次删除根节点元素,并存储最右边的叶子节点至根节点。但该操作很可能违反堆属性,因为如果新根节点大于任何子节点,需要和最小子节点进行交换。最小子节点是所有其他子节点中最小的,所有不会违反堆属性。
一直交换直到元素成为叶子节点或小于所有子节点。请看示例:

     1/ \/   \3     2/ \   / \
5   7 6   4

放最后元素在根节点上:

     4/ \/   \3     2/ \   /
5   7 6

4大于两个子节点,需要和两者最小的进行交换,即2:

     2/ \/   \3     4/ \   /
5   7 6

4小于6,操作结束。

3.1. 堆排序实现

利用前节代码,删除根节点方法(pop)如下:

class Heap<E extends Comparable<E>> {// ...E pop() {if (isEmpty()) {throw new IllegalStateException("You cannot pop from an empty heap");}E result = elementAt(0);int lasElementIndex = elements.size() - 1;swap(0, lasElementIndex);elements.remove(lasElementIndex);int elementIndex = 0;while (!isLeaf(elementIndex) && !isCorrectParent(elementIndex)) {int smallerChildIndex = smallerChildIndex(elementIndex);swap(elementIndex, smallerChildIndex);elementIndex = smallerChildIndex;}return result;}boolean isLeaf(int index) {return !isValidIndex(leftChildIndex(index));}boolean isCorrectParent(int index) {return isCorrect(index, leftChildIndex(index)) && isCorrect(index, rightChildIndex(index));}int smallerChildIndex(int index) {int leftChildIndex = leftChildIndex(index);int rightChildIndex = rightChildIndex(index);if (!isValidIndex(rightChildIndex)) {return leftChildIndex;}if (elementAt(leftChildIndex).compareTo(elementAt(rightChildIndex)) < 0) {return leftChildIndex;}return rightChildIndex;}// ...}

如前面分析,排序是通过不断删除根节点从新创建一个堆:

class Heap<E extends Comparable<E>> {// ...static <E extends Comparable<E>> List<E> sort(Iterable<E> elements) {Heap<E> heap = of(elements);List<E> result = new ArrayList<>();while (!heap.isEmpty()) {result.add(heap.pop());}return result;}static <E extends Comparable<E>> Heap<E> of(Iterable<E> elements) {Heap<E> result = new Heap<>();for (E element : elements) {result.add(element);}return result;}// ...}

我们能测试是否正确:

@Test
void givenNotEmptyIterable_whenSortCalled_thenItShouldReturnElementsInSortedList() {// givenList<Integer> elements = Arrays.asList(3, 5, 1, 4, 2);// whenList<Integer> sortedElements = Heap.sort(elements);// thenassertThat(sortedElements).isEqualTo(Arrays.asList(1, 2, 3, 4, 5));
}

请注意,我们可以提供另一个就地排序实现,这意味着只用一个数组获得结果。通过这种方式不需要任何中间内存分配。但这种实现可能比较难以理解。

3.2. 时间复杂度

堆排序由两个关键步骤,插入元素和删除根节点。两个步骤的实际复杂度都为O(log n)。因为需要重复两个步骤n次,整个排序复杂度为O(n log n)。

我们没有提到数组重新分配的成本,但是因为它是O(n),所以它不会影响整体的复杂性。另外,正如我们前面提到的,可以实现就地排序,这意味着不需要重新分配数组。同样值得一提的是,50%的元素是叶子,75%的元素位于两个最下面的层次。因此,大多数插入操作只需要两个步骤。

注意,在实际数据中,快速排序通常比堆排序性能更好。最坏情况下,堆排序的时间复杂度总是O(n log n)。

4. 总结

本文我们介绍了堆数据结构及其实现。因为其时间复杂度为O(n log n),实际上并不是最好的排序算法,但在优先队列场景中经常使用。

详解Java 堆排序相关推荐

  1. java 引用传递_详解java的值传递、地址传递、引用传递

    详解java的值传递.地址传递.引用传递 一直来觉得对值传递和地址传递了解的很清楚,刚才在开源中国上看到一篇帖子介绍了java中的值传递和地址传递,看完后感受颇深.下边总结下以便更容易理解. 按照以前 ...

  2. 异常处理器详解 Java多线程异常处理机制 多线程中篇(四)

    在Thread中有异常处理器相关的方法 在ThreadGroup中也有相关的异常处理方法 示例 未检查异常 对于未检查异常,将会直接宕掉,主线程则继续运行,程序会继续运行 在主线程中能不能捕获呢? 我 ...

  3. 详解Java解析XML的四种方法

    http://developer.51cto.com  2009-03-31 13:12  cnlw1985  javaeye  我要评论(8) XML现在已经成为一种通用的数据交换格式,平台的无关性 ...

  4. java使用集合存储过程_详解java调用存储过程并封装成map

    详解java调用存储过程并封装成map 发布于 2020-5-1| 复制链接 摘记: 详解java调用存储过程并封装成map           本文代码中注释写的比较清楚不在单独说明,希望能帮助到大 ...

  5. java同步异步调用_详解java 三种调用机制(同步、回调、异步)

    1:同步调用:一种阻塞式调用,调用方要等待对方执行完毕才返回,jsPwwCe它是一种单向调用 2:回调:一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口: 3:异步调用:一种类似消 ...

  6. java 死锁 内存消耗_详解Java中synchronized关键字的死锁和内存占用问题

    先看一段synchronized 的详解: synchronized 是 java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并 ...

  7. java lock unlock_详解Java中的ReentrantLock锁

    ReentrantLock锁 ReentrantLock是Java中常用的锁,属于乐观锁类型,多线程并发情况下.能保证共享数据安全性,线程间有序性 ReentrantLock通过原子操作和阻塞实现锁原 ...

  8. java 线程一直运行状态_详解JAVA 线程-线程的状态有哪些?它是如何工作的?

    线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在. 一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源.更加轻量化,也因 ...

  9. java斐波那契查找_详解Java Fibonacci Search斐波那契搜索算法代码实现

    一, 斐波那契搜索算法简述 斐波那契搜索(Fibonacci search) ,又称斐波那契查找,是区间中单峰函数的搜索技术. 斐波那契搜索采用分而治之的方法,其中我们按照斐波那契数列对元素进行不均等 ...

  10. 图文详解Java环境变量配置方法

    今天动力节点java学院小编为大家介绍"图文详解Java环境变量配置方法",希望对各位小伙伴有帮助,下面就和小编一起来看看Java环境变量配置方法吧. 首先是要安装JDK,JDK安 ...

最新文章

  1. python读取中文txt文本-python读取中文txt文本的方法
  2. IOS开发-地图 (mapkit)实验
  3. Java最大的优势真的在于跨平台吗?
  4. spring知识概要
  5. Ambari2.7.4+HDP3.1.4.0中配置fair-scheduler
  6. 46 道阿里巴巴 Java 面试题,你会几道?
  7. Java中的Error和Exceptiond的异同点
  8. java数组使用实验报告_JAVA数组与类的定义-java实验报告
  9. 知道IP地址如何查看主机名和MAC(网卡)地址
  10. Educoder 机器学习 神经网络 第四关:使用pytorch搭建卷积神经网络识别手写数字
  11. Regularized least-squares classification(RLSC)
  12. java反汇编_Java虚拟机学习总结(3)——JDK内置工具(jps、jstack、jmap、jstat)使用详解...
  13. C# WinForm技巧“将Form嵌入到Panel”
  14. Xshell、Xftp免费版获取教程
  15. 【领域建模】UML类图工具推荐
  16. 浏览器中使用Github
  17. oc引导windows蓝屏_使用Opencore引导ubuntu以及Linux的步骤
  18. java 方法详解(有参无参,有返回值无返回值) 局部变量
  19. SASS _ 入门版
  20. VOC数据集mAP计算(附带代码)解析

热门文章

  1. 关于举办2008年注册电气工程师执业资格考试供配电专业(基础)
  2. 30款硬盘有问题 细看希捷固件门事件.
  3. 冰点还原离线激活_冰点还原精灵密钥,小编教你如何激活冰点还原精灵
  4. html网页自动提示框代码,多种网页弹出窗口代码
  5. 梦幻家园前37关(iPad)
  6. 3dmax9.0 简体中文正式版(官方非汉化版本)下载网址
  7. ubuntu16.04安装rabbitVCS
  8. Photoshop平面设计:网页设计之论摹仿和抄袭——xiame.com
  9. Unity针对文件进行MD5码校验
  10. MongodB数据库安装教程