堆 HEAP
1 优先队列
- 优先队列(Priority Queue):特殊的“队列” ,取出元素的顺序是依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
- 优先队列的几种实现方式
- 数组
- 插入:元素总是插入尾部 O(1)
- 删除
- 查找最大(或最小)关键字 O(n)
- 从数组中删去需要移动的元素 O(n)
- 链表
- 插入:元素总是插入链表的头部 O(1)
- 删除
- 查找最大(或最小)关键字 O(n)
- 删除结点 O(1)
- 有序数组
- 插入
- 找到合适的位置 O(n)
- 移动元素,插入 O(n)
- 删除:删除最后一个元素 O(1)
- 插入
- 有序链表
- 插入
- 找到合适的位置 O(n)
- 插入元素 O(1)
- 删除:删除首元素 or 最后一个元素 O(1)
- 插入
- 二叉树
- 搜索二叉树:由于优先队列的特性,结点的插入和删除,可能将树变斜,使得 h ! = l o g 2 n h != log_2n h!=log2n 查找效率相对低
- 完全二叉树:一般而言,使用完全二叉树实现优先队列
- 数组
- 优先队列的完全二叉树表示
2 堆
- 堆的两个特性
- 结构性:用数组表示的完全二叉树;
- 有序性:任一结点的关键字是其子树所有结点的最大值(或最小值)
- “最大堆(MaxHeap)” ,也称“大顶堆”:最大值
- “最小堆(MinHeap)” ,也称“小顶堆” :最小值
- 堆的举例
- isHeap:注意从根节点到任意路径上结点序列的有序性
- isNotHeap
- isHeap:注意从根节点到任意路径上结点序列的有序性
3 堆的抽象数据类型描述(以最大堆为例)
类型名称:最大堆(MaxHeap)
数据对象集:完全二叉树,每个结点的元素值不小于其子结点的元素值
操作集:最大堆 H ∈ MaxHeap,元素item ∈ ElementType,主要操作有:
- MaxHeap Create( int MaxSize ):创建一个空的最大堆
- Boolean IsFull( MaxHeap H ):判断最大堆H是否已满
- Insert( MaxHeap H, ElementType item ):将元素item插入最大堆H
- Boolean IsEmpty( MaxHeap H ):判断最大堆H是否为空
- ElementType DeleteMax( MaxHeap H ):返回H中最大元素(高优先级)
4 最大堆相关操作实现
- 最大堆数据类型定义
- 注意
- 数组下标为 1 的位置存储的是堆的第一个元素
- 数组下标为 0 的位置存储一个极大值(比堆中所有元素都大),充当哨兵的作用,从而在关于堆的其他操作的实现过程中提供一个循环结束的边界条件
typedef int ElementType; // 定义堆元素的数据类型 typedef struct HeapStruct { ElementType *Elements; // 存储堆元素的数组 int Size; // 堆当前元素的个数 int Capacity; // 堆的最大容量 } HeapStruct; // typedef HeapStruct* MaxHeap; // MaxHeap 指向一个最大堆
- 注意
- 空堆的创建
- 注意:把 MaxData 换成小于堆中所有元素的 MinData,则以下代码同样适用于创建最小堆
MaxHeap Create(int MaxSize) { MaxHeap H = (MaxHeap)malloc(sizeof(HeapStruct)); H->Elements = (ElementType *)malloc(sizeof(ElementType) * (MaxSize + 1)); H->Size = 0; H->Capacity = MaxSize; H->Elements[0] = MaxData; // 定义“哨兵”为一个值,其大于堆中所有可能元素的值 return H; }
- 最大堆结点的插入
- 算法实现思路
- 将待插入结点插入到堆的尾部
- 将待插入结点和它的父节点比较,如果大于则交换,继续循环比较,小于则退出循环
- 算法分析
- 由于“哨兵”的存在,使得待插入结点最多存放到 Elements[1] 的位置
H->Elements[i] = H->Elements[i / 2];
直接过滤结点(更新子节点为父节点内容,并且更新待插入结点的位置)- 插入算法实现 T(n) = O(logN)
- 完全二叉树的性质 review (数组下标为 1 的位置,存储的是二叉树的第一个结点)
- f a t h e r ( i ) = ⌊ i / 2 ⌋ , i > 1 father(i) = \lfloor i/2 \rfloor,\ \ \ \ i>1 father(i)=⌊i/2⌋, i>1
- l e f t C h i l d ( i ) = 2 i , 2 i < = n leftChild(i) = 2i,\ \ \ \ 2i<=n leftChild(i)=2i, 2i<=n
- r i g h t C h i l d ( i ) = 2 i + 1 , 2 i + 1 < = n rightChild(i) = 2i+1,\ \ \ \ 2i+1<=n rightChild(i)=2i+1, 2i+1<=n
void Insert(MaxHeap H, ElementType item) { /* 将元素 item 插入最大堆 H,其中 H->Elements[0] 已经定义为哨兵 */ /* H->Elements[0] 是哨兵元素,不小于堆中的最大元素,控制循环结束 */ if (Full(H)) // heap is full,then 无法插入元素 return; int i; // i 用于记录待插入结点最终在二叉树中的位置 /* 初始化 i 指向插入后堆中的最后一个元素的位置 */ /* item > H->Elements[i / 2] 判断待插入结点和其父节点的大小关系 如果 item 更大,则将父节点移动到当前待插入结点的位置,同时更新子节点的位置 i */ for (i = ++H->Size; item > H->Elements[i / 2]; i /= 2) H->Elements[i] = H->Elements[i / 2]; // 过滤结点 H->Elements[i] = item; // 将 item 插入到应有的位置 }
- 算法实现思路
- 最大堆结点的删除
- 算法实现思路
- 取出最大元素结点(即根节点)
- 将当前堆的最后一个结点放到根节点的位置(其实本质上是通过 i 来记录其位置),这个结点记为 temp
- 通过 temp 和儿子结点的比较,进行结点位置的更新,如果 temp 比左右儿子都小,则用最大的儿子和 temp 进行位置交换,以此类推
- 循环结束
- 当 temp 比左右儿子的值都大时
- 或者当 temp 没有儿子结点的时候
- 算法分析:删除算法实现 T(N) = O(log N)
ElementType DeleteMax(MaxHeap H) { /* 从最大堆 H 中取出键值最大的元素,并删除一个结点 */ if (Empty(H)) // heap is empty,then 无法删除元素 return; int Parent, Child; // 分别表示父节点和子节点的数组下标 ElementType maxItem = H->Elements[1]; // 取出根节点的最大值 ElementType temp = H->Elements[H->Size--]; // 用最大堆中最后一个元素从根节点开始过滤下层结点 /* 如果 temp 要和儿子比较,前提是要有儿子 */ /* Parent * 2 <= H->Size 表示至少有左儿子 */ /* 表示更新 Parent 的位置 */ for (Parent = 1; Parent * 2 <= H->Size; Parent = Child) { Child = Parent * 2; // if (Parent * 2 <= H->Size + 1) // 表示有右儿子 // Child = H->Elements[Child] > H->Elements[Child + 1] ? Child : Child + 1; /* Child != H->Size 表明 Parent 位置的结点有右儿子 */ if ((Child != H->Size) && (H->Elements[Child] < H->Elements[Child + 1])) Child++; // 更新 Child 为 Parent 位置结点的儿子中值最大的哪个 // 即 Child 指向左右子节点的较大者 /* 如果 Parent 位置的结点的值 < 它最大的儿子的结点值,则将儿子结点拉上来做父节点,Parent 更新到对应的儿子节点的位置 */ if (temp < Child) // 移动 temp 元素到下一层 H->Elements[Parent] = H->Elements[Child]; else break; } H->Elements[Parent] = temp; return maxItem; }
- 算法实现思路
- 最大堆的建立
- 建立最大堆:将已经存在的 N 个元素按最大堆的要求存放在一个一维数组中
- 实现方法
- 方法1:通过插入操作,将 N 个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为 O(NlogN)
- 方法2:在线性时间复杂度下建立最大堆。
- 将 N 个元素按输入顺序存入,先满足完全二叉树的结构特性
- 调整各结点位置,以满足最大堆的有序特性
- 实现思路
- 先将 N 个元素按输入顺序存入
- 从倒数第一个有儿子的结点开始,记为 X
- 通过操作,使得该结点对应的是一个堆,记该结点上一个结点是 X’
- 通过操作,使得 X’ 结点对应的是一个堆
- 以此类推,直到根节点
- 根据上述代码实现分为两部分
- 第一部分:实现从最后一个有儿子的结点到根节点的遍历
- 第二部分:实现对每一个结点对应的子树过滤成堆(可以直接套用“ 堆删除元素 ”的几乎全部代码实现)
/* 根据的完全二叉树建立一个堆 */ /* 向下过滤,H 是一个二叉树,其以 H->Elements[p] 为根节点的子树的子树是堆。 通过向下过滤,使得 H->Elements[p] 对应的子树也变成堆 */ /* 传入的参数 H 确保是一颗完全二叉树,并且对应子树 H->Elements[p] 的对应的子树都是堆 */ void FilterDown(MaxHeap H, int p) { int Parent, Child; // 分别表示父节点和子节点的数组下标 ElementType temp = H->Elements[p]; for (Parent = 1; Parent * 2 <= H->Size; Parent = Child) { Child = Parent * 2; /* Child != H->Size 表明 Parent 位置的结点有右儿子 */ if ((Child != H->Size) && (H->Elements[Child] < H->Elements[Child + 1])) Child++; // 更新 Child 为 Parent 位置结点的儿子中值最大的哪个 // 即 Child 指向左右子节点的较大者 /* 如果 Parent 位置的结点的值 < 它最大的儿子的结点值,则将儿子结点拉上来做父节点,Parent 更新到对应的儿子节点的位置 */ if (temp < Child) // 移动 temp 元素到下一层,即向下过滤 H->Elements[Parent] = H->Elements[Child]; else break; } H->Elements[Parent] = temp; } /* 建立二叉树,根据传入的完全二叉树,调整使得其满足最大堆的有序性质,转换为最大堆 */ void BuildHeap(MaxHeap H) { /* 从最后一个有孩子的结点开始(或者说是最后一个结点的父节点),遍历到第一个结点 */ for (int i = H->Size / 2; i > 0; i++) FilterDown(H, i); }
- 其他代码实现
/* 堆已满或为空的判断 */ bool Full(MaxHeap H) { return H->Size == H->Capacity; } bool Empty(MaxHeap H) { return H->Size == 0; }
5 问题讨论
-
“哨兵”是在创建堆(Create 函数)时设置的:H->Elements[0] = MaxData?【√】
-
有个堆,其元素在数组中的序列为:58、25、44、18、10、26、20、12,如果调用 DeleteMax 函数删除其最大值函数,则程序中的 for 循环刚退出时变量 Parent 的值为?【6】
-
建堆时,最坏情况下需要挪动元素次数是的等于树中各结点的高度和。问,对于元素个数为 12 的堆,其结点的高度和是多少?【10】
-
在最大堆 {97,76,65,50,49,13,27} 中插入 83 后,该最大堆为 【{97,83,65,76,49,13,27,50}】
-
对由同样 n 个整数构成的二叉搜索树和最小堆,
- 正确的说法有
- 【二叉搜索树高度 >= 最小堆的高度】
- 【对该二叉搜索树进行中序遍历可以得到从小到大的序列】
- 【从最小堆根节点到其他任何叶节点的路径上的结点值构成从小到大的序列】
- 错误的说法有
- 【对该最小堆进行层序遍历可得到从小到大的序列】
- 正确的说法有