【数据结构 C 语言实现】堆和优先队列
目录
- 1 堆
- 1.1 堆是什么
- 1.2 基本操作
- 1.2.1 向上调整(上浮)
- 1.2.2 向下调整(下沉)
- 1.2.3 构建堆
- 1.3 参考代码
- 1.3.1 上浮
- 1.3.2 下沉
- 1.3.3 构建堆
- 2 优先队列
- 2.1 基本操作
- 2.1.1 出队
- 2.1.2 入队
- 2.2 代码实现
- 3 总结
1 堆
1.1 堆是什么
堆是一种特殊的完全二叉树,可以简洁地使用数组表示和维护。堆适合于需要维护最值的场景,根据其维护的最值种类,可分为 最小堆
和 最大堆
两类,也有许多其它叫法。
- 最大堆:每个父节点的值 ≥ 其子节点的值,根节点是最大值。
- 最小堆:每个父节点的值 ≤ 其子节点的值,根节点是最小值。
1.2 基本操作
堆的基本操作主要有 构建堆
,向上调整(上浮)
,向下调整(下沉)
等操作。其中 构建堆
操作是指将一个不是堆的数组转换为堆,本质上是由 下沉
操作组成的。
下面以构建 最小堆
为例。
1.2.1 向上调整(上浮)
如果某个元素 x
比其父结点的元素值 p
更小,则其不满足最小堆的定义,需要调整。以 x
为研究对象,则应该将 x
与其父结点元素值 p
做交换,直到满足最小堆的定义为止。
以上操作从一棵树的角度来看,相当于将 x
向上调整了。下面是具体的算法步骤:
1 堆 h
是一个整型数组,长度为 n
,需要调整的元素下标为 i
,注意下标从 0
开始;
2 计算父结点下标,易知 i
的父结点下标 p
为 (i-1)/2
向下取整,如果 p >= 0 && h[i] < h[p]
,则进行交换 swap(h[i], h[p])
,然后将研究对象转移到父结点上:i = p
,循环;否则退出循环,表示已经浮到顶了。其伪代码如下:
adjust_up(h, i):
if i < 0 or i >= h.length:
return
p = (i-1)/2
while p >= 0 && h[i] < h[p]:
swap(h[p], h[i])
i = p
p = (i-1)/2
1.2.2 向下调整(下沉)
如果某个元素 a
比其父结点的元素值 p
更小,则其不满足最小堆的定义,需要调整。以其 父结点 p
为研究对象,则应该将 p
与其子结点元素值的最小值 x
做交换,直到满足最小堆的定义为止。
以上操作从一棵树的角度来看,相当于将 p
向下调整了。下面是具体的算法步骤:
1 堆 h
是一个整型数组,长度为 n
,需要调整的元素下标为 i
,注意下标从 0
开始;
2 计算左子结点下标,易知 i
的左子结点下标 l
为 i * 2 + 1
,如果 i
还有右子结点,则应该比较左右子结点的值的大小,取最小的那个结点 c
,然后进行交换 swap(h[i], h[c])
,然后将研究对象转移到该子结点上:i = c
,循环;否则退出循环,表示已经沉到底了。其伪代码如下:
adjust_down(h, i):
if i < 0 or i >= h.length:
return
l = 2*i+1
while l < h.length:
if l + 1 < h.length && h[l+1] < h[l]:
l++
if h[i] <= h[l]:
return
swap(h[l], h[i])
i = l
l = 2*i+1
1.2.3 构建堆
给你一个无序数组,怎么将其变成堆呢?
首先我们知道,堆是完全二叉树,其最后一个非叶结点的下标是可以确定的,假设堆 h
是一个整型数组,长度为 n
,下标从 0
开始,则其最后一个非叶结点的下标是 (n-1)/2
向下取整。
为什么要关注最后一个非叶结点呢?因为从这个结点按照从右到左的顺序依次将元素下沉,直到操作完成,一个堆就建立好了。(每一次只会在一个满足定义的“堆”上面增加一个不确定的结点,那么将这个结点向下沉即可保证以该结点为根的堆是满足定义的)。
也就是调用大约 n/2
次下沉操作即可,算法步骤和伪代码省略。
1.3 参考代码
假设堆中元素为 int
整数,堆为最小堆。
1.3.1 上浮
void adjust_up(int* h, int pos) {
while (pos > 0 && h[pos] < h[(pos - 1) >> 1]) {
swap(&h[pos], &h[(pos - 1) >> 1]);
pos = (pos - 1) >> 1;
}
}
1.3.2 下沉
void adjust_down(int* h, int pos, int n) {
int k = (pos << 1) + 1;
while (k < n) {
if (k + 1 < n && h[k + 1] < h[k]) {
k++;
}
if (h[pos] <= h[k]) {
break;
}
swap(&h[pos], &h[k]);
pos = k;
k = (pos << 1) + 1;
}
}
1.3.3 构建堆
void heaplify(int* arr, int n) {
for (int i = (n - 1) / 2; i >= 0; i--) {
adjust_down(arr, i);
}
}
2 优先队列
虽然我们常常将堆和优先队列混合在一起说,但不得不搞清楚的是,优先队列算是堆的一种应用。我们平时用的“堆”这种数据结构,大多数是指的优先队列。
优先队列需要具备 上浮
和 下沉
这两个堆的基本操作,还要有 入队
和 出队
这两个队列的基本操作。
2.1 基本操作
2.1.1 出队
优先队列的出队操作即将堆数组的第一个元素即堆顶返回,然后将堆数组的最后一个元素放到堆顶处,然后将堆顶的元素向下调整,并将堆的大小减一。
2.1.2 入队
优先队列的入队操作即在堆数组的最后添加一个元素,堆的大小加一,然后将此时堆数组的最后一个元素(新元素)向上调整。
2.2 代码实现
这里将记录一个通用的以 C
语言实现的优先队列代码。
// 头文件省略
/** C 语言之 “泛型” **/
#define ElemType Message*
/** 优先队列元素: 结构体 **/
typedef struct _msg {
int priority;
char s[15];
} Message;
/** 优先队列结构体定义 **/
typedef struct _priority_queue {
ElemType* data;
int size, capacity;
} PriorityQueue;
/** 自定义比较函数 **/
int cmp(ElemType a, ElemType b) { return a->priority - b->priority; }
/** 交换两个元素 **/
void swap(ElemType* a, ElemType* b) {
ElemType t = *a;
*a = *b;
*b = t;
}
/** 初始化优先队列 **/
PriorityQueue* init_pq(int n) {
PriorityQueue* pq = (PriorityQueue*)malloc(sizeof(PriorityQueue));
if (!pq) {
return NULL;
}
pq->capacity = n;
pq->size = 0;
pq->data = (ElemType*)malloc(pq->capacity * sizeof(ElemType));
return pq;
}
/** 销毁优先队列 **/
void destroy_pq(PriorityQueue* pq) {
if (pq) {
for (int i = 0; i < pq->size; i++) {
free(pq->data[i]);
}
free(pq->data);
free(pq);
}
}
/** 上浮 **/
void adjust_up(PriorityQueue* pq, int pos) {
while (pos > 0 && cmp(pq->data[pos], pq->data[(pos - 1) >> 1]) < 0) {
swap(&pq->data[pos], &pq->data[(pos - 1) >> 1]);
pos = (pos - 1) >> 1;
}
}
/** 下沉 **/
void adjust_down(PriorityQueue* pq, int pos) {
int k = 0;
while ((pos << 1) + 1 < pq->size) {
k = (pos << 1) + 1;
if (k + 1 < pq->size && cmp(pq->data[k + 1], pq->data[k]) < 0) {
k++;
}
if (cmp(pq->data[pos], pq->data[k]) <= 0) {
break;
}
swap(&pq->data[pos], &pq->data[k]);
pos = k;
}
}
/** 入队 **/
void push(PriorityQueue* pq, ElemType msg) {
if (pq->size == pq->capacity) {
pq->capacity <<= 1;
pq->data =
(ElemType*)realloc(pq->data, pq->capacity * sizeof(ElemType));
}
pq->data[pq->size] = msg;
adjust_up(pq, pq->size++);
}
/** 出队 **/
ElemType pop(PriorityQueue* pq) {
if (pq->size == 0) {
return NULL;
}
ElemType ans = pq->data[0];
pq->data[0] = pq->data[--pq->size];
adjust_down(pq, 0);
return ans;
}
3 总结
上面只是以最小堆作为例子,其它的情况可以此类推,并且数组下标从 0
开始,也可以将数组下标从 1
开始,这样做的唯一好处就是计算父亲和儿子的下标时的表达式更简洁一些,只需要选择一个自己喜欢的即可。
关于优先队列的基本操作,这里省略了相对来说不太重要的基本操作(没有封装成函数),有兴趣可以自己封装一下。