数据结构——树

文章目录

一、为什么会有树这种数据结构
二、树知识点
三、二叉树知识点
四、二叉树的存储方式
五、二叉树的遍历方式
六、树、二叉树、森林之间的转化方式

一、为什么会有树这种数据结构

存储数据的方式:数组 链表 树

1.数组存储方式的分析
动态扩容:每次在底层都要创建新的数组,拷贝插入数据,效率低。
(1)优点 :通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找等方法提高检索速度。
(2)缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低 。
2.链式存储方式的分析
ArrayList按照一定的比例扩容,底层仍然是数组。
(1)优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)。
(2)缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
3.树存储方式的分析
能提高数据存储,读取的效率, 比如利用 二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

二、树知识点

1.定义
n个节点的有限集(n>=0)。(按逻辑分,树属于非线性数据结构。)
2.注意
线性表可以是空表,树可以是空树,图不可以是空图,图可以没有边,但是至少要有一个顶点。
3.树的表现形式
(1)树的结构定义是一个递归的定义,即在树的定义中又用到树的概念,它道出树的固有特性。
(2)树的其它表现形式
a.嵌套集合(一些集合的集体,任意两个或者不想交、或者包含另一个)的形式
b.是以广义的形式表示的
c嵌入凹入表示法
图示:
树的表现形式
4.基本术语
(1)节点:包含一个数据元素及若干指向其子树的分支。
(2)叶子(终端节点):度为0的节点。
(3)分支节点:度不为0的节点。
(4)度:节点拥有的子树的个数总数成为节点的度。
(5)树的度:树内各节点的度的最大值。
(6)孩子节点:该节点的子树的根。
(7)双亲节点(父节点):该节点。
(8)兄弟节点:拥有相同双亲的节点。
(9)堂兄节点:同一层上的节点。
(10)祖先节点:从根到该节点所经分支上的所有节点。
(11)子孙节点:以某节点为根的子树中任一节点都为该节点的子孙。
(12)深度:树中节点的最大层次。(从上往下)
(13)高度:值与深度一样。(从下往上)
(14)有序树:子树从左到右有次序。
(15)无序树:不考虑子树的顺序。
(16)森林:m棵互不相交的树的集合。

三、二叉树知识点

1.满足以下两个条件的树就是二叉树
(1)本身是有序树;
(2)树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2;
2.性质

(1)二叉树中,第 i 层最多有 2i-1 个结点。
(2) 如果二叉树的深度为 K,那么此二叉树最多有 2K-1 个结点。
(3)二叉树中,终端结点数(叶子结点数)为 n0,度为 2 的结点数为 n2,则 n0=n2+1。
(4)包含n个节点的二叉树高度至少为(log2n)+1。

性质 (3) 的计算方法为:
对于一个二叉树来说,除了度为 0 的叶子结点和度为 2 的结点,剩下的就是度为 1 的结点(设为 n1),那么总结点 n=n0+n1+n2。①
同时,对于每一个结点来说都是由其父结点分支表示的,假设树中分枝数为 B,那么总结点数 n=B+1。而分枝数是可以通过 n1 和 n2 表示的,即 B=n1+2n2。
所以,n 用另外一种方式表示为 n=n1+2
n2+1。②
由①②得到的 n 值组成一个方程组,就可以得出 n0=n2+1。

3.分类
3.1满二叉树
(1)定义
二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
(2)性质
a.满二叉树中第 i 层的节点数为 2n-1 个。
b.深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1。
c. 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。
d.具有 n 个节点的满二叉树的深度为 log2(n+1)。

3.2完全二叉树
(1)定义
二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。
(2)性质
a.n 个结点的完全二叉树的深度为(log2n)+1。
b.当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点)
c.如果 2i>n(总结点的个数),则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2i 。
如果 2i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2i+1 。
(3)总结规律:若一棵二叉树的节点总数为n,那么,
n为偶数时,叶子节点为n/2;
n为奇数时,叶子节点为(n+1)/2;

四、二叉树的存储方式

(一)二叉树:
1.顺序存储

import java.util.Arrays;
 
public class ArrayBinTree<T> {
 
