赞
踩
要学习堆,首先要先简单的了解一下二叉树,二叉树是一种常见的树形数据结构,每个节点最多有两个子节点,通常称为左子节点和右子节点。它具有以下特点:
二叉树的应用非常广泛,在后面我会详细介绍。
满二叉树:除了叶子结点外,每个结点都有两个子结点
一个深度为k的满二叉树有2的k次方减一个节点。
完全二叉树:除了最底层可能不是满的外,其它每一层从左到右都是满的。
满二叉树是完全二叉树的子集,满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
堆就是一种完全二叉树。
逻辑结构和物理结构是计算机科学中两个重要的概念,它们描述了数据在计算机中的不同组织方式。
逻辑结构:
物理结构:
逻辑结构关注数据之间的逻辑关系和操作规则,而物理结构关注数据在计算机内部的实际存储方式和组织形式。
二叉树有多种存储方式,常见的包括顺序存储和链式存储。
顺序存储: 顺序存储通常使用数组来表示二叉树。假设树的根节点存储在数组下标为0的位置,则对于任意一个下标为i的节点:
链式存储: 链式存储则是通过节点之间的引用来表示二叉树的结构,每个节点包含数据域和左右子节点指针域。
链式储存我们放在后边更新,在这里我们先学习顺序储存。
顺序储存用数组来储存,顺序存储一般只适合用来存储完全二叉树(堆),用顺序储存再存储非完全的二叉树会存在空间浪费
头文件:
- #define _CRT_SECURE_NO_WARNINGS 1
-
- #include <stdio.h>
- #include <assert.h>
- #include <stdlib.h>
- #include <stdbool.h>
-
- typedef int HPDatatype;
-
- typedef struct Heap
- {
- HPDatatype * a;
- int size;
- int capacity;
-
- }HP;
-
- //初始化
- void HPInit(HP* php);
-
- //插入数据
- void HPPush(HP* php, HPDatatype x);
-
- //交换
- void Swap(HPDatatype* a,HPDatatype * b);
-
- //销毁
- void HPDestroy(HP* php);
-
- //向上调整
- void AdjustUp(HPDatatype* a, int child);
-
- //向下调整
- void AdjustDown(HPDatatype* a,int n, int parent);
-
-
- //删除顶部数据
- void HPPop(HP* php);
-
- //返回顶部数据
- HPDatatype* HPTop(HP* php);
-
- //判空
- bool HPEmpty(HP* php);
实现文件:
- #define _CRT_SECURE_NO_WARNINGS 1
- #include "Heap.h"
-
- // 初始化
- void HPInit(HP* php)
- {
- assert(php);
- php->a = NULL;
- php->capacity = php->size = 0;
-
- }
-
- //插入数据
- void HPPush(HP* php, HPDatatype x)
- {
- assert(php);
- //判断空间够不够
- if (php->capacity == php->size)
- {
- int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
- HPDatatype* tmp = (HPDatatype* )realloc(php->a,newcapacity * sizeof(HPDatatype));
- if (tmp == NULL)
- {
- perror("realloc fail");
- exit(-1);
- }
- php->capacity = newcapacity;
- php->a = tmp;
- }
- php->a[php->size] = x;
- php->size++;
-
- AdjustUp(php->a, php->size - 1);
- }
-
- //交换
- void Swap(HPDatatype* a, HPDatatype* b)
- {
- HPDatatype cmp = *a;
- *a = *b;
- *b = cmp;
- }
-
- //销毁
- void HPDestroy(HP* php)
- {
- assert(php);
- free(php->a);
- php->a = NULL;
- php->capacity = php->size = 0;
- }
-
- //向上调整
- void AdjustUp(HPDatatype* 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 AdjustDown(HPDatatype* a, int n, int parent)
- {
- int child = 2 * parent + 1;//先假设左边的小
-
- while (child < n)
- {
- if (child + 1 < n && a[child + 1] < a[child])//规避chlid + 1 越界的风险
- {
- child++;
- }
- if (a[child] < a[parent])
- {
- Swap(&a[child], &a[parent]);
- parent = child;
- child = 2 * parent + 1;
- }
- else
- {
- break;
- }
- }
-
- }
-
-
- //删除顶部数据
- void HPPop(HP* php)
- {
- assert(php);
- assert(php->size > 0);
- Swap(&php->a[0], &php->a[php->size - 1]);
- php->size--;
- AdjustDown(php->a, php->size,0);
- }
-
- //返回顶部数据
- HPDatatype* HPTop(HP* php)
- {
- assert(php);
- assert(php->size > 0);
-
- return php->a[0];
- }
-
- //判空
- bool HPEmpty(HP* php)
- {
- assert(php);
-
- return php->size == 0;
- }
一般来说,堆分为两类
大堆(Max Heap):在最大堆中,每个节点的值都大于或等于其子节点的值。换句话说,堆顶部的元素是整个堆中的最大值。最大堆常用于实现优先队列,其中具有最高优先级的元素始终位于堆顶。
小堆(Min Heap):在最小堆中,每个节点的值都小于或等于其子节点的值。因此,堆顶部的元素是整个堆中的最小值。最小堆也常用于优先队列,其中具有最低优先级的元素位于堆顶。
简单来说大堆中,同一个分支中大的在上;小堆中,同一分支小的在上。
在这里以小堆为例:
往堆中插入一个数据时,先将插入的数据放到堆的最后一个节点,然后利用向上调整算法依次调整。
图示:
只要子节点不越界循环一直进行,当字节点不小于父节点时跳出if()语句进入else,跳出循环。
- //向上调整
- void AdjustUp(HPDatatype* 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;
- }
- }
- }
求一堆数据(储存在小堆中)中最最小的前几个数据:将数据插入堆中,小堆的堆顶中储存的就是堆中最小的数据,把堆顶的数据取下来,再将堆顶的数据释放;用向上调整算法调整堆,再依次取堆顶,重复。
- //TOP-K
- void HPtest02()
- {
- int a[] = { 5,6,1,4,2,8 };
- HP s;
- HPInit(&s);
- for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
- {
- HPPush(&s, a[i]);
- }
- int k = 0;
- scanf("%d", &k);
- while (k--)
- {
- printf("%d ", HPTop(&s));
- HPPop(&s);
- }
- HPDestroy(&s);
- }
-
-
- int main()
- {
- HPtest02();
-
- return 0;
- }
演示:
在TOP-K问题中,我们会发现,输出的数据是按顺序拍好的,那么我们可不可以在此基础上进行排序呢。 把数据储存到堆中之后,再依次拿出来。
- //排序
- void HPtest03()
- {
- int a[] = { 5,6,1,4,2,8 };
- HP s;
- HPInit(&s);
- for (size_t i = 0; i < sizeof(a) / sizeof(int); i++)
- {
- HPPush(&s, a[i]);
- }
- int i = 0;
- while (!HPEmpty(&s))
- {
- a[i++] = HPTop(&s);
- HPPop(&s);
- }
- HPDestroy(&s);
- }
- int main()
- {
-
- HPtest03();
-
- return 0;
- }
这样我们就可以对数据进行排序。
这个算法的时间复杂度非常低 。 一个有k个节点的对的深度为log(k),一条分支最多交换log (k) - 1次,所以
算法的时间复杂度为log N。 但是这并不能称作真正的排序,因为它在原数组的基础上开辟了新的空间。
- //堆排序
- void HeapSort(int* a, int n)
- {
- //建堆
- for (int i = 1; i < n; i++)
- {
- AdjustUp(a, i);
- }
- }
-
- void Heaptset()
- {
- int a[] = { 5,6,8,4,1,2,3 };
- HeapSort(a, 7);
- }
- int main()
- {
- //HPtest01();
- /*HPtest02();*/
- //HPtest03();
- Heaptset();
- return 0;
- }
在惯性思维中,要排降序应该会建大堆,排升序会建小堆。但这样会导致一个问题(以建排降序 为建小堆为例)
小堆的堆顶为这组数据中最小的数,我们将它取出,作为排序的第一个数
取出堆顶后,找出第二小的数据, 但是此时的堆各个节点已经不满足之前的大小关系了,4之前是6和5的父节点,比6和5大,但是与2为兄弟节点,兄弟节点之间的大小关系原来并不清楚,无法直接找出第二大的数据(可以重新把剩下的数据建堆,但是没必要,时间成本大)。在堆排序中不能让第一个数据直接拿出去,这样会改变节点之间的父子关系,不能确定大小关系,无法找出需要的节点。
接下来以排降序排降序为例演示过程。
- //堆排序
- void HeapSort(int* a, int n)
- {
- //建堆
- for (int i = 1; i < n; i++)
- {
- AdjustUp(a, i);
- }
-
- int end = n - 1;
- while (end > 0)
- {
- Swap(&a[0], &a[end]);
- AdjustDown(a, end, 0);
- --end;
- }
- }
-
- void Heaptset()
- {
- int a[] = { 5,6,8,4,1,2,3 };
- HeapSort(a, 7);
- }
- int main()
- {
- //HPtest01();
- /*HPtest02();*/
- //HPtest03();
- Heaptset();
- return 0;
- }
调试:
向下调整算法的时间复杂度为log N,堆排序在最坏的情况下N个数据要排N次,所以堆排序的时间复杂度为N log N。可以极大的提高程序的效率。
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。