【数据结构】-堆的概念及实现


堆 - 是一种完全二叉树

堆的概念及结构

如果有一个关键码的集合K = { k0, k1,k2 ,…,kn-1 },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

大堆:树中一个树及子树中,任何一个父亲都大于等于孩子

小堆:树中一个树及子树中,任何一个父亲都小于等于孩子


数据结构和操作系统都有栈和堆,数据结构的堆是完全二叉树 他们二者没有关系,他们是两个学科中不同物种


堆的性质

1.堆中某个节点的值总是不大于或不小于其父节点的值;

2.堆总是一棵完全二叉树


堆:

逻辑结构:我们想象出来的 ->完全二叉树
物理结构:实实在在在内存中存储的结构 ->数组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DjW7OISl-1638148040699)(E:\Believe everything maybe true\Bit\数据结构\08.堆\堆.assets\image-20211107145251328.png)]


习题练习

1.下列关键字序列为堆的是:()
A 100,60,70,50,32,65->
B 60,70,65,50,32,100
C 65,100,70,32,50,60
D 70,65,100,32,50,60
E 32,50,100,70,65,60
F 50,100,70,65,60,32

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NgvirOgN-1638148040700)(E:\Believe everything maybe true\Bit\数据结构\08.堆\堆.assets\image-20211108103350495.png)]


堆的实现

运用到二叉树顺序存储时的结论:

> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1iVNObW3-1638148040702)(E:\Believe everything maybe true\Bit\数据结构\08.堆\堆.assets\image-20211107145914358.png)]

这里我们以建大堆为例子

大堆:树中一个树及子树中,任何一个父亲都大于等于孩子


堆的结构

typedef int HPDataType;
typedef struct Heap
{
    
    
    HPDataType* a;//用数组存储
    int size;//标志元素个数,size标志的是数组最后一个元素的下一个位置,size-1才是最后一个元素的下标
    int capacity;//容量,不够则扩容
}HP;

初始化

void HeapInit(HP* hp)
{
    
    
    assert(hp);
    hp->a = NULL;
    hp->capacity = hp->size = 0;
}

释放

**堆是用顺序表存储 **

释放动态开辟的数组,然后把指针置空,防止造成野指针

void HeapDestory(HP* hp)
{
    
    
    assert(hp);
    free(hp->a);
    hp->a = NULL;
    hp->capacity = hp->size = 0;
}

插入数据

空间不够->扩容

HeapPush要求,插入数据之后还是堆(大堆/小堆保持不变)

在数组末尾插入数据:堆插入数据堆其它结点没有影响,只是可能会影响从他到根节点路径上节点关系


插入数据会影响原来堆的结构,树已经是堆了 然后插入一个元素后 这个元素就变成了树中的最后一个叶子 这个叶子节点可能会破坏堆的特性 那就得往上调整


void HeapPush(HP* hp, HPDataType x)
{
    
    
    assert(hp);
    //空间不够,扩容
    if (hp->capacity == hp->size)
    {
    
    
        int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
        HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
        if (tmp == NULL)
        {
    
    
            printf("realloc fail\n");
            exit(-1);//异常结束 stdlib头文件
        }
        else
        {
    
    
            hp->a = tmp;
            hp->capacity = newcapacity;
        }
    }

    //插入数据-直接在size位置插入
    hp->a[hp->size] = x;
    hp->size++;

    //插入后不一定是还是堆,
    AdjustUp(hp->a, hp->size - 1);
    //size标志的是数组最后一个元素的下一个位置,size-1才是最后一个元素的下标,所以要调整新插入的位置,就是size-1位置
}

交换两个数

void Swap(HPDataType* px, HPDataType* py)
{
    
    
    HPDataType tmp = *px;
    *px = *py;
    *py = tmp;
}

向上调整算法

向上调整有固定的路径可循,从该结点开始到根节点

循环结束条件:

1.插入的结点比 从它到根结点的所有结点的值都要,则要一直调整,child调到根跳出循环

或者:

2.插入的结点比 从它到根结点的部分结点的值大,当调整到某一个位置,孩子结点的值比父亲结点的值小,就不用调整了,走else跳出循环

