堆排序借用了完全二叉树的性质,通过将数组构造成具有特殊性质的完全二叉树(大顶堆和小顶堆),根节点就是最大值或最小值,我们将根节点与堆(元素个数为k)得最后一个元素进行交换,并将最后一个元素排除在堆之外,此时得到的新堆(元素个数为k-1)明显不再是一个大顶堆或小顶堆,需要对其进行重新构造。接下来便是堆排序的精髓了,理解了这点,便能明白为何堆排序的时间复杂度为nlogn。新堆除了根节点root外全部符合顶堆的性质,以大顶堆为例,设根节点的左右孩子为LChild和RChild,将root,LChild,RChild进行比较,假设LChild的值大于RChild的值(根节点一定小于它的左右孩子节点中的一个),将LChild和root进行交换,此时得到的新root值便是最大值,但经过此次调整,新root的左子树却不一定符合顶堆的性质,需要再次进行调整,但右子树之前就符合顶堆的性质,无需进行调整也符合顶堆的性质。对左子树进行调整时,可将左孩子作为根节点,重复上面的步骤,由于每次只需进行左或右子树的调整,运行次数小于等于log n,对数的底为2。就这样看来,堆排序实际上就如同冒泡排序一样,每次循环找出最值,放在最后面,循环n次即可完成排序。
为了对一个杂乱的数组进行堆排序,实际上需要两步,1.将数组变成大顶堆或小顶堆 2.每次交换根节点和最后一个元素,再将最后一个元素排除堆之外,对新堆进行调整,不断重复。具体源码如下:
#include <iostream>
#include <vector>
using namespace std;
/*************************************************************
由于树的节点从1开始,而数组从0开始,因此凡遇到与arrays[]有关,
都是代表下标+1 个节点,如arrays[j-1]代表二叉树中第j个节点
*************************************************************/
void HeapBuildFromArray(vector<int> &arrays) //这个函数可以和下面的函数写成一个,为了便于理解,特意写成两个函数
{
int length=arrays.size();
for(int j=length/2;j>0;j--) //先从最后一个非叶子节点开始构造,这样在进行其他非叶子节点的调整时,除该节点外,其
{ //他均符合顶堆的定义
int temp;
temp=arrays[j-1];
int nums=j;
for(int i=2*nums;i<=length;i=2*i) //以第j个节点为根节点,构造顶堆,直到结构符合顶堆性质,结束调整
{ //对于完全二叉树而言,节点*2就代表此节点的左孩子节点
if(i<length&&arrays[i-1]<arrays[i]) //当右孩子节点比左孩子节点的值大时,便将i变更为右孩子节点
{
i++;
}
if(temp>=arrays[i-1]) //说明不需要再进行调整,此时以节点j为根节点的二叉树调整完毕
break;
arrays[nums-1]=arrays[i-1];
nums=i;
}
arrays[nums-1]=temp;
}
}
void HeapBuild(vector<int> &arrays,int length)
{
int temp;
temp=arrays[0];
int nums=1;
for(int i=2*nums;i<=length;i=2*i) //进行堆的构造时,只需对左右子树的其中一边进行调整,与上面的第二层循环代码相同
{
if(i<length&&arrays[i-1]<arrays[i])
{
i++;
}
if(temp>=arrays[i-1])
break;
arrays[nums-1]=arrays[i-1];
nums=i;
}
arrays[nums-1]=temp;
}
void HeapSort(vector<int> &arrays)
{
HeapBuildFromArray(arrays);
int length=arrays.size();
for(int i=length;i>1;i--)
{
swap(arrays[0],arrays[i-1]);
HeapBuild(arrays,i-2);
}
}
int main()
{
vector<int> arrays={4,8,2,3,7,5,9,1,6,3,6,10,-1};
HeapSort(arrays);
for(auto c:arrays)
cout<<c<<endl;
return 0;
}