二叉树(二):堆的概念以及实现方法

基本概念:

堆也就是一棵完全二叉树

堆的分类:

大堆:所有的父节点都大于子节点

小堆:所有的父节点都小于子节点

堆的逻辑结构(便于理解想象出来的):

堆的实际存储方式:

本文主要讲解堆在数组中的存储。

堆在数组的存储方式下父亲节点和子节点的关系:

左孩子节点的数组下标一定是奇数,右子节点的数组下标一定是偶数

求子节点

leftchild = parents*2+1;

rightchild = parents*(2+1);

求父节点(因为再c/c++中"/"代表整除,再加上右节点是偶数,因此两个节点的下标减一除二都是父节点的下标)

parent = (child - 1)/ 2

代码实现:

堆的结构体的定义:

typedef  int HeapDataType;//以便修改堆的存储数据的类型,防止麻烦的修改

typedef struct Heap
{
    HeapDataType* a;
    int size;//记录当前存储数据的个数
    int capacity;//记录空间的容量,以便扩容
}HP;

堆的初始化:

void HeapInit(HP* php)
{
    assert(php);
    php->size = 0;
    php->a = NULL;//在push中再申请空间
    php->capacity = 0;
}

堆的销毁:

void HeapDestroy(HP* php)
{
    assert(php);
    free(php->a);
    php->size = php->capacity = 0;
}

交换数据:

参数传址调用来修改内容(形参只是原来数据的拷贝)

//写成函数是为了方便后面的复用
void Swap(HeapDataType* p1, HeapDataType* p2)
{
    HeapDataType tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

堆的插入数据(这里创建大堆):

插入的思路:

先将数据尾插到指针当中,再调整整个数组,使其的逻辑结构成为一个堆

如何调整数组?

确保插入数据前的整个数组都是一个堆的前提下,每次插入数据时,通过向上调整(将插入的数据不断和其父节点相比较,如果大于其父节点,那么交换两个数据,直到整个数组都是一个堆)

实现向上调整:
void  AdjustUp(HeapDataType* a, int child)
{
    //时间复杂度O(logN)
    int parent = (child - 1) / 2;
    //最好不用parent >= 0,因为如果child = 0时,parent仍然会等于0
    while (child > 0)
    {

        //创建大堆
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            child = parent;
            parent = (child - 1) / 2;
        }

        else break;
    }
}

代码实现:

void HeapPush(HP* php, HeapDataType x)
{
    assert(php);

    //插入数据
    //查看容量,看是否需要扩容
    if (php->capacity == php->size)
    {
        int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;//选择表达式
        HeapDataType* tmp = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newcapacity);
        if (tmp == NULL)
        {
            perror("realloc fail");
            exit(-1);
        }
        php->a = tmp;
        php->capacity = newcapacity;
    }
    php->a[php->size] = x;
    php->size++;

    //调整堆
    AdjustUp(php->a, php->size -1);
}

堆的删除堆顶数据:

删除数据的思路:

1.直接整体挪动数据来覆盖掉堆顶的数据,在进行调整?

不可以,首先不断移动数据效率过低.其次是整体挪动会打乱原来堆的整体结构(堆顶待删除节点的左子树和右子树全部被打乱)

2.将堆顶数据和最后一个数据进行交换,然后删除最后一个数据,这样维持了根节点的左右子树的结构,然后进行调整即可

实现向下调整:

这里由于在向下调整时需要判断左右子节点的大小,再与较大的子节点进行交换.

这里用假设法,先假设左节点为较大的节点,再进行比较,如果不是的话,就选择右节点进行判断是否需要交换

void AdjustDown(HeapDataType* a, int size, int parent)
{
    //假设左子节点大
    int child = parent * 2 + 1;
    while (child < size)
    {
        //判断左右孩子的大小,取大的
        if (a[child] < a[child + 1])
        {
            child++;
        }
        //孩子大于父亲,那么交换
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
        }
        parent = child;
        child = parent * 2 + 1;
    }
}

