如果把之前是链式存储的二叉树存入数组,变成顺序存储的方式,更多应用在完全二叉树。
已知根下标 rootIndex
孩子结点:
leftIndex
= 2 * rootIndex + 1
rightIndex
= 2 * rootIndex + 2
已知孩子下标 childIndex
双亲结点:
parentIndex
= (childIndex - 1)/2
- 如何判定一个结点在不在二叉树中?
结点的下标i
是否数组越界。
【注】:下面所有操作是以大堆形式进行演示,如需小堆情况,读者自行修改即可。
二叉堆
这里讲的堆与程序内存空间中的堆结构完全没有联系,是逻辑上的完全二叉树,利用顺序存储存在数组中。堆的作用:找最值,永远出现在二叉树逻辑上的根上。
对于大堆,任意取出一个结点,要求根的值 >=
左右孩子的值。
例如:{15,14,3,12,11,2,1,10}
同理得小堆,任意取出一个结点,要求根的值 <=
左右孩子的值。
例如:{3,6,10,6,6,11,12,7,8}
二叉堆:
- 逻辑上:完全二叉树
- 存储上:数组
堆的操作
堆化(Heapify)
int a[] = (27,15,19,18,28,34,65,49,25,37};
前提:除了一个位置之外(不一定为根),其余位置都满足堆的性质。
判断 rootIdx
是不是叶子:没有左右孩子
完全二叉树,没有左孩子就一定没有右孩子
判断有没有左孩子: 因为结点是存在数组里的,判断标准就是左孩子的下标是否数组越界
- 找到根、左孩子、右孩子,找到三个中最小的一个放在根上。
(只要不是叶子结点,一定有左孩子,但不一定有右孩子) - 停止条件:
1)走到了叶子位置
2)已经满足堆的性质,刚才三个中最小的是根
代码实现
/**
* 时间复杂度 O(lgN)
* 空间复杂度 O(1)
* int tree[] 和 int size 组合使用,表示装堆的值的数组
* int rootIdx 表示要调整的结点的下标
*/
void Heapify(int tree[], int size, int rootIdx) {
int leftIdx = 2 * rootIdx + 1;
int rightIdx = 2 * rootIdx + 2;
if (leftIdx >= size) {
return; // 是叶子,结束
}
// 不是叶子,在三者中找到最小值
// 先找到左右孩子中最小值
int minIdx = leftIdx;
if (rightIdx < size && tree[rightIdx] < tree[leftIdx]) { //短路,两条件顺序不可替换
minIdx = rightIdx;
}
// 三者中最小孩子的下标就是 minIdx
if (tree[rootIdx] <= tree[minIdx]) {
return; // 最小的已经是根了,满足堆的性质,停止
}
// 不满足堆性质,执行交换逻辑,左右孩子中小者与根调换
int t = tree[minIdx];
tree[minIdx] = tree[rootIdx];
tree[rootIdx] = t;
// 如果前面逻辑中发生了交换,则下面的树的堆性质可能被破坏了,继续调整
Heapify(tree, size, minIdx);
}
时间复杂度O(lgN)
计算过程:
(N
:结点个数)
-
20+21+22+…+2h-1 = N
-
(1-(1-2h))/(1-2) = N (等比数列求和公式)
-
2h - 1 = N
-
h = log2(N+1)
树的高度占主要时间复杂度,约为O(lgN)
。
向上调整
//结束条件:
//1. 当前array[i] >= array[parent]
//2. i == 0,已经成为堆的0号下标元素(堆顶)
void AdjustUp(int tree[], int size, int child) {
if (child == 0) {
return;
}
int parent = (child - 1) / 2;
if (tree[child] >= tree[parent]) {
return;
}
int t = tree[child];
tree[child] = tree[parent];
tree[parent] = t;
AdjustUp(tree, size, parent);
}
建堆
把一个完全无序的随机分布的数组变成满足堆的性质(数组 →
堆)。
方法:从最后一个非叶子结点,一直到下标0
进行堆化。
- 为什么需要倒着调整??
- 充分利用堆化的性质,向下调整的前提有限制:必须左右子树已经是堆,才可以向下调整(堆化)。找到最后一个非叶子结点,此时就只会有一个位置不满足堆的性质,直接进行堆化即可。
最后一个结点的下标是: size - 1
(根据数组的特性)
那么最后一个非叶子结点就是最后一个结点的双亲结点:parent = (child - 1) / 2
代入得到下标为 (size - 2) / 2
代码实现
// 粗略看,时间复杂度是 O(n * log(n))
// 精确算,是 O(n)
void CreateHeap(int tree[], int size) {
for (int i = (size - 2) / 2; i >= 0; i--) {
Heapify(tree, size, i);
}
}
堆的封装
封装堆的接口在一个结构中,本质是构建成为静态顺序表。
typedef struct Heap {
int array[100]; // 静态顺序表
int size; // 数据个数
} Heap;
初始化
void HeapInit(Heap *pH, int array[], int size) {
assert(size <= 100);
memcpy(pH->array, array, size * sizeof(int));
pH->size = size;
CreateHeap(pH->array, pH->size);
}
插入
// log(n)
//插入最后,然后往上升
void HeapPush(Heap *pH, int v) {
pH->array[pH->size++] = v;
AdjustUp(pH->array, pH->size, pH->size - 1);
}
出堆
// 每次出的是当前最小值
// O(log(n)),主要复杂度在调整上了。
// 把下标为0的值拷贝出来,把size - 1下标的数放在0处,然后对它进行堆化即可。
int HeapPop(Heap *pH) {
int v = pH->array[0];
pH->array[0] = pH->array[pH->size - 1];
pH->size--;
Heapify(pH->array, pH->size, 0);
return v;
}
测试代码
void Test() {
int array[] = { 9, 5, 7, 3, 8, 4, 2, 1, 0 };
int size = sizeof(array) / sizeof(int);
Heap heap;
HeapInit(&heap, array, size);
for (int i = 0; i < 3; i++) {
printf("%d\n", HeapPop(&heap));
}
printf("After pop\n");
for (int i = 0; i < 3; i++) {
HeapPush(&heap, i);
}
printf("After push\n");
int size2 = heap.size;
for (int i = 0; i < size2; i++) {
printf("%d\n", HeapPop(&heap));
}
}
堆的用途小结:
- 找最值:应用在动态找最值,多次有增减的情况下
O(lgN)
- 堆化
O(lgN)
- 建堆
O(N)
- 向上调整
O(lgN)
利用堆找最值的特点,现实具体应用:
- 优先级队列
TopK
问题. :给海量数据,个数为N
,找出其中最大的K
个
如果创建大堆会创建N
空间,麻烦费力。所以建小堆,数据与堆顶元素比较,如果满足条件替换了堆顶元素,进行一次堆化,更新堆顶元素,然后再处理之后的数据。- 堆排序
堆排序
冒泡排序是一个减治算法。
- 冒泡排序一次时间复杂度
O(N)
,一共要做N
次,所以排序时间复杂度为O(N2)。 - 堆排序一次时间复杂度
O(lgN)
,一共要做N
次,所以排序时间复杂度为O(N*lgN)。
找最大数,即排升序 →
建大堆
把最大值交换到最后一个元素,剩下的只有第一个不满足堆,做一次向下调整即可。
如果建小堆,进行一次交换,下一次就无法再以O(lgN)
的空间复杂度找到最大值了,会破坏堆结构,就要重新建堆了。
// 排降序,即找最小数,建小堆
// 如果建大堆,找出最大值后,会破坏堆结构
void HeapSort(int array[], int size) {
CreateHeap(array, size);
for (int i = 0; i < size; i++) {
int t = array[0];
array[0] = array[size - 1 - i];
array[size - 1 - i] = t;
Heapify(array, size - 1 - i, 0);
}
}
测试代码
void Test() {
int array[] = { 9, 4, 5, 7, 3, 8, 6, 2, 4, 0, 1, 7 };
int size = sizeof(array) / sizeof(int);
HeapSort(array, size);
printf("DESC order\n");
}