一、树的概念及结构
1.1树的概念
树是一种非线性的数据结构,它是由n(n≥0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
1.2树的相关基本概念
- 空集合也是树,称为空树。空树中没有节点;
- 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;B是A的孩子节点
- 节点的度:一个节点含有的子节点的个数称为该节点的度;如A的度是3,B的度是3...
- 叶节点或终端节点:度为0的节点称为叶节点;如E、F、G...
- 非终端节点或分支节点:度不为0的节点;如B、C、D、E...
- 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如A是B的父节点
- 兄弟节点:具有相同父节点的节点互称为兄弟节点;如B、C、D是兄弟结点
- 树的度:一棵树中,最大的节点的度称为树的度;如这颗树的度就是3
- 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
- 树的高度或深度:树中节点的最大层次;
- 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如 G和H就是堂兄弟
- 节点的祖先:从根到该节点所经分支上的所有节点;A是所有节点的子孙
- 子孙:以某节点为根的子树中任一节点都称为该节点的子孙;所有节点都是A的子孙
- 森林:由n(n>=0)棵互不相交的树的集合称为森林
1.3树的表示
树有很多种存储形式,这里展示的是最经典的一种--孩子兄弟表示法
struct TreeNode{
int val;
struct TreeNode* firstchild;//代表树的第一个孩子结点
struct TreeNode* offspring;//孩子的兄弟结点
};
二、二叉树的概念及结构
2.1二叉树的概念
二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树
任何一个二叉树都是由下面的结构复合组成
2.2特殊的二叉树(这里介绍两种基础的)
-
1、满二叉树:如果一棵二叉树只有度为0的节点和度为2的节点,并且度为0的节点在同一层上,则这棵二叉树为满二叉树 。(也可以说树的结点个数为2^h-1的树就是满二叉树,h是树的层数)
-
2、完全二叉树:深度为k,有n个节点的二叉树当且仅当其每一个节点都与深度为k的满二叉树中编号从1到n的节点一一对应时,称为完全二叉树
2.3二叉树的性质
性质1:二叉树的第i层上至多有2i-1(i≥1)个节点 [6] 。
性质2:深度为h的二叉树中至多含有2h-1个节点 [6] 。
性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1 [6] 。
性质4:具有n个节点的满二叉树深为log2n+1。
性质5:若对一棵有n个节点的完全二叉树进行顺序编号(从0开始),对于编号为i(i≥1)的节点,有以下几点性质:
(1)i的左孩子结点的编号是2*i+1
(2)i的右孩子的编号是2*i+1
(2)i的父节点的编号是(i-1)/2
2.4二叉树的存储结构
一般有两种:一种为链式结构,一种为顺序结构
1.顺序结构:顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树![](https://i-blog.csdnimg.cn/blog_migrate/fae153a675a9fb33a102cfa3adfc1abe.png)
2.链式结构:用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,高阶数据结构如红黑树等会用到三叉链
三、二叉树的顺序结构---堆的实现
堆的性质:
-
堆中某个结点的值总是不大于或不小于其父结点的值;
-
堆总是一棵完全二叉树
3.1堆的实现
3.1.1代码如下
//这里建立的是小堆---即所有孩子节点的值小于父节点的值
//大堆的代码与这个类似,留给读者思考
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap {
HPDataType* a;
int size;
int capacity;
}HP;
//初始化
void HeapInit(HP* php);
//销毁
void HeapDestroy(HP* php);
//插入
void HeapPush(HP* php, HPDataType x);
//删除堆顶元素
void HeapPop(HP* php);
//取出堆顶的元素
HPDataType HeapTop(HP*php);
//判断堆是否为空
bool HeapEmpty(HP* php);
//得到堆的大小
int HeapSize(HP* php);
//向下调整堆
void AdjustDown(HPDataType* a, int n, int parent);
//向上调整堆
void AdjustUp(HPDataType* a, int child);
//交换
void Swap(HPDataType* p1, HPDataType* p2);
//初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = 0;
php->capacity = 0;
}
//销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向上调整堆
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 = 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 HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc");
return;
}
php->a = tmp;
php->capacity = newcapacity;
}
php->a[php->size++] = x;
AdjustUp(php->a, php->size - 1);
}
//删除堆顶元素
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
//取出堆顶的元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
//判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
//得到堆的大小
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
3.1.2这里面有两个函数比较重要也比较难理解---AdjustUp函数和AdjustDown函数
3.2堆排序
3.2.1堆排序的实现
基本思想:先建立一个堆,根据堆的性质,堆顶的元素是最大值或最小值,那么我们只要将堆顶的元素与数组的最后一个元素交换,然后根据HeapPop函数的思路,调整前面的数据使得它还是一个堆,如此循环,得到一个有序的序列
void HeapSort(int*a,int n)
{
//建立堆
//...
//调整堆
for(int i=n-1;i>0;i--)//只要调整n-1次
{
Swap(&a[0],&a[i]);
AdjustDown(a,i,0);
}
}
这里建立堆有两种思路,分别是AdjustUp和AdjustDown,即向上调整和向下调整
代码如下
void HeapSort(int*a,int n)
{
//建立堆
//AdjustUp
for(int i=1;i<n;i++)//从第二个元素开始
AdjustUp(a,i);
//AdjustDown
for(int i=(n-1-1)/2;i>=0;i--)//从最后一个叶子节点的父节点开始
AdjustDown(a,n,i);
//调整堆
for(int i=n-1;i>0;i--)//只要调整n-1次
{
Swap(&a[0],&a[i]);
AdjustDown(a,i,0);
}
}
3.2.2上面两种建堆的时间复杂度分别时多少?
3.3Top-K问题(在大量的数据中找到前k个最大值或最小值)
思路:假设在n个数据中找前k个最大值,先建一个大小为k的小堆(反之,建大堆),然后依次比较堆顶元素和n-k个数据,不断调整小堆,最后堆中存放的就是前k个最大值,但不有序
void CreateNDate()
{
//造数据
int n = 10000;
srand(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;//数据在0~1000000
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
void PrintTopK(int k)
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
int* tmp = (int*)malloc(sizeof(int) * k);
//读取数据
for (int i = 0; i < k; i++)
{
fscanf(pf, "%d", &tmp[i]);
}
//建立小堆
for (int i = (k - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(tmp, k, i);
}
//将堆顶元素和其他元素比较,如果大于则交换,调整堆
while (!feof(pf))
{
int val = 0;
fscanf(pf, "%d", &val);
if (val > tmp[0])
{
tmp[0] = val;
AdjustDown(tmp, k, 0);
}
}
for (int i = 0; i < k; i++)
printf("%d ",tmp[i]);
free(tmp);
}
int main()
{
int k = 10;
//CreateNDate();//这个函数只要调用一次
PrintTopK(k);
return 0;
}
四、二叉树的链式结构
typedef struct TreeNode{
int val;
struct TreeNode*left;
struct TreeNode*right;
}TreeNode;
4.1二叉树的前、中、后序遍历
- 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。
- 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。
- 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。
代码如下
//先序
void PreOrder(TreeNode*root)
{
if(root==NULL)//结点为空,直接返回
return;
printf("%d",root->val);//遍历根结点
PreOrder(root->left);//遍历左子树
PreOrder(root->right);//遍历右子树
}
//中序
void InOrder(TreeNode*root)
{
if(root==NULL)//结点为空,直接返回
return;
InOrder(root->left);//遍历左子树
printf("%d",root->val);//遍历根结点
InOrder(root->right);//遍历右子树
}
//后序
void PostOrder(TreeNode*root)
{
if(root==NULL)//结点为空,直接返回
return;
PostOrder(root->left);//遍历左子树
PostOrder(root->right);//遍历右子树
printf("%d",root->val);//遍历根结点
}
PreOrder函数递归展开图
如上图,如果将函数递归展开,我们会发现这就是一颗树的形状,并且需要注意的是函数递归是经过NULL结点的!!!中序和后序的展开图与它类似,可以自己画画图加深对递归遍历的理解
4.2二叉树相关问题的思路
二叉树的很多题目都是用递归来做的,本质的思想其实就是分治思想,即将一个问题转化成两个子问题。
1.求二叉树的结点数
很多人的第一想法是用之前的遍历二叉树,将打印的代码变成计数的代码,这种方法当然可以,但是它其实本质是个深度优先遍历的思想,我们现在要求用分治,代码格式如下
int TNodeSize(TreeNode*root){
}
思路:求一颗二叉树的结点数=>求它的左子树的结点数+求它的右子树的结点数+它本身,当然前提是它不是一个空树,这样我们就将问题转换成了两个相同的子问题,代码如下
int TNodeSize(TreeNode*root){
//如果为空,结点个数就是0
if(root==NULL)
return 0;
//如果不为空,求二叉树节点个数就可以拆成求两个子树结点数的问题,记得加1
return TNodeSize(root->left)+TNodeSize(root->right)+1;
}
2.求叶子节点的个数
分治的思想依旧如上:求一颗二叉树的叶子结点数=>求它的左子树的叶子结点数+求它的右子树的叶子结点数,而函数返回的条件是树为空,或找到叶子节点
代码如下
int BTLeafSize(TreeNode*root){
if(root==NULL)//空树
return 0;
if(root->left==NULL||root->right==NULL)//叶子节点
return 1;
return BTLeafSize(root->left)+BTLeafSize(root->right);
}
3.求树的高度
分治的思想依旧如上:求一颗树的高度=>max{ 左子树的高度,右子树的高度 } +1
代码如下
int BTreeHeight(TreeNode*root){
if(root==NULL)
return 0;
return fmax(BTreeHeight(root->left),BTreeHeight(root->right))+1;
}
下面还有几个题目留给读者思考:
typedef struct TreeNode{
int val;
struct TreeNode*left;
struct TreeNode*left;
}TreeNode;
//求第K层的结点数
int BTLevelSize(TreeNode*root,int k);
//查找结点
TreeNode*BTreeFind(TreeNode*root,int x);
//两棵树是否相等
bool isSame(TreeNode*p,TreeNode*q);
如果觉得这篇博客对你有所帮助的话,请一定不要吝啬你的点赞加评论哦!!!