数据结构之二叉堆

二叉堆

二叉堆是一棵完全二叉树,什么是完全二叉树呢?简单来说,就是按照层的顺序,对树的节点标号,然后按照层次遍历的顺序来遍历,得到的结果是按照顺序来标号的,不能出现断点,这就是一个完全二叉树。这么说比较抽象,举个例子来说。

1.png

这个二叉树的层次遍历结果是0,1,2,3,4,5,6,7,8.所以是一个完全二叉树。

2.png

这里这棵二叉树进行层次遍历,得到就是0,1,2,3,4,5,6,7,10.编号不是连续的,就不是一棵完全二叉树。

堆就是一棵完全二叉树,不过因为完全二叉树的特性,在实现它的时候往往不是使用链表,而是使用数组来实现,我们可以把上面的二叉树的节点标号与数组的下标对应起来。二叉树的根节点标号从1开始,存储的数组也从下标1开始存储二叉树,一一对应,这种情况下,父节点的数组下标为i,则左子树的数组下标为2i,右子树的下标为2i+1(如果没有超出数组的界限的情况下)

3.png

上图的二叉树将被存储为下面的数组

4.png

在程序里就是如下的访问子节点和父节点。

int heap[10];
//访问第三个节点的父节点
heap[3/2];
//访问第三个节点的左子节点
heap[3*2]
//访问第三个节点的右子节点
heap[3*2+1]

如果是最大堆,那么父节点都要比两个子节点的数值大,相应的最小堆就是父节点比两个子节点都要小。而哨兵的作用就是充当边界检测的角色。如果是最大堆,那么这个值比堆中的数据都要大,如果是最小堆,那么这个值比堆中所有数据都要小。

堆的程序实现
堆的结构定义
typedef int Element;
typedef struct HeapNode
{
    int heapsize; //堆的最大尺寸,也就是数组的最大值
    int size;//堆的当前大小
    Element *data;//数组指针,动态分配数组的大小
};
typedef struct HeapNode* Heap;//堆的指针定义
#define MIN_DATA -20000 //这里实现最小堆,哨兵是最小值,堆中数据的范围是-10000,10000,这个值随需要更改
创建一个heapsize大小的堆
Heap creatHeap(int heapsize)
{
    Heap heap;
    heap=(Heap)malloc(sizeof(struct HeapNode));
    if(heap==NULL)
    {
        printf("内存不足");
        return NULL;
    }
    heap->data=(Element*)malloc(sizeof(Element)*(heapsize+1));
    if(heap->data==NULL)
    {
        printf("内存不足");
        return NULL;
    }
    heap->heapsize=heapsize;
    heap->size=0;
    heap->data[0]=MIN_DATA;
    return heap;
}
判断堆空或者堆满

这个比较简单,因为堆是一个数组,如果size变量为0,表示堆空。如果size变量等于堆数组的最大值,那么就是堆满。

int isFull(Heap heap)
{
    return heap->size==heap->heapsize;
}
int isEmpty(Heap heap)
{
    return heap->size==0;
}
堆的插入操作

堆的插入操作就是首先将size变量加1,当前节点i=size+1,然后访问它的父节点i/2,比较插入的元素和父节点的元素哪个元素小(这里以最小堆为例),如果新插入的元素比较小,那么就将父节点i/2的值赋值到当前节点i。此时将i更新(i=i/2),然后去比较此时i的节点的父节点(i/2,相当于(size+1)/4)与插入的元素,直到比较到哨兵,因为哨兵是最小值,所以插入的元素会放到数组下标的0的位置。如果比较到其中某个位置时,插入的节点比父节点的大,那么就把插入的节点放到当前位置。以图说话。

5.png

假设有如上的最小堆,堆目前的size是5,如果插入一个元素15,则size加1,相当于把31后面的数组位置空出来,等待插入,然后比较i=6的父节点i/2,此时是16,因为15小于16,所以16就需要下移到数组下标为6的位置了。

6.png