void AdjustUp(int* a,int child)
{
    
    
    assert(a);   
    int parent = (child - 1) / 2;
     //循环结束条件:child 调到根节点,即child为0
    while(child > 0)
    {
    
    
        //如果孩子结点比父亲结点的值大,就交换
        if(a[child] > a[parent])
        {
    
                   		
            Swap(&a[child],&a[parent]);    
            //父亲和孩子结点交换 再迭代
            child = parent;
            parent = (child - 1) / 2;
            //继续往上调整,直到child调到根或者父亲比孩子大
        }
        
        //如果孩子结点比父亲结点小了,说明不用调了
        else
        {
    
    
            break;
        }
    }
}

向下调整算法

建小堆前提:建根结点的左右子树都是小堆

向下调整没有固定的路径可循,和左孩子交换还是和右孩子交换?

->由于是向下调整建小堆,所以和左右孩子中小的孩子交换

循环结束条件

1.调整到叶子了(度为0的结点)

or :2.调整到父亲结点的值比孩子的值小


由于不知道左孩子大还是右孩子大,所以可以假设左孩子更大,如果不是 : 左孩子+1就是右孩子

(左孩子:parent * 2 + 1 右孩子: parent * 2 + 2 ,二者坐标差1)

注意:要防止最后一层只有左孩子没有右孩子导致越界的情况:要判断:child + 1 < n ==> 右孩子下标要在数组范围内

如果child+1 >= n说明没有右孩子


//向下调整
void AdjustDown(int* a,int n , int parent)
{
    
    
    assert(a);
	int child = parent * 2 + 1;//默认是左孩子
    while(child < n)
    {
    
    
         //如果有右孩子且右孩子的值<左孩子则child变成右孩子,选出较小的孩子
    if(child+1<n && a[child+1] < a[child])
    {
    
    
        child +=1;
    }
    //如果小的孩子的值比父亲小,则交换,迭代继续往下调整
    //parent的下标比child下标的值小,往下调整,parent下标变大,即把child的下标赋给parent,然后再迭代找孩子比较
    if(a[child] < a[parent])
    {
    
    
        Swap(&a[child],&a[parent]);
        //迭代
        parent = child;//切记这里是把孩子的下标给父亲,不要和向上调整弄混
        child = parent * 2 + 1;
    }
    //小的孩子的值比父亲的值大,不用调整了
    else
    {
    
    
        break;
    }
    }
}

打印

直接遍历打印数组元素即可

void HeapPrint(HP* hp)
{
    
    
    assert(hp);
    int i = 0;
    for(i = 0;i<hp->size;i++)
    {
    
    
        printf("%d ",hp->a[i]);
    }
    printf("\n");
}

删除堆顶元素

==错误想法:==直接把后面的元素往前移动覆盖

导致的后果:堆的结构变了,不一定是原来的大堆/小堆 而且往前覆盖时间复杂度:0(N)

如:原来是大堆:
在这里插入图片描述


优质思路:

  • 数组的最后一个元素和第一个元素交换,
  • 然后去掉数组最后一个元素(这样就去掉了原来堆顶的元素),这样不会改变原来堆的结构
  • 然后就可以对根结点使用向下调整法 进行调整(向下调整法前提:左右子树是大堆/小堆)

在这里插入图片描述


除次之外:出堆顶数据,要抱枕堆中至少有一个元素(堆不为空)

void HeapPop(HP* hp)
{
    
    
    assert(hp);
    //保证堆中要有元素
    //assert(hp->size);
    assert(!HeapEmpty(hp));

    //方法:第一个元素和最后一个元素交换。
    //最后一个元素下标为:size-1,所以传的是size-1
    Swap(&hp->a[0],&hp->a[hp->size-1]);
    //去掉最后一个元素->即原来堆顶的元素
    hp->size--;
    //向下调整
    //size标志的是元素个数,所以传的是size
    //从根开始往下调,所以第三个参数为0(根节点)
    //要调整的是堆的数组 所以第一个参数:hp->a
    AdjustDown(hp->a,hp->size,0);
}

得到堆顶数据

返回类型为我们自己typedef重新定义的类型

要保证堆中有数据

HPDataType HeapTop(HP* hp)
{
    
    
	assert(hp);
	//保证堆中有数据
	assert(!HeapEmpty(hp));

	return hp->a[0];
}

堆是否为空

size标志的就是堆中元素个数,如果为0,说明堆为空


bool HeapEmpty(HP* hp)
{
    
    
    assert(hp);
    return hp->size == 0;
}

堆中元素个数

size标志的就是堆中元素个数,指向数组最后一个元素的下一个位置

int  HeapSize(HP* hp)
{
    
    
    assert(hp);
    return hp->size;
}

