数据结构——堆

        堆的概念

        堆是什么,完全二叉树在上一篇的文章中提到过,堆也就是利用了完全二叉树的思想,但是它的结构定义没有像树一样定义,只是它的逻辑结构和完全二叉树一样;

        堆分为大顶堆和小顶堆:

        大顶堆:

        一个结点的左右子孩子的值都小于它自己的值,如图:

         小顶堆:

        一个结点的左右还在都大于它自己的值,如图:

        

        

        现在把这个小顶堆的数据带入到数组中,从上到小,从左到右分别带入到数组中,如图:

        为什么要把数组的0位置空出来,因为方便写代码的时候方便,只是我学到的一种编程技巧,这样可以使得代码更简洁;

        优先队列:

        堆还有一个名字叫优先队列,其实优先队列和堆的数据结构是相同的,优先队列,你直接可以理解字面上的意思,优先,要么就是最大的或者最小的先出队,而堆弹出元素也是从堆顶弹出,入堆也是从堆的最后一个位置进行入堆,而队列也是一样的;所以他们的数据结构是一样的,他的操作会在文章后面会讲。

        它的数据结构:

          结构定义:

            物理结构:

        他的物理结构有,第一个数据域,堆元素的个数,堆存储元素个数的最大值;结构体封装如下:

typedef struct priority_queue {
    int cnt, size;//堆里现在的元素个数和堆的大小
    void *data;//数据域
} priority_queue;
        逻辑结构:

        他的逻辑结构就是,优先(要么定义的是大顶堆,要么定义的是小顶堆),这个就是以情况而定来的逻辑,不过添加元素和弹出元素都是一样的,只不过相当于在代码中大于变小于或者小于变大于,中心的逻辑思想是没有变化的;还有一点,现在把堆看为完全二叉树,他的结点的值必须满足要么大于两个子孩子,要么小于两个子孩子;

        

        在这个图中,是一个小顶堆,根节点小于他的两个子孩子,3小于他的两个子孩子;所以必须要维持这个性质,才会不破坏堆的数据结构;

        结构操作:

        入堆:

        现在以这个大顶堆为例,入堆元素31,由于堆是完全二叉树,那么他就应该接在元素17的左子树上,也就是队列的入队,应该从队尾进行入队;


        然后开始进行比较,由于是大顶堆,如果它大于他的父节点就与父节点的值进行交换;

 最终变为:

        

        然后就完成了入堆操作;

        弹出:

        堆又名优先队列嘛,出队都是从头出,那堆也应该从头弹出嘛,那么如果进行弹出呢,将堆头的元素被堆尾的元素覆盖,然后将堆尾删除后,在进行排序维护堆的结构:

        

        现在要将35进行弹出那么将17覆盖掉35,删除结点17:

        

        利用大顶堆的性质进行从高往低进行去维护排序,最终结果如下:

        

        最后完成弹出35;

        代码实现:

        实现的是大顶堆:

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

//代码实现的大顶堆

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

typedef struct priority_queue {
    int cnt, size;//堆里现在的元素个数和堆的大小
    int *data;//数据域
} priority_queue;

priority_queue *init(int n) {//向计算机借空间
    priority_queue *q = (priority_queue *)malloc(sizeof(priority_queue));
    q->cnt = 0;
    q->size = n;
    q->data = (int *)malloc(sizeof(int) * (n + 1));
    return q;
}

int empty(priority_queue *q) {//判空操作
    return q->cnt == 0;
}

int top(priority_queue *q) {//获取堆顶元素
    return q->data[1];
}

//插入堆元素
int push(priority_queue *q, int val) {
    if (q->cnt == q->size) return 0;
    q->data[++q->cnt] = val;//向堆尾入堆
    int ind = q->cnt;//获取当前入堆元素的下标
    while (ind >> 1 && q->data[ind] > q->data[ind >> 1]) {//下标右移一位就是对应的父节点的坐标
    //这个过程需要去模拟一下才能明白
        swap(q->data[ind >> 1], q->data[ind]);
        ind >>= 1;
    }
    return 1;
}

