数据结构——堆与优先队列

本文详细阐述了堆与优先队列的关系,包括完全二叉树的性质、堆的性质与操作(如插入、删除调整),以及堆排序和哈夫曼编码的实现。通过实例展示了如何使用堆实现优先队列和堆排序算法,并介绍了通过Set模拟堆操作的方法。
摘要由CSDN通过智能技术生成

堆与优先队列实质上是同一种东西,可以认为优先队列是堆的别名

1. 完全二叉树

完全二叉树的性质:

如下的左右两个形式都可以表示完全二叉树,因为完全二叉树可以顺序编码,所以可以建立一个顺序结构进行存储,右边可以理解为代码表现形式。通常将 0 号位空出来存储根结点,找某个结点的左右孩子时也是通过编号,比如 1 号位结点的左孩子为 ( 2 ∗ 1 + 1 ) = 3号结点,右孩子为 4 号节点

2. 堆与优先队列

堆在思维逻辑上是一棵完全二叉树,但是堆之所以叫堆,因为它特有的性质:任何一个三元组(根节点,左孩子,右孩子)而言,根节点要么是最大的,要么是最小的

对于任何一个三元组,根节点是最小的,就叫做 小顶堆,反之就是 大顶堆
image-20201109194438353

堆能解决的问题 :

  1. 求所有元素的最大值或最小值(根节点)
  2. 找到所有元素中的次大值或次小值(二叉树的第二层)

2.1 堆的尾部插入调整

在大顶堆中插入 13,下面是两种实现方式:(左边是完全二叉树,右边是顺序表)

过程:自下向上调整结点以满足堆的性质

  1. 对于数组来说就是交换了 7 和 13 的位置
  2. 交换 11 和 13的位置
  3. 交换 13 和 12 的位置

平均时间复杂度为  O(logn),即二叉树的树高

2.2 堆的头部弹出调整

堆弹出元素只能删除堆顶元素

1. 删除堆顶元素,将堆尾元素放到堆顶

2. 自上向下调整堆内元素,使得满足堆的性质(三元组),12 > 7 ,于是将12 放到堆顶

3. 交换 7 和 11

堆为什么又叫优先队列呢?
答:堆删除元素是从堆顶删除,堆插入元素是从堆尾插入,这和队列的性质一样;而叫做优先队列是因为每次从堆顶删除元素,删除的都是所有元素中的最大值或最小值,这是优先的性质

2.3 队列 vs 堆 vs 优先队列

优先队列的结构定义和队列相同,但是入队和出队之后要进行堆的性质的维护

2.4 代码实现

priority_queue.h

#ifndef __PRIORITY_QUEUE_H__
#define __PRIORITY_QUEUE_H__

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAXSIZE 100

#define swap(a, b) { \
    __typeof(a) __c = (a); \
    (a) = (b); \
    (b) = __c; \
}

// 从1开始存储,根结点下标为1
#define FATHER(i) ((i) / 2)
#define LEFT(i) ((i) * 2)
#define RIGHT(i) ((i) * 2 + 1)

// 堆---数据类型
typedef struct PriorityQueue {
    int *__data, *data; // 从1开始存储
    int size; // 数组大小
    int n; // n:数组中实际元素个数
} PriorityQueue;

// 初始化一个堆
PriorityQueue *init_PQ(int size);

// 判空
bool empty(PriorityQueue *p);

// 判满
bool full(PriorityQueue *p);

// 获取堆顶元素
bool top(PriorityQueue *p, int *top_val);

// 向上调整(递归)
void up_update_1(int *data, int i);

// 向上调整(非递归)
void up_update_2(int *data, int i);

// 向下调整
void down_update(int *data, int i, int n);

// 入堆
bool push(PriorityQueue *p, int x);

// 出堆
bool pop(PriorityQueue *p);

// 销毁堆
void destroy_PQ(PriorityQueue *p);

// 打印堆
void output(PriorityQueue *p);

#endif
priority_queue.c
#include "priority_queue.h"

// 初始化一个堆
PriorityQueue *init_PQ(int size) {
    PriorityQueue *p = (PriorityQueue*)malloc(sizeof(*p));
    p->__data = (int*)malloc(sizeof(int) * (size));
    p->data = p->__data - 1;
    p->size = size;
    p->n = 0;
    return p;
}

