当前位置: 首页 > news >正文

【数据结构 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 的左子结点下标 li * 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 开始,这样做的唯一好处就是计算父亲和儿子的下标时的表达式更简洁一些,只需要选择一个自己喜欢的即可。
    关于优先队列的基本操作,这里省略了相对来说不太重要的基本操作(没有封装成函数),有兴趣可以自己封装一下。

相关文章:

  • 警惕AI神话破灭:深度解析大模型缺陷与禁用场景指南
  • 关于VScode终端无法识别外部命令
  • 如何使用Postman,通过Mock的方式测试我们的API
  • 【Kubernets】Kubernetes 的基础知识,Pod是什么? 和容器的关系?多个容器如何在同一个 Pod 里协作?
  • 【CXX】6.2 str — rust::Str
  • 几种linux获取系统运行时间的方法
  • Webservice创建
  • 技术进阶:数字人分身克隆系统源码+DeepSeek,实现前沿虚拟数字人应用的交互升级
  • 02.06、回文链表
  • 《深入浅出数据索引》- 公司内部培训课程笔记
  • 【MySQL_04】数据库基本操作(用户管理--配置文件--远程连接--数据库信息查看、创建、删除)
  • 【2025年28期免费获取股票数据API接口】实例演示五种主流语言获取股票行情api接口之沪深A股强势股池数据获取实例演示及接口API说明文档
  • 面试java做了一道逻辑题,人麻了
  • 你使用过哪些 Java 并发工具类?
  • 《人月神话》:软件工程的成本寓言与生存法则
  • 自动解单色数织程序(基于Python和Ortools)
  • 无人机的飞行路径规划之CH-PPO算法(思考)
  • 面试之《vue常见考题》
  • MySQL环境安装详细教程(Windows/macOS/Linux)
  • Spring 的三种注入方式?
  • 最高法:依法惩治损害民营企业合法权益的串通投标行为
  • 1块钱解锁2万部微短剧还能日更,侵权盗版难题怎么破?
  • 以色列在加沙发起新一轮强攻,同步与哈马斯展开“无条件谈判”
  • 蒲慕明院士:好的科普应以“质疑、讨论公众关切的科学问题”为切入点
  • 世界高血压日|专家:高血压患者控制血压同时应注重心率管理
  • 河南一女子被医院强制带走治疗,官方通报:当值医生停职