二叉树的基本概念与操作

树的定义及性质

树的基本概念

根节点,边,分支结点,叶子结点,子树。

空树:结点数为0的树

非空树:有且仅有一个根节点。没有后继的结点称为叶子结点(或终端结点),有后继的结点称为分支结点(或非终端结点)。除了根节点外,任何一个结点都有且仅有一个前驱。每个结点可以有0个或多个后继。

结点之间的关系描述:祖先结点,子孙结点,双亲结点(父节点),孩子结点,兄弟结点,堂兄弟结点

两个结点之间的路径:只能从上往下。路径长度:经过几条边。

结点的深度:从上往下数。结点的高度:从上往下数。

树的高度/深度:总共多少层。

结点的度:有几个孩子/分支。

树的度:各结点的度的最大值。

有序树:逻辑上看,树中结点各子树从左至右是有次序的,不能互换。

无序树:逻辑上看,树中结点各子树从左至右是无次序的,可以互换。

森林:m棵互不相交的树的集合。

树的性质

  1. 结点数 = 总度数+1

  2. 树的度(各结点的度的最大值)与m叉树(每个结点最多只能有m个孩子)的区别

  3. 度为m的树第i层至多有 m i − 1 m^{i-1} mi1个结点,m叉树第i层至多有 m i − 1 m^{i-1} mi1个结点

  4. 高度为hm叉树至多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1个结点

  5. 高度为hm叉树至少有h个结点;高度为h,度为m树至少有h+m-1个结点

  6. n个结点的m叉树的最小高度为 [ l o g m ( n ( m − 1 ) + 1 ) ] [log_m{(n(m-1)+1)}] [logm(n(m1)+1)]

二叉树的定义和基本术语

二叉树是n个结点的有限集合:

  • 或者为空二叉树,即n = 0
  • 或者由一个根节点和两个互不相交的被称为根的左子树右子树组成。左子树和右子树又分别是一棵二叉树。

特点:

  1. 每个结点至多只有两棵子树
  2. 左右子树不能颠倒(二叉树是有序树)