// 判空
bool empty(PriorityQueue *p) {
    if (p == NULL || p->__data == NULL || p->n == 0) {
        return true;
    }
    return false;
}

// 判满
bool full(PriorityQueue *p) {
    if (p == NULL || p->__data == NULL || p->n == p->size) {
        return true;
    }
    return false;
}

// 获取堆顶元素
bool top(PriorityQueue *p, int *top_val) {
    if (empty(p)) {
        return false;
    }
    *top_val = p->data[1];
    return true; 
}

// 向上调整(递归)
void up_update_1(int *data, int i) {
    if (i == 1) {
        return;
    }
    if (data[i] > data[FATHER(i)]) {
        swap(data[i], data[FATHER(i)]);
        up_update_1(data, FATHER(i));
    }
}

// 向上调整(非递归)
void up_update_2(int *data, int i) {
    while (i > 1 && data[i] > data[FATHER(i)]) {
        swap(data[i], data[FATHER(i)]);
        i = FATHER(i);
    }
}

// 向下调整
void down_update(int *data, int i, int n) {
    while (LEFT(i) <= n) {
        int ind = i, l = LEFT(i), r = RIGHT(i);
        if (data[l] > data[ind]) {
            ind = l;
        }
        if (r <= n && data[r] > data[ind]) {
            ind = r;
        }
        if (ind == i) {
            break;
        }
        swap(data[i], data[ind]);
        i = ind;
    }
}

// 入堆
bool push(PriorityQueue *p, int x) {
    if (full(p)) {
        return false;
    }
    p->n++;
    p->data[p->n] = x;
    up_update_1(p->data, p->n);
    return true;
}

// 出堆
bool pop(PriorityQueue *p) {
    if (empty(p)) {
        return false;
    }
    p->data[1] = p->data[p->n];
    p->n--;
    down_update(p->data, 1, p->n);
    return true;
}

// 销毁堆
void destroy_PQ(PriorityQueue *p) {
    if (p == NULL) {
        return;
    }
    if (p->__data != NULL) {
        free(p->__data);
    }
    free(p);
}

// 打印堆
void output(PriorityQueue *p) {
    if (p == NULL || p->data == NULL || p->n == 0) {
        return;
    }
    printf("PQ(%d) : ", p->n);
    for (int i = 1; i <= p->n; i++) {
        printf("%d ", p->data[i]);
    }
    printf("\n");
}
main.c
#include "priority_queue.h"

int main(int argc, char *argv) {
    int op, x;
    PriorityQueue *p = init_PQ(MAXSIZE);
    while (1) {
        scanf("%d", &op);
        if (op == 0) {
            break;
        }
        if (op == 1) { // 入堆
            scanf("%d", &x);
            printf("insert %d to priority_queue : \n", x);
            push(p, x);
            output(p);
        } else {
            int top_val;
            if (top(p, &top_val)) { // 获取堆顶元素
                printf("pop : %d\n", top_val);
            }
            pop(p); // 出堆
            output(p);
        }
    }
    destroy_PQ(p); // 销毁堆
    return 0;
}

3. 堆排序

要实现从小到大的排序,就要建大顶堆;

从大到小的排序,就要建小顶堆

3.1 线性建堆法

线性建堆法把原来建堆的时间复杂度 O(nlogn) 降低至 O(n),但是堆排序的时间复杂度整体还是 O(nlogn)

普通建堆(向上调整)

线性建堆(向下调整)

3.2 口诀

大顶堆

1、将堆顶元素与堆尾元素交换

2、将此操作看做是堆顶元素弹出操作

3、按照头部弹出以后的策略调整堆

3.3 普通堆排序和线性堆排序代码实现

heap_sort.h
#ifndef __HEAP_SORT_H__
#define __HEAP_SORT_H__

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define swap(a, b) { \
    __typeof(a) __c = (a); \
    (a) = (b); \
    (b) = __c; \
}

// 从1开始存储,根结点下标为1
#define FATHER(i) ((i) / 2)
#define LEFT(i) ((i) * 2)
#define RIGHT(i) ((i) * 2 + 1)

// 随机化得到一个数组
int* getRandData(int n);

// 向上调整(递归)
void up_update_1(int *data, int i);
// 向上调整(非递归)
void up_update_2(int *data, int i);