	// 使用数组来记录所有节点
	private Object[] datas;
	private final int DEFAULT_DEEP = 8;
	// 保存该树的深度
	private int deep;
	// 数组的长度
	private int arraySize;
 
	// 以默认深度创建二叉树
	public ArrayBinTree() {
		this.deep = DEFAULT_DEEP;
		this.arraySize = (int) Math.pow(2, deep);
		datas = new Object[arraySize];
	}
 
	// 以指定深度创建
	public ArrayBinTree(int deep) {
		this.deep = deep;
		this.arraySize = (int) Math.pow(2, deep);
		datas = new Object[arraySize];
	}
 
	// 以指定深度,指定根节点创建
	public ArrayBinTree(int deep, T data) {
		this.deep = deep;
		this.arraySize = (int) Math.pow(2, deep);
		datas = new Object[arraySize];
		datas[0] = data;
	}
 
	/**
	 * 为指定节点添加子节点
	 * 
	 * @param index
	 *            需要添加子节点的父节点索引
	 * @param data
	 *            新的子节点的数据
	 * @param left
	 *            是否为左节点
	 */
	public void add(int index, T data, boolean left) {
		if (datas[index] == null) {
			throw new RuntimeException(index + "处节点为空,无法添加子节点!");
		}
 
		if (2 * index + 1 > arraySize || 2 * index + 2 > arraySize) {
			throw new RuntimeException("树底层数组已满");
		}
 
		if (left) {
			if (datas[2 * index + 1] == null) {
				datas[2 * index + 1] = data;
			} else {
				throw new RuntimeException("该节点已存在!");
			}
		} else {
			if (datas[2 * index + 2] == null) {
				datas[2 * index + 2] = data;
			} else {
				throw new RuntimeException("该节点已存在!");
			}
		}
	}
 
	// 判断二叉树是否为空
	public boolean isEmpty() {
		return datas[0] == null;
	}
 
	// 获取根节点
	public T getRoot() {
		return (T) datas[0];
	}
 
	// 返回指定节点的父节点
	public T getParent(int index) {
		if (index == 0) {
			throw new RuntimeException("根节点不存在父节点!");
		}
 
		return (T) datas[(index - 1) / 2];
	}
	
	//获取右子节点
	public T getRight(int index){
		if (2 * index + 1 > arraySize || 2 * index + 2 > arraySize) {
			throw new RuntimeException("该节点不存在右子节点!");
		}
		
		return (T) datas[index * 2 + 2];
	}
	
	//获取左子节点
	public T getLeft(int index){
		if (2 * index + 1 > arraySize || 2 * index + 2 > arraySize) {
			throw new RuntimeException("该节点不存在左子节点!");
		}
		
		return (T) datas[index * 2 + 1];
	}
	
	//返回该二叉树的深度
	public int getDeep(){
		return deep;
	}
	
	//返回指定数据的位置
	public int getPos(T data){
		for(int i=0;i<arraySize;i++){
			if(datas[i].equals(data)){
				return i;
			}
		}
		
		return -1;
	}
	
	public String toString(){
		return Arrays.toString(datas);
	}
 
}

测试代码:

package com.liuhao.test;
 
import org.junit.Test;
 
import com.liuhao.DataStructures.ArrayBinTree;
 
public class ArrayBinTreeTest {
 
	@Test
	public void test() {
		ArrayBinTree<String> binTree = new ArrayBinTree<String>(4,"根");
		binTree.add(0, "0右", false);
		binTree.add(2, "2右", false);
		binTree.add(2, "2左", true);
		
		binTree.add(0, "0左", true);
		binTree.add(1, "1左", true);
		System.out.println(binTree);
		
		System.out.println(binTree.getLeft(2));
		System.out.println(binTree.getParent(6));
	}
 
}

2.链式存储

/**
 * 二叉树的二叉链表存储的是数据域、左子节点和右子节点
 */
