堆排序
快有一个月没有写博客了,还是得把这些东西好好梳理一下,今天写下堆排序的相关内容
堆排序和快速排序相比,可能相对会复杂一些,不那么容易理解
先给出结论,堆排序和快速排序一样,平均的时间复杂度都是o(N * logN),但是常数项的大小一般要大过快速排序,因此快速排序的速度通常比堆排序更快
一 堆结构
在开始了解堆排序前,首先需要知道的是,这种算法是基于堆的数据结构设计而来的一种排序算法
so,啥是堆咧?
堆是一种具备特殊性质的完全二叉树! 那啥又是二叉树,啥又是完全二叉树??
1.二叉树
- 二叉树其实指的就是子节点最多两个的一种树形结构,简单的来看有以下几种
- 层级多了之后 ,二叉树就有可能演变成这样
- 简单的说,一个完全二叉树就是从上向下,从左向右依次挂载成一棵二叉树的样子,中间不能缺失任何一个,但是不要求是满二叉树,即叶子节点只存在于层级为max和max-1的层次上
2.大根堆 & 小根堆
大/小根堆是在完全二叉树的基础上,引入了一些对数据的要求:
若每一个根节点都大于等于它的左右孩子节点,就认为这是一个大根堆结构
若每一个根节点都小于等于它的左右孩子节点,就认为这是一个小根堆结构
3.程序中的大根堆表示
因为堆排序用到的是大根堆结构,后面不再讲小根堆
在大根堆的结构上,在程序中可以用两个基本数据结构来表现
一是使用链表,不过这样操作指针过于麻烦
二是使用数组,因为它是一种完全二叉树的结构,只要约定好数组与根位置的约定,即可实现数组—完全二叉树节点的映射
4.堆结构中,每一个小二叉树的节点间的关系
0为根,1,2为子, (1+1)/2=(0+1) , (2+1)/2=(0+1) 即(子节点序号+1)/2 = 根节点序号+1
1为根,3,4为子,(3+1)/2=(1+1),(4+1)/2=(1+1) 即(子节点序号+1)/2 = 根节点序号+1
2为根,5,6为子,(5+1)/2 =(2+1),(6+1)/2+(2+1) 即(子节点序号+1)/2 = 根节点序号+1
3为根,7,8为子,(7+1)/2=(3+1),(8+1)/2=(3+1) 即(子节点序号+1)/2 = 根节点序号+1
注:以上算法均为int类型的计算,因此小数位会舍去
- 因此可以得出两个结论
- 任意一个给定的根节点,它的左子节点序号一定是 (自身序号+1)*2 -1 ,右子节点序号一定是(自身序号+1)*2
- 对于任意一个给定的子节点, 根节点的序号一定是 (自身序号+1)/2
二 如何在现有的大根堆结构上插入一个数之后,还可以保持大根堆结构
- 在将数据向堆内添加的时候,保证每一个数进来,都是一个大根堆结构,就能保证最终获得的一定是大根堆
- OK,先假设我们已经有了一个大根堆结构,就像上面那样,那么新来一个数后,如何计算才能保证调整后的结构一定是大根堆呢?
- 假设我们的新值为80,比赛正式开始
- 直接找到自己父亲4号位,和它进行比较,如果小于等于父亲,就不必再动了. 如果比父亲大,直接和父亲交换
- 此时继续刚才的逻辑,它在4号位,再向父亲发起挑战,发现自己还是比父亲大,再交换
- OK,继续,再和父亲干一架,发现父亲比自己厉害,那就找到自己的位置了-----1号位
总结一下,如果想在原大根堆结构中保持大根堆结构,只需要按以下步骤来:
1.找自己父亲PK,只要比自己父亲大就交换
2.交换之后就继续按1步骤进行比较,直到没有父节点,或者父节点比自己大就结束
/**
* @param heap 现成的大根堆结构
* @param currentLength 新来的值的索引位置
*/
private void heapInsert(int[] heap, int currentLength) {
//和父节点PK
while (heap[currentLength] > heap[(currentLength - 1) / 2]) {
//大于父亲就交换
swap(heap, currentLength, (currentLength - 1) / 2);
//交换索引位置
currentLength = (currentLength - 1) / 2;
}
}
三 如何在移除了最大的根节点之后,让余下的数组织成大根堆
- 以上面的大根堆为例子,先看下初始状态
-
还是以上面那个大根堆为例,将根节点0号位和最后一个数44交换,表示移除了最大值
-
这时候因为99已经确定是最大值,放到了队尾,因此10号索引已经不需要参与排序,接下来只要处理0~9即可,从0号位开始,算出自己的左节点的位置为(0+1)*2 -1 =1,先看下有没有右节点,如果有右节点,和右边比较一下,看看左右两个节点谁比较大,返回谁的索引,如果没有右节点,返回左节点索引,对于上例来讲,2号索引位比较大,将0位与2位进行交换
-
再次2号位当成根节点,找出左节点 (2+1)*2 -1 = 5 ,5号位和6号位比较,发现5号位大,则由5号位和2号位PK,2号位再次落败,交换
-
此时发现最大的86已经又到最上方了,再快速跑一轮,将0号位和9号位交换
-
1,2号位,1号最大,0号和1号交换
-
3,4号位PK,4号获胜,1,4号位PK,4号获胜,交换
-
此时的最大值80已经来到了0号位置,后面再将0~8号交换,因此能看出来,每一次的几步比较之后,就可以让当前的最大值来到根节点上,将最大值与当前的数组最后一个值进行交换,同时缩小数组范围,就可以不断的让每一次数组内的最大值放到当前数组的尾巴上,从而达到升序排序的结果
总结一下:
- 将根节点同现有大根堆最后一位进行交换
- 再从根节点出发,找到自己的左节点,如果找不到,则比较结束
- 如果找到了,再看下自己和右节点谁比较大(如果右节点不存在,认为左节点大),拿大的和父亲比较
- 如果父亲没有子节点中的最大值大,那么父节点和子节点中较大的交换,如果父亲大于子节点中较大值,比较结束
- 交换完成后,继续从2进行下一轮比较
/**
* 让数组从0-R范围组织成大根堆
* @param heap 已经将最大值和大根堆尾部交换过的堆结构
* @param index 交换后根的索引
* @param heapSize 堆的大小
*/
private void heapify(int[] heap, int index, int heapSize) {
//找到左节点索引
int leftIndex = index * 2 + 1;
//左节点索引如果越界了说明没有左节点了
while (leftIndex < heapSize) {
//右节点(不一定存在)
int rightIndex = leftIndex + 1;
//判断左右节点谁更大,返回对应的索引(右节点不存在的情况下以左节点为准)
int largerIndex = rightIndex < heapSize && heap[rightIndex] > heap[leftIndex] ? rightIndex : leftIndex;
//判断孩子中较大的值和根节点相比,哪个大
int compareWithRoot = heap[index] > heap[largerIndex] ? index : largerIndex;
//如果根节点大于自己的孩子,就停止执行
if (compareWithRoot == index) {
break;
}
//将较大的值上提
swap(heap, index, largerIndex);
//走到这里,说明根节点是比较小的,因此交换后,largerIndex里就是原来的根节点
index = largerIndex;
//计算新的左节点,重复
leftIndex = index * 2 + 1;
}
}
四 堆排序
在有了上面的基础之后,我们就可以将堆排序归纳为以下几步
- 将给定的数组组织为大根堆
- 将最大值扔到数组尾部,同时将要组织堆结构的数组大小减一
3.每一次堆排序结束后,都会将最大值置顶,因此再将最大值向数组尾部交换即可,直到数据循环完
public void sort (int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = arr.length - 1; i >= 0; i--) {
//倒序组织大根堆,时间复杂度最低
heapify(arr, i, arr.length);
}
int heapSize = arr.length;
//将最大的数扔到屁股上
swap(arr, 0, --heapSize);
// O(N)
while (heapSize > 0) {
// O(logN)
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}