前言
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。本文将要介绍的是二叉树的顺序存储结构。
1. 顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费,完全二叉树更适合使用顺序结构存储。
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
2. 实现顺序结构二叉树
一般堆使用顺序结构的数组来存储数据,堆是一种特殊的二叉树,具有二叉树的特性的同时,还具备其他的特性。
2.1 堆的概念与结构
如果有一个关键码的集合 K = { k 0 , k 1 , k 2 , . . . , k n − 1 } K = \{{k_0,k_1,k_2 , ...,k_{n−1}}\} K={k0,k1,k2,...,kn−1},把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足: K i < = K 2 ∗ i + 1 且 K i < = K 2 ∗ i + 2 ( K i > = K 2 ∗ i + 1 且 K i > = K 2 ∗ i + 2 ) K_i <= K_{2*i+1} 且K_i <=K_{2*i+2}(K_i >=K_{2*i+1}且K_i >= K_{2*i+2}) Ki<=K2∗i+1且Ki<=K2∗i+2(Ki>=K2∗i+1且Ki>=K2∗i+2),$i = 0、1、2… $,则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
堆具有以下性质:
堆中某个结点的值总是不大于或不小于其父结点的值;
堆总是一棵完全二叉树。
💡 二叉树性质
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序存储在数组中,并且对所有结点从0开始编号,则对于序号为 i 的结点有:
- 若 i > 0 i>0 i>0, i i i位置结点的双亲序号为: ( i − 1 ) / 2 (i-1)/2 (i−1)/2;若 i = 0 i=0 i=0,则 i i i为根结点编号,无双亲结点
- 若 2 i + 1 < n 2i+1<n 2i+1<n,左孩子序号为: 2 i + 1 2i+1 2i+1;若 2 i + 1 > = n 2i+1>=n 2i+1>=n则无左孩子
- 若 2 i + 2 < n 2i+2<n 2i+2<n,右孩子序号为: 2 i + 2 2i+2 2i+2;若 2 i + 2 > = n 2i+2>=n 2i+2>=n则无右孩子
2.2 堆的实现
堆底层结构为数组,因此定义堆的结构为:
typedef int HPDataType;
typedef struct Heap
{
HPDataType * a;
int size;
int capacity;
}HP;
//默认初始化堆
void HPInit(HP * php);
//利用给定数组初始化堆
void HPInitArray(HP * php, HPDataType * a, int n);
//堆的销毁
void HPDestroy(HP * php);
//堆的插入
void HPPush(HP * php, HPDataType x);
//堆的删除
HPDataType HPTop(HP * php);
// 删除堆顶的数据
void HPPop(HP * php);
// 判空
bool HPEmpty(HP * php);
//求size
int HPSize(HP * php);
//向上调整算法
void AdjustUp(HPDataType * a, int child);
//向下调整算法
void AdjustDown(HPDataType * a, int n, int parent);
上面这部分是堆实现所需要的一些方法,其中具体的方法由读者自己先来尝试实现,如有不会的可以在讨论区询问,将会由作者或者其它积极的读者来解答❤️❤️❤️
2.2.1 向上调整算法
堆的插入
将新数据插入到数组的尾上,再进行向上调整算法,直到满足堆。
💡 向上调整算法
先将元素插入到堆的末尾,即最后一个孩子之后
插入之后如果堆的性质遭到破坏,将新插入结点顺着其双亲往上调整到合适位置即可
代码实现:
//向上调整
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 = (parent - 1) / 2;
}
else
{
break;
}
}
}
//插入操作,插入之后利用向上调整算法来保持堆的结构
void HPPush(HP * php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
size_t newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType * tmp = realloc(php->a, sizeof(HPDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
计算向上调整算法建堆时间复杂度:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个结点不影响最终结果)
分析:
第1层, 2 0 2^0 20个结点,需要向上移动0层
第2层, 2 1 2^1 21个结点,需要向上移动1层
第3层, 2 2 2^2 22个结点,需要向上移动2层
第4层, 2 3 2^3 23个结点,需要向上移动3层
…
第h层, 2 h − 1 2^{h-1} 2h−1个结点,需要向上移动h-1层
则需要移动结的总移动步数为:每层结点个数 * 向上调整次数(第一层调整次数为0)
T ( h ) = 2 1 ∗ 1 + 2 2 ∗ 2 + 2 3 ∗ 3 + . . + 2 h − 2 ∗ ( h − 2 ) + 2 h − 1 ∗ ( h − 1 ) T(h) = 2^1∗1+2^2∗2+2^3∗3+..+2^{h−2}∗(h−2)+2^{h−1}∗(h−1) T(h)=21∗1+22∗2+23∗3+..+2h−2∗(h−2)+2h−1∗(h−1) ①
2 ∗ T ( h ) = 2 2 ∗ 1 + 2 3 ∗ 2 + 2 4 ∗ 3 + . . + 2 h − 1 ∗ ( h − 2 ) + 2 h ∗ ( h − 1 ) 2*T(h) = 2^2∗1+2^3∗2+2^4∗3+..+2^{h−1}∗(h−2)+2^h∗(h−1) 2∗T(h)=22∗1+23∗2+24∗3+..+2h−1∗(h−2)+2h∗(h−1) ②
② — ① 错位相减:
T ( h ) = − 2 1 ∗ 1 − ( 2 2 + 2 3 + . . + 2 h − 2 + 2 h − 1 ) + 2 h ∗ ( h − 1 ) T(h) = −2^1∗1−(2^2+2^3+..+2^{h−2}+2^{h−1})+2^h∗(h−1) T(h)=−21∗1−(22+23+..+2h−2+2h−1)+2h∗(h−1)
T ( h ) = − 2 0 − 2 1 ∗ 1 − ( 2 2 + 2 3 + . . + 2 h − 2 + 2 h − 1 ) + 2 h ∗ ( h − 1 ) + 2 0 T(h)=-2^0−2^1∗1−(2^2+2^3+..+2^{h−2}+2^{h−1})+2^h∗(h−1)+2^0 T(h)=−20−21∗1−(22+23+..+2h−2+2h−1)+2h∗(h−1)+20
T ( h ) = − ( 2 0 + 2 1 ∗ 1 + 2 2 + 2 3 + . . + 2 h − 2 + 2 h − 1 ) + 2 h ∗ ( h − 1 ) + 2 0 T(h)=-(2^0+2^1∗1+2^2+2^3+..+2^{h−2}+2^{h−1})+2^h∗(h−1)+2^0 T(h)=−(20+21∗1+22+23+..+2h−2+2h−1)+2h∗(h−1)+20
T ( h ) = − ( 2 h − 1 ) + 2 h ∗ ( h − 1 ) + 2 0 T(h)=-(2^h-1)+2^h∗(h−1)+2^0 T(h)=−(2h−1)+2h∗(h−1)+20
T ( h ) = 2 h ∗ ( h − 2 ) + 2 T(h)=2^h*(h−2)+2 T(h)=2h∗(h−2)+2
然后根据二叉树的性质: n = 2 h − 1 n=2^h−1 n=2h−1和 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)
T ( n ) = ( n + 1 ) ( l o g 2 ( n + 1 ) − 2 ) + 2 T(n)=(n+1)(log_2(n+1)−2)+2 T(n)=(n+1)(log2(n+1)−2)+2
由此可得:
💡 向上调整算法建堆时间复杂度为: O ( n ∗ l o g 2 n ) O(n∗log_2n) O(n∗log2n)
2.2.2 向下调整算法
堆的删除
删除堆是删除堆顶的数据,将堆顶的数据和最后一个数据交换,然后删除数组最后一个数据,再进行向下调整算法。
💡 向下调整算法
将堆顶元素与堆中最后一个元素进行交换
删除堆中最后一个元素
将堆顶元素向下调整到满足堆的特性为止
代码实现:
//向下调整算法
void AdjustDown(HPDataType* 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 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);
}
计算向下调整算法建堆时间复杂度
分析:
第1层, 2 0 2^0 20个结点,需要向下移动h-1层
第2层, 2 1 2^1 21个结点,需要向下移动h-2层
第3层, 2 2 2^2 22个结点,需要向下移动h-3层
第4层, 2 3 2^3 23个结点,需要向下移动h-4层
…
第h-1层, 2 h − 2 2^{h-2} 2h−2个结点,需要向下移动1层
则需要移动结点的总移动步数为:每层结点个数 * 向下调整次数
T ( h ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + 2 3 ∗ ( h − 4 ) . . + 2 h − 3 ∗ 2 + 2 h − 2 ∗ 1 T(h)=2^0∗(h-1)+2^1∗(h-2)+2^2∗(h-3)+2^3*(h-4)..+2^{h−3}∗2+2^{h−2}∗1 T(h)=20∗(h−1)+21∗(h−2)+22∗(h−3)+23∗(h−4)..+2h−3∗2+2h−2∗1 ①
2 ∗ T ( h ) = 2 1 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + 2 3 ∗ ( h − 3 ) + 2 4 ∗ ( h − 4 ) . . + 2 h − 2 ∗ 2 + 2 h − 1 ∗ 1 2*T(h)=2^1∗(h-1)+2^2∗(h-2)+2^3∗(h-3)+2^4*(h-4)..+2^{h−2}∗2+2^{h−1}∗1 2∗T(h)=21∗(h−1)+22∗(h−2)+23∗(h−3)+24∗(h−4)..+2h−2∗2+2h−1∗1 ②
② — ① 错位相减:
T ( h ) = 1 − h + 2 1 + 2 2 + 2 3 + 2 4 + . . + 2 h − 2 + 2 h − 1 T(h)=1-h+2^1+2^2+2^3+2^4+..+2^{h-2}+2^{h-1} T(h)=1−h+21+22+23+24+..+2h−2+2h−1
T ( h ) = 2 0 + 2 1 + 2 2 + 2 3 + 2 4 + . . + 2 h − 2 + 2 h − 1 − h T(h)=2^0+2^1+2^2+2^3+2^4+..+2^{h-2}+2^{h-1}-h T(h)=20+21+22+23+24+..+2h−2+2h−1−h
T ( h ) = 2 h − 1 − h T(h)=2^h−1−h T(h)=2h−1−h
然后根据二叉树的性质: n = 2 h − 1 n= 2^h−1 n=2h−1和 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)
T ( n ) = n − l o g 2 ( n + 1 ) ≈ n T(n)=n−log_2(n+1)≈n T(n)=n−log2(n+1)≈n
由此可得:
💡 向下调整算法建堆时间复杂度为: O ( n ) O(n) O(n)
3. 结语
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下。
也可以点点关注,避免以后找不到我哦!
Crossoads主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是作者前进的动力!