堆结构
堆是一棵特殊的完全二叉树,树中的节点满足一定的性质,根据性质的不同,堆有两种:
- 大根堆
- 小根堆
虽然堆是用完全二叉树表示的,但在使用时通常用一个数组来存储。以数组的下标来标识堆中的节点,并且可以根据下标来获得某一节点的父节点和左右子节点。
对于下标为i的节点,其父节点为(i - 1) / 2,其左子节点为2 * i + 1,右子节点为2 * i + 2。
我们定义一个节点的高度为从该节点到叶子节点的最长简单向下路径的边数。树的高度就是根节点的高度。对于具有n个元素的数组,将其看作完全二叉树的话,树的高度为lgn。
txt
arr = { 1, 2, 3, 4, 5, 6, 7 }
1
2 3
4 5 6 7
同时可以看到,对于大小为n的堆,n/2为该堆的第一个叶子节点。
大根堆
对于任意一个节点来说,它的值都比其子节点的值大。
txt
7
6 5
4 3 2 1
堆的调整
在讲如何构建堆之前,先讲讲堆的调整操作。以大根堆为例。
对于堆中的一个节点来说,调整的方式有两种:向上调整和向下调整。
什么是向上调整?对于节点i来说,以i为根的子树已经满足了大根堆的性质,但是i之上的节点不一定满足大根堆的性质。向上调整将i与其父节点作比较,如i节点的值比父节点大,则交换i和父节点。如此重复,直到i比父节点小或者i为根节点。
cpp
#define PARENT(i) ((i - 1) / 2)
#define LEFT(i) (2 * i + 1)
#define RIGHT(i) (2 * i + 2)
static void swap(int* arr, int i, int j)
{
arr[i] += arr[j];
arr[j] = arr[i] - arr[j];
arr[i] = arr[i] - arr[j];
}
//堆的插入操作,向上调整,将i位置的节点调整至符合大根堆性质的位置
static void MaxHeapInsert(int* arr, int i)
{
int p = PARENT(i);
if (p >= 0 && arr[i] > arr[p])
{
swap(arr, i, p);
MaxHeapInsert(arr, p);
}
}
了解了向上调整,向下调整就很好理解了:
cpp
//向下调整。
static void MaxHeapfiy(int* arr, int size, int i)
{
int l = LEFT(i);
int r = RIGHT(i);
int largest;
if (l < size && arr[l] > arr[i])
largest = l;
else largest = i;
if (r < size && arr[r] > arr[largest])
largest = r;
if (largest != i)
{
swap(arr, i, largest);
MaxHeapfiy(arr, size, largest);
}
}
构建大根堆
假如现在有一个数组,我们希望把它变为大根堆的形式,该如何做到?
很简单,我们只需要遍历数组,对每个节点都进行向上调整即可:
cpp
static void BuildMaxHeap(int* arr, int size)
{
for (int i = 0; i < size; i++)
MaxHeapInsert(arr, i);
}
小根堆
了解了大根堆及其调整方法之后,小根堆的调整和构建自然不是问题。
堆排序
我们现在有一个无序数组,我们可以利用堆的性质来对其进行排序。以从小到大排序为例,我们使用了大根堆。
堆排序步骤如下:
- 将数组构建为大根堆
- 将堆顶元素和堆最后一个叶子节点交换
- 堆的大小减一
- 维护大根堆
- 重复1到4的步骤直到堆的大小为1
cpp
static void HeapSort(int* arr, int size)
{
BuildMaxHeap(arr, size);
int heap_size = size;
while (heap_size != 1) //注意终止条件
{
swap(arr, 0, heap_size - 1); //将最大值与末尾元素交换
printArr(arr, size);
heap_size--; //调整堆的大小
MaxHeapfiy(arr, heap_size, 0); //维护大根堆的性质
}
}