优先队列之二叉堆与d-堆

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

二叉堆简介

平时所说的堆,若没加任何修饰,一般就是指二叉堆。同二叉树一样,堆也有两个性质,即结构性和堆序性。正如AVL树一样,对堆的以此操作可能破坏者两个性质中的一个,因此,堆的操作必须要到堆的所有性质都被满足时才能终止。

结构性质

堆是一棵完全填满的二叉树,因为完全二叉树很有规律,所以它可以用一个数组表示而不需要指针。如下图所示,图2中的数组对应图1中的堆。

                            

图1:二叉堆                                                                                   图2:二叉堆的数组存储

对于任意位置i上的元素,其左儿子在位置2i处,右儿子在位置2i+1处,而它的父亲在i/2。因此,不仅指针这里不需要,而且遍历该树所需要的操作也十分简单。这种表示法的唯一问题在于:最大的堆大小需要事先估计,但对于典型的情况者并不成问题,图2中堆的大小是13个元素。该数组有一个位置0,用做哨兵,后面会有阐述。

因此,一个堆的数据结构将由一个数组,一个代表最大值的整数以及当前堆的大小组成。代码如下:

struct HeapStruct
{
	int Capacity;
	int Size;
	ElementType *Elements;
};

堆序性质

使操作能快速执行的性质是堆序性。在一个堆中,对于每个节点X,X的父亲中的关键字小于(或等于)X中的关键字,根节点除外(根节点没有父亲)。图3中,左边的是堆,右边的不是(虚线表示堆序性质被破坏)。

        图3:两棵完全二叉树

基本操作

Insert(插入):

为了将一个元素X插入到堆中,我们在下一个空闲位置创建一个空穴,否则该堆将不是完全树。如果X可以放入到该空穴中,那么插入完成。否则,我们把空穴的父节点上的元素移入该空穴中,这样,空穴就朝着根的方向上行一步。继续该过程直到X能被放入到空穴中为止。图4表示,为了插入14,我们在堆的下一个可用位置建立一个空穴,由于将14插入空穴破坏了堆序性质,因此将31移入该空穴,图5继续这种策略,直到找到14的正确位置。

               

 图4:创建一个空穴,再将空穴上冒                                       图5:将14插入到前面的堆中的其余两步

这种策略叫做上虑。新元素在堆中上虑直到找出正确的位置;使用如下代码,很容易实现。

void Insert( ElementType X, PriorityQueue H )
{
	if (IsFull(H)){
		printf("Priority queue is full!");
		return;
	}
	int i;
	for (i = ++H->Size; H->Elements[i/2] > X; i /= 2){
		H->Elements[i] = H->Elements[i/2];
	}
	H->Elements[i] = X;
}

如果要插入的元素师新的最小值,那么它将一直被推向顶端,这样在某一时刻,i将是1,我们就需要令程序跳出while循环。当然可以通过明确的测试做到这一点。不过,这里采用的是把一个很小的值放到位置0处以使while循环终止,这个值必须小于堆中的任何值,称之为标记或哨兵。这类似于链表中头结点的使用。通过添加的这个标记,避免了每次循环都要执行一次测试,这是简单的空间换时间策略。

DeleteMin(删除最小元):

找出最小元是很容易的;困难的部分是删除它。当删除一个最小元时,在根节点处产生了一个空穴。由于现在堆少了一个元素,因此对中最后一个元素X必须移到该堆的某个地方。如果X可以被放入空穴中,那么DeleteMin完成。不过这一般都不可能,因此我们将空穴的两个儿子中较小者移入空穴中,这样就把空穴向下推了一层,重复该步骤,知道X可以被放入空穴中。因此,我们的做法是将X置入沿着从根开始包含最小儿子的一条路径上的一个正确的位置。

图6显示DeleteMin之前的堆,删除13之后,我们必须要正确的将31放到堆中,31不能放在空穴中,因为这将破坏堆序性质,于是,我们把较小的儿子14置入空穴,同时空穴向下滑一层,重复该过程,把19置入空穴,在更下一层上建立一个新的空穴,然后26置入空穴,在底层又建立一个新的空穴,最后,我们得以将31置入空穴中。这种策略叫做下虑。

                    

图6 在根处建立空穴                                                             图7:将空穴下滑一层 

 

 图8:空穴移到底层,插入31

