一棵高度为 h h h,且含有个 2 h − 1 2^h-1 2h−1结点的二叉树称为满二叉树,即树中的每层都含有最多的结点。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶子结点之外的每个结点度数均为 2 2 2。可以对满二叉树按层序编号:约定编号从根结点(根结点编号为 1 1 1)起,自上而下,自左向右。这样,每个结点对应一个编号,对于编号为 i 的结点,若有双亲,则其双亲为 i / 2 i/2 i/2,若有左孩子,则左孩子为 2 i 2i 2i;若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1。
2.3 完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于高度为 h h h的,有 n n n个结点的二叉树,当且仅当其每一个结点都与高度为 h h h的满二叉树中编号从 1 1 1至 n n n的结点一一对应时称之为完全二叉树。满二叉树就是一种特殊的完全二叉树。
若 i ⩽ n / 2 i \leqslant n/2 i⩽n/2,则结点 i i i为分支结点,否则为叶子结点。
叶子结点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。
若有度为 1 1 1的结点,则只可能有一个,且该结点只有左孩子而无右孩子(重要特征)。
按层序编号后,一旦出现某结点(编号为 i i i)为叶子结点或只有左孩子,则编号大于 i i i的结点均为叶子结点。
若 n n n为奇数,则每个分支结点都有左孩子和右孩子;若为偶数,则编号最大的分支结点(编号为 n / 2 n/2 n/2)只有左孩子,没有右孩子,其余分支结点左、右孩子都有。
3. 二叉树的性质
任意一棵树,若结点数量为 n n n,则边的数量为 n − 1 n-1 n−1。
非空二叉树上的叶子结点数等于度为 2 2 2的结点数加 1 1 1,即 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
非空二叉树上第 k k k层上至多有 2 k − 1 2^{k-1} 2k−1个结点( k ⩾ 1 k \geqslant 1 k⩾1)。
高度为 h h h的二叉树至多有 2 h − 1 2^h-1 2h−1个结点( h ⩾ 1 h \geqslant 1 h⩾1)。
对完全二叉树按从上到下、从左到右的顺序依次编号 0 , 1 , ... , n 0,1,...,n 0,1,...,n,则有以下关系:
i > 0 i>0 i>0时,结点 i i i的双亲的编号为 ( i − 1 ) / 2 (i-1)/2 (i−1)/2,即当 i i i为奇数时, 它是双亲的左孩子;当 i i i为偶数时,它是双亲的右孩子。
当 2 i + 1 < n 2i+1<n 2i+1<n时,结点的左孩子编号为 2 i + 1 2i+1 2i+1,否则无左孩子。
当 2 i + 2 < n 2i+2<n 2i+2<n时,结点 i i i的右孩子编号为 2 i + 2 2i+2 2i+2,否则无右孩子。
结点 i i i所在层次(深度)为 [ log 2 ( i + 1 ) ] + 1 [\log_2 (i+1)]+1 [log2(i+1)]+1。(方括号表示向下取整)
具有 n n n( n > 0 n>0 n>0)个结点的完全二叉树的高度为 [ log 2 n ] + 1 [\log_2 n]+1 [log2n]+1。(方括号表示向下取整)
具有 n n n( n > 0 n>0 n>0)个结点的满二叉树的高度为 log 2 ( n + 1 ) \log_2 (n+1) log2(n+1)。
4. 二叉树的存储结构
1. 二叉树的顺序结构
二叉树的顺序存储是指用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为 i i i的结点元素存储在一维数组下标为 i − 1 i-1 i−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 Ki \leqslant K_{2*i+1} Ki⩽K2∗i+1且 K i ⩽ K 2 ∗ i + 2 Ki \leqslant K_{2*i+2} Ki⩽K2∗i+2( K i ⩾ K 2 ∗ i + 1 Ki \geqslant K_{2*i+1} Ki⩾K2∗i+1且 K i ⩾ K 2 ∗ i + 2 Ki \geqslant K_{2*i+2} Ki⩾K2∗i+2)(其中 i = 0 , 1 , 2 , ... i = 0,1,2,... i=0,1,2,...),则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
2. 堆的性质
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。
3. 堆的实现
首先创建两个文件来实现堆:
Heap.h(节点的声明、接口函数声明、头文件的包含)
Heap.c(堆接口函数的实现)
如图:
Heap.h 文件内容如下:
C复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Heap;
// 堆的初始化
void HeapInit(Heap* php);
// 堆的向上调整
void AdjustUp(HPDataType* a, int child);
// 堆的向下调整
void AdjustDown(HPDataType* a, int n, int parent);
// 堆的插入
void HeapPush(Heap* php, HPDataType x);
// 堆的删除
void HeapPop(Heap* php);
// 获取堆顶数据
HPDataType HeapTop(Heap* php);
// 获取堆中数据个数
int HeapSize(Heap* php);
// 堆的判空
int HeapEmpty(Heap* php);
// 堆的销毁
void HeapDestory(Heap* php);
void HeapSort(HPDataType* a, int n)
{
// 向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
for (int i = n - 1; i >= 0; i--)
{
Swap(&a[0], &a[i]);
AdjustDown(a, i, 0);
}
}
4.2 Top-K问题
求数据集合中前K个最大元素或最小元素,一般情况下数据量都比较大。
基本思路:
用数据集合中前 K K K个元素来建堆。
求前 k k k个最大的元素,则建小堆。
求前 k k k个最小的元素,则建大堆。
用剩余的 N − K N-K N−K个元素依次与堆顶元素来比较,不满足则替换堆顶元素。
剩余 N − K N-K N−K个元素依次与堆顶元素比完之后,堆中剩余的 K K K个元素就是所求的前 K K K个最小或者最大的元素。
接下来,我们通过文件操作的方式来实现。首先在文件中造10000个随机数据,然后再执行以上思路。
代码实现:
c复制代码
// 造数据
void CreateNDate()
{
int n = 10000;
srand((unsigned int)time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (size_t i = 0; i < n; ++i)
{
int x = rand() % 1000000;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
//打印最大的前K个数据
void PrintTopK(int k)
{
const char* file = "data.txt";
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
perror("fopen error");
return;
}
int* kminheap = (int*)malloc(sizeof(int) * k);
if (kminheap == NULL)
{
perror("malloc error");
return;
}
// 存入前k个数据
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &kminheap[i]);
}
// 建小堆
for (int i = (k-1-1)/2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
// 比较
int val = 0;
while (!feof(fout))
{
fscanf(fout, "%d", &val);
if (val > kminheap[0])
{
kminheap[0] = val;
AdjustDown(kminheap, k, 0);
}
}
// 打印
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
}
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->lchild == NULL && root->rchild == NULL)
{
return 1;
}
return BinaryTreeLeafSize(root->lchild) +
BinaryTreeLeafSize(root->rchild);
}
5. 二叉树第k层结点个数
c复制代码
int BinaryTreeLevelKSize(BTNode* root, int k)
{
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return BinaryTreeLevelKSize(root->lchild, k - 1) +
BinaryTreeLevelKSize(root->rchild, k - 1);
}
6. 二叉树查找值为x的结点
c复制代码
BTNode* BinaryTreeFind(BTNode* root, TElemType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
BTNode* l = BinaryTreeFind(root->lchild, x);
if (l)
{
return l;
}
BTNode* r = BinaryTreeFind(root->rchild, x);
if (r)
{
return r;
}
return NULL;
}
7. 二叉树的深度
c复制代码
int BinaryTreeMaxDepth(BTNode* root) {
if (root == NULL)
return 0;
int l = BinaryTreeMaxDepth(root->lchild);
int r = BinaryTreeMaxDepth(root->rchild);
return l > r ? l + 1 : r + 1;
}
8. 判断二叉树是否是完全二叉树
通过层序遍历的方式找到第一个空节点,如果后面还有非空结点就不是完全二叉树,否则为完全二叉树。
c复制代码
bool BinaryTreeComplete(BTNode* root)
{
if (root == NULL)
return true;
Queue* qu = (Queue*)malloc(sizeof(Queue));
QueueInit(qu);
QueuePush(qu, root);
while (!QueueEmpty(qu))
{
BTNode* ret = QueueFront(qu);
QueuePop(qu);
if (ret)
{
QueuePush(qu, ret->lchild);
QueuePush(qu, ret->rchild);
}
else
break;
}
int result = true;
while (!QueueEmpty(qu))
{
BTNode* ret = QueueFront(qu);
if (ret)
{
result = false;
break;
}
QueuePop(qu);
}
QueueDestroy(qu);
free(qu);
return result;
}