树形结构:结点间具有层次关系,每一层的一个结点能且只能和上一层的一个结点相关,但同时可以和下一层的多个结点相关,即“一对多”
关系。常见树形结构有树、堆。
关于树的定义
树是递归定义的:由N(n>=0)个结点构成的集合。当n=0时,称为空树;当n>1时,树有:
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
![](https://i-blog.csdnimg.cn/blog_migrate/fb8c9d6e969367c0c8eab3f56f42b0ca.png)
树的相关概念(主要理解)
结点
:结点包括一个数据元素及若干指向其他子树的分支(指针(索引))
结点的度
:结点所拥有子树的个数称为该结点的度
树的度
:树中所有结点的度的最大值称为该树的度
叶子结点(终端节点)
:度为0的结点称为叶子结点
分支结点(非终端节点)
:度不为0的结点称为分支结点,一棵树中除叶节点外的所有节点都是分支结点
祖先结点
:从根节点到该结点所经分支上的所有节点
子孙结点
:以某节点为根节点的子树中所有节点
双亲结点(前驱结点)
:一个结点称之为其后继结点的双亲节点。
孩子结点(后继结点)
:树中一个节点的子树的根节点称为该结点的孩子结点
兄弟结点
:具有同一双亲的结点之间互称为为兄弟结点
结点层次
:从根节点到树中某节点所经路径上的分支数称为该结点的层次,根结点为第一层,其孩子节点为第二层,以此类推。
树的深度(高度)
:树中所有节点层次的最大值称为该树的深度
有序树和无序树
:树中各结点从左到右是有次序的,即若交换了某结点各子树的相对位置则构成了不同的树,那么称之为有序树,反之则为无序树。
森林
:m棵树的集合(m大于等于0)。在自然界中树和森林是两个不同的概念,但在数据结构中,它们之间的差别很小。删去一棵非空树的根节点,树就变成森林;反之若增加一个节点,让森林中每一棵树的根节点都变成他的子女,森林就变成一棵树
图解概念
树结构的实现方式
在计算机中存储树时,要求既要存储结点的数据元素信息,又要存储结点之间的逻辑关系信息。所以一共有四种表示方法。
1. 双亲表示法
对于上图中的树,双亲表示法有两种实现方式:
- 一维数组实现
用一维数组存储树中的各个结点,其中数组元素是个记录,包含data和parent两个字段,分别表示结点的数据值和其父节点在数组中的下标。
- 用指针表示出每个结点的双亲节点
双亲表示法的优缺点
- 优点:寻找一个节点的双亲结点操作实现很方便
- 缺点:寻找一个节点的孩子结点很不方便
2. 孩子表示法
孩子表示法的实现方式:用指针表示出每个结点的子结点,但是有个问题:怎么知道每个结点会有几个孩子结点呢?
答案是看树的度,如下图中,树的度为3,则应该设计3个指针指向孩子结点。
数据结构:
typedef int DataType;
struct Node
{
struct Node* _pChild1;
struct Node* _pChild2;
struct Node* _pChild3;
DataType _data; //数据域
};
孩子表示法的优缺点
- 优点:寻找一个节点的孩子结点比较方便
- 缺点:寻找一个节点的双亲结点很不方便
3. 双亲孩子表示法
双亲孩子表示法:用指针既表示出每个结点的父节点,也表示出每个结点的子结点,即:双亲表示法+孩子表示法
typedef int DataType;
struct Node
{
struct Node* _pParent; //指向双亲节点
struct Node* _pChild1;
struct Node* _pChild2;
struct Node* _pChild3;
DataType _data; //数据域
};
4. 孩子兄弟表示法
孩子兄弟表示法:即表示出每个结点的第一个孩子结点,也表示出每个结点的下一个兄弟结点。孩子兄弟链表存储结构是一种二叉链表,链表中的每一个结点包含两个指针,分别指向对应结点的第一个子结点和下一个兄弟结点。
typedef int DataType;
struct Node
{
struct Node* _pChild1;
struct Node* _pNextBrother;
DataType _data; //数据域
};
二叉树
二叉树是一种特殊又重要的树。二叉树的特点:
- 每个结点最多有两棵子树,即二叉树不存在度大于2的结点
- 二叉树的子树有左右之分,其子树的次序不能颠倒
- 二叉树即使只有一颗子树也要明确指出该子树是左子树还是右子树。
![](https://i-blog.csdnimg.cn/blog_migrate/c6f9ae6fa6215df963881b066cbf8233.png)
满二叉树&完全二叉树
-
满二叉树: 在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子节点都在同一层上
-
完全二叉树:从根起,自上而下,自左而右,给满二叉树的每个结点从1到n连续编号,如果每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应,则称为完全二叉树,满二叉树是完全二叉树。
![](https://i-blog.csdnimg.cn/blog_migrate/c29700365d56d91843691e5a303e3a55.png)
二叉树的性质
- 二叉树上叶子结点数等于度为2的节点数加1.
证明:
设在一棵二叉树中,n为所有节点总数,n0为叶子结点个数,n1为度为1的节点个数,n2为度为2的结点个数。则 n = n0 + n1 + n2 (1)
再看二叉树中的分支数:除根节点外,所有结点都有一个分支进入,设b为分支数,
则 n = b + 1
又因为分支是由度为1或2的结点分出的,因此:
b = n1 + 2 x n2
于是: n = n1 + 2 x n2 + 1 (2)
由(1)(2)得: n0 = n2 +1
- 二叉树上第i层上至少有2^(i-1)个结点。(i>=1,根结点的层数为1)
证明:数学归纳法
-
若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 2^k - 1 (k>=0)
-
对于完全二叉树(n≥1,n为节点数),如果按照
从上至下从左至右
的顺序对所有节点从1开始编号
,则对于序号为i的结点有:- 若编号为i的结点有左孩子,则左孩子结点编号为2i;若编号为i的结点有右孩子,则右孩子结点编号为2i+1
- 除树的根节点外,若一个结点的编号为i,则双亲编号为:i/2。
- 如果 2 * i ≤ n,则编号为i的结点为分支结点;若2 * i > n,i为叶子结点。
- 若n为奇数,则每个分支结点都既有左孩子又有右孩子;若n为偶数,则编号最大的分支结点只有左孩子,没有右孩子。
二叉树的存储结构
与线性表一样,二叉树也有顺序存储结构和链式存储结构。
- 顺序存储结构
typedef DataType SqBinTree[MaxSize]; //其中DataType为二叉树结点的数据类型;MAxSize为顺序表的最大长度。
用数组来存储一颗二叉树其实是不合适的,但是如果这个人二叉树是一个完全二叉树,则数组存储是很常见的,存储过程:首先,把根结点定为1,然后按照层次上从上到下、每层从左到右的顺序对每一结点进行编号。对于一般二叉树,也可以用数组存储,但是首先,得在二叉树上补上若干虚拟结点使其成为完全二叉树后,再按照上面的方法进行编号。下表中是个二叉树顺序存储结构的示例:
![]() | ![]() |
---|
顺序存储结构的优缺点
- 优点:存储完全二叉树,简单省空间 ;
- 缺点:存储一般二叉树尤其单支树,存储空间利用不高 。
- 二叉链存储结构
链表中每个结点包含两个指针,分别对应this结点的左孩子和右孩子:
struct BinTreeNode
{
struct BinTreeNode* _pLeft;
struct BinTreeNode* _pRight;
DataType _data;
};
这种存储结构的优点是访问结点的孩子很方便
- 三叉链存储结构
链表中每个结点包含三个指针,分别对应this结点的左孩子、右孩子和父节点。
struct BinTreeNode
{
struct BinTreeNode* _pParent;
struct BinTreeNode* _pLeft;
struct BinTreeNode* _pRight;
DataType _data;
};
1. 二叉树的实现(c++)
以下采用二叉链存储结构。
- 建立二叉链
用ch扫描采用括号表示法表示二叉树的字符串str。分为以下几种情况:
1)若 ch == ‘(’ ,则将前面刚创建的结点作为双亲节点进栈,并置k=1,表示其后创建的节点将作为这个节点的左孩子节点。
2)若 ch == ‘(’,表示栈中结点的左右孩子结点处理完毕,退栈。
3)若 ch == ‘,’,k=2,表示其后创建的节点为右孩子结点
4)若 ch 为一个字母,创建一个节点,并根据k值建立它与栈中结点之间的联系,当k=1表示这个节点作为栈中栈顶结点的左孩子结点;当k=2表示这个节点作为栈中栈顶结点的右孩子结点;如此循环直至str处理完毕。算法中使用一个栈st保存双亲节点,top为其栈顶指针;k指定其后处理的结点是双亲节点的左孩子结点还是右孩子。
#define MaxSize 20
typedef char DataType;
typedef struct tnode
{
DataType data;
struct tnode* pchild1;
struct tnode* pchild2;
}BTNode;
//建立二叉链
void CreateBTree(BTNode *&bt, char *str)
{
BTNode *st[MaxSize], *p = NULL;
int top = -1, k, j = 0;
char ch;
bt = NULL;//建立二叉树初始化为空
ch = str[j];
while ('\0' != ch) //str未扫描完时循环
{
switch (ch)
{
case '(':top++; st[top] = p; k = 1; break;//左孩子节点
case ')':top--; break;
case ',':k = 2; break; //右孩子结点
default:
p = (BTNode*)malloc(sizeof(BTNode));
p->data = ch;
p->pchild1 = p->pchild2 = NULL;
if (bt == NULL)
bt = p;
else
{
switch (k)
{
case 1:st[top]->pchild1 = p; break;
case 2:st[top]->pchild2 = p; break;
}
}
}
j++;
ch = str[j];
}
}
- 求二叉树高度(递归求法)
int BTHeight(BTNode *bt)
{
int left, right;
if (NULL == bt)
{
return 0;
}
else
{
left = BTHeight(bt->pchild1);
right = BTHeight(bt->pchild2);
return (left > right) ? (left + 1) : (right + 1);
}
}
- 求二叉树结点个数(递归算法)
int NodeCount(BTNode *bt)
{
int left, right;
if (NULL == bt)
{
return 0;
}
else
{
left = NodeCount(bt->pchild1);
right = NodeCount(bt->pchild2);
return left + right + 1;
}
}
- 求二叉树叶子结点个数
int LeafCount(BTNode *bt)
{
int left, right;
if (NULL == bt)
return 0;
else if (NULL == bt->pchild1 && NULL == bt->pchild2)
return 1;
else
{
left = LeafCount(bt->pchild1);
right = LeafCount(bt->pchild2);
return left + right;
}
}
- 以括号表示法输出二叉树
void DisBTree(BTNode *bt)
{
if (NULL != bt)
{
cout << bt->data;
if (NULL != bt->pchild1 || NULL != bt->pchild2)
{
cout << '(';
DisBTree(bt->pchild1);
if (NULL != bt->pchild2) cout << ",";
DisBTree(bt->pchild2);
cout << ")";
}
}
}
- 二叉树的遍历
所谓二叉树的遍历,是指按照一定次序访问树中的所有节点,使得每个结点恰好被访问一 次。二叉树常用的遍历有:先序遍历(先根遍历)、中序遍历、后序遍历和层次遍历。不同遍历方式区别在于访问根节点的顺序。假设我有一个二叉树
1. 先序遍历
先序遍历规则:访问根结点;------> 先序遍历左子树;------> 先序遍历右子树。
void PreOrder(BTNode *bt)
{
if (NULL != bt)
{
cout << bt->data;
PreOrder(bt->childleft);
PreOrder(bt->childright);
}
}
2. 中序遍历
中序遍历规则:1)中序遍历左子树;2)访问根节点;3)中序遍历右子树。
void InOrder(BTNode *bt)
{
if (NULL != bt)
{
InOrder(bt->childleft);
cout << bt->data;
InOrder(bt->childright);
}
}
3. 后序遍历
后序遍历规则:1)后序遍历左子树;2)后序遍历右子树;3)访问根节点。
void PostOrder(BTNode *bt)
{
if (NULL != bt)
{
PostOrder(bt->childleft);
PostOrder(bt->childright);
cout << bt->data;
}
}
4. 层次遍历
层次遍历规则:在进行层次访问的时候,对某一层的结点访问完,再按照他们的访问次序对各个结点的左孩子、右孩子顺序访问。
层次访问需要一个环形队列qu来实现:先将根结点进队,在队不空时循环;从队列中出列一个节点*p,访问它;若它有左孩子节点,将左孩子节点进队;若它有右孩子结点,将右孩子结点进队。如此操作直到队空为止。
void LevelOrder(BTNode *bt)
{
BTNode *p;
BTNode *qu[MaxSize];//定义环形队列
int front, rear;//对头与队尾指针
front = rear = -1;//队列为空
rear++;
qu[rear] = bt; //根节点进入队列
while (front != rear){ //队列不为空
front = (front + 1) % MaxSize;
p = qu[front];//队头出队列
printf("%c ", p->data);//访问结点
if (NULL != p->leftchild)//有左孩子时将其入队
{
rear = (rear + 1) % MaxSize;
qu[rear] = p->leftchild;
}
if (NULL != p->rightchild)//有右孩子时将其入队
{
rear = (rear + 1) % MaxSize;
qu[rear] = p->rightchild;
}
}
}
值得注意的是
- 对于不同的二叉树,先序遍历序列可能相同
- 对于不同的二叉树,中序遍历序列可能相同
- 对于不同的二叉树,先序遍历序列可能相同
- 对于不同的二叉树,先序遍历序列和后序遍历可能都相同
由先序遍历和中序遍历序列能够唯一确定一棵二叉树
由后序遍历和中序遍历序列能够唯一确定一棵二叉树
2. 二叉树的实现(java)
/**
* @author Emma
* @create 2020 - 03 - 21 - 9:01
* 结点代码
*/
public class TreeNode {
public int value;//结点的权
public TreeNode left;
public TreeNode right;
//构造方法
public TreeNode(int value){
this.value = value;
}
//设置左节点
public void setLeft(TreeNode left) {
this.left = left;
}
//设置右节点
public void setRight(TreeNode right) {
this.right = right;
}
//前序遍历:根-左-右 (遍历需要递归)
public void frontShow() {
System.out.println(value);
if(left != null)
left.frontShow();
if(right != null)
right.frontShow();
}
//中序遍历 左-根-右
public void midShow() {
if(left != null)
left.midShow();
System.out.println(value);
if(right != null)
right.midShow();
}
//后序遍历 左-右-根
public void afterShow() {
if(left != null)
left.afterShow();
if(right != null)
right.afterShow();
System.out.println(value);
}
//前序查找
public TreeNode frontSearch(int i) {
TreeNode temp = null;
if(this.value == i) {
// 判断根结点是否找到了,找到了就返回
return this;
}else{
//
if(left != null){
temp = left.frontSearch(i);
//判断左边是否找到了,找到了就返回
if(temp != null){
return temp;
}
}
if(right != null){
temp = right.frontSearch(i);
}
}
return temp;
}
//删除一个节点
public void delete(int i) {
TreeNode parent = this;
//判断左节点是否符合
if(parent.left != null && parent.left.value == i){
parent.left = null;
return;
}
//判断右节点是否符合
if(parent.right != null && parent.right.value == i){
parent.right = null;
return;
}
//如果左右结点都不是,将左节点赋值为parent,然后找左节点的左节点,以及左节点的右节点
parent = left;
if(parent != null){//左节点不为空才进入,为空就去考虑右节点
parent.delete(i);
}
parent = right;
if(parent != null){//右节点不为空才进入,为空就去返回null
parent.delete(i);
}
}
}
/**
* @author Emma
* @create 2020 - 03 - 21 - 8:57
* 二叉树代码
*/
public class BinaryTree {
TreeNode root;//根结点
public void setRoot(TreeNode root) {
this.root = root;
}
public TreeNode getRoot() {
return root;
}
//前序遍历
public void frontShow() {
if(root != null)
root.frontShow();
}
//中序遍历
public void midShow() {
if(root != null)
root.midShow();
}
//后序遍历
public void afterShow() {
if(root != null)
root.afterShow();
}
//前序查找
public TreeNode frontSearch(int i) {
return root.frontSearch(i);
}
//删除一个结点
public void delete(int i) {
if(root.value == i){
root = null;
}else{
root.delete(i);
}
}
}
/**
* @author Emma
* @create 2020 - 03 - 21 - 11:19
* 测试代码
*/
public class BinaryTreeTest {
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
TreeNode root = new TreeNode(1);//根结点
binaryTree.setRoot(root);
TreeNode rootleft = new TreeNode(2);//根结点的左节点
root.setLeft(rootleft);
TreeNode rootright = new TreeNode(3);//根结点的右节点
root.setRight(rootright);
TreeNode rootleftleft = new TreeNode(4);//根结点的左节点的左节点
rootleft.setLeft(rootleftleft);
TreeNode rootleftright = new TreeNode(5);//根结点的左节点的右节点
rootleft.setRight(rootleftright);
TreeNode rootrightleft = new TreeNode(6);//根结点的右节点的左节点
rootright.setLeft(rootrightleft);
TreeNode rootrightright = new TreeNode(7);//根结点的右节点的右节点
rootright.setRight(rootrightright);
//前序遍历
binaryTree.frontShow();
System.out.println("*************");
//中序
binaryTree.midShow();
System.out.println("*************");
//后序
binaryTree.afterShow();
System.out.println("*************");
//查找,可以有三种方式(前序查找,中序查找,后序查找
TreeNode node = binaryTree.frontSearch(5);
System.out.println(node);
System.out.println("*************");
//删除节点,如果有子节点一起删掉
binaryTree.delete(8);
binaryTree.frontShow();
}
}