网上看了下很多堆排序的介绍,有些介绍很容易懂,自己再结合书上介绍的,网上没有的,以自己的理解,就当做个笔记
概要
- code语言:java、c
- 测试环境:win、java8
- 参考书籍:《数据结构与算法分析java语言描述》 原书第三版、《数据结构:使用C语言》、《算法导论》原书第三版
- 参考链接:堆排序详解、图解排序算法(三)之堆排序
概述
先概述几个概念
- 二叉树:只有两个子节点的数结构
- 叶子结点:等同于子节点的说法。它自己没有叶子结点的话那么它就是一个叶子结点
- 完全二叉树:如图1,构造二叉树时,底层的元素节点从左到右填入,这样得出来的结构就是完全二叉树。性质:
- 假如高度为h,那么h层若全满了,叫做满完全二叉树
- 假如高度为h,那么h层的节点都是叶子节点
- 假如高度为h,那么他的查找效率是O(logN)
- 完全二叉树很有规律,故可以用一个数组表示而不需要一个链。如图1二叉树用数组表示呈图2
- 用数组表示的完全二叉树:数组任意位置i上的元素,其左儿子在位置2i上,右儿子在2i+1位置上,父亲节点在i/2上。从本性质看,遍历该树所需要的操作极其简单,因为计算机的二进制操作非常快
8 | 6 | 7 | 5 | 4 | 3 |
---|
- 堆:堆这个名词书上说是由堆排序得来的。指的是一个结构分为最大堆、最小堆。可以脑补下土堆、金字塔的模样
- 最大堆:数据大的作为根节点这样构成的完全二叉树就是一个最大堆。白话理解就是大的在上头。图1刚好就是一个最大堆
- 最小堆:数据小的作为根节点这样构成的完全二叉树就是一个最小堆。白话理解就是小的在上头。
- 初始堆:进行堆排序前堆的初始状态,其实就是最大堆和最小堆的状态。
- 每个子树都应该是堆结构,但没有要求左子节点大于或小于右子节点
- 堆排序:通过构造最大堆,然后凭靠完全二叉树的优秀遍历效率依次读取叶子结点来完成排序的过程。
堆排序
完全二叉树的构造过程上述概述中已经描述过了,不多加废话了啊
几个重要的点
- 堆结构是一个完全二叉树的结构
- 进行堆排序首先要构造一个初始堆,即构造一个最大堆,(最小堆也是行的)
- 构造最大堆时,需要保证每个子树是一个最大堆,不符合就需底部向上开始调整
- 堆排序时,每次都是堆顶元素与最后一个未完成排序的元素交换。交换后会造成未进行排序的元素不满足堆的性质了,故需要调整。然后依次类推,最后完成排序
- 堆是数组的一个逻辑上的结构,所有数据仍然是在数组上操作的。
构造初始堆图解
- 构造初始堆,是从下往上调整成最大堆结构
原始数组顺序为:
3 | 6 | 4 | 5 | 7 | 8 |
---|
注:完全二叉树的结构是数组的逻辑结构,构造方式也如上概述描述,从根节点开始,都是从左到右一个一个填上叶子结点,叶子结点填满了就下一层。
最后该初始堆对应的数组情况是:
8 | 6 | 7 | 5 | 4 | 3 |
---|
堆排序图解
重要操作口令:
- 每次调整为堆结构后,都是根顶节点与最后一个未完成排序的节点交换
- 交换后造成不符合堆性质,需要从上往下,从左往右的顺序从新调整为堆结构
- 图解说明:虚线圈表示该子树需要调整为满足堆结构、红色节点表示需要调整为堆结构的节点、黄色节点表示排序时交换的节点、绿色节点表示完成排序的节点
调整后数组对应情况:
7 | 6 | 3 | 5 | 4 | 8 |
---|
调整后数组对应情况:
6 | 5 | 3 | 4 | 7 | 8 |
---|
调整后数组对应情况:
5 | 4 | 3 | 6 | 7 | 8 |
---|
调整后数组对应情况:
4 | 3 | 5 | 6 | 7 | 8 |
---|
调整后数组对应情况:
3 | 4 | 5 | 6 | 7 | 8 |
---|
至此完成排序!
代码实现(java)
package com.mym.practice.lock;
/**
* 堆排序测试类
*/
public class HeapSort {
/**
* 调整为堆结构:从最后一个子树的根节点位置开始调整
* @param arr 数组
* @param lastRootIndex 最后一个子树的根节点位置
* @param endIndex 本次初始化堆结束位置
*/
public static void adjustHeap(int[] arr, int lastRootIndex, int endIndex){
// 下标从1开始
if(arr.length < endIndex && lastRootIndex < 1){
return;
}
int leftNodeIndex;
int rightNodeIndex;
for(int i = lastRootIndex;i > 0;i--){
leftNodeIndex = i << 1;
rightNodeIndex = (i << 1) + 1;
if(rightNodeIndex > endIndex){
rightNodeIndex = i;
}
int temp = arr[i];
if(arr[rightNodeIndex] < arr[leftNodeIndex] && temp < arr[leftNodeIndex]){
arr[i] = arr[leftNodeIndex];
arr[leftNodeIndex] = temp;
}else if(arr[rightNodeIndex] > arr[leftNodeIndex] && arr[rightNodeIndex] > temp){
arr[i] = arr[rightNodeIndex];
arr[rightNodeIndex] = temp;
}
}
}
/**
* 初始化堆:遍历每个局部子树进行构造堆结构
* @param arr 数组
* @param endIndex 本次初始化堆结束位置
*/
private static void initHeap(int[] arr, int endIndex){
for(int i = 1; i < endIndex; i++){
if(i << 1 > endIndex){
break;
}
adjustHeap(arr, i, endIndex);
}
}
/**
* 打印一个数组
* @param arr 数组
*/
private static void print(int[] arr){
for(int i = 0; i < arr.length; i++){
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
// 注意:index=0位不用管,方便计算。真实需要排序的数从index=1开始
int[] arr = {0,3,6,4,5,7,8};
// 构造初始堆:最大堆
initHeap(arr, arr.length - 1);
System.out.println("初始堆:");
print(arr);
// 进行堆排序
for(int i = arr.length - 1; i > 1; i--){
// 1.最后一个未排序的与第一位交换
int temp = arr[i];
arr[i] = arr[1];
arr[1] = temp;
// 2.经过交换后需要重新调整为堆结构(已排序完的数据不参与)
initHeap(arr, i - 1);
}
System.out.println("排序结果:");
print(arr);
}
}
执行结果:
初始堆:
0 8 6 7 5 3 4
排序结果:
0 3 4 5 6 7 8
该实现类估计还可以再优化,后续会改进优化代码,有建议或问题指出,非常感谢
堆排序特点
- 堆排序在最坏的情况下,其时间复杂度也能达到O(nlogn),此时较快排要好。
- 堆排序最好情况为O(1)
- 堆排序仅需要一个记录大小供交换用的辅助存储空间,即空间复杂度为O(1)。
- 堆排序一般都会舍弃0下标。
- 堆排序基础数据结构是完全二叉树的一个二叉堆,父节点大于两个子节点,子节点之间没有大小顺序要求
- 二叉堆i位置上的左子节点位置为2i,右子节点位置为2i+1,父节点为(1/2)*i
- 堆排序不是稳定性的排序方案
- 整个排序过程中主要时间消耗在交换上
堆排序应用场景
- 优先级队列(下个文章将会探讨)
- 对空间复杂度有要求的场景
- 等等等。。。