代码实现:

void HeapPop(HP* php)
{
    assert(php);
    assert(php->size > 0);
    //交换第一个数据和最后一个数据
    Swap(&php->a[0], &php->a[php->size - 1]);
    //无所谓是否删除最后一个数据,将存储数据个数的size-1即可
    php->size--;
    //向下调整
    AdjustDown(php->a,php->size,0);
}

堆的创建:

向上建堆法:

将每个数据利用上面讲过的插入数据的方式插入到堆的最后,然后通过向上调整的方法来完成建堆

void HeapCreate(HP* php, HeapDataType* a, int n)
{
    assert(php);
    //创建空间
    HeapDataType* tmp = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    php->a = tmp;
    //转移数据
    memcpy(php->a, a, sizeof(HeapDataType) * n);
    php->size = php->capacity = n;

    //时间复杂度(O(N*logN))
    //向上建堆
    for (int i = 1;i < n;i++)
    {
        AdjustUp(php->a, i);
    }
}
时间复杂度的计算:

假设数组中一共有n个数据,行数为h,第m行的数据个数为个。那么:

最坏的打算,每一行的每个数据都需要调整。

第一层,不调整

第二层,调整一次

......

第h-1层,调整h-2次

第h层,调整h-1次

通过错位相减法计算再带入h和n的关系化简再利用大O渐进表示法可得可得:

向下建堆法:

向下调整法:

如果二叉树的某个节点的两个子树都是满足大堆(假设是建大堆),那么该节点可以和两个子节点中较大的节点进行比较,如果小于则交换,循环直至建成一个堆,这种方法叫做向下调整法。

代码实现:
void AdjustDown(HeapDataType* a, int size, int parent)
{
    //假设左子节点大
    int child = parent * 2 + 1;
    while (child < size)
    {
        //判断左右孩子的大小并且右孩子必须在数组的有效范围内,取大的
        if (a[child] < a[child + 1] && child +1 <size)
        {
            child++;
        }
        //孩子大于父亲,那么交换
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
        }
        parent = child;
        child = parent * 2 + 1;
    }
}
思路分析:

因为向下调整的前提是两个子树都已经是堆了,那么调整需要从最后一个节点的父节点开始,向前面的节点依次向下调整,即可完成向下建堆。

