堆排序是将整个数组用完全二叉树的形式存,即
如果根节点的下标是0,那么左子树的下标2i+1,右子树的下标是2i+2
第一次先生成一个大顶堆或者小顶堆,然后把根节点与最后一个节点交换,这样就相当于在数组最后变成了最大或者最小的值,然后再遍历从n-1到1,之所以不用遍历0,因为0是根节点,已经变成有序.
时间复杂度:
一.初始化建堆
初始化建堆只需要对二叉树的非叶子节点调用adjusthead()函数,由下至上,由右至左选取非叶子节点来调用adjusthead()函数。那么倒数第二层的最右边的非叶子节点就是最后一个非叶子结点。
假设高度为k,则从倒数第二层右边的节点开始,这一层的节点都要执行子节点比较然后交换(如果顺序是对的就不用交换);倒数第三层呢,则会选择其子节点进行比较和交换,如果没交换就可以不用再执行下去了。如果交换了,那么又要选择一支子树进行比较和交换;高层也是这样逐渐递归。
那么总的时间计算为:s = 2^( i - 1 ) * ( k - i );其中 i 表示第几层,2^( i - 1) 表示该层上有多少个元素,( k - i) 表示子树上要下调比较的次数。
S = 2^(k-2) * 1 + 2(k-3)2……+2(k-2)+2(0)*(k-1) ===> 因为叶子层不用交换,所以i从 k-1 开始到 1;
S = 2^k -k -1;又因为k为完全二叉树的深度,而log(n) =k,把此式带入;
得到:S = n - log(n) -1,所以时间复杂度为:O(n)
二.排序重建堆
在取出堆顶点放到对应位置并把原堆的最后一个节点填充到堆顶点之后,需要对堆进行重建,只需要对堆的顶点调用adjustheap()函数。
每次重建意味着有一个节点出堆,所以需要将堆的容量减一。adjustheap()函数的时间复杂度k=log(n),k为堆的层数。所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明log(n!)和nlog(n)是同阶函数:
∵(n/2)n/2≤n!≤nn,∵(n/2)n/2≤n!≤nn,
∴n/4log(n)=n/2log(n1/2)≤n/2log(n/2)≤log(n!)≤nlog(n)∴n/4log(n)=n/2log(n1/2)≤n/2log(n/2)≤log(n!)≤nlog(n)
所以时间复杂度为O(nlogn)
三.总结
初始化建堆的时间复杂度为O(n),排序重建堆的时间复杂度为nlog(n),所以总的时间复杂度为O(n+nlogn)=O(nlogn)。另外堆排序的比较次数和序列的初始状态有关,但只是在序列初始状态为堆的情况下比较次数显著减少,在序列有序或逆序的情况下比较次数不会发生明显变化。
9, 79, 46, 30, 58, 49
1: 先将待排序的数视作完全二叉树(按层次遍历顺序进行编号, 从0开始),如下图:
2:完全二叉树的最后一个非叶子节点,也就是最后一个节点的父节点。最后一个节点的索引为数组长度len-1,那么最后一个非叶子节点的索引应该是为(len-1)/2.也就是从索引为2的节点开始,如果其子节点的值大于其本身的值。则把他和较大子节点进行交换,即将索引2处节点和索引5处元素交换。交换后的结果如图:
建堆从最后一个非叶子节点开始即可
3:向前处理前一个节点,也就是处理索引为1的节点,此时79>30,79>58,因此无需交换。
4:向前处理前一个节点,也就是处理索引为0的节点,此时9 < 79,9 < 49, 因此需交换。应该拿索引为0的节点与索引为1的节点交换,因为79>49. 如图:
5:如果某个节点和它的某个子节点交换后,该子节点又有子节点,系统还需要再次对该子节点进行判断。如上图因为1处,3处,4处中,1处的值大于3,4出的值,所以还需交换。
建立堆用递归
void maxHeapBuild(int arr[],int i,int n){
int leftIndex = 2*i+1;
int rightIndex = 2*i+2;
int maxIndex = index;
if(leftIndex<n && arr[leftIndex]>=arr[maxIndex]) maxIndex = leftIndex;
if(rightIndex<n && arr[rightIndex]>=arr[maxIndex]) maxIndex = rightIndex;
if(maxIndex!=index){
swap(arr[maxIndex],arr[index]);
maxHeapBuild(arr,maxIndex,n);//递归调整其他不满足堆性质的部分
}
}
void heapSort(int arr[], int size)
{
for(int i=size/2 - 1; i >= 0; i--) // 对每一个非叶结点进行堆调整(从最后一个非叶结点开始)
{
maxHeapBuild(arr, size, i);
}
for(int i = size - 1; i >= 1; i--)
{
swap(arr[0], arr[i]); // 将当前最大的放置到数组末尾
maxHeapBuild(arr, 0, i); // 将未完成排序的部分继续进行堆排序
}
}
非递归
//构造最大堆
void MaxHeapFixDown(int a[], int i, int n){
int j = 2*i+1;
int temp = a[i];
while(j<n){
if(j+1<n&&a[j]<a[j+1]) //取左右子树最大的一个
++j;
if(temp>a[j])
break;
else{
a[i]=a[j];
i=j;
j=2*i+1; 每次修改的值就在修改的子树再次遍历
}
}
a[i]=temp;
}
//堆排序
void HeapSort(int a[], int n){
for(int i= n/2-1;i>=0;i--)
MaxHeapFixDown(a,i,n);
for(int i=n-1;i>=1;i--){
swap(a[i],a[0]);
MaxHeapFixDown(a,0,i);
}
}