class TwoLinkBinTree<E>{
	public static class TreeNode{
		//数据域
		Object data;
		//左子节点
		TreeNode left;
		//右子节点
		TreeNode right;
		public TreeNode(){		
		}
		public TreeNode(Object data){
			this.data=data;
		}
		public TreeNode(Object data,TreeNode left,TreeNode right){
			this.data=data;
			this.left=left;
			this.right=right;
		}
	}
	private TreeNode root;
	public TwoLinkBinTree(){
		this.root=new TreeNode();
	}
	//以指定根元素来创建二叉树
	public TwoLinkBinTree(E data){
		this.root=new TreeNode(data);
	}
	/**
	 * 为指定节点添加子节点
	 * @param parent 需要添加子节点的父节点的索引
	 * @param data 新子节点的数据
	 * @param isLeft 是否为左节点
	 * @return 新增的节点
	 */
	public TreeNode addNode(TreeNode parent,E data,boolean isLeft){
		if(parent==null){
			throw new RuntimeException(parent+"节点为null,无法添加子节点");
		}
		if(isLeft&&parent.left!=null){
			throw new RuntimeException(parent+"节点已有左子节点,无法添加左子节点");
		}
		if(!isLeft&&parent.right!=null){
			throw new RuntimeException(parent+"节点已有右子节点,无法添加右子节点");
		}
		TreeNode newNode=new TreeNode(data);
		if(isLeft){
			//让父节点的left引用指向新节点
			parent.left=newNode;
		}else{
			//让父节点的right引用指向新节点
			parent.right=newNode;
		}
		return newNode;
	}
	//判断二叉树是否为空
	public boolean empty(){
		return root.data==null;
	}
	//返回根节点
	public TreeNode root(){
		if(empty()){
			throw new RuntimeException("树为空,无法访问根节点");
		}
		return root;
	}
	//返回指定节点(非根节点)的父节点
	public E parent(TreeNode node){
		//对于二叉链表存储法,如果要访问指定节点的父节点,必须遍历二叉树
		return null;
	}
	//返回指定节点(非叶子节点)的左子节点
	public E leftChild(TreeNode parent){
		if(parent==null){
			throw new RuntimeException("节点为Null,无法添加子节点");
		}
		return parent.left==null?null:(E)parent.left.data;
	}
	//返回指定节点的右子节点
	public E rightChild(TreeNode parent){
		if(parent==null){
			throw new RuntimeException(parent+"节点为null,无法添加子节点");
		}
		return parent.right==null?null:(E)parent.right.data;
	}
	//返回二叉树的深度
	public int deep(){
		return deep(root);
	}
	//递归,第棵子树的深度为其所有子树的最大深度+1
	private int deep(TreeNode node){
		if(node==null){
			return 0;
		}
		//没有子树
		if(node.left==null&&node.right==null){
			return 1;
		}else{
			int leftDeep=deep(node.left);
			int rightDeep=deep(node.right);
			//记录其所有左、右子树中较大的深度
			int max=leftDeep>rightDeep?leftDeep:rightDeep;
			//返回其左、右子树中较大的深度+1
			return max+1;
		}
	}
}
public class TwoLinkBinaryTreeDemo {
	public static void main(String[] args) {
		TwoLinkBinTree<String> binTree=new TwoLinkBinTree<>("根节点");
		TwoLinkBinTree.TreeNode tn1=binTree.addNode(binTree.root(),"第二层左节点",true);
		TwoLinkBinTree.TreeNode tn2=binTree.addNode(binTree.root(),"第二层右节点",false);
		TwoLinkBinTree.TreeNode tn3=binTree.addNode(tn2,"第三层左节点",true);
		TwoLinkBinTree.TreeNode tn4=binTree.addNode(tn2,"第三层左节点",false);
		TwoLinkBinTree.TreeNode tn5=binTree.addNode(tn3,"第四层左节点",true);
		System.out.println("tn2的左子节点:"+binTree.leftChild(tn2));
		System.out.println("tn2的右子节点:"+binTree.rightChild(tn2));
		System.out.println(binTree.deep());
	}
}
 

