堆排序就是借助堆的性质来实现排序,通过建堆,然后调整可以实现对需排序数组排序。下面来具体解释:
1:堆是完全二叉树,分为大根堆和小根堆,大根堆即是每个父结点大于等于左右子结点,小根堆即是每个父结点小于等子结点,下面两种图分别是一个大根堆和一个小根堆:
这里的70相对于56和30就是父结点,而56和30就是70的左右子结点。
2:还需要知道的是,完全二叉树中,若知父结点位置为i,则左右子结点为2i+1和2i+2,若已知子结点为i,则父结点为(i-1)/2,
例如此处0,1,2,3,4,5,6代表其在数组中的下标,对于5和6来说,2是其父结点,(5-1)/2和(6-1)/2都是2,有的地方写的是i/2-1,如果这样的话5/2-1就是1了,所以应该是(i-1)/2。
3:向下调整算法,这个算法的前提是要调整的根结点的左右子树必须是堆,所以要先建堆。例如:
以27为根结点,左右两个红框分别是其左右子树,并且观察可知,左右子树都是小根堆,那么就可以利用向下调整算法来实现对根结点的调整,使得变成小根堆。具体流程如下:
1)找到左右子结点小的那一个结点;
2)比较该子结点与父结点,如果子结点小,则与父结点交换;
3)交换完再更新父结点的位置,继续向下调整。
最后一个与父结点比较之后,可以看出父结点比子结点小,则不需要进行交换。根据上面步骤完成后,可以得到最后变成了小根堆。需要注意的是,上面过程只是一个例子,如果要把输入的数组建堆,由于向下调整算法根结点的左右子树必须是堆,就必须要从最后一个父结点开始,都要进行向下调整,使得其本身是堆,然后再做为父结点的子树,这样父结点的左右子树是堆,才能进行向下调整。
口语化表述就是:
以这颗树为例,想要调整为小根堆,对27这个结点调整就得保证以15和19为根的树是小根堆堆,同理,想要把15和19为根的树变成小根堆,就得保证15的左右子树(即以18和28为根的树)和19的左右子树(即以34和65为根的树)为小根堆,同理想要18、28、34和65的左右子树为小根堆,就得对其左右子树进行调整,直到叶子结点(左右子结点为空)为止,因为叶子结点就一个树,可以认为其是堆。
通过上述过程,即可建出一个小堆,建大堆方法类似,下面是建堆的参考代码:
void AdjustDown(int* a, int k, int parent)
{
int child = 2 * parent + 1;
while (child < k)
{
//找左右孩子小的那一个
if (child + 1 < k && a[child + 1] < a[child])
child++;
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
int main()
{
int arr[10] = {3,1,5,23,64,7,45,37,9,23};
int k = sizeof(arr)/sizeof(arr[0]);
int i = 0;
for (i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(arr,k,i);
}
}
上述代码注意的是:找左右子结点小的那个,我是先让左为小,然后去和右比较,右如果小的话,则对child++。循环停止条件为child>k,当父结点更新到叶子结点时,左右子结点就会大于边界,即数组个数k。所以小于k时继续调整。
这是进行堆排序的前提,后面还会实现堆排序,以及TopK问题的解决。