二叉树的性质:

  1. n 0 = n 2 + 1 n_0 = n_2+1 n0=n2+1(度为0,1,2的结点个数分别为 n 0 n_0 n0 n 1 n_1 n1 n 2 n_2 n2
  2. 二叉树的第i层至多有 2 i − 1 2^{i-1} 2i1个结点
  3. 高度为h的二叉树至多有 2 h − 1 2^h-1 2h1个结点(满二叉树)
  4. n个结点的完全二叉树的高度h l o g 2 [ n + 1 ] log_2[n+1] log2[n+1] [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1
  5. 对于完全二叉树,可以有结点数n推出度为0,1,2的结点个数分别为 n 0 n_0 n0 n 1 n_1 n1 n 2 n_2 n2

完全二叉树最多只有一个度为1的结点,且 n 0 = n 2 + 1 n_0 = n_2+1 n0=n2+1

  • n = 2 k − 1 n=2k-1 n=2k1,则 n 1 = 0 n_1 = 0 n1=0 n 0 = k n_0=k n0=k n 2 = k − 1 n_2=k-1 n2=k1
  • n = 2 k n=2k n=2k,则 n 1 = 1 n_1 = 1 n1=1 n 0 = k n_0=k n0=k n 2 = k − 1 n_2=k-1 n2=k1

几个特殊的二叉树

  1. 满二叉树:一棵高度为h,且含有 2 h − 1 2^h-1 2h1个结点的二叉树。其特点为:
  • 只有最后一层有叶子结点
  • 不存在度为1的结点
  • 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为[i/2]
  1. 完全二叉树:当且仅当其每个结点都与高度为h的满二叉树中编号为 1 ∼ n 1\sim n 1n的结点一一对应时,称为完全二叉树。其特点为:
  • 只有最后两层可能有叶子结点
  • 最多只有一个度为1的结点
  • 按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为[i/2]
  • i ≤ [ n / 2 ] i \leq [n/2] i[n/2]为分支结点, i > [ n / 2 ] i > [n/2] i>[n/2]为叶子结点
  1. 二叉排序树:一棵二叉树或者是空二叉树,具有如下性质的二叉树:
  • 左子树上所有结点的关键字均小于根节点的关键字
  • 右子树上所有结点的关键字均大于根节点的关键字
  • 左子树和右子树又各是一棵二叉排序树
  1. 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1

二叉树的定义

链式存储

struct TreeNode {
    
    
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(NULL), right(NULL) {
    
    }
};

二叉树的前中后序遍历

前序遍历

  • 伪代码
typedef struct BiTNode {
    
    
	ElemType data;
	struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;

//先序遍历
void PreOrder(BiTree T) {
    
    
	if (T != NULL) {
    
    
		visit(T);
		PreOrder(T->lchild);
		PreOrder(T->rchild);
	}
}

中序遍历

  • 伪代码
typedef struct BiTNode {
    
    
	ElemType data;
	struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;

//中序遍历
void InOrder(BiTree T) {
    
    
	if (T != NULL) {
    
    
		InOrder(T->lchild);
		visit(T);
		InOrder(T->rchild);
	}
}

后序遍历

  • 伪代码
typedef struct BiTNode {
    
    
	ElemType data;
	struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;

//先序遍历
void PostOrder(BiTree T) {
    
    
	if (T != NULL) {
    
    
		PostOrder(T->lchild);
		PostOrder(T->rchild);
		visit(T);
	}
}

二叉树的层次遍历

  1. 初始化一个辅助队列
  2. 根结点入队
  3. 若队列非空,队头结点出队,访问该结点,并将该结点的左右孩子依次入队
  4. 重复3直至队列为空
//二叉树结点,链式存储
typedef struct BiTNode {
    
    
	char data;
	struct BiTNode* lchild, * rchild;
}BiTNode, *BiTree;

//链式队列结点
typedef struct LinkNode {
    
    
	BiTNode* data;   //存指针
	struct LinkNode* next;
}LinkNode;

typedef struct {
    
    
	LinkNode* front, rear; // 队头队尾
}LinkQueue;

// 层序遍历
void LevelOrder(BiTree T) {
    
    
	LinkQueue Q;
	InitQueue(Q);    // 初始化辅助队列
	BiTree p;
	EnQueue(Q, T);   // 将根节点入队
	while (!IsEmpty(Q)) {
    
      // 队列不空则循环
		DeQueue(Q, p);     // 队头结点出队
		visit(p);          // 访问出队结点
		if (p->lchild != NULL) {
    
    
			EnQueue(Q, p->lchild);  // 左孩子入队
		}
		if (p->rchild != NULL) {
    
    
			EnQueue(Q, p->rchild);  // 右孩子入队
		}
	}
}

由遍历序列构造二叉树

若只给出一棵二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一棵二叉树

由二叉树的遍历序列构造二叉树:

  1. 前序+中序遍历序列
    前序遍历:根节点,左子树的前序遍历序列,右子树的前序遍历序列
    中序遍历:左子树的中序遍历序列,根节点,右子树的前序遍历序列
  2. 后序+中序遍历序列
    后序遍历:左子树的前序遍历序列,右子树的前序遍历序列,根节点
    中序遍历:左子树的中序遍历序列,根节点,右子树的前序遍历序列
  3. 层序+中序遍历序列
    层序遍历:根节点,左子树的根节点,右子树的根节点
    中序遍历:左子树的中序遍历序列,根节点,右子树的前序遍历序列

技巧:先找整棵树的根节点,再根据中序序列划分左右子树,再找左右子树的根节点

树的存储结构

  1. 双亲表示法(顺序存储):每个节点中保存指向双亲的指针
#define MAX_TREE_SIZE 100
typedef struct {
    
           //树的结点定义
	int data;          //数据元素
	int parent;        //双亲位置域
}PTNode;
typedef struct {
    
    
	PTNode nodes[MAX_TREE_SIZE]; //双亲表示
	int n;                       //结点数
}PTree;  

新增数据元素:无需按逻辑上的次序存储
删除数据元素:直接删除指定结点/将尾部元素移动至待删除结点
优点:查指定结点的双亲很方便,但查找指定指定结点的孩子只能从头遍历

  1. 孩子表示法(顺序+链式存储):顺序存储各个结点,每个结点中保存孩子链表头指针
struct CTNode {
    
    
	int child;   //孩子结点在数组中的位置
	struct CTNode* next; //下一个孩子
};
typedef struct {
    
    
	int data;
	struct CTNode* firstChild;  //第一个孩子
}CTBox;
typedef struct {
    
    
	CTBox nodes[MAX_TREE_SIZE];
	int n, r; //结点数和根的位置
}CTree;
  1. 孩子兄弟表示法(链式存储)
typedef struct CSNode {
    
    
	int data;             //数据域
	struct CSNode* firstchild, *nextsibling; //第一个孩子和右兄弟指针
}CSNode, *CSTree;

优点:可以用二叉树的操作来处理树。用孩子兄弟表示法存储的树在物理上呈现出二叉树的样子。(左孩子,右兄弟)

森林和二叉树的转化:每棵树先转换为二叉树,再将每棵二叉树根节点串联起来。(本质:用二叉链表存储森林)

树和森林的遍历

树的遍历:先根遍历、后根遍历、层序遍历

树是一种递归定义的数据结构

  1. 先根遍历:树的先根遍历序列与这棵树相应二叉树的先序序列相同
void PreOrder(TreeNode* R) {
    
    
	if (R != NULL) {
    
    
		visit(R); //访问根节点
		while (R还有下一个子树T)
			PreOrder(T); //先根遍历下一棵子树
	}
}
  1. 后根遍历:树的后根遍历序列与这棵树相应二叉树的中序序列相同
void PostOrder(TreeNode* R) {
    
    
	if (R != NULL) {
    
    
		while (R还有下一个子树T)
			PostOrder(T); //后根遍历下一棵子树
		visit(R);  //访问根结点
	}
}
  1. 层次遍历:队列实现

森林的遍历:先序遍历、中序遍历

  1. 先序遍历:效果等同于依次对各个树进行先根遍历

若森林非空,则按如下规则遍历:访问森林中第一棵树的根节点。先序遍历第一棵树中根节点的子树森林。先序遍历除去第一棵树之后剩余的树构成的森林。

  1. 中序遍历:效果等同于依次对各个树进行先根遍历

若森林非空,则按如下规则遍历:中序遍历第一棵树中根节点的子树森林。访问第一棵树的根节点。中序遍历除去第一棵树之后剩余的树构成的森林。

森林 二叉树
先根遍历 先序遍历 先序遍历
后根遍历 中序遍历 中序遍历

二叉排序树

左子树结点值 < 根节点值 < 右子树结点值

typedef struct BSTNode{
    
    
	int key;
	struct BSTNode* lchild, *rchild;
}BSTNode, *BSTree;

二叉排序树的查找:若树非空,目标值与根节点的值比较,若相等,则查找成功;若小于根节点,则再左子树上查找,否则在右子树上查找。查找成功,返回结点指针;查找失败则返回NULL。

//在二叉排序树中查找值为key的结点
BSTNode* BST_Search(BSTree T, int key) {
    
    
	while (T != NULL && key != T->key) {
    
     // 若树空或等于根结点值,则结束循环
		if (key < T->key) T = T->lchild; //小于,则在左子树上查找
		else T = T->rchild;              //大于,则在右子树上查找
	}
	return T; 
}

//在二叉排序树中查找值为key的结点(递归实现)
BSTNode* BSTSearch(BSTree T, int key) {
    
    
	if (T == NULL) return NULL; //查找失败
	if (key == T->key) return T; //查找成功
	else if (key < T->key) return BSTSearch(T->lchild, key); //在左子树中找
	else return BSTSearch(T->rchild, key);                   //在右子树中找
}

二叉排序树的插入:若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根节点值,则插入到左子树,若关键字k大于根节点值,则插入到右子树。

// 在二叉排序树中插入关键字为k的新节点(递归实现)
int BST_Insert(BSTree& T, int k) {
    
    
	if (T == NULL) {
    
    
		T = new BSTNode;
		T->key = k;
		T->lchild = T->rchild = NULL;
		return 1;
	}
	else if (k == T->key) return 0;
	else if (k < T->key) return BST_Insert(T->lchild, k);
	else return BST_Insert(T->rchild, k);
}

构造二叉排序树

// 按照 str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree& T, int str[], int n) {
    
    
	T = NULL; 
	int i = 0;
	while (i < n) {
    
    
		BST_Insert(T, str[i]);
		i++;
	}
}

不同的关键字序列可能得到相同的二叉排序树;也可能得到不同的二叉排序树。

二叉排序树的删除:1.若被删除得结点是叶子结点,则直接删除,不会破坏二叉排序树的性质。2.若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。3.若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。

查找长度——在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度。

查找成功的平均查找长度ASL(Average Search Length)

最好情况:n个结点的二叉树最小高度为 [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1,平均查找长度= O ( l o g 2 n ) O(log_2n) O(log2n)

最坏情况:每个结点只有一个分支,树高h=结点数n。平均查找长度=O(n)

平衡二叉树

AVL树 —— 树上任一结点的左子树和右子树的高度之差不超过1

结点的平衡因子 = 左子树高 - 右子树高

//平衡二叉树结点
typedef struct AVLNode {
    
    
	int key; //数据域
	int balance; //平衡因子
	struct AVLNode *lchild, *rchild;
}AVLNode, *AVLTree;

在平衡二叉树中插入新结点后,如何保持平衡? → \to 调整最小不平衡子树

LL 在A的左孩子的左子树中插入导致不平衡
RR 在A的右孩子的右子树中插入导致不平衡
LR 在A的左孩子的右子树中插入导致不平衡
RL 在A的右孩子的左子树中插入导致不平衡

为什么要假定所有子树的高度都是H

目标:1.恢复平衡 2.保持二叉排序树特性
在这里插入图片描述

LL平衡旋转(右单旋转):由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根节点,将A结点向右下旋转成为B的右子树的根节点,而B的原右子树则作为A结点的左子树。

在这里插入图片描述
RR平衡旋转(左单旋转):由于在结点A的右孩子®的右子树®上插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A成为根节点,将A结点向左下旋转成为B的右子树的根节点,而B的原右子树则作为A结点的左子树。

代码思路:

在这里插入图片描述
至于LR先左旋C,再右旋C

在这里插入图片描述
LR平衡旋转(先左后右双旋转):由于在结点A的左孩子(L)的右子树®上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根节点C向左上旋转提升到B结点的位置,然后再C结点向右上旋转提升到A结点的位置。

RL:先右旋C,后左旋C

在这里插入图片描述
若树高h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h)

假设以 n h n_h nh表示深度为h的平衡树中含有的最少结点数。

则有 n 0 = 0 n_0 = 0 n0=0 n 1 = 1 n_1 = 1 n1=1 n 2 = 2 n_2 = 2 n2=2,并且有 n h = n h − 1 + n h − 2 + 1 n_h = n_{h-1} + n_{h-2} + 1 nh=nh1+nh2+1

含n个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O(log_2n) O(log2n)

猜你喜欢

转载自blog.csdn.net/Star_ID/article/details/126370008