堆结构
堆是一种数据结构,它的结构和完全二叉树 一致,分为大顶堆和小顶堆。什么是大顶堆、小顶堆呢?以大顶堆为例,堆顶元素是整个堆内值最大的元素,小顶堆反之保存的是最小的元素。如下图所示:
堆结构数据保存
堆结构原本是一个完全二叉树的结构,但是这里我们是借用堆来实现对数组的排序,所以我们用数组来保存堆中的数据,比如用下面的数组来保存上面堆结构里的数据:
除此之外我们还要知道在数组中如何对节点进行操作,比如如何获取当前节点的左子节点lchild、右子节点节点rchild和父节点parent。通过上面两个图的对比我们可以得出如下结论:
lchild = 2 * i+ 1;
rchild = 2 * i+ 2;
parent = (i - 1) / 2;
注意:这里的lchild、rchild、i(当前节点)都是数组中的下标索引。
heapify过程
heapify是将当前节点和它的两个子节点进行比较,把最大的值与父节点(当前节点)中的值进行交换,但是如果父节点(当前节点)的值就是三个节点中值最大的那么就不用交换
但是要注意的是:这里经过一趟heapify过程之后其实堆顶不一定是最大值,因为heapify只是保证了当前节点与它子节点值的大小关系,并没有保证整个堆的大小关系。
递归实现heapify过程:
/* arr[]: 待排序数组
* len: 数组的长度
* i: 当前要执行heapify过程的节点在数组中的索引
*/
void heapify(int arr[], int len,int i)
{
if (i >= len) return; // 递归边界
int lchild = 2 * i + 1; // 获取左子节点索引
int rchild = 2 * i + 2; // 获取右子节点索引
int max = i; // 保存最大值节点对应的索引
if (lchild < len && arr[max] < arr[lchild])
{
max = lchild;
}
if (rchild < len && arr[max] < arr[rchild])
{
max = rchild;
}
// 如果最大值保存在子节点中就交换 并且继续递归执行
if (max != i)
{
mySwap(arr, max, i);
heapify(arr, len, max);
}
}
buildHeapify过程
由于heapify过程执行一遍后,堆顶元素不一定是最大值,所以我们要想办法将最大值移动到堆顶去,所以就需要用到buildHeapify。但是怎么实现buildHeapify才能将最大值移动到堆顶呢?
我们可以尝试一下从后向前挨个执行heapify过程然后观察结果。由于heapify里面每次比较的都是当前和子节点这三个节点的值,我们可以把它看做一个三元的最小单位,所以我们的开始位置是最后一个节点的父节点,依次从后向前遍历。这样说可能不太好理解 下面我们再通过画图的方式来实现下这个过程
如下面这个待排序的数组以及对应的完全二叉树:
(1)找到开始位置并对该位置进行一次heapify
开始位置 = (数组长度 - 1) / 2;
(2)继续向前遍历
(3)最后执行结果可见堆顶元素就是最大值
这里为了方便展示所以画的图比较简单,大家可以尝试一下其它比较复杂的图,比如9个节点的完全二叉树,然后按照这个过程自己再实现一遍。
void buildHeap(int arr[],int len)
{
int parent = (len - 1) / 2; // 获取开始位置
// 依次向前执行 heapify过程
for (; parent >= 0; parent--)
{
heapify(arr, len, parent);
}
}
堆排序 heapSort
了解了上面两个过程之后想必你已经对数组排序大致的思路,没错就是取出最大值然后再进行一次buildHeapify过程保证最大值在堆顶,然后再取再buildHeapify直到将元素取完。不过我们这里不是取值而是交换,怎么交换呢?就是将最大的值与数组最后一个元素的值进行交换然后再执行buildHeapify(注意这里的最后一个数组原数是相对的,也就是还没取出的前面一段子数组的最后一个元素 后面会在代码里面标出)这样做的目的就是为了节省额外额空间。
void heapSort(int arr[], int len)
{
buildHeap(arr, len);
for (int i = len - 1; i >= 0; i--)
{
mySwap(arr, i, 0);
heapify(arr, i, 0); // 这里的i对应的就是未取出子数组的最后一个元素
}
}
稳定性分析
由于在堆排序过程中有很多次的交换操作,所以不能保证大小相等元素之间初始位置不变,所以堆排序是不稳定的排序算法。
复杂度分析
堆排序是一种选择排序,整体主要由构建初始堆、交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以一般认为堆排序 时间复杂度O(nlogn) 级。这里我们并没有申请额外的空间所以空间复杂度为O(1) 级。