C语言数据结构:堆

1.什么是堆?

堆是一个 完全二叉树 ,它的数据是 按完全二叉树的顺序存储方式存储在一个一维数组中 ,堆分为为两种:
1.大根堆 根节点最大的堆叫做大根堆 ,并且父亲的值必须大于等于它的左孩子和右孩子,如下图所示:
2.小根堆 根节点最小的堆叫做小根堆 ,并且父亲的值必须小于等于它的左孩子和右孩子,如下图所示:
堆的性质:1.堆中某个节点的值总是不大于或不小于其父节点的值;2.堆总是一棵完全二叉树;
没有学过堆的同学,上面两种图片中堆的逻辑结构在存储结构中是怎么去存的,一定要仔细去看,下面的实现中为了大家的理解只画了逻辑结构,如果不清楚下面的部分操作会看不懂。

2.堆数组下标中父亲、左孩子、右孩子之间的计算方式

1.已知父亲的下标,计算它左孩子和右孩子的下标:

父亲的下标为i,获取它左孩子和右孩子的下标的计算公式分别为:
1.左孩子: Leftchildi = i * 2 + 1;
2.右孩子: rightchildi = i * 2 + 2;
上图堆数组中45的下标为1,分别求出它左孩子和右孩子的下标:
1.左孩子:1 * 2 + 1 = 3;
2.右孩子:1 * 2 + 2 = 4;
所以45的左孩子下标为3,右孩子的下标为4。
在计算左孩子和右孩子下标的时候,需要注意一下数组的长度,否则会造成越界访问。

2.已知左孩子和右孩子的下标,计算它俩父亲的下标:

计算父亲下标这一点非常的特殊,不管是左孩子还是右孩子都能套这个公式。
Parenti = (childi - 1) / 2;
上图堆数组中55下标为4,所以它父亲的下标为:(4 - 1) / 2  = 1
(4 - 1) / 2在数学中是1.5,但是在C语言中是1,原因是因为 C语言中整数除法的结果只能是整数,小数部分会舍弃掉 所以这就是上面这个公式左孩子和右孩子都能套用的原因。

3.堆的实现(小堆)

堆和顺序表一样,都是使用的是数组来存储,但是堆的逻辑结构很复杂,它虽然使用的是数组来存,但是它本质上还是一个二叉树。

1.堆的定义

typedef int HPDataType;
typedef struct Heap
{
    HPDataType* _a;
    int _size;
    int _capacity; 
}Heap;

size记录的是有效数据的个数,capacity记录的是容量的大小。

2.堆的初始化

初始化部分建议开4个的空间,检查一下是否开辟成功后将size初始化为0,将capacity初始化为4

// 堆的初始化
void HeapInit(Heap* hp)
{
    assert(hp);
    hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * 4);
    if(hp->_a == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    hp->_size = 0;
    hp->_capacity = 4;
}

3.堆的销毁

将在堆上开辟的空间释放掉以后,将指针设置为空,将capacity和size设置为0

// 堆的销毁
void HeapDestory(Heap* hp)
{
    assert(hp);
    free(hp->_a);
    hp->_a = NULL;
    hp->_capacity = 0;
    hp->_size = 0;
}

4.堆的插入

1.实现步骤:

插入部分这里是尾插,尾插完后要向上调整一下,所以堆的插入这个函数实现的步骤:
1.检查容量,判断是否需要扩容;
2.尾插,需要向上调整;
3.尾插完成,size++;

2.向上调整:

如果不向上调整,插入数据后就不能保证这个数组是小堆了,看下面的分析步骤:

1.下图堆中,我们尾插了一个数据20:

在插入数据前,这棵树还是个小堆,但是插入数据后就不复合小堆的定义了,所以需要向上调整。

2.如果要确保它还是一个小堆,就需要去调整数据,为了不影响其他子树(如上图中的左子树),新插入的数据只能和父亲做调整,调整方式就是交换位置,如下图所示:

3.调整完后,不一定还是小堆,还需要继续向上调整直到它的父亲小于等于新插入的数据为止,或者是新插入的数据为根节点为止,如下图所示:

随着新插入的数据20的调整过程中,它的父亲也一直在变如上面的图片中,它的父亲由40变成了20,最后20变成了祖先。

小堆向上调整的总结:

1.为了不影响其他子树,只能和父亲做调整;

2.当新插入的数据小于父亲时,需要交换它们两个的位置;

3.直到新插入的数据大于等于父亲或者是新插入的数据成为了根节点时调整才能停止。

向上调整的代码:
void Adjust_up(HPDataType* _a,int childi)
{
    int parenti = (childi - 1) / 2;//计算出父亲下标的
    while(childi > 0)//结束条件2,当新插入的数据成为根节点
    {
        if(_a[childi] < _a[parenti])//当父亲大于新插入的数据,需要调整
        {
            Swap(&_a[childi],&_a[parenti]);
            
            //更新孩子和父亲下标,继续向上调整
            childi = parenti;
            parenti = (childi - 1) / 2;
        }
        else
            break;//结束条件1,新插入的数据大于等于父亲时
    }
}

