数据结构:树(基础概念操作图文解释)

树的定义

树是n(n>0)个结点的有限集T,有且仅有一个特定的结点,称为树的根(root)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,……Tm,其中每一个集合本身又是一棵树,称为根的子树(subtree)。

例:在这里插入图片描述

基本术语

结点(node)——表示树中的元素,包括数据项及若干指向其子树的分支

结点的度(degree)——结点拥有的子树数

叶子(leaf)——度为0的结点

孩子(child)——结点子树的根称为该结点的孩子

双亲(parents)——孩子结点的上层结点叫该结点的双亲

兄弟(sibling)——同一双亲的孩子

树的度——一棵树中最大的结点度数

结点的层次(level)——从根结点算起,根为第一层,它的孩子为第二层……

深度(depth)——树中结点的最大层次数

森林(forest)——m(m>0)棵互不相交的树的集合

例:
在这里插入图片描述

二叉树及相关定义

二叉树定义

二叉树是n(n>0)个结点的有限集,它或为空树(n=0),或由一个根结点和两棵分别称为左子树和右子树的互不相交的二叉树构成。

特点:

  1. 每个结点至多有二棵子树(即不存在度大于2的结点)
  2. 二叉树的子树有左、右之分,且其次序不能任意颠倒

满二叉树定义

一棵深度为k,且有2k-1个结点。

特点:
每一层上的结点数都是最大结点数。
在这里插入图片描述

完全二叉树定义:

深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应。

特点:
叶子结点只可能在层次最大的两层上出现,对任一结点,若其右分支下子孙的最大层次为L,则其左分支下子孙的最大层次必为L 或L+1。
在这里插入图片描述

二叉树性质

  1. 在二叉树的第 i 层上至多有 2^(i-1) 个结点。
  2. 深度为 k 的二叉树至多有 2^k-1 个结点(k>1)。
  3. 对任何一棵二叉树T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则 n0=n2+1。
  4. 具有 n 个结点的完全二叉树的深度为 ㏒₂n 向下取整再加 1。
  5. 如果一个有 n 个结点的完全二叉树从上到下,从左到右依次编号,则对任意结点有
    • 若 i=1,则结点i是二叉树的根,无双亲,如果i>1,则其双亲是i/2向下取整。
    • 如果 2i>n,则结点i无左孩子,否则其左孩子是结点2i。
    • 如果 2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1.

二叉树的存储方式

顺序存储结构

实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素。

特点:
结点间关系蕴含在其存储位置中;
浪费空间,适于存满二叉树和完全二叉树;
例:
在这里插入图片描述

链式存储结构

二叉链表

结点结构

typedef    struct  node
{    datatype    data;
      struct  node  *lchild,  *rchild;
}JD;

在这里插入图片描述

三叉链表
结点结构:
typedef   struct  node
{   datatype   data;
      struct   node  *lchild,  *rchild,  *parent;
}JD;

在这里插入图片描述
三叉链表存储结构易于找到结点双亲,但是一般建立树结构都使用二叉链表结构。

缺点:
在n个结点的二叉链表中,有n+1个空指针域,造成存储空间浪费。

二叉树的遍历

在这里插入图片描述

写在前面的话:在以下树的遍历实现中你会看到递归和非递归两个版本,但在我看来递归和非递归完成遍历在代码上或许有很大的不同,但是原理上是很相似的,甚至可以说是一致的,递归本质上也是用到栈存储结构,所以理解好非递归实现代码会更好的理解递归方法。加油哦!

先序遍历

先访问根结点,然后分别先序遍历左子树、右子树;
先序遍历序列为:A B D C

  • 递归实现
void preoder(node* root)//先序递归 
{
	if(root!=NULL)
	{
		cout<<root->data;
		preoder(root->lc);
		preoder(root->rc);
	}
}
  • 非递归实现
    c 语言版
void preoder(node* root)//先序非递归 
{
	if(root!=NULL)
	{
		stack<node*> tem;//声明一个栈叫tem
		tem.push(root);//根结点入栈 
		
		while(!tem.empty())
		{
			root=tem.top();//取出栈顶结点 
			tem.pop();//更新栈顶元素 
			cout<<root->data;
			
			//栈先入后出,所以先将右孩子入栈 
			if(root->rc!=NULL)//右孩子不为空入栈 
				tem.push(root->rc);
			if(root->lc!=NULL)//左孩子不为空入栈
				tem.push(root->lc);
		}
	}
}

JS 版
这里把先序遍历结果放在 res 数组里。

