[数据结构] 二叉堆


如果把之前是链式存储的二叉树存入数组,变成顺序存储的方式,更多应用在完全二叉树。

已知根下标 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. 停止条件:
    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));
	}
}

堆的用途小结:

  1. 找最值:应用在动态找最值,多次有增减的情况下O(lgN)
  2. 堆化O(lgN)
  3. 建堆O(N)
  4. 向上调整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");
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

giturtle

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值