关于数据结构中的堆你知道多少?

what is the heap?

现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >=
) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值; 堆总是一棵完全二叉树。

堆可以是完全二叉树和满二叉树

什么是完全二叉树和满二叉树?
首先二叉树必须保证每个父亲结点的度最大是2,也就是父亲结点最多只有两个子节点,才能叫二叉树。
完全二叉树就是可以不是每个父亲结点都必须有俩个子节点,但是必须满足最下面的的叶子结点是连续的
而满二叉树则是每个父亲结点都必须有两个子节点
具体如下图:
在这里插入图片描述
堆可以是完全二叉树 也可以是满二叉树 完全二叉树和满二叉树对其结构丝毫没有影响

堆中父亲结点和子节点的关系(下标)

左孩子下标=父亲下标2+1
右孩子下标=父亲下标
2+2
父亲下标=(孩子下标-1)/ 2
具体如图:
在这里插入图片描述

大堆和小堆

大堆就是所有的父亲结点的值都得大于子结点的值 ;小堆就是所有的父亲结点的值都得小于子结点的值
在这里插入图片描述
大堆
在这里插入图片描述

向上调整堆

对于一个(大/小)堆 如果插入了一个数据后怎么才能使其结构不被破坏 还是(大/小)堆呢?

这里就需要向上调整堆了
思想:既然堆的物理结构是一个数组,那么插入就是在最后面插入(尾插效率高O(1),头插效率低下O(n)),那么插入的结点必定是子结点(叶子结点),这样我们就可以通过下标去找它的父亲跟父亲比较,按结构需要进行交换,然后在迭代着往上进行判断是否需要交换父亲结点和孩子结点即可。

在这里插入图片描述
代码实现:

//向上调整算法
void AdjustUP(void* hp, int child)
{
	HPDataType* p = (HPDataType*)hp;
	assert(hp);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (p[child] < p[parent])
		{
			swap(&p[child], &p[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}

}

向下调整堆

既然有向上调整堆,那肯定少不少向下调整堆,现在它来了!
假如给我们一个随机的数组,让我们把它建成堆,我们该怎么做?
这就需要向下调整堆了
请添加图片描述
这也是一个迭代的过程!
代码实现:

typedef int HPDataType;
struct Heap
{
	HPDataType* _a;
	size_t _size;
	size_t _capacity;
};

//向下调整算法
void AdjustDown(void* hp, int size, int parent)
{
	assert(hp);
	HPDataType* p = (HPDataType*)hp;
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size && p[child + 1] < p[child])
		{
			child++;
		}
		if (p[child] < p[parent])
		{
			swap(&p[child], &p[parent]);
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
		
	}
}

如何建堆

建堆就要用到向下调整算法,从最后一个父亲结点开始进行向下调整,直到父亲结点变成0或者已经是堆了就停止
借鉴一下图:
请添加图片描述

//堆的构造
void HeapCreat(Heap* hp, HPDataType* a, int n)//a是给的数组 拿这个数组的数据建堆
{
	assert(hp);
	
	HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) * n);
	if (tmp)
	{
		hp->_a = tmp;
		memcpy(hp->_a, a, sizeof(HPDataType) * n);
		for (int i = (n - 1 - 1) / 2; i >= 0; --i)
		{
			AdjustDown(hp->_a, n, i);//向下建堆
		}
		hp->_capacity = n;
		hp->_size = n;
	}
	else
	{
		printf("creat fail!!!");
		exit(-1);
	}
}

向堆插入元素(插入后还是大堆或者小堆)

我们知道插入数据是在最后面插入的,但是我们插入数据前堆是完整的,插入后堆就不一定还是堆了,可能结构就会被破坏了,所以我们要从插入的最后一个结点开始进行向上调整,使插入数据后堆的结构不被破坏。
插入前要判断是否需要扩容

//堆的插入 保证插入后还是堆
void HeapPushBack(Heap* hp, int val)
{
	assert(hp);
	if (hp->_size == hp->_capacity)
	{
		int newcapacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->_a, newcapacity * sizeof(HPDataType));
		if (tmp)
		{
			hp->_a = tmp;
			hp->_capacity = newcapacity;
		}
		else
		{
			printf("realloc fail!!!");
			exit(-1);
		}
	}
	hp->_a[hp->_size] = val;
	hp->_size++;
	//插入了数据之后还要保证是堆 所以要进行向上调整
	AdjustUP(hp->_a, hp->_size - 1);
}

在这里插入图片描述

获取堆顶元素