// 向下调整
void dowm_update(int *data, int i, int n);

// 将大顶堆调整成从小到大排序
void heap_sort_final(int *data, int n);

// 普通建堆(向上建堆)
void normal_heap(int *arr, int n);

// 线性建堆(向下调整)
void linear_heap(int *arr, int n);

// 打印数组
void print_arr(int *arr, int n);

#endif
heap_sort.c
#include "heap_sort.h"

// 随机化得到一个数组
int* getRandData(int n) {
    int *arr = (int*)malloc(sizeof(int) * n);
    for (int i = 0; i < n; i++) {
        arr[i] = rand() % 100;
    }
    return arr;
}

// 向上调整(递归)
void up_update_1(int *data, int i) {
    if (i == 1) {
        return;
    }
    if (data[i] > data[FATHER(i)]) {
        swap(data[i], data[FATHER(i)]);
        up_update_1(data, FATHER(i));
    }
}

// 向上调整(非递归)
void up_update_2(int *data, int i) {
    while (i > 1 && data[i] > data[FATHER(i)]) {
        swap(data[i], data[FATHER(i)]);
        i = FATHER(i);
    }
}

// 向下调整
void down_update(int *data, int i, int n) {
    while (LEFT(i) <= n) {
        int ind = i, l = LEFT(i), r = RIGHT(i);
        if (data[l] > data[ind]) {
            ind = l;
        }
        if (r <= n && data[r] > data[ind]) {
            ind = r;
        }
        if (ind == i) {
            break;
        } 
        swap(data[i], data[ind]);
        i = ind;
    }
}

// 将大顶堆调整成从小到大排序
void heap_sort_final(int *data, int n) {
    if (data == NULL) {
        return;
    }
    for (int i = n; i >= 2; i--) {
        swap(data[1], data[i]);
        down_update(data, 1, i - 1); // 向下调整
    }
}

// 普通建堆(向上调整)
void normal_heap(int *arr, int n) {
    int *data = arr - 1;
    for (int i = 2; i <= n; i++) {
        up_update_1(data, i); // 向上调整
    }
    heap_sort_final(data, n);
}

// 线性建堆(向下调整)
void linear_heap(int *arr, int n) {
    int *data = arr - 1;
    for (int i = n / 2; i >= 1; i--) {
        down_update(data, i, n);
    }
    heap_sort_final(data, n);
}

// 打印数组
void print_arr(int *arr, int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}
main.c
#include "heap_sort.h"

int main(int argc, char *argv[]) {
    srand(time(0));
    int n;
    scanf("%d", &n);
    int *arr = getRandData(n);
    print_arr(arr, n);
    // normal_heap(arr, n); // 向上建堆
    linear_heap(arr, n); // 线性建堆(向下调整)
    print_arr(arr, n);
    return 0;
}

4. 哈夫曼编码优化(通过小顶堆方式优化步骤2)

4.1 构造算法:

贪心算法:构造哈夫曼树时首先选择权值小的叶子节点

1、首先,统计得到每一种字符的概率

2、每次将最低频率的两个结点合并成一棵子树

3、经过了 n-1 轮合并,就得到了一棵哈夫曼树

4、按照左0,右1的形式,将编码读取出来

4.2 代码实现:

heap_haffman.c

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_CHAR_NUM 128

//创建二叉树结构体定义
typedef struct Node 
{
    char ch;
    int freq; //概率(频次)
    struct Node *lchild;
    struct Node *rchild;
}Node;

//创建堆结构体定义
typedef struct Heap
{
    Node **__data, **data;
    int n, size;
}Heap;

//初始化一个堆
Heap* getNewHeap(int size) 
{
    Heap *h = (Heap*)malloc(sizeof(Heap));
    h->__data = (Node**)malloc(sizeof(Node*) * size);
    h->data = h->__data - 1;
    h->n = 0;
    h->size = size;
    return h;
}

//判满(堆)
bool fullHeap(Heap *h) 
{
    if (h == NULL || h->__data == NULL || h->n == h->size)
        return true;

    return false;
}

//判空(堆)
bool emptyHeap(Heap *h) 
{
    if (h == NULL || h->__data == NULL || h->n == 0)
        return true;

    return false;
}

