二项队列的介绍
二项队列是一种基于二项树集合实现的优先队列,相对于左式堆,它的合并操作更高效,时间复杂度为O(LogN)。
二项队列由多个二项树组成,每个二项树都满足最小堆序列。比如一个由7个元素构成的二项队列,那它就由节点数分别为1,2,4的项树组成。(这种情况和二进制数很有关系)
二项树的结构里有两个指针,一个指向子节点,一个指向兄弟节点,树的节点个数必须是2的n次方,二项树的结构和前面讲第一篇讲树的文章中树的结构差不多,一个节点可以有多个子节点,一个节点的子节点由其子节点和子节点的兄弟节点(兄弟节点还能有兄弟节点)构成。
更详细的介绍如下,算法由豆包生成。
二项队列的实现
类型声明
cpp
typedef int ElementType;
// 最大二项树阶数(MaxTrees=10 支持最多 2^10=1024 个节点)
#define MaxTrees 10
// 二项树节点结构前向声明
struct BinNode;
// 二项树节点指针类型
typedef struct BinNode *Position;
// 二项树类型(指向根节点的指针)
typedef Position BinTree;
// 二项队列集合结构前向声明
struct Collection;
// 二项队列类型(指向集合的指针)
typedef struct Collection *BinQueue;
// 二项树节点结构(左孩子-右兄弟表示法)
struct BinNode {
ElementType Element; // 节点存储的元素
Position LeftChild; // 左孩子指针
Position NextSibling; // 右兄弟指针
};
// 二项队列集合结构
struct Collection {
int CurrentSize; // 队列中总节点数
BinTree TheTrees[MaxTrees]; // 存储各阶二项树的数组,TheTrees[k]表示k阶二项树
};
二项队列由一个数组构成,数组中每个元素都是一个二项树。如图所示:
简单函数操作
cpp
static void FreeTree(Position T) {
if (T != NULL) {
FreeTree(T->LeftChild); // 先释放左孩子子树
FreeTree(T->NextSibling); // 再释放右兄弟子树
free(T); // 释放当前节点
}
}
BinQueue Initialize(void) {
BinQueue Q = (BinQueue)malloc(sizeof(struct Collection));
if (Q == NULL) {
fprintf(stderr, "内存分配失败:无法创建二项队列!\n");
exit(EXIT_FAILURE);
}
Q->CurrentSize = 0;
// 初始化所有阶数的二项树为空
for (int i = 0; i < MaxTrees; i++) {
Q->TheTrees[i] = NULL;
}
return Q;
}
int IsEmpty(BinQueue Q) {
return Q->CurrentSize == 0;
}
void MakeEmpty(BinQueue Q) {
for (int i = 0; i < MaxTrees; i++) {
FreeTree(Q->TheTrees[i]);
Q->TheTrees[i] = NULL;
}
Q->CurrentSize = 0;
}
void Destroy(BinQueue Q) {
MakeEmpty(Q);
free(Q);
}
合并操作
cpp
// 合并两棵同阶的二项树(遵循最小堆序性)
static BinTree CombineTrees(BinTree T1, BinTree T2) {
// 保证T1的根节点值更小,作为新树的根
if (T1->Element > T2->Element) {
BinTree temp = T1;
T1 = T2;
T2 = temp;
}
// 将T2作为T1左孩子的右兄弟,维持二项树结构
T2->NextSibling = T1->LeftChild;
T1->LeftChild = T2;
return T1;
}
// 合并两个二项队列(核心操作,返回新的合并队列)
BinQueue Merge(BinQueue Q1, BinQueue Q2) {
// 检查合并后节点数是否超过最大容量
if (Q1->CurrentSize + Q2->CurrentSize > (1 << MaxTrees) - 1) {
fprintf(stderr, "队列合并失败:总节点数超过最大限制!\n");
exit(EXIT_FAILURE);
}
BinQueue Q = Initialize();
Q->CurrentSize = Q1->CurrentSize + Q2->CurrentSize;
BinTree T1, T2, Carry = NULL; // Carry存储合并产生的进位二项树
// 按二项树阶数遍历,类似二进制加法处理合并
for (int i = 0, j = 1; j <= Q->CurrentSize; i++, j *= 2) {
T1 = Q1->TheTrees[i];
T2 = Q2->TheTrees[i];
// 根据T1、T2、Carry的存在情况处理(共8种组合)
switch ((T1 != NULL) + 2 * (T2 != NULL) + 4 * (Carry != NULL)) {
case 0: // 无树无进位
case 1: // 只有T1
Q->TheTrees[i] = T1;
break;
case 2: // 只有T2
Q->TheTrees[i] = T2;
break;
case 3: // T1+T2:合并后产生进位
Q->TheTrees[i] = NULL;
Carry = CombineTrees(T1, T2);
break;
case 4: // 只有进位
Q->TheTrees[i] = Carry;
Carry = NULL;
break;
case 5: // T1+进位:合并后产生新进位
Q->TheTrees[i] = NULL;
Carry = CombineTrees(T1, Carry);
break;
case 6: // T2+进位:合并后产生新进位
Q->TheTrees[i] = NULL;
Carry = CombineTrees(T2, Carry);
break;
case 7: // T1+T2+进位:T1+T2合并为进位,原进位存入当前阶
Q->TheTrees[i] = Carry;
Carry = CombineTrees(T1, T2);
break;
}
}
return Q;
}
合并过程类似于二进制的加法。首先创建一个队列Q,Q的总节点数就是Q1和Q2的节点数之和,按阶数遍历合并,情况有8种,这里简单解释几种:如果Q1,Q2的这一阶数对于的二项树都没有,那Q的这一块就先为空,如果其中有一个有,那就把这个有的放到Q的对于阶数的位置上。如果两棵树都有那就合并为阶数+1的树,然后放进Q中阶数+1的位置中去,同时对应原来阶数的位置为空。如果这一阶数对应一个进位树和一个本来就是这个阶数的树,依然是同时进位,同时置空原始位置。
合并同阶树的操作是,让根节点小的作为新树的根,再让根节点大的树作为根节点小的树的子节点的兄弟节点。
插入操作
cpp
// 向二项队列插入元素(将元素视为单节点队列与原队列合并)
void Insert(ElementType X, BinQueue Q) {
// 创建仅含一个节点的二项队列(0阶二项树)
BinQueue SingleNode = Initialize();
SingleNode->CurrentSize = 1;
SingleNode->TheTrees[0] = (BinTree)malloc(sizeof(struct BinNode));
if (SingleNode->TheTrees[0] == NULL) {
fprintf(stderr, "内存分配失败:无法创建插入节点!\n");
exit(EXIT_FAILURE);
}
SingleNode->TheTrees[0]->Element = X;
SingleNode->TheTrees[0]->LeftChild = NULL;
SingleNode->TheTrees[0]->NextSibling = NULL;
// 合并原队列与单节点队列
BinQueue Temp = Merge(Q, SingleNode);
// 将合并结果复制回原队列
for (int i = 0; i < MaxTrees; i++) {
Q->TheTrees[i] = Temp->TheTrees[i];
}
Q->CurrentSize = Temp->CurrentSize;
// 释放临时队列内存
free(Temp);
free(SingleNode);
}
这里插入操作其实就是将由单个插入元素组成的二项树和原队列的合并。
查找最小值
cpp
Position FindMinNode(BinQueue Q) {
if (IsEmpty(Q)) {
fprintf(stderr, "查找失败:二项队列为空!\n");
return NULL;
}
Position MinNode = NULL;
ElementType MinVal = INT_MAX;
// 遍历所有二项树的根节点,找最小值
for (int i = 0; i < MaxTrees; i++) {
if (Q->TheTrees[i] != NULL && Q->TheTrees[i]->Element < MinVal) {
MinVal = Q->TheTrees[i]->Element;
MinNode = Q->TheTrees[i];
}
}
return MinNode;
}
其逻辑就是遍历所有二项树的根节点,因为每个二项树的根节点的值是整个二项树所有节点的最小值。
删除最小值
cpp
// 删除并返回二项队列的最小值
ElementType DeleteMin(BinQueue Q) {
if (IsEmpty(Q)) {
fprintf(stderr, "删除失败:二项队列为空!\n");
exit(EXIT_FAILURE);
}
int MinIndex = -1;
ElementType MinVal;
Position MinNode = NULL;
// 第一步:找到最小值所在的二项树阶数和节点
for (int i = 0; i < MaxTrees; i++) {
if (Q->TheTrees[i] != NULL) {
if (MinNode == NULL || Q->TheTrees[i]->Element < MinVal) {
MinVal = Q->TheTrees[i]->Element;
MinNode = Q->TheTrees[i];
MinIndex = i;
}
}
}
// 第二步:移除最小值所在的二项树,更新队列节点数
Q->TheTrees[MinIndex] = NULL;
Q->CurrentSize -= (1 << MinIndex); // 2^MinIndex 个节点被移除
// 第三步:拆分被删除的二项树为若干子树,组成新队列
BinQueue TempQueue = Initialize();
TempQueue->CurrentSize = (1 << MinIndex) - 1; // 拆分后节点数为2^k -1
Position OldRoot = MinNode;
Position NextChild = OldRoot->LeftChild;
free(OldRoot); // 释放最小值节点
// 左孩子-右兄弟结构逆序拆分,存入临时队列
for (int i = MinIndex - 1; i >= 0; i--) {
TempQueue->TheTrees[i] = NextChild;
NextChild = NextChild->NextSibling;
TempQueue->TheTrees[i]->NextSibling = NULL;
}
// 第四步:合并原队列与拆分后的子树队列
BinQueue Temp = Merge(Q, TempQueue);
// 复制合并结果到原队列
for (int i = 0; i < MaxTrees; i++) {
Q->TheTrees[i] = Temp->TheTrees[i];
}
Q->CurrentSize = Temp->CurrentSize;
// 释放临时队列内存
free(TempQueue);
free(Temp);
return MinVal;
}
其步骤为先找到最小值节点,再移除最小值节点,再把被删除元素的二项式(假设为k阶)进行拆分,因为此时的二项式是不符合元素个数为2的n次方这一规定的,拆成k-1阶,k-2阶,...一直到0阶(对应只有一个元素的二项树)。然后再将这些树和原队列挨个合并。
实例
cpp
int main() {
// 初始化二项队列
BinQueue Q = Initialize();
printf("二项队列初始化完成\n");
// 插入测试元素
int elements[] = {5, 3, 7, 2, 4};
int n = sizeof(elements) / sizeof(elements[0]);
for (int i = 0; i < n; i++) {
Insert(elements[i], Q);
printf("插入元素 %d 后,队列当前节点数:%d\n", elements[i], Q->CurrentSize);
}
// 查找并输出最小值
Position minNode = FindMinNode(Q);
printf("队列中的最小值:%d\n", minNode->Element);
// 删除最小值
ElementType delVal = DeleteMin(Q);
printf("删除的最小值:%d,删除后节点数:%d\n", delVal, Q->CurrentSize);
printf("删除后队列的最小值:%d\n", FindMinNode(Q)->Element);
// 再次插入并删除
Insert(1, Q);
printf("插入元素 1 后,队列最小值:%d\n", FindMinNode(Q)->Element);
delVal = DeleteMin(Q);
printf("删除的最小值:%d,删除后节点数:%d\n", delVal, Q->CurrentSize);
// 清空并销毁队列
MakeEmpty(Q);
Destroy(Q);
printf("二项队列已销毁\n");
return 0;
}
运行结果如下:
