堆结构就是用数组实现的一颗完全二叉树。完全二叉树指的是除了叶子节点不满,其他层都是满节点的二叉树,而且叶子节点是从左到右依次遍满的。
给一个数组int[] nums=new int[]{1,2,3,4,5,6},指定数组从位置0出发连续一段的构成一个堆,长度6即为堆的大小,那么由该数组构成的堆结构父节点位置(i-1)/2,左节点 位置2*i+1,右节点位置2*i+2,所以构成的堆结构为:nums[0]是根节点。
1
2 3
4 5 6
如何实现堆排序呢?我们先来了解下大根堆和小根堆。
* 大根堆:堆的每个节点的值都大于等于它的左右孩子节点的值,堆的根节点是最大的值。
* 小根堆:堆的每个节点的值都小于等于它的左右孩子节点的值,堆的根节点是最小的值。
那么如何实现堆排序呢?给定一个数组,假设我们给它调整为大根堆,这也就代表着nums[0]位置的数为当前数组的最大值,我们将0位置的数和数组最后一个位置的数交换,那么数组的最大值位置就排好了。这样调整以后,大根堆就被破坏了,那么我们在数组nums.length-1这个范围内继续调整出一个大根堆,就可以把倒数第二个位置的数排好了。依次处理,最终就实现了数组排序。
那么问题来了,如何插入一个数或者删除堆里一个数后,依然将其调整为大根堆呢?
// index位置加入一个数
// heapInsert是一个向上调整的过程 时间复杂度 (走一个二叉树的高度 )O(logN)
public void heapInsert(int[] nums,int index){
//如果加入的节点值大于其父节点的值,就交换。如果交换到头节点了停止或者不大于其父节点值停止
while(nums[index]>nums[(index-1)/2]){
//数组位置交换
swap(nums,index,(index-1)/2);
index=(index-1)/2;
if(index==0){
break;
}
}
}
/**
* heapify是一个向下调整的过程 时间复杂度 (走一个二叉树的高度 )O(logN)
* @param heapSize 堆中元素个数
* @param nums 堆数组
* @param index 从index位置向下堆化
*/
public void heapify(int[] nums,int index,int heapsize){
//左节点位置
int leftIndex=(index*2)+1;
while(leftIndex<heapsize){
//找到左右孩子中的大值
int childMaxIndex=left+1<heapsize&&nums[left+1]>nums[left]?left+1:left;
if(nums[childMaxIndex]>nums[index]){
//说明我的左或右孩子值比我大,交换
swap(nums,childMaxIndex,index);
index=childMaxIndex;
}else{
//不比我大,不需要调整
break;
}
leftIndex=(index*2)+1;
}
}
对于堆而言最重要的就是上述两个调整方法。如果我们随意修改已经数组中的一个数,我们依然可以在O(logN)的时间复杂度上将堆重新调整为大根堆。(我们将修改的值和原值比较,如果加入的值比原值大,就向上做heapInsert;加入的值比原值小,就向下做heapify。)依然用我们最开始的数组int[] nums=new int[]{1,2,3,4,5,6},我们看一下下面的过程是怎么调整数组为大根堆的。我们看数组的变化,123456,213456,321456,431256,541236,645231,调整完毕。
//初始堆大小是0,依次加入数让整个数组范围变成大根堆
for(int i=0;i<nums.length;i++){
heapInsert(nums,i);
}
我们看下,如果按照如下过程调整,数组是怎么变化的?变化过程是:123456,123456,123456,126453,156423,653421。所以不论向上还是向下,都可以将数组调整为大根堆。
for(int i=nums.length-1;i>=0;i--){
heapify(nums,i,nums.length);
}
下面我们看下堆排序。
public void heapSort(int[] nums){
if(nums==null||nums.length<2){
return;
}
//1、我们考虑遍历数组,依次加入数,将数组调整为大根堆
//时间复杂度是O(N*logN)
for(int i=0;i<nums.length;i++){
heapInsert(nums,i);
}
//2、除了方案一,我们知道堆本身就是用一段连续数组实现的结构,
//我们假设现在的数组就是一个堆,我们要做的是把它调整成一个大根堆,只需要从最后开始向下heapify
/**
* N个节点满二叉树
* 叶子节点N/2,每个节点往下heapify,只需要看一眼 复杂度1
* 再上一层N/4,每个节点往下heapify,最多移动2层
* 再上一层N/8,最多移动3层
* 所以时间复杂度为 T(N)=(N/2)+(2*N/4)+(3*N/8)+...
* 2T(N)=(2*N/2)+(2*N/2)+(3*N/4)+(4*N/8)
* T(N)=N+N/2+N/4+N/8+... 等比数列 求和,时间复杂度为O(N)
*/
for(int i=nums.length-1;i>=0;i--){
heapify(nums,i,nums.length);
}
int heapsize=nums.length;
//经过上述调整,已经是大根堆了,
while(--heapsize>0){
//把最大位置的数放到数组最后一个位置,然后堆上释放最后一个元素
swap(nums,0,heapsize);
//然后堆上释放最后一个元素,因为0位置的数变了,重新调整堆,周而复始,
//直到堆大小为空,顺序就调好了
heapify(nums,0,heapsize);
}
}