什么时候用向上调整,什么时候用向下调整

首先明确一点:向上调整和向下调整都可以构建堆


删除堆顶数据:

  • 先交换首尾元素
  • 去掉末尾元素(原来的堆顶元素)
  • 交换后,最上面的根位置可能导致整棵树不满足堆的结构,但是它的左子树和右子树都是堆
  • 所以就可以使用向下调整构建成堆

向下调整前提:左右子树都是堆(大堆/小堆) 要建什么堆,就要保证左右子树是什么堆

删除:从根节结点调到叶子 / 调到满足堆结构, 所以用的是向下调整


插入元素:

  • 在数组最后一个位置插入元素。
  • 树原本已经是堆了,然后插入一个元素后 这个元素就变成了树中的最后一个叶子
  • 这个叶子结点可能会破坏堆的特性
  • 所以需要向上调整

插入:从叶子调到根结点/调到满足堆,所以用向上调整


向上调整函数接口:

void AdjustUp(int* a,int child)
  • a:要调整的数组
  • child:要向上调整的孩子位置

向下调整函数接口:

void AdjustDown(int* a,int n , int parent)
  • a:要调整的数组
  • n:堆中元素个数(认为堆中有多少个元素)
  • parent:从哪个位置往下调整

向上/向下调整的时间复杂度分析

以向下调整为例

最坏情况:调整高度次,即从根结点一直调整到叶子


以向上调整为例

最坏情况:调整高度次,即从叶子结点一直调整到根


所以一次调整的时间复杂度为:O(高度)

共有N个元素,则总的时间复杂度是:0(高度*N)


完全二叉树高度分析:

在这里插入图片描述


完全二叉树结点范围:[2(h-1),2h -1 ]

2^(h -1) = N ==> h= logN +1 2^h -1 = N ==> h = log (N+1) (log 以2为底的

完全二叉树高度范围:[ logN +1 , log (N+1) ]


结论:

+1可以忽略, N:结点个数

所以调整一次:O(logN)

调整N次: O(N*logN)


建堆的时间复杂度分析

堆是完全二叉树,而满二叉树也是完全二叉树,为了简化,使用满二叉树来证明(时间复杂度本来看的
就是近似值,多几个节点不影响最终结果):

向下调整建堆 - O(N)

例子:

在这里插入图片描述


以向下调整为例子

最坏情况:每一层的每一个结点都要向下调整到叶子结点 从倒数第二层开始调,叶子结点(最后一层)不需要调整

在这里插入图片描述


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hikXIITL-1638148040709)(E:\Believe everything maybe true\Bit\数据结构\08.堆\堆.assets\image-20211115112807482.png)]


如何求出T(n) ->错位相减法
在这里插入图片描述


因为有n个结点,高度为h 2^h - 1 = n ->h = log(n+1)

代入T(n)得到:

在这里插入图片描述

因此:向下调整建堆的时间复杂度为O(N)。


test.c

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include"Heap.h"
void TEST1()
{
    
    
	HP hp;
	HeapInit(&hp);
    int a[] = {
    
    9,8,9,0,2,4,1,7,5 };
	int i = 0;
	int sz = sizeof(a) / sizeof(a[0]);
	for (i = 0; i < sz; i++)
	{
    
    
		HeapPush(&hp, a[i]);
		HeapPrint(&hp);
		printf("HeapSize = %d\n", HeapSize(&hp));
	}
	while (!HeapEmpty(&hp))
	{
    
    
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);
		printf("HeapSize = %d\n", HeapSize(&hp));
		printf("\n");
	}
	
	HeapDestory(&hp);
	//printf("%p\n", &hp);
	//printf("%p\n", hp.a);//NULL


}
int main()
{
    
    
	TEST1();
}

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include"Heap.h"

void HeapInit(HP* hp)
{
    
    
    assert(hp);
    hp->a = NULL;
    hp->capacity = hp->size = 0;
}

void HeapDestory(HP* hp)
{
    
    
    assert(hp);
    free(hp->a);
    hp->a = NULL;
    hp->capacity = hp->size = 0;
}



void Swap(HPDataType* px, HPDataType* py)
{
    
    
    HPDataType tmp = *px;
    *px = *py;
    *py = tmp;
}

