上次通过数组实现堆排序的时间复杂度为O(NlogN),空间复杂度为O(N)。
优化后时间复杂度O(NlogN),空间复杂度O(1).
考虑优化方案:
1.向下调整建堆。
2.向上调整建堆。
建堆只是为了找到堆中最大或者最小的元素。
1.向上调整建堆
使用向上调整,插入数据的思想建堆。
优化方案本质上就是不开辟新的空间在原数组上进行处理,使其空间复杂度变为O(1)。
- void HeapSort2(int* a,int n)
- {
- for (int i = 1; i < n; i++)
- {
- AdjustUp(a, i);
- }
- for(int i= 0;i<n;i++)
- printf("%d ", a[i]);
- printf("\n");
- }
现在对其时间复杂度进行计算,为方便计算,采用满二叉树来计算。
以下考虑的是最坏的境况:
第二层: 2^1 个结点 ,向上调整1层;
第三层: 2^2 个结点 ,向上移动2层;
……
第h层:2^(h-1)个结点,向上移动h-1层。
则需要移动的结点的次数:
T (n) = 2^1 * 1+ 2^2*2 +…+ 2^(h-1)*(h-1)
根据数列错位相减法求和公式可知
T(n) = (N+1) * ( log(N+1) - 2 )+2 当n->无穷大 T(n) = NlogN
向上建堆的时间复杂度为O(N*logN).
2.向下调整建堆
前提要求:对必须是大堆或者小堆。
所以要先对数组进行调整。
从倒数第一个非叶子结点开始(最后一个结点的父亲结点)。
因为叶子结点可以单独看作大堆或者小堆。
画图:
调整次数:4次
代码:
- void HeapSort3(int* a, int n)
- {
- for (int i = (n - 1 - 1) / 2; i >= 0; i--)
- {
- AdjustDown(a, n, i);
- }
-
- for (int i = 0; i < n; i++)
- printf("%d ", a[i]);
- printf("\n");
- }
空间复杂度:O(1),时间复杂度O(N)
现证明时间复杂度:
第一层:2^0个结点,需要向下移动h-1层。
第二层:2^1个结点,需要向下移动h-2层。
第三层:2^2个结点,需要向下调整h-3层。
……
第h-1层,2^(h-2)个结点,需要向下移动1层。
所以移动的总次数:
T(n) = 2^0 *(h-1) + 2^1 *(h-2) + …+ 2^(h-2)* 1
根据错位相减法可得出:
T(n) = 2^h - 1 - h
又 n = 2^h - 1
T(n) = n - log(n+1)
当n无穷大时,近似认为T(n) = n
证明完毕。
既然已经有向上建堆和向下建堆两种方法,那么哪种情况更是配建小队,哪种更适合见大堆??
升序 -- 建大堆
理由:若是升序建小堆最小的数已经在第一个位置了,剩下的关系全都乱了,需要重新建堆,建堆要O(N),再选出次小的,不断建堆选数,如此以时间复杂度O(N)的情况还不如直接遍历轻松。
总之,升序建小堆,效率低,还复杂。
降序--建小堆
那如何建大堆完成排序呢?
建大堆本质上就是尽可能维持堆的稳定。
找到最大的数后,将根位置的数与最后一个数相交换,在n-1组数据中进行向下调整,找到次小的数,将根与第n-1位置的数交换,以此实现升序。
相关代码:
- void HeapSort4(int* a, int n)
- {
- for (int i = (n - 1 - 1) / 2; i >= 0; i--)
- {
- //建大堆
- AdjustDown(a, n, i);
- }
- //找到最后一个数的下标
- int end = n - 1;
- while (end > 0)
- {
- //交换根和最后一个位置的值
- Swap(&a[0], &a[end]);
- AdjustDown(a, end, 0);
- end--;
-
- }
- for (int i = 0; i < n; i++)
- printf("%d ", a[i]);
- printf("\n");
- }
Top - k 问题
求数据结合中前k个最大元素或者最小的元素,一般情况下数据量都比较大。
Top-k中最简单的方法就是排序,如果数据量非常大,排序就会出现问题(数据不能一次性加载到内存中),但是堆排序可以解决。
1.用数组中前看个元素来建堆。
求最大建小堆,求最小则建大堆。
与升序建大堆,降序建小堆原理相似。
2.用剩余的n-k个元素与对顶的元素作比较,来替换对顶的元素。
记得向下调整。
- void TopK(int* a,int n,int k)
- {
- //建小堆
- int* topk = (int*)malloc(sizeof(int) * k);
- assert(topk);
- for (int i = 0; i < k; i++)
- {
- topk[i] = a[i];
- }
- //向下调整建堆
- for (int i = (k - 1 - 1) / 2; i >= 0; i--)
- {
- AdjustDown(topk, k, i);
- }
- for (int j = k; j < n; j++)
- {
- if (topk[0] < a[j])
- {
- topk[0] = a[j];
- AdjustDown(topk, k, 0);
- }
- }
- for (int i = 0; i < k; i++)
- {
- printf("%d ", topk[i]);
- }
- printf("\n");
- free(topk);
- }
- int main()
- {
- int n = 10000;
- int* a = (int*)malloc(sizeof(int) * n);
- assert(a);
- srand(time(0));
- for (int i = 0; i < n; i++)
- {
- a[i] = rand() % 10000;
- }
- a[0] = 100001;
- a[101] = 100002;
- a[159] = 123456;
- a[7459] = 100003;
- a[7412] = 100009;
- a[7826] = 111111;
- a[7712] = 222222;
- a[9635] = 999999;
- TopK(a,n,8);
- return 0;
- }
时间复杂度 O( K+ K*log(N-K) )
空间复杂度:O(K)
当K<<N时,效率也蛮高的。
有不对的地方还请大佬指出。