赞
踩
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费,因此完全二叉树更适合使用顺序结构来存储。
二叉树一般有两种结构存储: 顺序结构和链式结构。
顺序结构也就是顺序表(数组)来存储,一般只有完全二叉树和满二叉树适合数组来存储。其他二叉树会有空值——会造成空间浪费。而我们通常把堆(一种二叉树)使用顺序结构的数组来存储。
二叉树顺序存储在物理上是一个数组,在逻辑上(想象中的)是一颗二叉树。
小堆:每个父亲都小于等于孩子 parent<=child
大堆:每个父亲都大于等于孩子 parent>=child
leftchild=parent*2+1 rightchild=parent*2+2
那么parent怎么计算呢?推导一下即可
parent=(child-1)/2;
如果插入一个数据元素90 直接尾插即可 顺序表尾插效率高,而且没有破坏小堆的结构。
如果插入一个数据元素55呢?求出父亲是60
从上面情况可以看出有两种情况
1.直接插入不会影响原来堆的结构 直接尾插即可。
2.插入数据会出现比父亲小的情况(小堆),这时候就需要去调整堆的关系,才能形成正确的堆关系,好的情况下一次调整就完成了调整,那么最坏的情况就是调整到根节点才会结束调整。
就比如下面这种情况,尾插的数据元素5直接从孙子变成了太爷爷
把数组中最后一个数据元素作为孩子,根据孩子求出父亲位置,再判断孩子是否小于父亲,如果符合条件就调整位置,再把父变子,再求父亲位置,循环以上步骤,最坏判断到根节点。
怎么判断循环结束条件呢?根据上面的画图,不难看出当parent<0 循环就结束了,但不能这样判断,因为当parent为0时 -1/2还是0 因为是除 余为0。在我们画图分析时,parent看起来会往上走,但实际还是0,因此我们要以孩子作为循环执行条件,从上面画图分析可以看出child>0时循环执行,当child==0时循环结束,因此循环执行条件应该是child>0。
说了那么多理论知识和方法,接下来开始写代码。
- void Adjustup(Heapdatatype* a, int child)
- {
- int parent = (child - 1)/2;
- while (child>0)
- {
- if (a[child] >a[parent])
- {
- Swap(&a[child], &a[parent]);
-
- child = parent;
- parent = (child - 1) / 2;
- }
- else
- {
- break;
- }
- }
- }
向上调整的前提:数据是堆
如果插入数据不符合条件break跳出循环即可
那么向下调整的时间复杂度是多少呢?深度为h的二叉树最大结点为2^h-1,假设有N个数据
那么时间复杂度就是O(logN) 如果有10亿数据也只需调整30次。
那么如果堆要删除数据,怎么删呢?直接尾删吗?
把70删掉并没有实际意义,把根节点删掉才有意义
挪动覆盖大概率会打乱堆的之间的关系——形不成堆。
那么怎么删除呢?前辈研究发现结果是——先把根节点和最后一个结点先交换,再删除
通过根节点和最后一个结点数据交换再删除,可以发现左右子树都没有被动到,左右子树依旧是小堆。但"爷爷"隐退江湖,给孙子让位当掌门,底下的其他人坐得住吗?显然是坐不住的,因此要进行比武大会,打赢的人才有资格当下一个掌门人。
经过一系列的比武,70不出所料的又到最下面去了,德不配位了属于是。
因此向下调整的前提是:左右子树是大堆/小堆
时间复杂度也是log(N)
向下调整怎么写呢?大部分人都会先比较左右孩子(小堆为例,找出最小孩子),再上去调整,但这样写太麻烦了,不太推荐
- void AdjustDown(Heapdatatype*a,int n,int parent)
- {
- int child = parent * 2 + 1;//默认左孩子最小
- while (child<n)
- {
- //如果假设左孩子小了是错误的 就再判断一次(找出最小孩子)
- if (a[child + 1] < a[child])
- {
- ++child;
- }
- //光标闪动之处就是最小孩子位置//最小孩子 不关心是左/右子
- if (a[child] < a[parent])
- {
- Swap(&a[child], &a[parent]);
-
- parent = child;
- child = parent * 2 + 1;
- }
- else
- {
- break;
- }
- }
- }
我们演示的是小堆的向下调整,因此我们先默认左孩子是最小的孩子,但我们的判断可能会出错误,因此在循环里面再判断一次 ,判断完成后,光标闪动的地方就是最小孩子的位置(我们不关心是左孩子最小还是右孩子最小,找出最小孩子即可),再跟向上调整一样,判断父子之间的关系,进行调整。
从画图可以看出当child大于size循环结束,因此循环继续条件就是child小于n。
大家继续看看这段代码有什么bug吗? 有的
万一右孩子不存在呢?如果右孩子不存在,child++就越界了,因为在判断条件还要补上右孩子是存在的条件。
实验代码
- int main()
- {
- int arr[] = { 65,100,70,32,50,60 };
- Hp ph;
- HpInit(&ph);
- for (size_t i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
- {
- //push进去以后才是堆 大堆还是小堆 具体看调整符号
- //我们建的是小堆
- HpPush(&ph,arr[i]);
- }
- HpPrint(&ph);
-
- //int k = 5;//取前5个最大的 k--走k次 --k走k-1次
- while (!HpEmpty(&ph))
- {
- printf("%d ", HpTop(&ph));
- HpPop(&ph);
- }
-
- HpDestroy(&ph);
- return 0;
- }
当push进去向上调整后,成为小堆以后,取栈顶元素再pop后,小堆变成了有序
当是大堆时,大堆变成了降序
大堆可以依次排出降序 小堆可以依次排出升序
但注意,目前还不算堆排序,
如果有100家外卖店,要排出了前10个最高评分的店,是不是topk问题?根本不用一个个比较,直接把数据push进去(建堆),再不断Top pop就可以依次取出最高/最小元素排列顺序。
如果要取出前5个最大的 直接设一个变量 再--就可以取出来
- void HeapSort(int* a, int n)
- {
- Hp ph;
- HpInit(&ph);
- for (size_t i = 0; i <n; i++)
- {
-
- HpPush(&ph,a[i]);
- }
- //HpPrint(&ph);不需要这步
-
- int i = 0;
- while (!HpEmpty(&ph))
- {
- //printf("%d ", HpTop(&ph));
- a[i++] = HpTop(&ph);//直接覆盖
-
- HpPop(&ph);
- }
- HpDestroy(&ph);
- return 0;
- }
这个堆排序有什么缺陷呢?
1.首先我们得有个数据结构,把结构体 destroy push之类的构建出来,而且人家只需要堆排序以后的结果,不需要我们打印大堆/小堆数据,那么我们直接建立个新数组直接把堆排序结果覆盖进去。
2. 空间复杂度的消耗,数据结构申请了空间资源,因为排序额外开了新的数据空间,造成了二次空间数据空间浪费。
正确的堆排序
怎么改善堆排序呢?
直接在原数组上建堆(实际上是插入)
向上建堆的时间复杂度O(N*logN)
- void HeapSort(int* a, int n)
- {
- int i = 0;
- for (i = 1; i < n; i++)
- {
- Adjustup(a, i);
- }
-
- }
直接在原数组上建堆,省去了push的申请空间资源,节省了空间浪费。
如果我们要建升序 是建小堆还是大堆
如果要升序建小堆
选出了最小的数据,要选次小,只能把剩下的数据看做堆
但关系全乱了,剩下的数据不一定是堆,要重新建堆,建堆代价太大 时间复杂度为(N*log(N))*N
因此如果要建升序 最好是建大堆+删除思想
首尾交换 最大的数据排好了,剩下的数据向下调整,选出次大的即可。
时间复杂度是N*log(N)
向下建堆时间复杂度是O(N)
向下调整建堆会根据不同情况 有不同数据排序变化
大堆时是升序 小堆是降序 向上调整建堆一样。
因此在建堆时,向下调整建堆优势于向上调整建堆。
Topk问题:就是求出数据中前k个最大或最小的元素,一般情况下数据量比较大。
比如:一个游戏内全服最强前10名玩家、世界500强公司等等......
对于TopK问题,最简单的办法就是排序,建个小堆或者大堆 不断取出堆顶数据然后pop数据 就可以排出来 前k个最大或最小的元素数据(在数据量少时),但在数据量比较大时,将所有数据全部加入内存再进行排序是不太可能的,会超过电脑本身的内存空间,造成大量的空间浪费。
因此我们就想出一下办法,在大量数据中排出前k个最大元素。
假设10亿个数据,内存存不下,数据在文件中找出最大的前K个 K==100
1.读取文件的前100个数据,在内存数组中建立一个小堆
2、再依次读取剩下数据,跟堆顶数据比较,大于堆顶,就替换它进堆,向下调整
3、所有数据读完,堆里面数据就是最大的前100个
为什么求出前k个最大元素要建小堆?如果建立大堆,如果在N个元素中,最大的元素数据第一个就来了进堆顶,只能找出最大的一个元素,其他比它小的元素数据将进不了堆,因此不能建大堆。
而且建小堆,当我们把前100个数据元素建立好堆后,剩下的前k个最大数据元素都能进堆,因为是小堆,不会出现占在栈顶 不让其他数据元素进堆的情况,因为会向下调整。
因此Topk的空间复杂度和时间复杂度都很优秀。
时间复杂度是O(N*logK) 空间复杂度是O(K) 但这种情况都是N大于K K都可以忽略不计
因此空间复杂度可以是O(1) 时间复杂度O(N)
-
- void CreateNDate()
- {
- // 造数据
- int n = 10000;
- srand(time(0));
- const char* file = "data.txt";
- FILE* fin = fopen(file, "w");
- if (fin == NULL)
- {
- perror("fopen error");
- return;
- }
-
- for (int i = 0; i < n; ++i)
- {
- int x = rand() % 1000000;
- fprintf(fin, "%d\n", x);
- }
-
- fclose(fin);
- }
-
- int main()
- {
- CreateNDate();
- }
创造了10000个数据
- void PrintTopK(const char*filename, int k)
- {
- // 1. 建堆--用a中前k个元素建堆
- FILE* fout = fopen(filename, "r");
- if (fout == NULL)
- {
- perror("fopen fail");
- return;
- }
-
- int* minheap = (int*)malloc(sizeof(int) * k);
- if (minheap == NULL)
- {
- perror("malloc fail");
- return;
- }
-
- for (int i = 0; i < k; i++)
- {
- fscanf(fout, "%d", &minheap[i]);
- }
-
- //前k个数建小堆
- for (int i = (k - 2) / 2; i >= 0; --i)
- {
- AdjustDown(minheap, k, i);
- }
-
- // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
- int x = 0;
- while (fscanf(fout, "%d", &x) != EOF)
- {
- if (x > minheap[0])
- {
- // 替换你进堆
- minheap[0] = x;
- AdjustDown(minheap, k, 0);
- }
- }
-
-
- for (int i = 0; i < k; i++)
- {
- printf("%d ", minheap[i]);
- }
- printf("\n");
-
- free(minheap);
- fclose(fout);
- }
-
-
- void CreateNDate()
- {
- // 造数据
- int n = 10000;
- srand(time(0));
- const char* file = "data.txt";
- FILE* fin = fopen(file, "w");
- if (fin == NULL)
- {
- perror("fopen error");
- return;
- }
-
- for (int i = 0; i < n; ++i)
- {
- int x = rand() % 1000000;
- fprintf(fin, "%d\n", x);
- }
-
- fclose(fin);
- }
-
- int main()
- {
- //CreateNDate();
- PrintTopK("data.txt", 10);
- }
但是,我们怎么知道这10个数据就是10万个数据中最大的10个数据呢?
我们可以在data.txt可以自己去修改随机10个数字,让他大于10万即可,再运行程序,看看结果是不是我们修改的那10个数字就可以了。
-
- #pragma once
- #include<stdio.h>
- #include<stdlib.h>
- #include<assert.h>
- #include<stdbool.h>
- #include<string.h>
- typedef int Heapdatatype;
- typedef struct Heap
- {
- Heapdatatype* _a;
- int _capacity;
- int _sz;
- }Hp;
- void Swap(Heapdatatype* p1, Heapdatatype* p2);
- void HpInit(Hp* ps);
- void HpDestroy(Hp* ps);
- void HpPush(Hp* ps, Heapdatatype x);
- void HpPrint(Hp* ps);
- void HpPop(Hp* ps);
- bool HpEmpty(Hp* ps);
- Heapdatatype HpTop(Hp* ps);
- void AdjustDown(Heapdatatype* a, int n, int parent);
- void Adjustup(Heapdatatype* a, int child);
- void HpAryy(Hp* ps, int* a, int n);
- #include"Heap.h"
- void HpInit(Hp* ps)
- {
- assert(ps);
-
- ps->_a = NULL;
- ps->_capacity = ps->_sz = 0;
- }
- void HpDestroy(Hp* ps)
- {
- free(ps->_a);
-
- ps->_a = NULL;
- ps->_capacity = ps->_sz == 0;
- }
- void HpPrint(Hp* ps)
- {
- assert(ps);
-
- for (size_t i = 0; i < ps->_sz; i++)
- {
- printf("%d ", ps->_a[i]);
- }
- printf("\n");
- }
- void Swap(Heapdatatype* p1, Heapdatatype* p2)
- {
- Heapdatatype tmp = *p1;
- *p1 = *p2;
- *p2 = tmp;
- }
- void Adjustup(Heapdatatype* a, int child)
- {
- int parent = (child - 1)/2;
- while (child>0)
- {
- if (a[child] >a[parent])
- {
- Swap(&a[child], &a[parent]);
-
- child = parent;
- parent = (child - 1) / 2;
- }
- else
- {
- break;
- }
- }
- }
- void HpPush(Hp* ps, Heapdatatype x)
- {
- assert(ps);
-
- if (ps->_sz == ps->_capacity)
- {
- int newcapcity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
- Heapdatatype* tmp = (Heapdatatype*)realloc(ps->_a, sizeof(Heapdatatype) * newcapcity);
- if (tmp == NULL)
- {
- perror("realloc fail");
- exit(-1);
- }
- ps->_a = tmp;
- ps->_capacity = newcapcity;
- }
- ps->_a[ps->_sz] = x;
- ps->_sz++;
-
- Adjustup(ps->_a, ps->_sz - 1);
- }
- void AdjustDown(Heapdatatype*a,int n,int parent)
- {
- int child = parent * 2 + 1;//默认左孩子最小
- while (child<n)
- {
- //如果假设左孩子小了是错误的 就再判断一次(找出最小孩子)
- if (child + 1 < n&&a[child + 1] >a[child])
- {
- ++child;
- }
- //光标闪动之处就是最小孩子位置//最小孩子 不关心是左/右子
- if (a[child]>a[parent])
- {
- Swap(&a[child], &a[parent]);
-
- parent = child;
- child = parent * 2 + 1;
- }
- else
- {
- break;
- }
- }
- }
- void HpAryy(Hp* ps, int* a, int n)
- {
- assert(ps);
- assert(a);
-
- ps->_a = (Heapdatatype*)malloc(sizeof(Heapdatatype) * n);
- if (ps->_a == NULL)
- {
- perror("malloc fail");
- return;
- }
- ps->_sz = n;
- ps->_capacity = n;
-
- memcpy(ps->_a, a, sizeof(Heapdatatype) * n);
- for (int i = 1; i < n; i++)
- {
- Adjustup(ps->_a, i);
- }
- }
- void HpPop(Hp* ps)
- {
- assert(ps);
- assert(ps->_sz > 0);
-
- Swap(&ps->_a[0], &ps->_a[ps->_sz - 1]);
-
- ps->_sz--;
-
- AdjustDown(ps->_a,ps->_sz,0);
-
- }
- bool HpEmpty(Hp* ps)
- {
- assert(ps);
-
- return ps->_sz == 0;
- }
- Heapdatatype HpTop(Hp* ps)
- {
- assert(ps);
- assert(ps->_sz > 0);
-
- return ps->_a[0];
- }
- #include"Heap.h"
- #include<time.h>
-
- //缺陷 1.首先得有一个堆的数据结构 2.空间复杂度的消耗 因为排序额外开了一段空间
- //void HeapSort(int* a, int n)
- //{
- // Hp ph;
- // HpInit(&ph);
- // for (size_t i = 0; i <n; i++)
- // {
- //
- // HpPush(&ph,a[i]);
- // }
- // //HpPrint(&ph);
- // //堆排要的是排序结果 不用你把原来数据打印
- // int i = 0;
- // while (!HpEmpty(&ph))
- // {
- // //printf("%d ", HpTop(&ph));
- // a[i++] = HpTop(&ph);//直接覆盖
- //
- // HpPop(&ph);
- // }
- // HpDestroy(&ph);
- // return 0;
- //}
- //为什么向上调整和向下调整传的不是hp 而是数组的结构 就是为了方便这里的建堆
- void HeapSort(int* a, int n)
- {
- //int i = 0;
- //建堆-向上调整
- //建升序 是大堆还是小堆?
- //大堆
- //for (i = 1; i < n; i++)//N*log(N)
- //{
- // Adjustup(a, i);
- //}
- //向下调整建堆—大堆
- //时间复杂度O(logN)
- int i = 0;
- for (i = (n - 1 - 1) / 2; i >=0; i--)
- {
- AdjustDown(a, n, i);
- }
-
- //O(N*logN)
- int end = n - 1;
- while (end > 0)
- {
- Swap(&a[0], &a[end]);
-
- AdjustDown(a, end, 0);
- --end;
- }
-
- }
- int main()
- {
- //随便一个数组可以看成是二叉树 但不可能是堆 所以要先建堆
- int arr[] = { 65,100,70,32,50,60 };
- Hp ph;
- HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
- int i = 0;
- for (i = 0; i < 6; i++)
- {
- printf("%d ", arr[i]);
- }
- }
- //int main()
- //{
- // int arr[] = { 65,100,70,32,50,60 };
- // Hp ph;
- // HpInit(&ph);
- // for (size_t i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
- // {
- // //push进去以后才是堆 大堆还是小堆 具体看调整符号
- // HpPush(&ph,arr[i]);
- // }
- // HpPrint(&ph);
- //
- // int k = 5;//取前5个最大的 k--走k次 --k走k-1次
- // while (!HpEmpty(&ph)&&k--)
- // {
- // printf("%d ", HpTop(&ph));
- // HpPop(&ph);
- // }
- //
- // HpDestroy(&ph);
- // return 0;
- //}
- //void PrintTopK(const char*filename, int k)
- //{
- // // 1. 建堆--用a中前k个元素建堆
- // FILE* fout = fopen(filename, "r");
- // if (fout == NULL)
- // {
- // perror("fopen fail");
- // return;
- // }
- //
- // int* minheap = (int*)malloc(sizeof(int) * k);
- // if (minheap == NULL)
- // {
- // perror("malloc fail");
- // return;
- // }
- //
- // for (int i = 0; i < k; i++)
- // {
- // fscanf(fout, "%d", &minheap[i]);
- // }
- //
- // //前k个数建小堆
- // for (int i = (k - 2) / 2; i >= 0; --i)
- // {
- // AdjustDown(minheap, k, i);
- // }
- //
- // // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
- // int x = 0;
- // while (fscanf(fout, "%d", &x) != EOF)
- // {
- // if (x > minheap[0])
- // {
- // // 替换你进堆
- // minheap[0] = x;
- // AdjustDown(minheap, k, 0);
- // }
- // }
- //
- //
- // for (int i = 0; i < k; i++)
- // {
- // printf("%d ", minheap[i]);
- // }
- // printf("\n");
- //
- // free(minheap);
- // fclose(fout);
- //}
- //
- //
- //void CreateNDate()
- //{
- // // 造数据
- // int n = 10000;
- // srand(time(0));
- // const char* file = "data.txt";
- // FILE* fin = fopen(file, "w");
- // if (fin == NULL)
- // {
- // perror("fopen error");
- // return;
- // }
- //
- // for (int i = 0; i < n; ++i)
- // {
- // int x = rand() % 1000000;
- // fprintf(fin, "%d\n", x);
- // }
- //
- // fclose(fin);
- //}
- //
- //int main()
- //{
- // //CreateNDate();
- // PrintTopK("data.txt", 10);
- //}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。