堆排序,形成的堆就是一颗完全二叉树 1
堆排序用于很多贪心算法的问题中
以数组形式表示二叉树
下标i的左孩子是2i+1,右孩子是2i+2,一个节点的父节点是(i-1)/2
大根堆
在这棵完全二叉树中任何一棵子树的最大值是头部
建立大根堆
给一个数组:2 1 3 6 0 4
我们首先人为规定这棵树只有一个节点2,依次增加一个节点并调整为大堆过程如下:演示过程使用graphviz,一个图一份源码
1
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('2', '2')
2
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('2', '2')
dot.node('1', '1')
dot.edges(['21'])
3
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('2', '2')
dot.node('1', '1')
dot.node('3', '3')
dot.edges(['21', '23'])
调整: 3和2节点交换
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('3', '3')
dot.node('1', '1')
dot.node('2', '2')
dot.edges(['32', '31'])
4
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('3', '3')
dot.node('1', '1')
dot.node('2', '2')
dot.node('6', '6')
dot.edges(['31', '32', '16'])
调整之后:
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('6', '6')
dot.node('3', '3')
dot.node('2', '2')
dot.node('1', '1')
dot.edges(['63', '62', '31'])
5
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('6', '6')
dot.node('3', '3')
dot.node('2', '2')
dot.node('1', '1')
dot.node('0', '0')
dot.edges(['63', '62', '31', '30'])
6
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('6', '6')
dot.node('3', '3')
dot.node('2', '2')
dot.node('1', '1')
dot.node('0', '0')
dot.node('4', '4')
dot.edges(['63', '62', '31', '30', '24'])
调整之后:
from graphviz import Digraph
dot = Digraph(comment='The Round Table')
dot.node('6', '6')
dot.node('3', '3')
dot.node('2', '4')
dot.node('1', '1')
dot.node('0', '0')
dot.node('4', '2')
dot.edges(['63', '62', '31', '30', '24'])
此时大根堆数组表示为:6 3 4 1 0 2
如上过程展示了建立大根堆的详细过程,代码如下:
package yzy.algorithm;
//大根堆
public class heapSort {
public static void heapSort(int[] arr){
if(arr == null || arr.length<2)
return ;
for(int i=0; i<arr.length; ++i)
heapInsert(arr, i); // 0~i
}
public static void swap(int[] arr, int index1, int index2){
arr[index1] ^= arr[index2];
arr[index2] ^= arr[index1];
arr[index1] ^= arr[index2];
}
//调整新增节点
public static void heapInsert(int[] arr, int index) {
while(arr[index] > arr[(index-1)/2]) { //精华语句,考虑了index=0时while终止
swap(arr, index, (index - 1) >> 1);
index = (index - 1) / 2;
}
}
//数组中一个元素值发生变化,则调整使其继续成为大根堆
public static void heapify(int[] arr, int index, int heapSize) { //[0, heapSize]范围上形成的堆
int left = index*2+1;
while(left < heapSize){
int largest = left + 1 < heapSize && arr[left+1] > arr[left]
? left + 1
: left;
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index)
break;
swap(arr, largest, index);
index = largest;
left = index*2+1;
}
}
public static void showArr(int[] arr){
int i = 0;
while(i<arr.length)
System.out.print(arr[i++]);
System.out.println();
}
public static void main(String[] args) {
int arr[] = {2, 1, 3, 6, 0, 4};
showArr(arr);
heapSort(arr);
showArr(arr);
}
}
建立大根堆的复杂度
每加入一个节点的时间复杂度就是,在加入这个节点之前完全二叉树的高度,在加入第i个节点时,时间复杂度就是
l
o
g
2
(
i
−
1
)
log_2(i-1)
log2(i−1)
建立N个节点的大堆的过程时间复杂度就是
l
o
g
2
1
+
l
o
g
2
2
+
l
o
g
2
3
+
.
.
.
+
+
l
o
g
2
N
−
1
log_2 1+log_2 2+log_2 3+...++log_2 N-1
log21+log22+log23+...++log2N−1
收敛于N,最终时间复杂度就是O(N)
弹出大根
使用一个变量heapSize人为规定大根堆的大小,堆顶和最后一层最右的叶节点交换并且heapSize减一则表示弹出堆顶元素,heapify用于index位置节点改变的情况下调整二叉树依然为大根堆
package yzy.algorithm;
//大根堆
public class heapSort {
public static void heapSort(int[] arr){
if(arr == null || arr.length<2)
return ;
for(int i=0; i<arr.length; ++i)
heapInsert(arr, i); // 0~i
}
public static void swap(int[] arr, int index1, int index2){
arr[index1] ^= arr[index2];
arr[index2] ^= arr[index1];
arr[index1] ^= arr[index2];
}
//调整新增节点
public static void heapInsert(int[] arr, int index) {
while(arr[index] > arr[(index-1)/2]) { //精华语句,考虑了index=0时while终止
swap(arr, index, (index - 1) >> 1);
index = (index - 1) / 2;
}
}
//数组中一个元素值发生变化,则调整使其继续成为大根堆
public static void heapify(int[] arr, int index, int heapSize) { //[0, heapSize]范围上形成的堆
int left = index*2+1;
while(left < heapSize){
int largest = left + 1 < heapSize && arr[left+1] > arr[left]
? left + 1
: left;
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index)
break;
swap(arr, largest, index);
index = largest;
left = index*2+1;
}
}
public static void showArr(int[] arr){
int i = 0;
while(i<arr.length)
System.out.print(arr[i++]);
System.out.println();
}
public static void main(String[] args) {
//建立大根堆
int arr[] = {2, 1, 3, 6, 0, 4};
showArr(arr);
heapSort(arr);
showArr(arr);
System.out.println("弹出大根堆顶级大根");
// 弹出大根
int heapSize = arr.length;
swap(arr, 0, --heapSize); // 先将大根和最后一层最右的叶节点交换,heapSize同时减1
heapify(arr, 0, heapSize); // 将[0,heapSize)调整为大根堆
// 此时相比之前根弹出,有效大根堆范围时[0,heapSize)
showArr(arr);
System.out.println("循环依次弹出:");
// 循环依次弹出
while(heapSize>0){
swap(arr, 0, --heapSize);
heapify(arr, 0, heapSize);
showArr(arr);
}
}
}
观察以上弹出堆顶元素的操作,如果依次弹出堆顶元素直到人为规定的堆大小heapSize为0,就会发现数组元素从小到大依次有序,小根堆则同样可以使数组元素从大到小有序
掌握弹出堆顶元素,可以巧妙的解决很多问题,并且加速很多问题的算法时间复杂度,请继续观看后续博文
满二叉树、每一层从左往右依次补齐的一系列树都是(意思就是右孩子存在则左孩子必须先存在) ↩︎