只是将16复制到了数组下标为6的地方,数组下标为3的值没有改变,还是16.然后此时i移动到位置3,比较i=3的父节点i/2,此时是数组的下标1的数据,13<15,说明符合最小堆的定义,父节点小于子节点,所以15应该插入到数组下标为3的地方。插入完成。


7.png

在比如在此时直接插入20元素,size加1等于7,它的父节点的值为15,比20小,所以20直接放到数组下标为7的位置。

插入操作的程序如下:

void insert(Heap heap,Element x)
{
    int i=0;
    if(isFull(heap))
    {
        printf("堆满了");
        return;
    }
    for(i=++heap->size;x<heap->data[i/2];i=i/2) //这里因为有哨兵,所以当i等于0时,是一定会结束循环的,如果不使用哨兵,需要判断一下i为0,否则导致死循环
    {
        heap->data[i]=heap->data[i/2];
    }
    heap->data[i]=x;
}
堆的删除操作

堆的删除操作就是将数组的下标1的数据返回,然后重新构造堆,将size减1,并将最后一个数据找到合理的位置插入。就是从根节点开始(也就是下标1)找到两个子节点的最小值,与最后一个数据比较,如果最后一个数据比两个子节点的最小值还要小的话,那么就把这个数据复制到这两个子节点的父节点;如果最后一个数据比两个子节点的最小值还要大的话,那么就将两个子节点的最小值节点复制到它的父节点,然后以这个子节点作为父节点,重复上述过程,直到找到合适位置。

7.png

假设执行删除操作,13删除,最后一个元素16被取出,size减1,比较13的子节点21和15,15位最小的,那么比较15和16,发现15小,所以将15复制到13的位置。

8.png

此时以数组下标3作为父节点,比较它的两个子节点,但是发现已经没有了,所以把16复制到数组下标3的位置就完成了删除操作。

假设第一次删除13时,它的两个子节点都比16大,那么直接将16复制到数组下标为1的地方,就完成了删除。这就是堆的删除操作的执行过程。

程序代码如下:

Element deleteHeap(Heap heap)
 {
    int child,parent;
    Element x,num;
    if(isEmpty(heap))
    {
        printf("堆空");
        return MIN_DATA;
    }
    x=heap->data[1];//保存要删除的元素,作为返回值
    num=heap->data[heap->size--];//堆的尺寸小1,将这个数值保存以免丢失
    for(parent=1;parent*2<=heap->size;parent=child)
    {
        child=2*parent;
        //寻找两个子节点中最小的一个,&&前面的判断是防止child已经是堆的最后一个数据,加1后超出堆的范围
        if((child+1)<=heap->size&&heap->data[child]>heap->data[child+1])
        {
            child=child+1;
        }
        //比较两个子节点的最小值和要原堆中最后一个元素,如果最后一个元素小,说明找到了插入的位置,结束循环
        if(num<=heap->data[child])
        {
            break;
        }
        else
        {
            heap->data[parent]=heap->data[child];
        }
    }
    heap->data[parent]=num;
    return x;
 }
堆的构建

将一个数组变成最小堆,思路就是从根本一个子树一个子树的逐渐变换,假设数组大小为size,那么它的父节点就是i=size/2,以i为起点,执行与删除操作相同的下滤操作,直到i变成0,就完成了堆的构建,其实就是堆排序的思路。

void perdown(Heap heap,int d)
 {
    int child,parent;
    Element x;
    x=heap->data[d];
    for(parent=d;parent*2<=heap->size;parent=child)
    {
        child=2*parent;
        if((child+1)<=heap->size&&heap->data[child]>heap->data[child+1])
        {
            child=child+1;
        }
        if(x<=heap->data[child])
        {
            break;
        }
        else
        {
            heap->data[parent]=heap->data[child];
        }
    }
    heap->data[parent]=x;
 }
 void buildHeap(Heap heap)
 {
     int i=0;
     for(i=heap->size/2;i>0;i--)
     {
        perdown(heap,i);
     }
 }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351