代码实现

头文件:binheap.h

typedef int ElementType;

#ifndef _BINHEAP_H
#define _BINHEAP_H

struct HeapStruct;
typedef struct HeapStruct *PriorityQueue;

PriorityQueue Initialize( int MaxElements );
void Destroy( PriorityQueue H );
void MakeEmpty( PriorityQueue H );
void Insert( ElementType X, PriorityQueue H );
ElementType DeleteMin( PriorityQueue H );
ElementType FindMin( PriorityQueue H );
int IsEmpty( PriorityQueue H );
int IsFull( PriorityQueue H );

#endif

实现文件:binheap.c

#include "binheap.h"
#include <stdlib.h>
#include <stdio.h>

#define MinPQSize 10
#define MinData -32767	

struct HeapStruct
{
	int Capacity;
	int Size;
	ElementType *Elements;
};

PriorityQueue Initialize( int MaxElements )
{
	if (MaxElements < MinPQSize){
		printf("Priority queue is too small!\n");
		return NULL;
	}
	PriorityQueue H;
	H = malloc(sizeof(struct HeapStruct));	//为整个堆分配空间,其实大小只有12字节
	if (H == NULL){                         //两个int共8个字节,加上指针4个字节
		printf("Out of space!");
		return NULL;
	}
	H->Elements = malloc(sizeof(ElementType) * (MaxElements + 1));   //为该指针指向的数组分配空间
	if (H->Elements == NULL){
		printf("Out of space!");
		return NULL;
	}
	H->Capacity = MaxElements;
	H->Size = 0;
	H->Elements[0] = MinData;	//留出一个元素,作为哨兵;
	return H;
}

void MakeEmpty( PriorityQueue H )
{
	H->Size = 0;
}

void Insert( ElementType X, PriorityQueue H )
{
	if (IsFull(H)){
		printf("Priority queue is full!");
		return;
	}
	int i;	//若位置0没有存放标记,那么for循环的判断条件该改为:H->Elements[i/2] > X && i != 0
	for (i = ++H->Size; H->Elements[i/2] > X; i /= 2){	//遍历所有非树叶节点(在位置i/2处),比较它的值与X的大小
		H->Elements[i] = H->Elements[i/2];              //若X大于它,表明X可以作为它的新孩子,否则,就进行上虑
	}
	H->Elements[i] = X;
}

ElementType DeleteMin( PriorityQueue H )
{
	if (IsEmpty(H)){
		printf("Priority queue is full!");
		return H->Elements[0];
	}
	int i, child;
	ElementType MinElement, LastElement;
	MinElement = H->Elements[1];
	LastElement = H->Elements[H->Size--];

	for (i = 1; i * 2 < H->Size; i = child){		//还是遍历所有的非树叶节点
		child = i * 2;
		if (child != H->Size && H->Elements[child] > H->Elements[child+1])
			++child;								//找出每个节点的最小儿子
		if (LastElement > H->Elements[child])       //若最后一个值比这个最小儿子还要小,表明它可以放入空穴中,
			H->Elements[i] = H->Elements[child];    //作为这个最小儿子的新父亲,若比它大,表明最后一个
		else                                        //值不能放入这个空穴中,空穴下移
			break;
	}
	H->Elements[i] = LastElement;
	return MinElement;
}

ElementType FindMin( PriorityQueue H )
{
	if (!IsEmpty(H)){
		return H->Elements[1];	//位置0存放的是标记,值为-32767
	}
	printf("Priority queue is Empty!");
	return H->Elements[0];
}

int IsFull( PriorityQueue H )
{
	return H->Size == H->Capacity;
}

int IsEmpty( PriorityQueue H )
{
	return H->Size == 0;
}

void Destroy( PriorityQueue H )
{
	free(H->Elements);
	free(H);
}

d-堆