(二)普通树:
普通树存储
1.树的双亲表示法
双亲表示法采用顺序表(也就是数组)存储普通树,其实现的核心思想是:顺序存储各个节点的同时,给各节点附加一个记录其父节点位置的变量。
注意:根节点没有父节点(父节点又称为双亲节点),因此根节点记录父节点位置的变量通常置为 -1。
例如,采用双亲表示法存储图 1 中普通树,其存储状态如图 2 所示:
普通树存储方式之双亲表示法
2.树的孩子表示法
孩子表示法存储普通树采用的是 “顺序表+链表” 的组合结构,其存储过程是:从树的根节点开始,使用顺序表依次存储树中各个节点,需要注意的是,与双亲表示法不同,孩子表示法会给各个节点配备一个链表,用于存储各节点的孩子节点位于顺序表中的位置。
如果节点没有孩子节点(叶子节点),则该节点的链表为空链表。
例如,使用孩子表示法存储图中的普通树,则最终存储状态如图 b) 所示:
普通树存储方式之孩子表示法
3.树的孩子兄弟表示法
因此,该链表中的节点应包含以下 3 部分内容(如图所示):
(1)节点的值;
(2)指向孩子节点的指针;
(3) 指向兄弟节点的指针;
在这里插入图片描述
普通树存储方式之孩子兄弟表示法

五、二叉树的遍历方式

先序,中序,后序针对二叉树。深度、广度针对普通树。
二叉树的遍历
1.前序遍历(根左右):ABCDEFGHK
2.中序遍历(左根右):BDCAEHGKF
3.后序遍历(左右根):DCBHKGFEA
4.深度优先遍历(DFS):(相当于二叉树中的前序遍历):ABCDEFGHK
5.广度优先遍历(BFS):(相当于二叉树中的层次遍历):ABECFDGHK

六、树、二叉树、森林之间的转化方式

图解见:
https://jingyan.baidu.com/album/19020a0a743851529d28421a.html?picindex=2
1、树转换为二叉树
由于二叉树是有序的,为了避免混淆,对于无序树,我们约定树中的每个结点的孩子结点按从左到右的顺序进行编号。
将树转换成二叉树的步骤是:
(1)加线。就是在所有兄弟结点之间加一条连线;
(2)抹线。就是对树中的每个结点,只保留他与第一个孩子结点之间的连线,删除它与其它孩子结点之间的连线;
(3)旋转。就是以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。
树转换为二叉树
2、森林转换为二叉树
森林是由若干棵树组成,可以将森林中的每棵树的根结点看作是兄弟,由于每棵树都可以转换为二叉树,所以森林也可以转换为二叉树。
将森林转换为二叉树的步骤是:
(1)先把每棵树转换为二叉树;
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子结点,用线连接起来。当所有的二叉树连接起来后得到的二叉树就是由森林转换得到的二叉树。
森林转换为二叉树
3、二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,其步骤是:
(1)若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结点的右孩子结点……都作为该结点的孩子结点,将该结点与这些右孩子结点用线连接起来;
(2)删除原二叉树中所有结点与其右孩子结点的连线;
(3)整理(1)和(2)两步得到的树,使之结构层次分明。
二叉树转换为树
4、二叉树转换为森林
二叉树转换为森林比较简单,其步骤如下:
(1)先把每个结点与右孩子结点的连线删除,得到分离的二叉树;
(2)把分离后的每棵二叉树转换为树;
(3)整理第(2)步得到的树,使之规范,这样得到森林。

根据树与二叉树的转换关系以及二叉树的遍历定义可以推知:
树的先序遍历与其转换的相应的二叉树的先序遍历的结果序列相同;
树的后序遍历与其转换的二叉树的中序遍历的结果序列相同;
树的层序遍历与其转换的二叉树的后序遍历的结果序列相同。

由森林与二叉树的转换关系以及森林与二叉树的遍历定义可知:
森林的先序遍历和中序遍历与所转换得到的二叉树的先序遍历和中序遍历的结果序列相同。

参考文献:
http://data.biancheng.net/view/196.html
https://jingyan.baidu.com/album/19020a0a743851529d28421a.html?picindex=2
https://blog.csdn.net/linraise/article/details/11745559
https://blog.csdn.net/bruce_6/article/details/38111321
https://blog.csdn.net/codefunjava/article/details/39122499

如有错误之处,还望指正,谢谢!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值