//获取堆顶元素
Node* top(Heap *h) 
{
    if (emptyHeap(h)) 
        return NULL;

    return h->data[1];
}

//比较两者的freq
int cmpHeap(Heap *h, int i, int j) 
{
    return h->data[i]->freq < h->data[j]->freq;
}

//交换
void swap_node(Node **m, Node **n)
{
    Node *temp = *m;
    *m = *n;
    *n = temp;
}

//向上调整
void up_maintain(Heap *h, int i) 
{
    while (i > 1 && cmpHeap(h, i, i / 2)) 
    {
        swap_node(&h->data[i], &h->data[i / 2]);
        i = i / 2;
    }
    return ;
}

//向下调整
void down_maintain(Heap *h, int i) 
{
    while (i * 2 <= h->n) 
    {
        int ind = i, l = i * 2, r = i * 2 + 1;
        if (cmpHeap(h, l, ind)) 
            ind = l; 
        if (r <= h->n && cmpHeap(h, r, ind)) 
            ind = r;
        if (ind == i) 
            return ;
        swap_node(&h->data[i], &h->data[ind]);
        i = ind;
    }
    return ;
}

//入堆
bool pushHeap(Heap *h, Node *n) 
{
    if (fullHeap(h)) 
        return false;
    h->n += 1;
    h->data[h->n] = n;
    up_maintain(h, h->n);
    return true;
}

//出堆
bool popHeap(Heap *h) 
{
    if (emptyHeap(h))
        return false;
    h->data[1] = h->data[h->n];
    h->n -= 1;
    down_maintain(h, 1);
    return true;
}

//摧毁堆
void clearHeap(Heap *h) 
{
    if (h == NULL) 
        return ;
    free(h->__data);
    free(h);
    return ;
}

//得到一个树结点
Node* getNewNode(int freq, char ch)
{
    Node *p = (Node*)malloc(sizeof(Node));
    p->ch = ch;
    p->freq = freq;
    p->lchild = p->rchild = NULL;
    return p;
}

//找出最小frep
int find_min_node(Node **node_arr, int n) 
{
    int ind = 0;
    for (int j = 1; j <= n; j++) 
        if (node_arr[ind]->freq > node_arr[j]->freq) 
            ind = j;

    return ind;
}

Node* buildHaffmanTree(Node **node_arr, int n) 
{
    Heap *h = getNewHeap(n);
    for (int i = 0; i < n; i++) 
        pushHeap(h, node_arr[i]);
    for (int i = 1; i < n; i++) 
    {
        Node *node1 = top(h);
        popHeap(h);
        Node *node2 = top(h);
        popHeap(h);
        Node *node3 = getNewNode(node1->freq + node2->freq, 0);
        node3->lchild = node1;
        node3->rchild = node2;
        pushHeap(h, node3);
    }
    Node *ret = top(h);
    clearHeap(h);
    return ret;
}

//销毁二叉树
void clear(Node *root) 
{
    if (root == NULL) 
        return ;

    clear(root->lchild);
    clear(root->rchild);
    free(root);
    return ;
}

char *char_code[MAX_CHAR_NUM] = {0};

void extractHaffmanCode(Node *root, char buff[], int k) 
{
    buff[k] = 0;
    if (root->lchild == NULL && root->rchild == NULL) 
    {
        printf("%c : %s\n", root->ch, buff);
        return ;
    }
    buff[k] = '0';
    extractHaffmanCode(root->lchild, buff, k + 1);
    buff[k] = '1';
    extractHaffmanCode(root->rchild, buff, k + 1);
    return ;
}

int main(int argc, char *argv[])
{
    char s[10];
    int n, freq;
    scanf("%d", &n);
    Node **node_arr = (Node**)malloc(sizeof(Node *) * n);
    for (int i = 0; i < n; i++) 
    {
        scanf("%s%d", s, &freq); //%s会忽略换行符
        node_arr[i] = getNewNode(freq, s[0]);
    }

    Node *root = buildHaffmanTree(node_arr, n);
    char buff[1000];
    extractHaffmanCode(root, buff, 0);
    for (int i = 0; i < MAX_CHAR_NUM; i++) 
    {
        if (char_code[i] == NULL) 
            continue;
        printf("%c : %s\n", i, char_code[i]);
    }
    clear(root);
    return 0;
}

5. 用set模拟堆操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值