二叉树基本操作

参考:
https://blog.csdn.net/hellowd123/article/details/99692395
https://blog.csdn.net/qq_35644234/article/details/53013738

第五章 树

树——“分层次组织在管理上具有更高的效率!”

1. 引:查找

(没啥用,直接跳过)

根据某个给定 关键字K,从 集合R中找出关键字与 K相同的记录

静态查找:集合中记录是固定的

  • 没有插入和删除操作,只有查找

动态查找:集合中记录是动态变化的

  • 除查找,还可能发生插入和删除

2. 树的定义和术语

2.1 树的定义

树:由n(n>=1)个有限结点组成一个具有层次关系的集合。

当n=0时,称为空树。

对于任一颗非空树(n>0),具备以下性质:

  • 树中有一个称为“根”的特殊结点,用r表示
  • 其余结点可分为m(m>0)个互不相交的有限集T1, T2, … , Tm,其中每个集合本身又是一棵树,称为原来树的“子树”
  • 子树是不相交的
  • 除了根结点外,每个结点有且仅有一个父结点;
  • 一颗N个结点的树有N-1条边

2.2 树的一些基本术语

若一个结点有子树,那么该结点称为子树根的“双亲”,子树的根称为该结点的“孩子”。有相同双亲的结点互为“兄弟”。一个结点的所有子树上的任何结点都是该结点的后裔。从根结点到某个结点的路径上的所有结点都是该结点的祖先

  • 度:结点拥有的子树的数目
  • 叶子或终端结点:度为0的结点(没有子树的结点)
  • 分支结点:度不为0的结点
  • 树的度:树中结点的最大的度
  • 层次:根的层次为1,根的孩子为2,以此类推
  • 树的深度:树中结点的最大层次
  • 森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。

3. 二叉树

3.1 二叉树定义

二叉树是一种每个结点至多只有两个子树(即二叉树的每个结点的度不大于2),并且二叉树的子树有左右之分,其次序不能任意颠倒。

3.2 二叉树分类

  • 完全二叉树
    • 若设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布。
    • 堆排序使用的数据结构就是完全二叉树。
  • 满二叉树
    • 国际标准定义是除了叶结点外每一个结点都有左右子结点的二叉树
    • 国内的定义是:除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。
  • 扩充二叉树
    • 对已有二叉树的扩充,扩充后的二叉树的节点都变为度数为2的分支节点。也就是说,如果原节点的度数为2,则不变,度数为1,则增加一个分支,度数为0的叶子节点则增加两个分支。
  • 平衡二叉树
    • 是一棵空树或者它的任意节点的左右两个子树的高度差的绝对值不超过1

3.3 二叉树重要性质

  • 二叉树第i层上的结点数目最多为2^(i-1)
  • 深度为k的二叉树至多有2^k-1个结点
  • 包含n个结点的二叉树的高度至少为 logn+1
  • 在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1

3.4 遍历方式

根据第几次经过一个结点,可分为先、中、后序遍历。

  • 先序遍历DRL :root -> left -> right

在这里插入图片描述

  • 中序遍历LDR :left -> root -> right

    在这里插入图片描述

  • 后序遍历LRD :left ->right -> root

    在这里插入图片描述

  • 层次遍历:从上到下、从左到右

3.5 存储结构

3.5.1 顺序存储结构

在一个连续存储单元里,从根结点开始,像对完全二叉树编号的顺序一样,把二叉树的内容存储在一个一维数组中,一般顺序存储结构是拿来存储完全二叉树的,但是也可以拿来存储一般的二叉树,只是要按照完全二叉树的规则来编号,如果没有的就存 # 。

顺序存储结构可能过于浪费空间!

3.5.2 链式存储结构

一个结点至少包括 一个数据域和两个指针域,一个指向左孩子,另一个指向右孩子,我们称这种链表为二叉链表,另外还可以添加一个指针域,指向其父亲结点,我们称为三叉链表。

3.6 遍历实现

3.6.1 先、中、后序遍历

下面主要以链式存储结构为主:

  • 递归实现:

  • 非递归实现:

基本思路:使用堆栈(参考浙大数据结构视频 P35 中序非递归遍历)

中序遍历非递归遍历算法

  • 遇到一个结点,就把它压栈,并去遍历它的左子树
  • 当左子树遍历结束后,从栈顶弹出这个结点并访问它
  • 然后按其右指针再去中序遍历该结点的右子树
3.6.2 层次遍历

二叉树遍历的核心问题:二维结构的线性化

  • 访问左儿子后,右儿子结点怎么办?
    • 需要一个存储结构保存暂时不访问的结点
    • 存储结构:堆栈、队列

队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子入队

具体过程:

  • 从队列中取出一个元素
  • 访问该元素所指结点
  • 若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队
3.6.3 具体实现
#include <iostream>
#include <stack>
#include <queue>

using namespace std;

typedef char ElemType;
struct BiTreeNode {
	ElemType data;
	BiTreeNode* left;
	BiTreeNode* right;
};

// 按照先序遍历的方式,建立二叉树
void createBiTree(BiTreeNode*& T)
{
	char str;
	cin >> str;
	if (str == '#') {
		T = nullptr;
	}
	else {
		T = new BiTreeNode;
		if (!T) {
			exit(-1);
		}
		T->data = str;
		createBiTree(T->left);	// 构造左子树
		createBiTree(T->right);	// 构造右子树
	}
}