二叉堆因为实现简单,因此在需要优先队列的时候几乎总是使用二叉堆。d-堆是二叉堆的简单推广,它恰像一个二叉堆,只是所有的节点都有d个儿子(因此,二叉堆又叫2-堆)。下图表示的是一个3-堆。注意,d-堆要比二叉堆浅得多,它将Insert操作的运行时间改进为 。然而,对于大的d,DeleteMin操作费时得多,因为虽然树浅了,但是d个儿子中的最小者是必须找到的,如果使用标准算法,将使用d-1次比较,于是将此操作的时间提高到 。如果d是常数,那么当然两种操作的运行时间都为 O(logN)。虽然仍可以使用一个数组,但是,现在找出儿子和父亲的乘法和除法都有个因子d,除非d是2的幂,否则会大大增加运行时间,因为我们不能再通过二进制移位来实现除法和乘法了。D-堆在理论上很有趣,因为存在许多算法,其插入次数比删除次数多得多,而且,当优先队列太大不能完全装入内存的时候,d-堆也是很有用的,在这种情况下,d-堆能够以与B-树大致相同的方式发挥作用。

除了不能执行Find操作外(指以对数执行),堆的实现最明显的两个缺点是:将两个堆合并成一个堆是很困难的。这种附加的操作叫做Merge。存在许多实现堆的方法使得Merge操作的运行时间为O(logN),如下篇介绍的左式堆。



算法自学笔记:二叉堆实现优先队列 二叉堆是一种特殊的二叉树,满足两个条件: 1 二叉堆完全平衡,就是说除了最底层,上面都完全填满 2 二叉堆节点有序,对于每一个父节点都大于等于子节点。 尽管书上一般用图表示二叉堆,但是在计算机里实现二叉堆并不需要真正构造节点和边,而是构造一个数组,每个元素代表节点,而索引之间代数关系代表链接。 二叉堆的性质: 1 a[1](根节点)为最大值 2 使用数组索引在上移动: 对于k索引节点: 父节点 k / 2 子节点2k 2k + 1 ... 阅读详情

相关推荐

d-heap

child(i,j) = d*(i-1)+2+j parent(i) = (i-2+d)/d

peanWang的博客 1952

d-,左式及斜的简单介绍

本文简单介绍了三种结构:d-、左式和斜

2402_86950791的博客 444

D-的C++实现

D-

naturerun的博客 299

各种——二叉堆,d,左式,斜,二项队列,斐波那契

二叉堆 二叉堆就是一个完全二叉树。几乎在所有需要用到优先队列的时候,使用它就完事了。 D- D就是一个完全d叉树。所以,d会比二叉堆浅的多。 左式 Clark Allan Crane,1972年发明。叫这个名字地原因就是这个树左边比右边高。 零路径长:从节点X到一个没有两个子节点的(有一个子节点或没有子节点)节点的最短距离 左式首先是一个二叉查找树。附加一个条件 要求任一节点的左子节点...

define_us的专栏 1406

d-

