当前位置:   article > 正文

数据结构——二叉树的顺序存储(堆)_顺序存储二叉树

顺序存储二叉树

二叉树的顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。 

 




概念

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


性质

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

堆总是一棵完全二叉树。


堆的实现

(本篇文章均采用小堆)

由于在物理上是一个数组,我们可以借助顺序表的思想

  1. typedef int hpDataType;
  2. typedef struct heap
  3. {
  4. hpDataType* a;
  5. int size;
  6. int capacity;
  7. }HP;

初始化与销毁

  1. void HeapInit(HP* hp)
  2. {
  3. hp->a = NULL;
  4. hp->size = 0;
  5. hp->capacity = 0;
  6. }
  7. void HeapDestroy(HP* hp)
  8. {
  9. assert(hp);
  10. hp->a = NULL;
  11. hp->size = 0;
  12. hp->capacity = 0;
  13. }

插入

由于堆的特性,头插改变堆的结构,所以我们选择尾插

  1. void HeapPush(HP* hp, hpDataType x)
  2. {
  3. if (hp->size == hp->capacity)
  4. {
  5. int newCapacity = (hp->capacity + 1) * 2 - 1;
  6. hpDataType* tmp = (hpDataType*)realloc(hp->a, sizeof(hpDataType)*newCapacity);
  7. if (tmp == NULL)
  8. {
  9. printf("realloc error\n");
  10. exit(-1);
  11. }
  12. hp->a = tmp;
  13. hp->capacity = newCapacity;
  14. }
  15. hp->a[hp->size] = x;
  16. hp->size++;
  17. Adjustup(hp->a, hp->size - 1);
  18. }

 

例如这样的一个堆,当我们在进行尾插时,这个数组的结构已经不符合堆的性质,所以我们要讲新插入的数据调整到合适的位置,我们称之为上调。

例如我们在上面的堆中插入一个20

我们通过比较它与它的父节点来判断是否进行交换

当它小于它的父节点或者它变为根节点时,变为小堆,停止交换

  1. void Swap(hpDataType* m, hpDataType* n)
  2. {
  3. hpDataType mid = 0;
  4. mid = *m;
  5. *m = *n;
  6. *n = mid;
  7. }
  8. void Adjustup(hpDataType* a, int child)
  9. {
  10. int parent = (child - 1) / 2;
  11. while (child!=parent&&a[child] < a[parent])
  12. {
  13. Swap(&a[child], &a[(child - 1)/2]);
  14. child = (child - 1) / 2;
  15. parent = (child - 1) / 2;
  16. }
  17. }
  18. void HeapPush(HP* hp, hpDataType x)
  19. {
  20. if (hp->size == hp->capacity)
  21. {
  22. int newCapacity = (hp->capacity + 1) * 2 - 1;
  23. hpDataType* tmp = (hpDataType*)realloc(hp->a, sizeof(hpDataType)*newCapacity);
  24. if (tmp == NULL)
  25. {
  26. printf("realloc error\n");
  27. exit(-1);
  28. }
  29. hp->a = tmp;
  30. hp->capacity = newCapacity;
  31. }
  32. hp->a[hp->size] = x;
  33. hp->size++;
  34. Adjustup(hp->a, hp->size - 1);
  35. }

删除

同样,删除分为头删和尾删,但在二叉树中,尾删并没有什么作用,而头删可以帮助我们选出最小(最大的数据),所以我们只考虑头删。

但若按照常规思路进行头删,会破坏堆的结构。

所以我们并不能采用常规的思路

这里我们可以将根节点与最后一个节点进行交换后进行尾删,然后将根节点(原本的最后一个节点)向下调整

 

 可以看到,在我们进行向下调整的时候,我们比较的是父节点和左右孩子节点中较小的一个,这是因为若是比较较大的孩子节点,交换后被交换的孩子节点依然大于未被交换的孩子节点。无法构成小堆(大堆相反)

例如在第一次调整中,若是比较较大的孩子节点