这步很简单 判断堆是否为空 非空就返回堆中_a的小标为0的数据即可

typedef int HPDataType;
struct Heap
{
	HPDataType* _a;
	size_t _size;
	size_t _capacity;
};
//取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	return hp->_a[0];
}

删除堆顶元素(删除后还是大堆或者小堆)

删除堆顶或许我们会觉得很简单就是把堆中_a的下标为0的元素删除即可,但是这样我们的堆的结构就被破坏了,这种方法显然不行;
这样想,既然是要删除元素那么元素个数总得减1吧,也就是等于最后一个元素会被舍弃掉,既然如此我们就可以把堆顶的元素和最后的元素进行交换,把要删掉的数据换到会被舍弃的位置,然后再对堆从下标0开始向下调整即可(这里只需要调整根节点的一边,另一边是不会动的,因为未插入数据前,两边都是符合堆的结构的)

代码实现:

//堆的头删 删除后还得是堆
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	swap(&hp->_a[0], &hp->_a[hp->_size - 1]);//交换堆顶元素和最后的元素
	hp->_size--;
	AdjustDown(hp->_a, hp->_size, 0);//从根节点开始往下调整,维持堆的结构

}

返回堆的元素个数

typedef int HPDataType;
struct Heap
{
	HPDataType* _a;
	size_t _size;
	size_t _capacity;
};

没什么好说的。直接返回_size即可

判断堆是否为空

typedef int HPDataType;
struct Heap
{
	HPDataType* _a;
	size_t _size;
	size_t _capacity;
};
//堆的判空
bool HeapEmpty(Heap* hp)
{
	assert(hp);
	return hp->_size == 0;
}

直接返回堆中的_size是否为0

堆的销毁

释放堆中_a指向的空间

//堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	free(hp->_a);
	hp->_a = nullptr;
}

堆排序思想

建堆虽然不能使序列直接就是有序的,但是可以使父子结点间的关系是确定的,如果建的是大堆,那么根结点一定是堆中最大的;如果建的是小堆,那么根结点一定是堆中最小的。

根据上面的推断,我们可以引申到排序的思想,建好堆取出堆顶元素(最大或最小),与最后一个元素交换位置,把最大的或最小的元素放到最后(这样最大或最小的元素就被我们选出来了),再让堆的元素个数减1把最大或最小的不包括在内,再从根节点开始往下调整建堆(此时的堆的元素个数相比上次少了1(最大或最小的元素在上次操作中移出了堆)),那么我们就又可以挑选出当前堆中最大的或最小的元素,把它与最后一个元素交换位置,元素个数减1,再从根节点开始往下调整建堆;如此往复就可以实现堆中的元素是按照 最大(小)的->次大(小)的->…->最小(大)的顺序排列的,也就是实现了堆排序。

注:如果是要排升序就建大堆,排降序就建小堆

动图演示:
排升序
请添加图片描述

经典的Top K问题解析

给一组数据(海量数据)挑选出前k个最大的数据
这个问题的解法有很多种,但是最优解是用建堆的思想
思想:既然是要前k个最大的,那么我们可以随意拿其中k个数据出来建小堆(保证了上面是小的,下面是大的),再去遍历数据,如果有比堆顶大的元素就删除堆顶数据,再把数据中比堆顶数据大的数据插入到堆中,完成这组操作后,堆的结构没有被破坏(删除和插入中都有自动调整的功能,会保证堆的结构不被破坏),这样只要遍历完了数据,我们建的只有k个数据的小堆就会是全部数据中最大的前k个了。

代码实现:

void TestTopK()
{
	int n = 100000;
	int* a = (int*)malloc(sizeof(int) * n);
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
		a[i] = rand() % 1000000;
	}
	a[343] = 1000004;
	a[4532] = 1000009;
	a[432] = 1000030;
	a[532] = 1000013;
	a[1532] = 1000099;
	a[4000] = 1000119;
	a[10029] = 1000899;
	a[34532] = 1000049;
	a[94532] = 1000001;
	a[64532] = 1000044;
	PrintTopK(a, n, 10);

}
//top K问题
void PrintTopK(int* a, int n, int k)
{
	assert(a);
	Heap hp;
	HeapCreat(&hp, a, k);
	for (int i = k; i < n; i++)
	{
		if (a[i] > HeapTop(&hp))
		{
			HeapPop(&hp);
			HeapPushBack(&hp, a[i]);
		}
	}
	HeapPrint(&hp);
	HeapDestory(&hp);
	
}

在这里插入图片描述
在这里插入图片描述

  • 43
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 38
    评论
评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值