什么是堆?如何区分大堆和小堆?什么是向上调整和向下调整算法?如何实现堆排序?一篇文章带你秒杀考研数据结构堆。

堆简介

堆可以理解为一种完全二叉树,因此它可以存储在数组里。他的逻辑结构是一颗完全二叉树,物理结构是一个一维数组。

堆分类

堆被分为大堆和小堆

大堆

每一个子树都满足双亲大于两个孩子
在这里插入图片描述

小队

每个子树都满足双亲小于孩子
在这里插入图片描述
注意,堆是无序的,也就是左右可以互换

堆的数据结构

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a; //指针,指向一个数组,数组里面是HPDataType
	size_t size; //当前长度
	size_t capacity; //最大长
}HP;

堆的基本操作

创建堆

void HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

没啥好说的,基本操作[滑稽]

销毁堆

void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->size = hp->capacity = 0;
}

入堆

入堆有两步基本操作,先入堆,再向上调整。
在这里插入图片描述
以这个大堆为例,我们想插入一个60元素,第一步先把他放在堆中,也就是放在完全二叉树对应的数组的最后一个位置。随后进行向上调整。
先看60和他的双亲哪个大,如果双亲小于60则把60和双亲交换。
** 前面讲过 parent = (child-1)/2 ** 可以拿到双亲下标
第一次向上调整:
在这里插入图片描述
再拿60和他的双亲比较,发现60还大于双亲,再调整一次
在这里插入图片描述
再比较双亲,发现还是大于,再调整一次。
在这里插入图片描述
这时候发现已经到堆顶了,不能再调整,整个堆已经重新被我们调整成为大堆的形式。
据此我们总结出向上调整算法的具体步骤。

  1. 比较自己和双亲大小
  2. 根据大堆/小堆判断条件决定是否交换
  3. 当不满足交换条件或者到堆顶的时候结束向上调整算法。

代码实现:

void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->size == hp->capacity) {
		//堆满了
		int newCpacity = hp->capacity == 0 ? 4 : 2 * (hp->capacity);
		HPDataType* n = (HPDataType*)realloc(hp->a, newCpacity*sizeof(HPtataType);
		if (n == NULL) {
			exit(-1);
		}
		hp->a = n; //完成扩容
		hp->capacity = newCpacity;
	}
	(hp->a)[hp->size] = x;//先入堆
	int child = hp->size;
	hp->size++;
	while (child != 0) {
		int parent = (child - 1) / 2;
		if (hp->a[parent] < hp->a[child]) {
			//swap(parnt,child)
			int t = hp->a[parent];
			hp->a[parent] = hp->a[child];
			hp->a[child] = t;
			child = parent;//下标往上走
		}
		else {
			//不发生交换直接结束
			return;
		}
	}
}

删除堆顶数据

删除堆顶数据就是删除最小或最大数据,以大堆为例,删除大堆的堆顶,就是删除这个大堆的最大值。
在处理这个问题,我们往往采用堆顶堆底交换+向下调整算法
堆顶堆底交换很好理解,就是把数组第一个元素和最后一个元素交换,然后把最后一个元素拿出来即可。
在这里插入图片描述
将堆顶的50与堆底的15交换位置。然后把50Pop出去,得到如图所示的完全二叉树,下面要开始向下调整。
第一次向下调整,根据leftChild = parent2 +1 ; rightChild = parent2+2得到两个child,选出最大的那个,与parent交换,随后把parent赋给那个比较大的child上。(这时大堆调法,小堆只需要找到那个更小的即可)
在这里插入图片描述
第二次向下调整,25比20大,25与15交换
在这里插入图片描述
这时发现parent没有child了,结束向下调整。
因此我们总结向下调整的步骤

  1. 比较两个child哪个大(或小)
  2. 将child与更大的交换
  3. 看看child是否没有孩子,如果没有孩子就结束调整

代码实现(通俗易懂版):

void HeapPop(Heap* hp)
{
	HPDataType root = hp->a[0]; //拿到根
	hp->size--;
	hp->a[0] = hp->a[hp->size];//把最后一个放到根的位置
	int parent = 0; //
	while (parent < (int)hp->size) {
		int leftChild = parent * 2 + 1;
		int rightChild = parent * 2 + 2; //方便理解实际上不用这莫写
		leftChild = leftChild < hp->size ? leftChild : -1;//判断是否出界
		rightChild = rightChild < hp->size ? rightChild : -1;
		if (leftChild == -1 && rightChild == -1) {
			//没有孩子直接调完了
			break;
		}
		else if (rightChild == -1) {
			//右孩子为空,也是调完了
			//只需要最后一次交换
			swap(&(hp->a[parent]), &(hp->a[leftChild]));
			break;
		}
		else {
			if (hp->a[leftChild] >= hp->a[rightChild]) {
				//左孩子大,左孩子调上去
				swap(&(hp->a[parent]), &(hp->a[leftChild]));
				parent = leftChild;
			}
			else {
				//右孩子大,右孩子调上去
				swap(&(hp->a[parent]), &(hp->a[rightChild]));
				parent = rightChild;
			}
		}
	}
	return;
}