// 递归实现 先序遍历
void PreOrderTraverse(BiTreeNode* T)
{
	if (!T) {
		return;
	}
	if (T->data != '#')
		cout << T->data << " ";	// 访问根结点
	PreOrderTraverse(T->left);	// 访问左子树
	PreOrderTraverse(T->right);	// 访问右子树
}

// 非递归实现 先序遍历
void PreOrderTraverse(BiTreeNode* T)
{
	stack<BiTreeNode*> s;
	BiTreeNode* p = T;
	// 结点p不为空或栈不为空
	while (p || !s.empty()) {
		if (p) {
			s.push(p);	// 根结点入栈(实则为指向根结点的指针入栈)
			if (p->data != '#')
				cout << p->data << " ";
			p = p->left;	// 先遍历左子树
		}
		else {
			p = s.top();	// p指向根结点
			s.pop();		// 根结点出栈
			p = p->right;	// 遍历右子树
		}
	}
}


// 递归实现 中序遍历
void inOrderTraverse(BiTreeNode* T)
{
	if (!T) {
		return;
	}
	inOrderTraverse(T->left);	// 访问左子树
	if (T->data != '#')
		cout << T->data << " ";	// 访问根结点
	inOrderTraverse(T->right);	// 访问右子树
}

// 非递归实现 中序遍历
void inOrderTraverse(BiTreeNode* T)
{
	stack<BiTreeNode*> s;
	BiTreeNode* p = T;
	// 结点p不为空或栈不为空
	while (p || !s.empty()) {
		if (p) {
			s.push(p);		// 根结点入栈
			p = p->left;	// 遍历左子树
		}
		else {
			p = s.top();
			s.pop();		// 根结点出栈
			if (p->data != '#')
				cout << p->data << " ";
			p = p->right;	// 遍历右子树
		}
	}
}


// 递归实现 后序遍历
void postOrderTraverse(BiTreeNode* T)
{
	if (!T) {
		return;
	}
	postOrderTraverse(T->left);	// 访问左子树
	postOrderTraverse(T->right);	// 访问右子树
	if (T->data != '#')
		cout << T->data << " ";	// 访问根结点
}

// 非递归实现 后序遍历
/*
* 思路:
* 后序遍历与先、中序遍历还不太一样,必须保证根结点的左孩子和右孩子访问之后才能访问
* 因此对于任一结点p,先将其入栈。
* 
* 如果P不存在左孩子和右孩子、或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了
* 则可以直接访问该结点;
* 否则,将P的右孩子和左孩子依次入栈。
*/
void postOrderTraverse(BiTreeNode* T)
{
	stack<BiTreeNode*> s;
	BiTreeNode* pre = nullptr;	// 记录被访问的前一个结点
	BiTreeNode* cur = nullptr;	// 记录栈顶的结点
	s.push(T);					// 根结点入栈

	// 结点p不为空或栈不为空
	while (!s.empty()) {
		cur = s.top();
		if ((!cur->left && !cur->right) ||
			(pre && (pre == cur->left || pre == cur->right))) {
			// cur满足了不存在左孩子和右孩子,或者存在左孩子和右孩子但已经被访问
			cout << cur->data << " ";	
			s.pop();
			pre = cur;	// 更新被访问的前一个结点
		}
		else {
			if (cur->right)		// 一定是右子树先入栈,这样才能在左子树之后访问
				s.push(cur->right);
			if (cur->left)
				s.push(cur->left);
		}
	}
}

// 层次遍历
void levelOrderBiTree(BiTreeNode* T)
{
	queue<BiTreeNode*> q;
	BiTreeNode* cur = nullptr;
	q.push(T);			// 根结点进入队列
	while (!q.empty()) {
		cur = q.front();
		cout << cur->data << " ";
		q.pop();		// 出队当前结点
		// 有孩子时将其入队,注意先左后右
		if (cur->left) {
			q.push(cur->left);
		}
		if (cur->right) {
			q.push(cur->right);
		}
	}
}

// 复制二叉树
bool copyBiTree(BiTreeNode* T, BiTreeNode*& newT)
{
	if (!T) {
		newT = nullptr;
		return false;
	}
	else {
		newT = new BiTreeNode;
		newT->data = T->data;
		copyBiTree(T->left, newT->left);
		copyBiTree(T->right, newT->right);
	}
}

// 计算二叉树的深度
int depthBiTree(BiTreeNode* T)
{
	if (!T) {
		return 0;
	}
	else {
		return max(depthBiTree(T->left), depthBiTree(T->right)) + 1;
	}
}

// 计算二叉树结点的个数
int getNodeNum(BiTreeNode* T)
{
	if (!T) {
		return 0;
	}
	else {
		return getNodeNum(T->left) + getNodeNum(T->right) + 1;
	}
}

// 计算二叉树叶子结点总个数
int getLeafNum(BiTreeNode* T)
{
	if (!T) {
		return 0;
	}
	else if (!T->left && !T->right) {
		return 1;
	}
	else {
		return getLeafNum(T->left) + getLeafNum(T->right);
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ClimberCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值