function preoder(root) {//先序非递归 
  if (!root) {
    return [];
  }
  const stack = [], res = [];
  stack.push(root);
  while (stack.length > 0) {
    const temp = stack.pop();
    res.push(temp.val)
    if (temp.right) {
      stack.push(temp.right);
    }
    if (temp.left) {
      stack.push(temp.left);
    }
  }
  return res;
}

代码解析(非递归先序遍历):

以上图为例

注意:因为栈有先进后出的特性(把栈想象成一个桶),所以先序遍历时节点的右孩子要先入栈,这样在出站的时候左孩子会先出栈,以此实现左孩子、根、右孩子的输出顺序,完成先序遍历。

第一步:A入栈,栈不为空, 进入循环,然后指向A,然后A出栈,若A左孩子不为空则入栈,若A右孩子不为空则入栈,A左右孩子均不为空,所以此时栈中自上而下存着C B;

第二步:指向A的左孩子B,B出栈,B左孩子为空,右孩子不为空,所以此时栈中自上而下存着C D;

第三步:指向D,D出栈,同理判断D的左右孩子是否不为空,不为空则入栈,此时D无孩子,所以此时栈中存着C;

第四步:指向C,C出栈,此时C无孩子,没有入栈操作,所以此时栈为空结束循环,先序遍历结束

中序遍历

先中序遍历左子树,然后访问根结点,最后中序遍历右子树;
中序遍历序列为:B D A C

由于中序和后序遍历都利用了栈存储结构,它们的实现思路和先序遍历极为相似,只是在入栈和出栈顺序上有一点差异,这里就不再做代码分析。各位读者朋友们可以好好思考一下哟!!!

  • 递归实现
void inoder(node* root)//中序 
{
	if(root!=NULL)
	{
		inoder(root->lc);
		cout<<root->data;
		inoder(root->rc);
	}
}
  • 非递归实现
    c 语言版
void inoder(node* root)//中序遍历非递归 
{
	if(root!=NULL)
	{
		stack<node*> tem;
		while(root||!tem.empty())//初始root不为空,栈为空 
		{
			if(root)
			{
				tem.push(root);
				root=root->lc;
			}
			else
			{
				root=tem.top();//取出栈顶结点
				cout<<root->data;
				tem.pop();//删除(或更新)栈顶结点
				root=root->rc;         
			}
		}
	}
}

JS 版
这里把遍历结果存在 res 数组中。

function inoder(root) {//中序遍历非递归 
  const stack = [], res = [];
  while (root || stack.length > 0) {
    if (root) {
      stack.push(root);
      root = root.left;
    } else {
      root = stack.pop();
      res.push(root.val);
      root = root.right;
    }

  }
  return res;
}

后序遍历

先后序遍历左、右子树,然后访问根结点;
后续遍历序列为:D B C A
先序遍历序列为:A B D C
逆先序遍历序列:A C B D
由这三个遍历序列可以看出后序遍历序列可由逆先序遍历序列 (先序遍历顺序是根、左孩子、右孩子,逆先序遍历顺序是根、右孩子、左孩子) 从后往前输出得到。所以这里需要两个栈结构。

  • 递归实现
void hoder(node* root)//后序 
{
		if(root!=NULL)
	{
		hoder(root->lc);
		hoder(root->rc);
		cout<<root->data;
	}
}
  • 非递归实现
    c 语言版
void hoder(node* root)//后序遍历非递归 
{
	if(root!=NULL)
	{
		stack<node*> tem,h;
		tem.push(root);//根结点入栈 
		
		while(!tem.empty())
		{
			root=tem.top();//取出栈顶结点 
			tem.pop();//删除(或更新)栈顶结点 
			h.push(root);
			
			//因为要逆先序,栈先入后出,所以先将做孩子入栈 
			if(root->lc!=NULL)//左孩子不为空入栈 
				tem.push(root->lc);
			if(root->rc!=NULL)//右孩子不为空入栈
				tem.push(root->rc);
		}
		
		while(!h.empty())//输出栈 
		{
			node*p;
			p=h.top();
			h.pop();
			cout<<p->data;
		}
	}
}

JS 版

function hoder(root) {//后序遍历非递归 
  if (!root) {
    return [];
  }
  const stack = [], tem = [], res = [];
  stack.push(root);
  while (stack.length > 0) {
    const temp = stack.pop();
    tem.push(temp.val)
    if (temp.left) {
      stack.push(temp.left);
    }
    if (temp.right) {
      stack.push(temp.right);
    }
  }
  while (tem.length > 0) {
    res.push(tem.pop());
  }
  return res;
}

