前情了解:
要想了解堆排序的过程,首先得知道什么是堆?
要想了解什么是堆,首先得知道什么是二叉树?
而在堆排序中使用的完全二叉树只是二叉树的一种,所以先得了解什么是完全二叉树?
完全二叉树(百度百科):
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
简单来说,就是一棵从上往下,从左往右的必须依次布满
节点的树就是完全二叉树。
例如:
了解了完全二叉树之后,就可以来看一下什么是堆?
堆实际上就是一个完全二叉树,不同的地方在于:它是有规律的完全二叉树,也就是说堆一定是完全二叉树,但完全二叉树却不一定是堆。
对于一棵完全二叉树来说,如果它的任意一个父节点的值都大于或等于它的两个子节点,这样的堆叫做大根堆
,又称最大堆(大顶堆);
相反如果一棵完全二叉树,它的任意一个父节点的值都小于或等于它的两个子节点,这样的堆叫做小根堆
,又称最小堆(小顶堆)。
如下图示:
了解完以上概念之后,就可以正式进行堆排序了,拿到一组无序的数据后,先把这组数据整理成一个大根堆或小根堆,至于选择哪一个,取决于你的排序是升序还是降序,一般来说升序建大根堆,降序建小根堆
主题思路:
堆排序主要分为两步,以大根堆为例进行讲解:
- 将初始堆也就是一个完全二叉树构建成大根堆
- 将堆顶数据与末节点进行交换得到最大的数据
- 重复上述步骤
注:在整个排序过程中,堆必须始终保证是大根堆,为森么嘞?
只有堆始终是大根堆,才能保证每次拿到的数据是最大的,并且在调整的过程中,当前子树中的最大数据位于根节点,便于下一次拿到最大的数据
具体过程:
拿到一组数据{ 1,3,4,8,9,2 }之后:
Step1:将初始堆构建成大根堆
a.无序序列的结构如下:
b.大根堆调整的过程如下:
我们先从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点size/2-1=2,也就是上图的4结点),从左至右,从下至上进行调整。
- 在以4节点为根节点的子树中,符合大根堆规律,不需要调整;
- 在以3节点为根节点的子树中,{3, 9, 8}三个节点中,节点9最大,此时交换节点3与节点9,如下图示:
交换完毕后,在以9节点为根节点的子树中,符合大根堆规律,所以不再进行调整; - 在以1为根节点的子树中,{1, 4 , 9}三个节点中,节点9最大,此时交换节点1与节点9,如下图示:
交换完毕后,在以9节点为根节点的子树中,符合大根堆规律,所以不再进行调整; - 在以1为根节点的子树中,{1, 8 , 3}三个节点中,节点8最大,此时交换节点1与节点8,如下图示:
交换完毕后,在以8节点为根节点的子树中,符合大根堆规律,所以不再进行调整;
此时,整个二叉树符合大根堆的特点,构建大根堆至此结束。
Step2:将堆顶数据与末节点进行交换得到最大的数据
- 首先将节点9与末节点交换
交换完毕后,对当前二叉树进行调整
- 再将节点8与末节点交换
交换完毕后,对当前二叉树进行调整
- 再将节点4与末节点交换
交换完毕后,对当前二叉树进行调整
重复上面的过程,直至循环结束,就会得到一组有序的数据
稳定性、时间复杂度与空间复杂度:
时间复杂度:
主要在初始化堆过程和每次选取最大数后重新建堆的过程,初始化建堆过程时间:O(n),更改堆元素后重建堆时间:O(nlogn)
综上所述:堆排序的时间复杂度为:O(nlogn)
空间复杂度:
因为堆排序是就地排序,空间复杂度为常数:O(1)
稳定性:不稳定
在一个长为n 的序列,堆排序的过程是从第n / 2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n / 2 - 1, n / 2 - 2, … 1这些个父节点选择元素时,就会破坏稳定性。有可能第n / 2个父节点交换把后面一个元素交换过去了,而第n / 2 - 1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
代码如下(含详细注释):
void AdjustDown(int array[],int size,int root)
{
int left = 2 * root + 1;
int right = 2 * root + 2;
//左孩子越界
if (left >= size)
{
return;
}
//假设左孩子最大
int max = left;
//存在右孩子,右>左
if (right < size && array[right] > array[left])
{
//选出子节点中大的那个作为max
max = right;
}
//如果大于,表示符合大根堆的条件,不需要再进行调整,直接返回
if (array[root] >= array[max])
{
return;
}
//将子节点中较大的那个和根节点进行交换
Swap(array + root, array + max);
//判断这次交换是否对其余子树有影响,有影响则进行调整,递归执行
AdjustDown(array, size, max);
}
void CreateHeap(int array[], int size)
{
//最后一个非叶子节点(最后一个结点的双亲节点)->0
for (int i = size / 2 - 1; i >= 0; i--)
{
AdjustDown(array, size, i);
}
}
void HeapSort(int array[], int size)//升序建大堆
{
CreateHeap(array, size);
for (int i = 0; i < size; i++)
{
//堆中最大的数据位于堆顶,将其与末节点进行交换
Swap(&array[0], &array[size - i - 1]);
//判断 减去交换完最大节点之后 堆是否为大堆,不是则进行调整,
//只有让二叉树始终保证是大堆,才能在每次交换时拿到最大的数据放置在末尾
AdjustDown(array, size - 1 - i, 0);
}
}