树的概念与结构
线性表:是一种具有n个相同特性的数据元素的有限序列。线性表逻辑上是线性结构,也就是连成的一条直线,但一条直线上的数据元素并不是一定要物理结构连续的。
讲到二叉树之前,我们要先了解一下什么是树,首先树也是一种数据结构,只不过与栈和队列不同,树并不是线性表的一种,因为树的结构组成并不是直线型的,是有分支的。
树的概念
树是一种非线性的数据结构,由n(n>=0)有限的节点构成的具有层次关系的结构。而结构十分像一棵倒着的树,节点A就像树根一样(根节点)没有前驱节点,而A下面就类似于一个个树枝(子树)一样,而树枝又会分支,分支又有分支...是否会联想到递归了你。
注意:树的结构中,子树是不存在交集的(除了根节点,每个节点有且只有一个父节点),否则就不是树的结构
而以上的三种结构就都不是树结构。
树中的常见名词
有关树的名词可以联想到家里的亲戚关系
节点的度:一个节点含有的子树(分支)的个数称为该节点的度; 如上图: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是所有节点的祖先,E也是Q的祖先
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙森林:多棵互不相交的树的集合称为森林(并查集)
树的结构表示
树的表示方式就不会像单链表一样简单,树中要存放数据,和节点之间的关系,而节点之间的关系并不像单链表一样,因为树并不是线性结构,树的分支并不是一定只有一个,可能没有,也可能有多个,那么我们要创建多个指针吗,如果不知道树的高度呢,就算知道了会不会太复杂呢。所以我们一般会有更加简便的定义方式:孩子兄弟表示法。即创建树的节点存放实际数据,孩子指针,兄弟指针。
typedef int DataType;
struct Node
{
DataType data; // 结点中的数据域
struct Node* firstChild1; // 第一个孩子结点
struct Node* pNextBrother; // 指向其下一个兄弟结点
};
这样是不是就将每一个节点都串联起来了呢,没有的时候就指向空。
二叉树的概念
概念
1. 二叉树中每个节点的度都小于等于2。
2. 二叉树的子树分左右,次序不能颠倒,所以二叉树称为有序数。
二叉树的链式结构
前面谈到的是完全二叉树,因为结构特殊,所以我们是通过顺序表的方式存储的,但是对于一般的二叉树是并不适合的,那样会存在空间的大大浪费,并且也不方便。所以我们还是回归自然,用链表来存储二叉树。
typedef struct TreeNode
{
int val;
struct TreeNode* left;
struct TreeNode* right;
}TN;
每一个节点基本的结构就是存放的数据加左右两个子节点,但是二叉树的左右子节点又可以看作是新的二叉树,这样是不是很像递归在自己调用自己呢。
链式二叉树中的问题一般都是用递归的方式实现,所以我们就要了解一下递归,递归其实是和函数放到一起的,但是函数就实现一次,而递归是实现多次相同功能的函数,而且想要实现递归的精髓就要知道两点
- 找到递归执行的停止条件(即找到向下创建函数栈帧的最后一次)
- 将问题转换成子问题(即找到等价问题进行转换)
二叉树前序中序后序遍历
我们知道完全二叉树是用顺序表来存储的,所以我们遍历顺序表,用下标就可以十分轻松的实现打印二叉树,可是链表又该如何打印数据呢?这里我们就有几种递归实现的方式。
1. 前序遍历(也称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。即:根 左 右
2. 中序遍历——访问根结点的操作发生在遍历其左右子树之中(间)。即:左 根 右
3. 后序遍历——访问根结点的操作发生在遍历其左右子树之后。即:左 右 根
这里的左节点与右节点并不一定是单纯的左右节点,他可能也看作是左子树、右子树。所以我们在不知道二叉树的图形结构式,遍历二叉树就要用到上面的方法。
二叉树前序遍历
假设我们实现上面结构的二叉树并且想要打印出来:
typedef struct TreeNode//创建节点类型 { int val; struct TreeNode* left; struct TreeNode* right; }TN; TN* Malloc(int x)//动态开辟节点并初始化 { TN* new = (TN*)malloc(sizeof(TN)); assert(new); new->left = new->right = NULL; new->val = x; return new; } void Print(TN* node)//前序遍历 { if (node == NULL) printf("N "); else { printf("%d ", node->val); Print(node->left); Print(node->right); } } int main() { TN* node1 = Malloc(1); TN* node2 = Malloc(2); TN* node3 = Malloc(3); TN* node4 = Malloc(4); TN* node5 = Malloc(5); TN* node6 = Malloc(6); node1->right = node4; node1->left = node2; node2->left = node3; node4->left = node5; node4->right = node6; Print(node1);//前序打印(根左右) return 0; }
递归部分分析图:
二叉树的中序后序遍历
其实二叉树的的中序和后序遍历就是将打印顺序改变一下就行了:
void Print(TN* node)//中序遍历
{
if (node == NULL)
printf("N ");
else
{
Print(node->left);
printf("%d ", node->val);
Print(node->right);
}
}
void Print(TN* node)//后序遍历
{
if (node == NULL)
printf("N ");
else
{
Print(node->left);
Print(node->right);
printf("%d ", node->val);
}
}
二叉树的层序遍历
层序遍历就是从二叉树的根节点开始出发遍历,然后一层一层的的从左至右遍历每一个节点,而对于二叉树存数据时也是从根节点开始再向根的左右节点插入数据,即存数据和拿数据是相同的顺序,即:先进先出。恰恰符合队列的存储形式,所以就可以通过队列来实现层序遍历。
求二叉树节点个数
int GetNodeSize(TN* node)//求节点的个数
{
if (node==NULL)//结束条件,而且一般二叉树问题都要考虑执行到NULL的情况
return 0;
else
return GetNodeSize(node->left) + GetNodeSize(node->right) + 1;
//左子树的节点个数+右子树的节点个数+自生的一个节点
}
求二叉树的叶子结点个数
int GetLeafSize(TN* node)//求叶子结点的个数
{
if (node == NULL)//终止条件
return 0;
if (node->left == node->right && node->left == NULL)//是叶子结点
return 1;
else
return GetLeafSize(node->left) + GetLeafSize(node->right);
//等价成 左子树的叶子结点个数+右子树的叶子结点个数
}
求二叉树的高度
int GetTreeHeight(TN* node)//求二叉树的高度
{
if (node == NULL)//终止
return 0;
//return GetTreeHeight(node->left) > GetTreeHeight(node->right) ?
// 1 + GetTreeHeight(node->left) : 1 + GetTreeHeight(node->right);//这样实现会先判断大小再计算值,导致重复实现递归
//上述递归调用太多,导致反复调用,成等比增长,所以就会导致花费时间更久
int left = GetTreeHeight(node->left);//左子树的高度
int right = GetTreeHeight(node->right);//右子树的高度
return left > right ? left + 1 : right + 1;//返回更大的高度
}
求二叉树第K层节点个数
int GetKNode(TN* node,int k)
{
//停止条件
if (k == 1 && node != NULL)//因为传过来的节点就看作第一层,所以k==1就是所求
return 1;
if (node == NULL)
return 0;
return GetKNode(node->left, k - 1) + GetKNode(node->right, k - 1);
//等价转换成 左子树的k-1层节点数+右子树的k-1层节点数
}
查找二叉树中值为x的节点
TN* GetPointNode(TN* node, int x)
{
if (node == NULL)
return NULL;
if (node->val == x)//相同就直接返回节点指针
return node;
//if (GetPointNode(node->left, x) != NULL)
// return GetPointNode(node->left, x);
//if (GetPointNode(node->right, x) != NULL)
// return GetPointNode(node->right, x);
//消耗过大
//以下是该节点既不是空也不是指定节点的情况,继续向下找:
TN* leftnode = GetPointNode(node->left, x);//记录下来
if (leftnode != NULL)//判断左子树是否存在指定节点
return leftnode;
TN* rightnode = GetPointNode(node->right, x);
if (rightnode != NULL)//右子树
return rightnode;
return NULL;//该节点处没找到并且左右节点也不是
}
二叉树的构建
typedef struct TN
{
char val;
struct TN* left;
struct TN* right;
}TN;
TN* Malloc(char c)
{
TN* newnode = (TN*)malloc(sizeof(TN));
assert(newnode);
newnode->val=c;
newnode->left=newnode->right=NULL;
return newnode;
}
void GetTree(char* p,TN** node,int* i)//按照前序存储数据
{
//这里也可以不用考虑越界的情况,因为只要数据给的是二叉树的结构就没问题
if(p[*i]=='#')
{
(*i)++;
return;
}
TN* newnode = Malloc(p[*i]);
*node = newnode;
(*i)++;
GetTree(p,&(*node)->left,i);
GetTree(p,&(*node)->right,i);
}
int main() {
char arr[100];
gets(arr);//存放输入的字符
TN* node=NULL;//开辟头结点
int i=0;//数组下标
GetTree(arr,&node,&i);//想要改变数据就要传地址
return 0;
}
二叉树的销毁
void Destroy(TN* node)
{
//如果按照前序遍历就会导致后面的数据找不到了
//所以就考虑从后续遍历,即从后面开始销毁数据
if (node == NULL)
return;
Destroy(node->left);
Destroy(node->right);
free(node);
}