层次遍历

从上到下、从左到右访问各结点。
层次遍历序列为:A B C D

void leorder(node* root)//层次遍历 
{
	node* p;
	queue<node*> q;
	q.push(root);//根节点入队 
	
	while(!q.empty())
	{
		p=q.front();//取队列的头 
		q.pop();//更新队列头 
		cout<<p->data;
		if(p->lc!=NULL)//左子树不为空则入队 
			q.push(p->lc);
		if(p->rc!=NULL)
			q.push(p->rc);//右子树不为空则入队 
	}
} 

JS 版

function leorder(root) {//层次遍历 
  if (!root) {
    return [];
  }
  const queue = [], res = [];
  queue.push(root);
  while (queue.length > 0) {
    root = queue.shift();
    res.push(root.val);
    if (root.left != null)//左子树不为空则入队 
      queue.push(root.left);
    if (root.right != null)
      queue.push(root.right);//右子树不为空则入队
  }
  return res;
}

代码解析:
以上图为例:
因为队列是先进先出,所以很符合层次遍历输出的特点。

第一步:A入队,队不为空, 进入循环,然后指向A,然后A出队,若A左孩子不为空则入栈,若A右孩子不为空则入栈,因为A左右孩子均不为空,所以此时队列中存着B C,B为队头,C为队尾;

第二步:指向A的左孩子B,B出队,B左孩子为空,右孩子不为空,所以此时队列中存着C D,C为队头,D为队尾;

第三步:指向C,C出队,此时C无孩子,所以此时栈中存着D;

第四步:指向D,D出队,此时D无孩子,没有入队的节点,所以此时队列为空结束循环,层次遍历结束

遍历小结: 先序、中序和后序遍历都以深度优先搜索为核心实现,而层次遍历是以广度优先搜索为核心,用到了队列存储结构,而且先序、中序和后序遍历非递归方法都用到了栈存储结构,那么为什么不用队列呢?个人观点是因为深度搜索有一个回溯动作,而栈能够很好地实现这一动作,所以才会选择栈存储结构。

二叉树其它基础操作

求树高

void treehight(node* root,int num)//求树高 
{
	if(root!=NULL)
	{
		treehight(root->lc,num+1);
		treehight(root->rc,num+1);
	}
	//
	if(MAX<num)//若当前层数大于记录数则更新记录数 
		MAX=num;
}

到达递归出口(即执行一次第二个if语句)说明经过一次叶子结点,此时比较这一枝高度与记录的最大高度,当前高度大于记录高度则交换,程序结束时就得到了最大高度即树高。

求叶子数

void leafnum(node* root)//计算叶子节点数 
{
	if(root!=NULL)
	{
		leafnum(root->lc);
		leafnum(root->rc);
	}
	if(root!=NULL&&(root->lc==NULL&&root->rc==NULL))
		sum++;
}

每到一次递归出口,说明经过一次叶子结点,但并不是每一次递归出口都是叶子结点,(已上图为例,当递归到D结点时,会继续将D的左孩子当做参数执行leafnum函数,此时root为空,递归结束,回溯到D结点)所以需要判断一下是否为叶子结点。

完整代码展示

#include <iostream>
#include <string>
#include <malloc.h>
#include <queue> 
#include <stack> 

using namespace std;
int i=0,MAX=0,sum=0;
string s;

typedef struct Tree
{
	char data;
	struct Tree* lc;
	struct Tree* rc;
}node;

node* pcreat_tree(string s)//前序建树 
{
	node* bt;
	if(s[i]=='?')
	{
		bt=NULL;
		i++;
	}
	else
	{
		bt=(node*)malloc(sizeof(node));
		bt->data=s[i++];
		bt->lc=pcreat_tree(s);//增加左子树 
		bt->rc=pcreat_tree(s);
	}
	return bt;
}

/*void preoder(node* root)//前序递归 
{
	if(root!=NULL)
	{
		cout<<root->data;
		preoder(root->lc);
		preoder(root->rc);
	}
}*/

void preoder(node* root)//前序非递归 
{
	if(root!=NULL)
	{
		stack<node*> tem;
		tem.push(root);//根结点入栈 
		while(!tem.empty())
		{
			root=tem.top();//取出栈顶结点 
			tem.pop();//更新栈顶结点 
			cout<<root->data;
			//栈先入后出,所以先将右孩子入栈 
			if(root->rc!=NULL)//右孩子不为空入栈 
				tem.push(root->rc);
			if(root->lc!=NULL)//左孩子不为空入栈
				tem.push(root->lc);
		}
	}
}
/*void inoder(node* root)//中序 
{
	if(root!=NULL)
	{
		inoder(root->lc);
		cout<<root->data;
		inoder(root->rc);
	}
}*/