交换后30依然会大于26 

  1. void Adjustdown(hpDataType* a, int size , int parent)
  2. {
  3. int leftChild = (parent + 1) * 2 - 1;
  4. int rightChild = (parent + 1) * 2;
  5. int min = 0;
  6. while (leftChild<size)
  7. {
  8. if (rightChild < size && a[rightChild] < a[leftChild])
  9. min = rightChild;
  10. else
  11. min = leftChild;
  12. if (a[min] < a[parent])
  13. {
  14. Swap(&a[parent], &a[min]);
  15. parent = min;
  16. leftChild = (parent + 1) * 2 - 1;
  17. rightChild = (parent + 1) * 2;
  18. }
  19. else
  20. break;
  21. }
  22. }
  23. void HeapPop(HP* hp)
  24. {
  25. assert(hp);
  26. assert(hp->size);
  27. Swap(&(hp->a)[0], &(hp->a)[hp->size - 1]);
  28. hp->size--;
  29. Adjustdown(hp->a,hp->size, 0);
  30. }

堆的相关问题

TopK问题

TopK问题,指的是在一个大小为n的数组中寻找最小(最大)的前K个的问题

在此之前,我们通常会运用排序来解决。

但若n较大,时间复杂度会很大。

同时,我们也可以构建大小为n的小堆(大堆)来解决。

但这同样存在问题,那就是若n较大,栈区无法存储。

因此我们可以构建一个大小为K的小堆(大堆),当存储前K个后,通过比较新数据与根节点数据,来判断是否进行插入,最后选择出最小(最大)的前K个数据。

  1. void PrintTopK(int* a, int n, int k)
  2. {
  3. HP hp;
  4. HeapInit(&hp);
  5. for (int i = 0; i < k; i++)
  6. {
  7. HeapPush(&hp, a[i]);
  8. }
  9. for (int i = k; i < n; i++)
  10. {
  11. if (a[i] > HeapTop(&hp))
  12. {
  13. (hp.a)[0] = a[i];
  14. Adjustdown(hp.a, k ,0);
  15. }
  16. }
  17. for (int i = 0; i < k; i++)
  18. {
  19. printf("%d ", (hp.a)[i]);
  20. }
  21. printf("\n");
  22. }

堆排序

堆排序,指的是给出一个大小为n的数组,通过堆进行排序。

通常我们可以想到构建一个小堆(大堆),将数组一个个插入进去。

这样做空间复杂度为O(n)。

那么我们如何使空间复杂度达到O(1)呢?

首先我们默认为降序。

由于堆在物理上就是一个数组,所以我们可以直接对数组进行操作,使其首先构建成小堆(大堆)。我们可以先将第一个数据当做堆,在将后面的数据一个个插入(向上调整)

 

  1. for (int i = 1; i < n; i++)
  2. {
  3. Adjustup(a, i);
  4. }

另外,我们还有另外一种方法

我们可以向下调整。但向下调整的前提是左右子树都为小堆(大堆)。因此,我们需要从最后一个非叶子节点(叶子节点没有左右子树,不需要调整)开始向下调整。

在构建好小堆(大堆)后,我们便可以进行排序的操作。

由于是对原数组进行操作,我们无法做到将根节点赋值给数组后进行删除。但在头删中,我们是将根节点与最后一个节点交换后向下调整,由于小堆的根节点最小,那么最小的数便被放置在数组的末尾,并且将其排除在堆外。

那么在第二次删除后,数组中倒数第二个数据便为15(第二小)。

由此可知,我们只需要不断进行删除操作,便能将小堆转变为降序

(注意:降序需要小堆,升序需要大堆,一定不要搞反)

如此,我们便可以完成堆排序

  1. void HeapSort(int* a, int n)
  2. {
  3. for (int i = 1; i < n; i++)
  4. {
  5. Adjustup(a, i);
  6. }
  7. for (int i = n-1; i > 0 ; i--)
  8. {
  9. Swap(&a[i], &a[0]);
  10. Adjustdown(a, i, 0);
  11. }
  12. }

构建堆的时间复杂度

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

 在堆排序中,我们讲述了构建堆的方法,我们可以统计各层节点的数量和移动层数来进行计算。

 

因此可以得出:构建堆的时间复杂度为O(N) 

 

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号