代码实现:
void HeapCreate(HP* php, HeapDataType* a, int n)
{
    assert(php);
    HeapDataType* tmp = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
    if (tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    php->a = tmp;
    //将数组的数据拷贝过去
    memcpy(php->a, a, sizeof(HeapDataType) * n);
    php->size = php->capacity = n;
    //向下调整建堆
    //计算时间复杂度(O(N))
    for (int i = (n - 1 - 1) / 2;i >= 0;i--)
    {
        AdjustDown(php->a,n,i);
    }
}
时间复杂度计算:

假设数组中一共有n个数据,行数为h,第m行的数据个数为个。那么:

最坏的打算,每一行的每个数据都需要调整。

第一层,调整h-1次

第二层,调整h-2次

......

第h-1层,调整1次

第h层,调整0次

通过错位相减法的计算和带入h和n的关系化简可得:,再利用大O渐进表示法可得:

向上调整建堆和向下调整建堆的区别:

向上调整法下面的节点调整次数多,向下调整法上面节点调整次数多,而二叉树的结构又是上面节点少,下面节点多,因此更推荐使用向下调整法来进行建堆

堆排序:

首先先要进行建堆,那么问题来了:如果要排出升序的序列的话,应该用大堆还是小堆呢?

我们大多数人可能会想,应该排出小堆,因为小堆上面的数据是最小的,然后Pop掉第一个数据,不断循环,可这样话Pop掉第一个数据之后会打乱所有的堆的结构,并且时间复杂度太高。

因此我们可以参考前面Pop数据的方式,建出大堆,然后将堆顶也就是最大的数据和最后一个数据交换,然后堆内的数据减1,这样的话根节点的两个子树都仍是一个大堆,然后选择向下调整来建堆,重复进行,最终完成排序

代码实现:
void HeapSort(HeapDataType* a, int n)
{
    //对该数组进行向下建堆
    for (int i = (n - 1 - 1) / 2;i >= 0;i--)
    {
        AdjustDown(a, n, i);
    }

    //记录需要交换的次数,n个数据交换n-1次,此时
    int end = n - 1;
    while (end > 0)
    {
        Swap(&a[0], &a[end]);
        //交换完成之后,end也就刚好成了新的堆的数据的个数(即原来堆顶的数据放到最后一个位置了,然后认为堆的数据少1)
        AdjustDown(a, end, 0);
        end--;
    }
}

TopK问题:

TopK问题即是选出数据中的最大或者最小的k个数据(如世界财富榜前十位,国服前十李元芳等等)

这里以计算最大的数据来举例

方法一:

当数据较少,可以直接输入数据时,可以通过向下建堆法来将这些数据进行建大堆,然后取堆顶的数据Pop掉然后再向下调整这样不断循环,就可以得到前k个数据

时间复杂度:

建堆:

Pop前k个数据(循环k次,每次将堆顶的数据和最后一个数据交换后然后再向下调整):

总体为:

方法二:

当数据非常多时,采用上面的方法会导致时间复杂度过高,效率太低

因此我们选择别的方法:

首先,将前k个数字进行建立一个小堆,这样堆顶的数据在堆里是最下的,然后依次遍历剩余的每个数据,如果大于堆顶的数据,那么就替换它入堆,然后再向下调整,得到一个新的小堆。最后得到的就是最大的k个数

时间复杂度:

前k个数据建堆:

N-K次循环然后入堆向下调整(最坏的结果,数据本来为升序,每次遍历的数据都需要入堆,每次向下调整的复杂度为):

代码实现:

这里为了模拟大量的数据,首先在一个文件中写入了大量的随机数

void HeapTest5()
{
    //手动输入所要总体数据的个数n和选择前k个数据
    printf("请输入n和k::");
    int k, n;
    scanf("%d%d", &n, &k);
    //动态申请空间
    int* minHeap = (int*)malloc(sizeof(int) * k);
    if (minHeap == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    //后面要利用函数rand()来获得随机数,但rand()获得的随机数在重复运行时是固定的,而函数srand()中输入的种子数可以影响rand()获得的随机数,因此为了模拟随机性,在每次运行时要在srand()中输入不同的种子数,而函数time(0)可以在每次运行时获得当前的时间戳,这样就保证了rand()获得数据的随机性
    srand(time(0));
    //在文件中写入n个数据


    FILE* fin = fopen("data.txt", "w");
    if (fin == NULL)
    {
        perror("fin fail");
        exit(-1);
    }
    for (int i = 0;i < n;i++ )
    {
        int val = rand();
        fprintf(fin, "%d ", val);
    }
    //关闭文件
    fclose(fin);

    //在文件中读取n个数据
    FILE* fout = fopen("data.txt", "r");
    if (fout == NULL)
    {
        perror("fout fail");
        exit(-1);
    }
    //读取数据
    for(int i = 0;i<k;i++)
    {
        fscanf(fout, "%d", &minHeap[i]);
    }
    //建小堆
    for (int i = (k - 1 - 1) / 2;i >= 0;i--)
    {
        AdjustDown(minHeap, k, i);
    }
    //继续读取数据
    int val;
    while (fscanf(fout, "%d", &val) != EOF)
    {
        //如果读取的数据大于堆顶的数据,就入堆
        if (val > minHeap[0])
        {
            minHeap[0] = val;
            AdjustDown(minHeap, k, 0);
        }
    }
    //打印所得到topK个数据
    for (int i = 0;i < k;i++)
    {
        printf("%d ", minHeap[i]);
    }
    fclose(fout);
    free(minHeap);
}

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值