目录
树概念及结构
树的概念
树的相关概念
树的表示
树在实际中的运用(表示文件系统的目录树结构)
二叉树概念及结构
概念
现实中的二叉树
特殊的二叉树
二叉树的性质
二叉树的存储结构
二叉树的顺序结构及实现
二叉树的顺序结构
堆的概念及结构
堆的实现
堆的初始化
堆的销毁
堆的打印
堆的插入
堆的删除
堆的判空
堆的数据个数
取堆顶数据
堆的应用
堆排序
TOP-K问题
二叉树链式结构的实现
前置说明
二叉树的遍历
前序、中序以及后序遍历
层序遍历
节点个数以及高度等
判断二叉树是否是完全二叉树
二叉树销毁
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫
做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
注意:树形结构中,子树之间不能有交集,否则就不是树形结构
节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
森林:由m(m>0)棵互不相交的树的集合称为森林;
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。
我们这里就简单的了解其中最常用的孩子兄弟表示法。
typedef int DataType;
struct Node
{
struct Node* _firstChild1; // 第一个孩子结点
struct Node* _pNextBrother; // 指向其下一个兄弟结点
DataType _data; // 结点中的数据域
};
即这样表示:
一棵二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
从上图可以看出:
注意:对于任意的二叉树都是由以下几种情况复合而成的:
1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是结点总数是 2^k-1,则它就是满二叉树。
2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1)个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h-1 .
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有 n0= n2+1
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h= . (ps: 是log以2为底,n+1为对数)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
练习题:
1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199 提示:n0 = n2+1
2.下列数据结构中,不适合采用顺序存储结构的是( A)
A 非完全二叉树
B 堆
C 队列
D 栈
3.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2
4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )
A 11
B 10
C 8
D 12
5.一个具有767个节点的完全二叉树,其叶子节点个数为()
A 383
B 384
C 385
D 386
答案:
1.B
2.A
3.A
4.B
5.B
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1. 顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。
而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。
2. 链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。
通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面学到高阶数据结构如红黑树等会用到三叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。
而完全二叉树更适合使用顺序结构存储。
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2…,则称为小堆(或大堆)。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
练习:
1.下列关键字序列为堆的是:()
A 100,60,70,50,32,65
B 60,70,65,50,32,100
C 65,100,70,32,50,60
D 70,65,100,32,50,60
E 32,50,100,70,65,60
F 50,100,70,65,60,32
2.已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次
数是()。
A 1
B 2
C 3
D 4
3.一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为
A(11 5 7 2 3 17)
B(11 5 7 2 17 3)
C(17 11 7 2 3 5)
D(17 11 7 5 3 2)
E(17 7 11 3 5 2)
F(17 7 11 3 2 5)
4.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A[3,2,5,7,4,6,8]
B[2,3,5,7,4,6,8]
C[2,3,4,5,7,8,6]
D[2,3,4,5,6,7,8]
答案:1.A
2.C
3.C
4.C
堆的实现基于顺序表,所以一些地方与顺序表类似,所以就不再重点讲解。
这里以实现小堆为例。
#pragma once
#include
#include
#include
#include
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;//避免空间不够,使用动态顺序表
int size;
int capacity;
}HP;
//初始化堆
void HeapInit(HP* hp);
//销毁堆
void HeapDestroy(HP* hp);
//打印堆
void HeapPrint(HP* hp);
//堆的插入
void HeapPush(HP* hp, HPDataType x);
//堆的删除
void HeapPop(HP* hp);
//向上调整
void AdjustUp(int* a, int size);
//向下调整
void AdjustDown(int* a, int size, int parent);
void Swap(int* a, int* b);
//堆的判空
bool HeapEmpty(HP* hp);
// 堆的数据个数
int HeapSize(HP* hp);
//取堆顶的数据
int HeapTop(HP* hp);
void HeapInit(HP* hp)
{
assert(hp);
hp->a = NULL;
hp->size = hp->capacity = 0;
}
void HeapDestroy(HP* hp)
{
assert(hp);
free(hp->a);
hp->capacity = hp->size = 0;
}
void HeapPrint(HP* hp)
{
assert(hp);
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
首先插入数据就必须要检查空间是否足够,不够需要扩容。
其次堆与顺序表不同的是,将数据插入到到末尾之后,为了使堆的结构保持不变需要将堆中的元素调整。
代码如下:
void HeapPush(HP* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity)
{
int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
int* tmp = (int*)realloc(hp->a ,sizeof(int) * newcapacity);
if (tmp == NULL)
{
perror("realloc");
exit(-1);
}
hp->a = tmp;
hp->capacity = newcapacity;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);
}
void AdjustUp(int* a, int size)//参数为数组地址,需要调整下标位置
{
assert(a);
int child = size;//记录孩子位置
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 Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
删除元素时不能随意位置删除,只能删除堆顶。
但又与顺序表头删不同,删除堆顶后如果将后面的数据向左移动,可能整个堆的结构全部会被破坏掉。
所以这里我们采用的方法是:
void HeapPop(HP* hp)
{
assert(hp);
assert(!HeapEmpty(hp));//保证堆不为空
Swap(hp->a, hp->a + hp->size - 1);//交换首尾
hp->size--;//删除末尾
AdjustDown(hp->a, hp->size, 0);//向下调整
}
void AdjustDown(int* a, int size, int parent)//参数为数组地址,数组大小,调整位置
{
assert(a);
int child = parent * 2 + 1;//记录孩子位置
while (child < size)//调整结束条件为调整到末尾或者不再大于孩子
{
if (child + 1 < size && a[child + 1] < a[child])//如果有孩子存在且右孩子<左孩子
{
child++;//记录较小孩子的位置
}
if (a[child] < a[parent])
{
Swap(a + child, a + parent);//交换
parent = child;//迭代
child = parent * 2 + 1;
}
else
{
break;
}
}
}
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
int HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
int HeapTop(HP* hp)
{
assert(hp);
return hp->a[0];
}
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
1. 建堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
这里以升序为例:
首先应该建一个大堆,不能直接使用堆来实现。
可以将需要排序的数组看作是一个堆,但需要将数组结构变成堆。
我们可以从堆从下往上的第二行最右边开始依次向下调整直到调整到堆顶,这样就可以将数组调整成一个堆,且如果建立的是大堆,堆顶元素为最大值。
然后按照堆删的思想将堆顶和堆底的数据交换,但不同的是这里不删除最后一个元素。
这样最大元素就在最后一个位置,然后从堆顶向下调整到倒数第二个元素,这样次大的元素就在堆顶,重复上述步骤直到只剩堆顶时停止。
如图:
代码如下:
void HeapSort(int* arr,int n)
{
int i = 0;
// 1.创建大堆找出最大元素
// 2.交换首尾元素
// 3.选出次大元素,即将前n-1个数再向下调整
// 4.重复上述步骤直到只剩堆顶就不需调整
for (i = (n-1-1)/ 2; i >= 0; i--)
{
AdjustDown(arr, n, i);
}
for (i = n-1; i > 0; i--)
{
Swap(arr + i, arr);
AdjustDown(arr, i, 0);
}
for (i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
}
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可
取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆
前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
void Topk()
{
//TopK问题:在一组数据中找到最大(小)的k个数
//方法: 创建容量为k的堆(找最大的数就创建小堆,反之就创建大堆),
// (例如这里我们找最大的数据,创建小堆是确保每一个比堆顶大的数据能够沉到堆底去)
// 将剩余的n-k个数依次与堆顶的数据相比,比他大就替换掉堆顶,然后向下调整;
// 直到将所有数据比较完。
HP hp;
HeapInit(&hp);
int arr[] = { 1,6,8,9,7,5,10,20 };
int sz = sizeof(arr) / sizeof(arr[0]);
int k = 3;
int i = 0;
for (i = 0; i < k; i++)
{
HeapPush(&hp, arr[i]);
}
for (i = k; i < sz; i++)
{
if (arr[i] > HeapTop(&hp))
{
hp.a[0] = arr[i];
AdjustDown(hp.a, hp.size, 0);
}
}
HeapPrint(&hp);
HeapDestroy(&hp);
}
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在对二叉树结构掌握还不够深入,为了降低学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType _data;
struct BinaryTreeNode* _left;
struct BinaryTreeNode* _right;
}BTNode;
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(1);
BTNode* node2 = BuyNode(2);
BTNode* node3 = BuyNode(3);
BTNode* node4 = BuyNode(4);
BTNode* node5 = BuyNode(5);
BTNode* node6 = BuyNode(6);
node1->_left = node2;
node1->_right = node4;
node2->_left = node3;
node4->_left = node5;
node4->_right = node6;
return node1;
}
注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。
再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:
1. 空树
2. 非空:根节点,根节点的左子树、根节点的右子树组成的。
从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。
学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:
由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
// 二叉树前序遍历
void PreOrder(BTNode* root);
// 二叉树中序遍历
void InOrder(BTNode* root);
// 二叉树后序遍历
void PostOrder(BTNode* root);
前序遍历递归图解:
中序遍历和后序遍历类似,只是访问根结点的顺序不同,中序遍历是左 根 右,后序遍历是左 右 根。这里就不再细说,直接上代码吧:
// 二叉树前序遍历
void PreOrder(BTNode* root)
{
if (root == NULL)//如果左右子树是空树就打印NULL
{
printf("NULL ");
return;
}
printf("%c ", root->data);//先访问根结点
PreOrder(root->left);
PreOrder(root->right);
}
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%c ", root->data);//中间访问根结点
InOrder(root->right);
}
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);//最后访问根结点
}
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
层序遍历是逐层遍历,所以递归就不再适用了。
这里的方法是使用队列来做。
方法:
注意:队列使用链表实现的,这里我为了方便画图表示,将他看作了整体。
且队列中的每个结点如果存放该树根节点的数据,这样就找不到左右子树;所以应该存该根节点。
在队列的头文件中改变节点数据类型时须在前面 前置声明 该二叉树结构体。
代码如下:
// 层序遍历
void LevelOrder(BTNode* root)
{
if (root == NULL)
{
return;
}
Queue q;//创建队列
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%c ", front->data);
//如果左右子节点不为空则入队
if (front->left)
{
QueuePush(&q, front->left);
}
if (front->right)
{
QueuePush(&q, front->right);
}
}
printf("\n");
QueueDestroy(&q);
}
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树的深度
int BinaryTreeDeepth(BTNode* root);
二叉树节点个数:采用分治思想,即节点个数等于根结点+左子树节点个数+右子树节点个数。
// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : //空树
BinaryTreeSize(root->left) + //左子树节点个数
BinaryTreeSize(root->right) + //右子树节点个数
1;//根节点
}
二叉树叶子节点个数:叶子节点即度为0的节点,左右子树都为NULL
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
if (root == NULL)//空树
{
return 0;
}
else if (root->left == NULL && root->right == NULL)//叶子节点
{
return 1;
}
else
{ //左子树叶子节点+右子树叶子节点
return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}
}
二叉树第k层节点个数:分治思想,第k层节点个数等于左子树第k-1层节点个数+右子树第k-1层节点个数,且当k=1时当前根节点就是第k层。
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
else
{
return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}
}
二叉树查找值为x的节点 :遍历二叉树,注意找到节点时需将该节点保存,不然不能返回该节点。
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;//找到了就返回该节点
}
BTNode* nodeleft = BinaryTreeFind(root->left, x);//创建变量保存左子树返回的节点
if (nodeleft)
{
return nodeleft;//如果返回的节点不为NULL,就直接返回,不必再遍历右子树
}
BTNode* noderight = BinaryTreeFind(root->right, x);
if (noderight)
{
return noderight;
}
return NULL;
}
二叉树的深度 :可以理解为找出左右子树深度较大的那一个,然后整个深度为较大的那一个+1。
int BinaryTreeDeepth(BTNode* root)
{
if (root == NULL)
{
return 0;
}
int leftdepth = BinaryTreeDeepth(root->left);//保存左子树深度
int rightdepth = BinaryTreeDeepth(root->right);//保存右子树深度
return leftdepth > rightdepth ? leftdepth + 1 : rightdepth + 1;//返回较大值+1
}
判断一棵树是否是完全二叉树,则需要知道什么是完全二叉树:若一颗二叉树的高度为k,则从该树第0层到第k-1层的左右子节点全部不为NULL,第k层从左到右节点连续。
既然要判断每一层,所以这里还需要使用层序遍历的方法(即队列)来做:
方法:
将二叉树的所有节点入队(方法同层序遍历一样,出头结点,入左右子节点,空节点也入)。
若出队的节点为NULL,则退出循环。
检查队列中剩余节点是否全为NULL,全为NULL则是完全二叉树,否则不是完全二叉树。
// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//若出的节点为NULL,退出循环
if (front == NULL)
{
break;
}
//否则继续入左右子节点,空节点也入
else
{
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}
//判断队列中剩余节点
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
//若存在不为空的节点,则说明该树不是完全二叉树
if (front)
{
//返回之前需销毁队列
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
按照后序遍历的方式销毁二叉树。
// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
if (root == NULL)
{
return;
}
BinaryTreeDestory((*root)->left);
BinaryTreeDestory((*root)->right);
free(*root);
*root = NULL;
}
创作不易,留个赞再走吧~