//弹出堆顶元素
int pop(priority_queue *q) {
    if (empty(q)) return 0;
    q->data[1] = q->data[q->cnt--];//堆顶元素被堆尾元素覆盖
    int ind = 1;//标记当前堆顶进行向下排序
    while (ind << 1 <= q->cnt) {
        int l = ind << 1, r = ind << 1 | 1, temp = ind;//l代表左孩子,r代表有孩子的坐标,temp记录需要交换位置的坐标
        if (q->data[temp] < q->data[l]) temp = l;//如果左孩子的元素大于父节点,temp记录左孩子坐标
        if (r <= q->cnt && q->data[temp] < q->data[r]) temp = r;
        //如果有右孩子,如果右孩子大于temp现在位置的值,是temp位置因为现在temp可能是父节点也可能是左孩子
        if (temp == ind) break;//如果temp == ind 说明没有发生交换,说明父节点大于左右孩子的值 
        swap(q->data[temp], q->data[ind]);//交换值
        ind = temp;
    }
    return 1;
}

void output(priority_queue *q) {
    printf("priority_queue(%d) = [", q->cnt);
    for (int i = 1; i <= q->cnt; i++) {
        i != 1 && printf(", ");
        printf("%d", q->data[i]);
    }
    printf("]\n");
    return ;
}

void clear(priority_queue *q) {
    if (!q) return ;
    free(q->data);
    free(q);
    return ;
}

int main() {
    srand(time(0));
    int op, val;
    priority_queue *q = init(10);
    for (int i = 0; i < 20; i++) {
        op = rand() % 4;
        val = rand() % 100; 
        switch (op) {
            case 0:
            case 1:
            case 2: {
                printf("%d push in priority_queue is %d\n", val, push(q, val));
            } break;
            case 3: {
                int num = top(q);
                printf("%d pop in priority_queue is %d\n", num, pop(q));
            }
        }
        output(q);
    }
    clear(q);
    return 0;
}

堆排序:

        我的理解是实现了大顶堆,可以去排序升序的序列,小顶堆可以去实现排序降序序列;

        下面是代码实现:

        

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

#define swap(a, b) {\
    __typeof(a) __a = a; a = b; b = __a;\
}
void Updown_val(int *arr, int n, int ind) {//将一部分形成大顶堆
    while (ind << 1 <= n) {
        int l = ind << 1, r = ind << 1 | 1, temp = ind;
        if (arr[l] > arr[temp]) temp = l;
        if (r <= n && arr[r] > arr[temp]) temp = r;
        if (temp == ind) break;
        swap(arr[temp], arr[ind]);
        ind = temp;
    }
    return ;
}

void heap_sort(int *arr, int n) {
    for (int i = n >> 1; i >= 1; i--) {//从下往上形成大顶堆,最终堆顶是最大的元素
        Updown_val(arr, n, i);
    }
    for (int i = n; i > 1; i--) {
        swap(arr[1], arr[i]);//每次将目前堆中最大的元素向后交换
        Updown_val(arr, i - 1, 1);//交换后,再次进行维护堆的性质
    }
    return ;
}

void output(int *arr, int n) {
    for (int i = 1; i <= n; i++) {
        printf("%d ", arr[i]);
    }
    putchar(10);
    return ;
}


int main() {
    srand(time(0));
    int val, n = 10;
    int arr[100];
    for (int i = 1; i <= n; i++) {
        val = rand() % 100;
        arr[i] = val;
    }
    output(arr, n);
    heap_sort(arr, n);
    output(arr, n);
    return 0;
}

           下面是画图理解:

        这是最初arr数组的情况,转化为了二叉树的形式,是无序的:

        

for (int i = n >> 1; i >= 1; i--) {//从下往上形成大顶堆,最终堆顶是最大的元素
        Updown_val(arr, n, i);
    }

        现在执行这段代码,现在n是等于10的,右移就是5,i = 5,也就是元素2的位置进行排序,也就是函数Updown_val进行排序,然后排序后的结果

        i--,i = 4,也就是62的位置;

          这样一直进行下去,最后形成一个标准的大顶堆:

        然后执行代码:

for (int i = n; i > 1; i--) {
        swap(arr[1], arr[i]);//每次将目前堆中最大的元素向后交换
        Updown_val(arr, i - 1, 1);//交换后,再次进行维护堆的性质
    }

        将堆顶元素排到堆尾去,然后再次排序时,将排序的数组的长度进行-1,第一次得到的结果位:

        现在69再队尾了,也不参与后面的排序了,然后一直这样进行下去,直到排完最终结果就是这样:

        

        最后形成单调递增序列,完成排序;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

初猿°

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值