void inoder(node* root)//中序遍历非递归 
{
	if(root!=NULL)
	{
		stack<node*> tem;
		while(root||!tem.empty())//初始root不为空,栈为空 
		{
			if(root)
			{
				tem.push(root);
				root=root->lc;
			}
			else
			{
				root=tem.top();
				cout<<root->data;
				tem.pop();
				root=root->rc;         
			}
		}
	}
}

/*void hoder(node* root)//后序 
{
		if(root!=NULL)
	{
		hoder(root->lc);
		hoder(root->rc);
		cout<<root->data;
	}
}*/

void hoder(node* root)//后序遍历非递归 
{
	if(root!=NULL)
	{
		stack<node*> tem,h;
		tem.push(root);//根结点入栈 
		while(!tem.empty())
		{
			root=tem.top();//取出栈顶结点 
			tem.pop();//更新栈顶结点 
			h.push(root);
			//栈先入后出,所以先将右孩子入栈 
			if(root->lc!=NULL)//右孩子不为空入栈 
				tem.push(root->lc);
			if(root->rc!=NULL)//左孩子不为空入栈
				tem.push(root->rc);
		}
		while(!h.empty())//输出栈 
		{
			node*p;
			p=h.top();
			h.pop();
			cout<<p->data;
		}
	}
}

void leoder(node* root)//层次遍历 
{
	node* p;
	queue<node*> q;
	q.push(root);//根节点入队 
	while(!q.empty())
	{
		p=q.front();//取队列的头 
		q.pop();//更新队列头 
		cout<<p->data;
		if(p->lc!=NULL)//左子树不为空则入队 
			q.push(p->lc);
		if(p->rc!=NULL)
			q.push(p->rc);//右子树不为空则入队 
	}
} 

void treehight(node* root,int num)//求树高 
{
	if(root!=NULL)
	{
		treehight(root->lc,num+1);
		treehight(root->rc,num+1);
	}
	if(MAX<num)//若当前层数大于记录数则更新记录数 
		MAX=num;
}

void leafnum(node* root)//计算叶子节点数 
{
	if(root!=NULL)
	{
		leafnum(root->lc);
		leafnum(root->rc);
	}
	if(root!=NULL&&(root->lc==NULL&&root->rc==NULL))
		sum++;
}

int main()
{
	node* b;
	int i=0;
	cin>>s;
	b=pcreat_tree(s);//前序递归建树 
	preoder(b);//前序遍历 
	cout<<endl;
	
	inoder(b);//中序遍历 
	cout<<endl;
	
	hoder(b);//后序遍历 
	cout<<endl;
	 
	leoder(b);//层次遍历 
	cout<<endl;
	
	treehight(b,0);//求树高 
	cout<<MAX<<endl;
	
	leafnum(b);//求树的叶子数 
	cout<<sum<<endl;
 	return 0;
}

小结

二叉树还是比较难的,主要是它的各种专业术语以及性质比较零散,涉及到遍历这一块就需要多琢磨一下了,要掌握栈、递归、队列的概念才能更好的掌握树的遍历。

我是孤城浪人,一名正在前端路上摸爬滚打的菜鸟,欢迎你的关注。

  • 6
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
二叉排序是一种二叉树数据结构,也称为二叉查找或二叉搜索。它要么是一棵空,要么满足以下条件:对于任意节点,其左子中的所有节点的值小于它的值,而右子中的所有节点的值大于它的值。这个特点使得在二叉排序中进行查找操作具有高效性能,接近于折半查找。如果二叉排序不平衡,它的深度可能达到n,查找效率将变为O(n),相当于顺序查找。因此,为了获得较好的查找性能,需要构造一棵平衡的二叉排序。 二叉排序的存储结构一般使用链式存储结构,每个节点包含一个数据元素以及两个指向左子和右子的指针。可以使用递归或迭代的方式实现创建、查找、插入、删除等操作,以及计算平均查找长度等指标。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [数据结构-二叉排序图文详细版)](https://blog.csdn.net/qq_55660421/article/details/122530387)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [数据结构实验-二叉排序算法](https://download.csdn.net/download/whales996/10746805)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值