程序中的堆存储区,是通过链表实现的。堆数据结构是一棵完全二叉树,对于最大堆来说,每个根节点的值不小于其子节点,也就是根节点,具有最大值。实际中可以用一个数组来表示完全二叉树,那么对于索引为 i 的节点来说,其左右子节点的索引分别为 2*i + 1 和 2*i + 2.
堆有两种操作:
1、上浮操作:在堆的末尾添加一个元素,然后使这个元素上浮,使维持堆结构。(也就是数组的某个元素的前边的所有元素都已经是堆结构,加入这个元素,调整,使维持堆结构)。
上浮的过程只需要和其父节点相比较。如果比其父节点大,上浮到父节点(最小堆);再和新位置的父节点相比较,这样进行下去:
void MaxHeapFixup(int arr[], int i) //索引为i的元素的前边的所有元素已经满足堆结构
{
if (i == 0)
return;
int temp = arr[i];
int parent = (i - 1) / 2; //父节点的索引
while (parent >= 0)
{
if (arr[parent] <= temp)
break;
else
{
arr[i] = arr[parent]; //父节点往下覆盖 相当于空洞往上浮
i = parent;
parent = (i - 1) / 2;
}
}
arr[i] = temp; //最后填补空洞
}
2、下沉操作:下沉操作是某节点以下的所有元素都满足堆结构,需要使根节点下沉,使维持堆结构(也就是说数组的某个元素之后的所有元素都是堆结构,调整,使维持堆结构)。
下沉的过程因为是最大堆,需要和左右子节点的较大者交换,一直往下交换下去即可:
void MaxHeapFixdown(int arr[], int i, int len) //索引为i的元素之后的所有元素 都满足堆结构 len表示数组的长度
{
if (i == len - 1)
return;
int temp = arr[i];
int child = 2 * i + 1; //左子节点的索引
while (child < len)
{
if ((child + 1 < len) && (arr[child + 1] > arr[child])) //这里有个短路计算 如果没有右子节点 不会计算与运算右边的
++child;
if (arr[child] <= temp)
break;
else
{
arr[i] = arr[child]; //往上覆盖 相当于空洞往下沉
i = child;
child = 2 * i + 1;
}
}
arr[i] = temp; //填补空洞
}
构造一个最大堆:
对于叶子节点来说,由于没有子节点,已经满足堆结构,所以只需要从最后边一个非叶子节点往前进行下沉操作来调整,便可以构造一个堆出来。最后一个非叶子节点的索引为 len/2 - 1(归纳一下很容易得出)。
void MakeMaxHeap(int arr[], int len) //len表示数组的长度
{
for (int i = len / 2 - 1; i >= 0; --i)
{
MaxHeapFixdown(arr, i, len);
}
}
堆排序就是借助堆这种数据结构进行排序。
对于一个长度为 len 的数组,由于是最大堆,索引为0 的元素是最大元素,且索引为 0 的元素后边的所有元素,都满足最大堆;如果将索引为 0 的元素与最后一个元素交换之后,那么前 len - 1 个元素除了第 0 个元素之外,都满足最大堆,这样对第 0 个元素进行下调操作;然后再将第0 个元素与倒数第2个元素交换..., 一直这样进行下去直到最后第 0 个元素都与后边所有的元素交换过,那么数组就是已经按照升序排好了。
void myHeapSort(int arr[], int len) //最大堆的降序排序
{
MakeMaxHeap(arr, len); //先构造一个最大堆
for (int i = len - 1; i >= 1; --i)
{
std::swap(arr[0], arr[i]); //每次与 0 元素交换
MaxHeapFixdown(arr, 0, i); //重新交换到后边的元素之前的所有元素为堆
}
}
或者这样,每次得到一个最大堆之后,索引为 0 的元素是最大元素;接着对后边的 元素进行最大堆调整,使索引为1的元素为后边的最大。。。这样一直到最后只剩下一个元素(STL中的partial_sort() 函数 就是这个原理):
void myHeapSort(int arr[], int len) //最大堆的升序排序
{
for(int i = 0; i < len; ++i)
{
MakeMaxHeap(arr+i, len - i);
}
}