3.堆的插入代码:

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
    assert(hp);
    if(hp->_size == hp->_capacity)//需要扩容
    {
        HPDataType* temp = (HPDataType*)realloc(hp->_a,sizeof(HPDataType) * 2 * hp->_capacity);
        if(temp == NULL)
        {
            perror("realloc fail");
            exit(-1);
        }
        hp->_capacity *= 2;
    }

    hp->_a[hp->_size] = x;
    Adjust_up(hp->_a,hp->_size);
    hp->_size++;
}

5.堆的删除

堆如果要删除数据,一般只会删除头部(根节点)的数据,不会去尾删,尾删没有任何意义。
但是删除方法并不是和顺序表一样去挪动数据,以此来将头部删除掉,而是 先将头部和尾部的数据进行交换,如下图所示:

将记录有效数据个数的size--后,头部删除就完成了从上图中可以看见,当头部和尾部的数据交换后堆就不再是小堆了为了保证存储的数据的数组继续是小堆,就要进行向下调整操作。

1.向下调整

当头部的数据删除以后,左子树和右子树其实还是小堆,如下图所示:

要进行向下调整,就要从80它的两个孩子中选出一个来调整,因为我们要实现的小堆,只能选择一个最小的,也就是它的右孩子,下图是它们两个的顺序交换后:

交换完后,从上图中可以看见,这些数据依旧不是小堆,就要继续向下调整,直到它的左孩子和右孩子的大小大于等于它为止,或者是已经没有左右孩子可以来调整了,向下调整就完成了,如下图所示:

随着80的不断的向下调整,它的左右孩子都会变,最后可能就没有儿子了(变成孤家寡人了)

小堆向下调整的总结:

1.从左右孩子中选出最小的那一个去和它做调整(交换)。

2.调整完后,它的位置发生了改变,它对应的左右孩子也会发生改变。

3.当它的左右孩子的值大于等于它时或者是没有孩子时可以调整时,向下调整完成。

2.向下调整代码:

void Adjust_down(HPDataType* _a,int parenti,int len)
{
    int left = parenti * 2 + 1;//左孩子的下标
    while(parenti < len && left < len)//结束条件2:当没有孩子可以进行交换
    {
        if(left + 1 < len && _a[left] > _a[left + 1])//找出最小的孩子
        {
            left++;
        }

        if(_a[parenti] > _a[left])//如果父亲大于孩子,需要进行交换
        {
            Swap(&_a[left],&_a[parenti]);

            //更新下标
            parenti = left;
            left = parenti * 2 + 1;
        }
        else
            break;//结束条件1:父亲小于孩子
    }
}

3.堆的删除代码:

// 堆的删除
void HeapPop(Heap* hp)
{
    assert(hp);
    assert(hp->_size > 0);
    if(hp->_size == 1)//只有一个数据时,没有必要交换和向下调整
    {
        hp->_size--;
    }
    else
    {
        Swap(&hp->_a[0],&hp->_a[hp->_size - 1]);//交换头部和尾部的数据
        hp->_size--;//删除数据
        Adjust_down(hp->_a,0,hp->_size);//向下调整
    }
}

6.取堆顶的数据

先检查数据的有效个数,返回数组0号下标的位置的值

// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
    assert(hp);
    assert(hp->_size > 0);
    return hp->_a[0];
}

7.堆的数据个数

返回size,size记录的是数据的有效个数

// 堆的数据个数
int HeapSize(Heap* hp)
{
    assert(hp);
    return hp->_size;
}

8.堆的判空

判断size,size为0就返回真,否则返回假

// 堆的判空
int HeapEmpty(Heap* hp)
{
    assert(hp);
    return hp->_size == 0;
}

9.用数组来初始化堆

如果用数组来初始化的话,我们可以遍历数组的时候,复用堆的插入这个代码,向上调整的方式一个个的去依次插入,理论上时间复杂度是n*logn。
我们还可以用向下调整的方式去建堆,从 倒数的第一个非叶子节点的子树开始向下调整,一直调整到根节点的树,就可以调整成堆,倒数第一个非叶子节点就是数组最后一个数据的父亲
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
    assert(hp);
    hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);
    if(hp->_a == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    hp->_capacity = n;
    hp->_size = n;

    memcpy(hp->_a,a,sizeof(HPDataType) * n);//将a的数据拷贝进去

    int len = n - 1;//最后一个数据的下标
    for(int i = ( len - 1) / 2; i >= 0; i--)//向下调整建堆
    {
        Adjust_down(hp->_a,i,n);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值