已经学过很多数据结构了,我们需要明晰两个概念:
逻辑结构:我们想象出来的结构
物理结构:内存中实在存储的结构
线性表是逻辑结构的描述。树是一种非线性的数据结构,它是由n (n>=0) 个有限结点组成的一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树(根朝上,叶朝下)
有一个特殊的结点,称为根结点,根结点没有前驱结点
除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、......Tm,其中每一个集合Ti(1<= i<=m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,树是递归定义的,任何一棵树都会被拆解成根和子树。
tips: 树形结构中,子树之间不能有交集,否则就不是树形结构
概念
结点的度: 一个结点含有的子树的个数称为该结点的度; 如上图: A的度为6
叶结点或终端结点: 度为0的结点称为叶结点; 如上图: B、C、H、l...等结点为叶结点
非终端结点或分支结点: 度不为0的结点; 如上图: D、E、F、G.等结点为分支结点
双亲结点或父结点: 若一个结点含有子结点,则这个结点称为其子结点的父结点,如上图: A是B的父结点
孩子结点或子结点: 一个结点含有的子树的根结点称为该结点的子结点; 如上图: B是A的孩子结点
兄弟结点: 具有相同父结点的结点互称为兄弟结点, 如上图: B、C是兄弟结点
树的度:一棵树中,最大的结点的度称为树的度; 如上图: 树的度为6
结点的层次: 从根开始定义起,根为第1层,根的了结点为第2层,以此类推
树的高度或深度: 树中结点的最大层次; 如上图: 树的高度为4
堂兄弟结点: 双亲在同一层的结点互为堂兄弟;如上图: H、I互为堂兄弟结点
结点的祖先:从根到该结点所经分支上的所有结点,如上图: A是所有结点的祖先
子孙: 以某结点为根的子树中任一结点都称为该结点的子孙。如上图: 所有结点都是A的子孙
森林: 由m (m>0) 棵互不相交的树的集合称为森林,
表示方式
树的存储表示有很多种,假设树的度为6,则可以使用指针数组这样定义:
#define N 6
struct TreeNode
{
int val;
struct TreeNode* childArr[N];
};
但是这样有缺点:有很多指针会浪费。
那我们可以怎么定义呢?用顺序表:
struct TreeNode
{
int val;
SeqList childSL;
};
但这也不是最优结构,有一个非常优秀的表示方法:左孩子右兄弟表示法。
即定义两个指针,左指针指向孩子,右指针指向兄弟。例如:
typedef int DataType;
struct Node
{
struct Node* firstChild1;
struct Node* pNextBrother;
DataType data;
};
在Linux学习的过程中,我们会遇到很多指令,指令也是程序,例如cd,它的底层实现可能就是链表的遍历:
Windows系统下的目录结构就是一棵森林:C盘是一棵树,D盘是一棵树...
了解完树的相关知识了,那么什么是二叉树呢?
二叉树
二叉树是结点的一个有限集合,该集合:
1. 或者为空
2. 由一个根节点加上两棵别称为左子树和右子树的二叉树组成
二叉树类似对树进行的计划生育 (度<=2)
还需注意:
1. 二叉树不存在度大于2的结点
2. 二叉树的子树有左右之分,次序不能颠倒,是有序树
tips:对于任意的二叉树都是由以下几种情况复合而成的:
特殊情况
满二叉树
如果一个二叉树的每一个层的结点数都达到最大值,则它为满二叉树。也就是说,如果一个二叉树的层数为K,结点总数是 ,则它是满二叉树。
完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一 一对应时称之为完全二叉树。
前K-1层满,第K层可以不满,但从左到右必须连续
tips:满二叉树是一种特殊的完全二叉树。
顺序存储
使用数组实现二叉树是将数据一层一层存到数组中,父子结点下标有一个规律关系:
当parent为奇数时,有
当parent为偶数时,有
(奇偶数不影响,偶数-1和-2结果相同)
这种存储方式只适用于满二叉树或者完全二叉树存储,非完全二叉树不适合数组结构存储(会浪费很多空间),只适合链式结构存储
链式存储
二叉树的链式存储结构是用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。
链式结构又分为二叉链和三叉链,当前我们学习的一般都是二叉链,后面学到高阶数据结构如红黑树等会用到三叉链。
任何一个二叉树都可以被拆解成三个部分
1.根
2.左子树
3.右子树
遍历顺序
二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。
按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历,三种遍历方式的分类依据是访问根节点的顺序,前序指访问根节点的操作发生在遍历左右子树之前(根、左、右),以此类推。
前序
前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前
前序遍历示意:每一个节点的访问顺序都为根左右(递归到不可再拆解的子问题:空树)
中序
中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)
后序
后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后
实现
二叉树的实现要进行哪些操作呢?增删查改没有意义(不需要用这么复杂的数据结构存储数据,用它解决一些问题)
那么首先进行的操作是:手动构建一棵二叉树
声明
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}TreeNode;
创建结点
TreeNode* CreateNode(int x)
{
TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
assert(node);
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
构建二叉树
以手动构建这个二叉树为例(比较简单,易于理解)
TreeNode* CreateTree()
{
TreeNode* node1 = CreateNode(1);
TreeNode* node2 = CreateNode(2);
TreeNode* node3 = CreateNode(3);
TreeNode* node4 = CreateNode(4);
TreeNode* node5 = CreateNode(5);
TreeNode* node6 = CreateNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
前序遍历
void PrevOrder(TreeNode* root)
{
if (root == NULL) //为空打印空
{
printf("N ");
return;
}
printf("%d ", root->data); //先访问根
PrevOrder(root->left); //访问左子树
PrevOrder(root->right); //访问右子树
}
遍历使用递归,那么递归的每步具体是怎样执行的呢?
首先我们根据函数栈帧的知识可以知道,当前函数调用结束回到调用处继续执行,则有示意图:
中序遍历
void InOrder(TreeNode* root)
{
if (root == NULL) //为空打印空
{
printf("N ");
return;
}
InOrder(root->left); //访问左子树
printf("%d ", root->data); //访问根
InOrder(root->right); //访问右子树
}
后序遍历
void PostOrder(TreeNode* root)
{
if (root == NULL) //为空打印空
{
printf("N ");
return;
}
InOrder(root->left); //访问左子树
InOrder(root->right); //访问右子树
printf("%d ", root->data); //访问根
}
计算结点个数
计算结点个数的函数怎么写呢?
首先肯定是用递归写啦,那可不可以这样?
int TreeSize(TreeNode* root)
{
int size = 0;
if (root == NULL)
{
return 0;
}
size++;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
乍一看是不是还挺可行的,但是,有一个致命的问题:每个栈帧里面都有一个size,这时候会导致不为空的结点没累加起来(求了个寂寞)
那这样是不是就可以了呢?
使用静态的变量是不是可行呢(全局只有一份,不存到栈帧里)
int TreeSize(TreeNode* root)
{
static int size = 0;
if (root == NULL)
{
return 0;
}
size++;
TreeSize(root->left);
TreeSize(root->right);
return size;
}
答案当然是:不行!!!
局部的静态变量只会初始化一次,生命周期是整个程序,每次计算以上一次的结果为初始值进行累加,所以也不行(结果会变成第一次:6,第二次:12,第三次:18)。
那怎么办呢?
干脆直接用全局变量得了
每次使用前置个空就好了
int size = 0;
void TreeSize(TreeNode* root)
{
if (root == NULL)
{
return;
}
size++;
TreeSize(root->left);
TreeSize(root->right);
return;
}
还有一种比较巧妙的写法:子问题的划分
思路:如果是空树,则返回0,如果不是空树,则返回左子树结点个数+右子树结点个数+1(根节点)
int TreeSize2(TreeNode* root)
{
return root == NULL ? 0 : TreeSize2(root->left) + TreeSize2(root->right) + 1;
}
方法名:递归分治
想要写好递归需要控制好两个条件
1.子问题分治
2.返回条件
计算叶子结点个数
子问题分治:左子树叶子结点个数+右子树叶子结点个数
返回条件:
1.空返回0
2.叶子返回1
不是空,也不是叶子,分治=左右子树叶子之和
int TreeLeafSize(TreeNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL && root->right == NULL)
{
return 1;
}
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}
一直cv只会害了你(乐)
求树高度
分治子问题
1.为空返回0
2.左子树高度和右子树高度大的那个+1
可能这样的代码就被敲出来了:
int TreeHeight(TreeNode* root)
{
return root == NULL ? 0 :
TreeHeight(root->left) > TreeHeight(root->right) ?
TreeHeight(root->left) + 1 : TreeHeight(root->right) + 1;
}
这样的代码时间复杂度很可怕(因为这样的递归没有记录数据,只有比较)
优良的写法是怎么样的呢?
int TreeHeight(TreeNode* root)
{
if (root == NULL)
{
return 0;
}
int leftHeight = TreeHeight(root->left);
int rightHeight = TreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
这样才比较靠谱
还可以使用一个函数:fmax(求最大值)
那么就可以简化成这样,也是正确的(实参传给形参再返回):
int TreeHeight(TreeNode* root)
{
if (root == NULL)
{
return 0;
}
return fmax(TreeHeight(root->left),TreeHeight(root->right))+1;
}
求第K层结点个数
这个问题也要用到分治的思想。
那么到底该怎么分呢?
分治子问题
1. 空 返回0
2.不为空且k==1 返回1
3.不为空且k>1 返回左子树的k-1+右子树的k-1层
int TreeLevelK(TreeNode* root, int k)
{
assert(k > 0);
if (root == NULL)
{
return 0;
}
if (k == 1)
{
return 1;
}
return TreeLevelK(root->left,k-1)+ TreeLevelK(root->right,k-1);
}
二叉树查找值为x的结点
在二叉树中查找值为x的结点应该怎么做呢?
这份代码可不可行?
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
TreeFind(root->left, x);
TreeFind(root->right, x);
}
答案当然是不可行,每次return的时候return到上一次调用它的地方。
和上面的错处一样,没有对返回值进行接收,那么我们怎样改呢?
可以对每次的返回值进行记录。
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
TreeNode* ret1 = TreeFind(root->left, x);
if (ret1)
{
return ret1;
}
TreeNode* ret2 = TreeFind(root->right, x);
if (ret2)
{
return ret2;
}
return NULL; //不代表找不到,只代表返回上一层的为空
}
那这样可不可以呢?
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
return TreeFind(root->left, x) || TreeFind(root->right, x);
}
完全不可以哦,要求返回的是指针,不能用或。
如果是返回值类型为bool值则可行(仅仅表示找到or找不到)
那逻辑或不可以,那按位或可不可以呢?
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
return TreeFind(root->left, x) | TreeFind(root->right, x);
}
当然不可以!!
按位或只适用于整形,不可应用于指针
这样写对不对呢?
TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
if (root == NULL)
{
return NULL;
}
if (root->data == x)
{
return root;
}
TreeNode* ret1 = TreeFind(root->left, x);
if (ret1)
{
return ret1;
}
return TreeFind(root->right, x);
}
完全正确!!!但是可读性会比上一种差一点