《数据结构算法分析——c语言描述》 第六章代码和二叉堆差不多,有意思的是父亲,儿子编号的推导。第i行,第j行节点(均从1开始)的编号推导: 前面i-1行的节点加起来:1+d1+d2+d3+……+di−2=1−di−11−d 1+d^1+d^2+d^3+……+d^{i-2}=\dfrac{1-d^{i-1}}{1-d} 然后加上剩下的,就是1−di−11−d+j\dfrac{1-d^{i-1}

wwj_bupt 1344

优先队列——二叉堆、d、左式、斜

完整的PPT下载:http://download.csdn.net/detail/sangni007/4850483   实现优先队列结构主要是通过完成,主要有:二叉堆、d、左式、斜、二项、斐波那契、pairing 等。     1. 二叉堆   1.1. 定义 完全二叉树,根最小。 存储时使用层序。   1.2. 操作 (1). insert(上滤)

nashouat个人空间 2095

结构之美——优先队列基本结构(四)——二叉堆、d、左式、斜

实现优先队列结构主要是通过完成,主要有:二叉堆、d、左式、斜、二项、斐波那契、pairing 等。   1. 二叉堆   1.1. 定义 完全二叉树,根最小。 存储时使用层序。   1.2. 操作 (1). insert(上滤) 插入末尾 26,不断向上比较,大于26则交换位置,小于则停止。   (2). del

Aiphis的博客 695

数据结构--二叉堆、d、左式和斜

本文是转载,出自:http://blog.csdn.net/yangtrees/article/details/8252760点击打开链接 实现优先队列结构主要是通过完成,主要有:二叉堆、d、左式、斜、二项、斐波那契、pairing 等。   1. 二叉堆   1.1. 定义 完全二叉树,根最小。 存储时使用层序。   1.2.

never give up的博客 1571

优先队列的实现

实现优先队列结构主要是通过完成,主要有:二叉堆、d、左式、斜、二项、斐波那契、pairing 等。 1. 二叉堆 1.1. 定义 完全二叉树,根最小。 存储时使用层序。 1.2. 操作 (1). insert(上滤) 插入末尾 26,不断向上比较,大于26则交换位置,小于则停止。 (2). deleteMin(下滤) 提取末尾元素,放在...

weixin_34417814的博客 160

数据结构:优先队列

操作系统进行任务调度: (1)如果任务数量是固定的,不需要制作新的数据结构来处理,可以按照优先级进行排序然后执行,这个过程需要的是一个排序算法而不是优先队列 (2)实际中需要对源源不断的任务进行优先级排序并处理,不能在一开始就确定需要处理多少个任务,需要使用优先队列来解决 (3)在实现优先队列时不用管什么是优先级高的 对数据结构来说,如果有一项是O(n)复杂度的话,进行n个元素的操作,整个过程的时间复杂度是O(n2),相对耗时 设计一个抽象的数据结构,队列本身是一个抽象的数据结构,在这个基础上限制它的性.

weixin_44333294的博客 412

数据结构之优先队列剖析

数据结构之优先队列剖析 文章目录数据结构之优先队列剖析概述原理时间复杂度分析相关的更多话题:实现基于数组实现最大基于数组实现最小使用实现优先队列LeetCode 347. Top K Frequent Elements 前K个高频元素牛客网-CD80-随时找到数据流的中位数相关链接公众号参考 概述 在计算机的世界里,很多的应用场景只需要取得当前数据集中最大或者最小的元素,而对于数据集中其它数据,并不需要他们一定是有序的。 那么,我们如何高效快速地取得当前数据集中最大或者最小的元素呢?此时,新

chen 604

数据结构原理高效实现详解

(Heap)作为完全二叉树的特殊实现,是计算机科学中重要的基础数据结构。其核心特性在于序性质:最大保证父节点值大于等于子节点,最小则相反。这种特性使成为优先队列的理想实现方式,时间复杂度稳定在O(log n)。通常采用数组存储,通过数学关系定位节点,既节省指针开销又保持数据紧凑性。在工程实践中,结构广泛应用于排序、TopK问题求解等场景,特别是在处理海量数据时,基于的解决方案能显著降低内存消耗。现代系统如任务调度、Dijkstra算法等都依赖的高效操作,而多叉堆、缓存优化等进阶技术进一步

weixin_30431445的博客 242

第6章 排序

6.1 6.1-1 在高度为h的中,元素个数最多是,最少是。 6.1-2 证明:。 6.1-3 证明:在最大中,某个结点的值至多其父结点一样大。经过递推易得,结点的值小于等于它的任一祖先。所以,在最大的任一子树中,该子树所包含的最大元素在该子树的根结点上。 6.1-4 假设一个最大的所有元素都不相同,那么该的最小元素应该位于它的叶结点。 6.1-5 一个已排好序...

江湖乱飘 3255

数据结构之优先队列

目录 优先队列 为什么使用优先队列优先队列不同底层实现复杂度分析 ​的基本结构 ​实现一个底层数组的最大中添加元素和Sift Up 取出中的最大元素和Sift Down 基于优先队列 优先队列的经典问题 LeetCode347 d 叉堆 d-ary heap(用的不多) ​索引(了解)二项(了解)斐波那契(了解)广义队列 优先队列 普通队列...

LifeBackwards的专栏 357

Algorithms_4th学习:优先队列 -- 上的算法 (Algorithms on Heaps)

问题回答队列里有多少个元素?最多NNN个(流的数量),不是MMM个(总元素数)每个元素进出队列几次?恰好各1次: push 1次, pop 1次怎么知道从哪个流补充?IndexedItem 的 streamIndex 字段记录了来源怎么保证输出有序?每个流本身有序 + 每次取全局最小 = 输出有序pos 数组干什么?记录每个流的读取进度, 知道下一个该读哪个流读完了怎么办?pos[i] >= size 时不补充, 队列自然缩小总时间复杂度?OMlog⁡NOM。

虾球 363
上一篇: 散列表
下一篇: 左式堆
红桃Ace
博客等级 码龄15年 5粉丝 16原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值