代码实现(高端版):

void HeapPop(Heap* hp)
{
	HPDataType root = hp->a[0]; //拿到根
	hp->size--;
	hp->a[0] = hp->a[hp->size];//把最后一个放到根的位置
	int parent = 0; //
	int child = parent * 2 + 1; //拿到左孩子
	while (child < hp->size) {
		if (child + 1 < hp->size && hp->a[child] < hp->a[child + 1]) {
			//右孩子存在且右孩子比左孩子大
			child++; //拿出哪个比较大的孩子
		}
		if (hp->a[child] > hp->a[parent]) {
			//孩子比父母大
			swap(&(hp->a[parent]), &(hp->a[child]));
			parent = child;
			child = parent * 2 + 1;
		}else {
			//根据堆的特新,一旦不换就结束了
			break;
		}
	}
}

取堆顶

取堆顶本质上是拿到最大或最小元素

HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	if (hp->size <= 0) {
		printf("empty Heap\n");
		assert(hp->size<=0);
	}
	//取堆顶,取堆顶本质上是拿到最小或最大的数据
	return hp->a[0];
}

和HeapPop接口构成了堆排序!

堆的元素个数

int HeapSize(Heap* hp)
{
	assert(hp);//hp指针不为空
	return hp->size;
}

判断堆是否为空

int HeapEmpty(Heap* hp)
{
	assert(hp);
	if (HeapSize(hp) != 0) {
		return 1;
	}
	else {
		return -1;
	}
}

堆排序

堆排序是堆的应用重点。
堆排序本质上很简单,他分为建堆,循环出堆两个操作。核心思想就是利用了大堆的堆顶是一串数字的最大值,小堆堆顶是一串数字的最小值。
因此原则上,把数字降序排列用到大堆,因为拿到的元素是从大到小的堆顶,但是不绝对,咋都可以实现。

代码实现:

void HeapSort(int* nums, int numSize)
{
	HP hp;
	HeapInit(&hp);//初始化堆
	//建堆
	for (int i = 0; i < numSize; i++) {
		HeapPush(&hp, nums[i]); //依次把数组中的数插到堆里面
	}
	//出堆
	int p = 0;//从0开始放,可以自己改变
	while (HeapSize(&hp) != 0) {
		//堆不为空
		nums[p] = HeapTop(&hp);
		HeapPop(&hp);
		p++;
	}
}

一些小问题

无论向上调整还是向下调整算法,他们本质上都是调整完全二叉树高度次,根据前面讲过的 h = log2(n-1) ,所以向上\下调整算法的时间复杂度是log(n)

而堆排序在建立堆的时候需要对n个元素都进行向上调整算法,所以时间复杂度是n*logn。

在这里插入图片描述

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
堆排序是一种基于数据结构的排序算法,其中包括向下调整向上调整两个关键步骤。 1. 向下调整(AdjustDown):在堆排序中,向下调整用于将一个元素下沉到合适的位置,以维持的性质。具体步骤如下: - 首先,将当前节点标记为根节点。 - 比较根节点与其左右子节点的值,找到最大(或最小)的节点。 - 如果根节点的值小于(或大于)最大(或最小)的子节点的值,则交换根节点与最大(或最小)子节点的值。 - 将当前节点更新为最大(或最小)子节点的位置,并重复上述步骤,直到当前节点不再有子节点或满足的性质。 2. 向上调整(AdjustUp):在堆排序中,向上调整用于将一个元素上浮到合适的位置,以维持的性质。具体步骤如下: - 首先,将当前节点标记为叶子节点。 - 比较叶子节点与其父节点的值,如果叶子节点的值大于(或小于)父节点的值,则交换叶子节点与父节点的值。 - 将当前节点更新为父节点的位置,并重复上述步骤,直到当前节点不再有父节点或满足的性质。 以下是一个示例代码,演示了堆排序中的向下调整向上调整的过程: ```python def adjust_down(arr, n, i): largest = i left = 2 * i + 1 right = 2 * i + 2 if left < n and arr[i] < arr[left]: largest = left if right < n and arr[largest] < arr[right]: largest = right if largest != i: arr[i], arr[largest] = arr[largest], arr[i] adjust_down(arr, n, largest) def adjust_up(arr, i): parent = (i - 1) // 2 if parent >= 0 and arr[parent] < arr[i]: arr[parent], arr[i] = arr[i], arr[parent] adjust_up(arr, parent) # 示例数据 arr = [4, 10, 3, 5, 1] n = len(arr) # 向下调整示例 adjust_down(arr, n, 0) print("向下调整后的结果:", arr) # 输出:[10, 5, 3, 4, 1] # 向上调整示例 arr.append(7) n += 1 adjust_up(arr, n-1) print("向上调整后的结果:", arr) # 输出:[10, 7, 3, 5, 1, 4] ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值