在本文中使用到的升序,降序,交换函数的代码实现见:这篇博客
在介绍堆排序之前,首先要明白堆的概念:
堆的概念
堆实际上还是一棵二叉树,不过它还满足下列两点要求:
(1)堆是一棵完全二叉树。(关于完全二叉树的定义见:数据结构------二叉树的面试题)
(2)堆中元素满足:对任一棵树来说,它的根节点的值比它的左,右孩子都大或都小(注意:左右孩子之间没有明确的大小关系)。
根节点的值比它的左,右孩子元素都大的堆称为大(根)堆。
根节点的值比它的左,右孩子元素都小的堆称为小(根)堆。
根据堆的结构特点,可以将堆用数组来进行存储,因此,若在一个堆中父节点的下标为i,则它的孩子节点的下标为:2*i+1和2*i+2。若孩子节点的下标为i,则它的父节点的下标为:(i-1)/2
本文中将利用堆的结构特点对一个数组进行升序排序。
堆排序
在堆的基本操作一文中,有一个操作是通过堆来对数组进行排序。在该操作中,
(1)遍历数组,根据数组元素构建一个大堆,此处构建的大堆有另外开辟空间;
(2)逐个删除(1)中创建的大堆的堆顶元素。
(3)删除完毕之后,大堆所在的内存空间上存放的就是已排好序的数组元素,此时将该空间上的元素在逐个赋值到原数组上即可实现升序排序。
在本文中,也是利用堆来对数组进行排序,也是根据上述的思路来进行操作。不同的是,在本次的堆排序中,不用在额外开辟空间来构建堆,而是在数组所在空间的基础上将数组够构建成一个堆,然后在数组的基础上在对堆顶元素进行删除,删除完毕后数组即排好序。
因此,根据上述思路:创建堆,删除堆。可以实现堆排序:
void HeapSort(int arr[],uint64_t size) { if(arr == NULL || size <= 1) { return; } //首先根据数组构建大堆(在数组的基础上构建) //构建堆 CreateHeap(arr,size); //Print(arr,size); //再对堆元素进行删除 DeleteHeap(arr,size); //删除完之后的堆(数组)即为升序后的数组 return; }
1. 创建堆
堆有两种创建方式:上浮式调整创建堆,下沉式调整创建堆。
(1)上浮式调整创建堆
从头开始遍历数组,遍历到一个元素,将该元素插入到堆中,然后对该元素进行上浮式调整。
设置child来遍历数组,其中,[0,child)为堆中的元素,[child,size)为待插入到堆中的元素。child初始为0。
(1)遍历到一个数组元素child,将child插入到堆中。为实现插入元素时之后还满足堆的结构特点,因此,需要进行如下调整
i)取child处的值与其父节点parent进行比较:
ii)如果parent处的值大于child处的值,说明child插入之后还符合堆的条件,所以就不用在进行调整了。
iii)如果parent处的值小于child处的值 ,则将child上浮,即交换child处的值和parent处的值。然后将parent赋值给child,重新计算parent的值,再次进行i)ii)iii)的操作。直到child上浮到下标为0时,此时说明child是堆中的最大元素,所以将child处的值放置在下标为0处,调整结束;
(2)经过上述(1)的操作之后,可以实现将child处的值插入到了堆中,此时堆中元素个数加1。所以,再遍历数组的下一个元素,执行(1)中的操作,将该元素也插入到堆中。待遍历完整个数组之后,即实现了将数组转化为堆。
根据上述思路,实现代码如下:
//上浮式调整创建堆 void CreateHeap(int arr[],uint64_t size) { //根据上浮式调整创建 //将数组中的元素从后往前一个个的进行上浮时调整 //遍历数组元素插入到堆中 int index = 0; for(;index < size;index++) { //对插入到堆中的元素进行上浮式调整,以满足堆的特点。每次调整index处的值 AdjustUp(arr,size,index,Greater); } return; }
上浮式调整函数的实现思路即为(1)中i)ii)iii)所述,代码实现如下:
//上浮式调整 void AdjustUp(int arr[],uint64_t size,int index,Compare cmp) { if(index >= size || index < 0) { return; } //上浮式调整时: //比较孩子节点与其父节点的大小关系, //如果不满足条件(此处条件为父节点应该大于子节点的值),则进行交换 //如果满足条件,则停止调整 int child = index; int parent = (child - 1)/2; while(child > 0) { if(cmp(arr[child],arr[parent] == 1)) { Swap(&arr[parent],&arr[child]); child = parent; parent = (child - 1)/2; } else { break; } } }
(2)下沉式调整创建堆
假设数组中的所有元素都已经插入到一棵二叉树中,此时这棵二叉树是完全二叉树。所以要满足堆的结构特点,就要父节点的值大于其孩子节点的值。因此要从后往前遍历父节点,对父节点进行下城。下沉时,父节点要与孩子节点处的值进行对比,所以有孩子结点的父节点才能进行下沉。因此,要从最后一个父节点开始从后往前遍历,根据数组下标的对应关系,可以知道最后一个有孩子节点的父节点是最后一个叶子结点的父节点,最后一个叶子结点的下标为size - 1
因此,定义变量parent初始为((size-1) - 1)/2,从后往前遍历,对各父节点进行下沉。其中[0,parent]为需要进行下沉调整的堆中元素,(parent,size)为已经满足条件的堆中元素。
在下沉进行调整时:保证父节点的值大于孩子节点的值。
i)如果parent的左右孩子节点都存在的话,左右孩子节点的最大值。如果左孩子存在,右孩子不存在,则左右孩子的最大值即为左孩子的值。如果左右孩子均不存在,则调整结束;
ii)然后将parent处的值与孩子节点的最大值进行比较。
如果parent处的值大于孩子节点的最大值,则说明满足堆的条件,调整结束;
如果小于,则将parent处的值与孩子节点的最大值进行交换即下沉,此时更新parent的值为孩子节点最大值的下标,重新计算孩子节点的下标,进行i)ii)的操作。
代码实现如下:
//创建堆 void CreateHeap(int arr[],uint64_t size) { //根据下沉式调整创建堆 //从第一个非叶子节点开始下沉,依次往前,每次下沉一个 //第一个非叶子节点为最后一个节点的父节点 int index = ((size - 1) - 1)/2;//第一个非叶子节点 for(;index >= 0;index--)//注意,下标为0的元素也需要进行下沉调整 { AdjustDown(arr,size,index,Greater); } return; }
下沉式函数实现思路即为上述的i)ii):
//对堆顶元素进行下沉式调整 void AdjustDown(int arr[],uint64_t heap_size,int index,Compare cmp) { int parent = index; int child = parent*2 + 1; while(child < heap_size)//左孩子节点存在 { //右孩子节点存在,且右孩子节点的值大于右孩子节点的值 if(child + 1 < heap_size && cmp(arr[child + 1],arr[child]) == 1) { child = child + 1; } if(cmp(arr[child],arr[parent]) == 1) { Swap(&arr[child],&arr[parent]); parent = child; child = parent*2 + 1; } else { break; } } return; }
2. 删除堆顶元素
经上述1将堆创建完毕之后,再对堆进行删除堆顶元素操作。在删除一个堆顶元素时,因为堆顶元素不方便删除,所以进行如下操作:
(1)将堆顶元素与堆中最后一个元素值进行交换,此时最后一个元素即为数组元素中的最大值,将最后一个元素从堆中删除,即将堆的元素个数减1即可
(2)此时,交换后的堆顶元素可能不满足堆的要求,所以要对堆顶元素进行下沉式调整;
(3)调整之后,堆顶元素又变成了堆中最大元素。再次对堆顶元素进行删除,进行(1)~(3),直到堆中元素删除完毕为止。
代码实现如下:
//下沉式删除堆 void DeleteHeap(int arr[],uint64_t size) { //对堆的删除进行下沉式调整 int index = size - 1; //每次从最后一个堆元素开始 for(;index > 0;index--) { //删除堆顶元素时: //1. 将堆的最后一个元素与堆顶元素进行交换, // 这里堆的最后一个元素下标为index,此时堆中的元素个数为index + 1 //2. 然后使堆元素个数减1,此时堆中元素个数为index //3. 最后在新堆中对新堆顶元素进行下沉式调整,此时堆的元素个数依然为index //每次将最后一个堆元素与堆顶元素交换 Swap(&arr[0],&arr[index]); //然后堆顶元素进行下沉式调整 AdjustDown(arr,index,0,Greater); //这里的参数index表示的是堆中的元素个数,该步操作隐含了堆元素个数减1的操作 } }
上述下沉式调整函数即为上述1(2)中的下沉式函数。
堆排序的时间复杂度为:nlog(n),空间复杂度为:O(1),稳定性为:不稳定