//向上调整_小堆
void AdjustUp(int* a, int child)
{
    
    
    assert(a);
    int parent = (child - 1) / 2;
    //循环结束条件:child 调到根节点,即child为0
    while (child > 0)
    {
    
    
        //如果孩子结点比父亲结点的值小,就交换
        if (a[child] < a[parent])
        {
    
    
            Swap(&a[child], &a[parent]);
            //父亲和孩子结点交换 再迭代
            child = parent;
            parent = (child - 1) / 2;
            //继续往上调整,直到child调到根/父亲比孩子小
        }

        //如果孩子结点比父亲结点大了,说明不用调了
        else
        {
    
    
            break;
        }
    }
}

void HeapPush(HP* hp, HPDataType x)
{
    
    
    assert(hp);
    //空间不够,扩容
    if (hp->capacity == hp->size)
    {
    
    
        int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
        HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
        if (tmp == NULL)
        {
    
    
            printf("realloc fail\n");
            exit(-1);//异常结束 stdlib头文件
        }
        else
        {
    
    
            hp->a = tmp;
            hp->capacity = newcapacity;
        }
    }

    //插入数据-直接在size位置插入
    hp->a[hp->size] = x;
    hp->size++;

    //插入后不一定是还是堆,
    AdjustUp(hp->a, hp->size - 1);
    //size标志的是数组最后一个元素的下一个位置,size-1才是最后一个元素的下标,所以要调整新插入的位置,就是size-1位置
}




//向下调整_小堆
void AdjustDown(int* a, int n, int parent)
{
    
    
    assert(a);
    int child = parent * 2 + 1;//默认是左孩子
    while (child < n)
    {
    
    
        //如果有右孩子且右孩子的值<左孩子则child变成右孩子,选出较小的孩子
        if (child + 1 < n && a[child + 1] < a[child])
        {
    
    
            child += 1;
        }
        //如果小的孩子的值比父亲小,则交换,迭代继续往下调整
        //parent的下标比child下标的值小,往下调整,parent下标变大,即把child的下标赋给parent,然后再迭代找孩子比较
        if (a[child] < a[parent])
        {
    
    
            Swap(&a[child], &a[parent]);
            //迭代
            parent = child;//切记这里是把孩子的下标给父亲,不要和向上调整弄混
            child = parent * 2 + 1;
        }
        //小的孩子的值比父亲的值大,不用调整了
        else
        {
    
    
            break;
        }
    }
}

void HeapPrint(HP* hp)
{
    
    
    assert(hp);
    int i = 0;
    for (i = 0; i < hp->size; i++)
    {
    
    
        printf("%d ", hp->a[i]);
    }
    printf("\n");
}

void HeapPop(HP* hp)
{
    
    
    assert(hp);
    //保证堆中要有元素
    //assert(hp->size);
    assert(!HeapEmpty(hp));

    //方法:第一个元素和最后一个元素交换。
    //最后一个元素下标为:size-1,所以传的是size-1
    Swap(&hp->a[0], &hp->a[hp->size - 1]);
    //去掉最后一个元素->即原来堆顶的元素
    hp->size--;
    //向下调整
    //size标志的是元素个数,所以传的是size
    //从根开始往下调,所以第三个参数为0(根节点)
    //要调整的是堆的数组 所以第一个参数:hp->a
    AdjustDown(hp->a, hp->size, 0);
}

HPDataType HeapTop(HP* hp)
{
    
    
    assert(hp);
    //保证堆中有数据
    assert(!HeapEmpty(hp));

    return hp->a[0];
}

bool HeapEmpty(HP* hp)
{
    
    
    assert(hp);
    return hp->size == 0;
}

int  HeapSize(HP* hp)
{
    
    
    assert(hp);
    return hp->size;
}

Heap.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
    
    
    HPDataType* a;//用数组存储
    int size;//标志元素个数,size标志的是数组最后一个元素的下一个位置,size-1才是最后一个元素的下标
    int capacity;//容量,不够则扩容
}HP;

//初始化
void HeapInit(HP* hp);

//释放
void HeapDestory(HP* hp);

//打印
void HeapPrint(HP* hp);

//得到堆顶数据
HPDataType HeapTop(HP* hp);

//出堆顶数据
void HeapPop(HP* hp);

//插入元素
void HeapPush(HP* hp,HPDataType x);

//堆是否为空
bool HeapEmpty(HP* hp);

//堆元素个数
int  HeapSize(HP* hp);

猜你喜欢

转载自blog.csdn.net/chuxinchangcun/article/details/121602151