一、为什么需要树这种数据结构
是因为之前的数组和链表两种存储结构对增删改查都各有利弊,而且差异明显。所以树这种结构就是增删改查效率差异不是很明显,增删改查所耗费时间都比较均衡的一种数据结构。
1、数组存储方式
1.1、优点
通过下标方式访问元素,速度快。对于有序数组,还可以使用二分查找来提高检索速度。
1.2、缺点
若检索具体某个值,或插入值(按一定顺序),会整体移动。效率较低。
2、链表存储方式
2.1、优点
在一定程度上对数组存储方式进行了优化(比如插入一个节点,只需要将插入节点连接到链表中即可,删除效率也很好)
2.2、缺点
在进行检索时,效率仍然很低,比如检索某个值,需要从头节点开始遍历进行判断。
3、树存储方式
3.1、说明
可以利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入、删除、修改的速度。
二叉排序树就是要求任何左侧子节点的值都大于当前父节点,右侧子节点的值都小于当前父节点。
3.2、案例
3.2.1、假设有一颗二叉排序树【8 4 11 2 6 10 13】,如下图所示
二叉排序树,左侧子节点的值都大于当前父节点,右侧子节点的值都小于当前父节点。
3.2.2、如下需求
3.2.2.1、查找10
具体步骤:
总共需要两次查找
1、判断10与根节点大小,大于根节点,在右侧查找
2、判断10与11的大小,小于11,则在11的左侧查找,找到!
3.2.2.2、添加15
15大于8,在8的右侧找,15大于11,在11的右侧找,15大于13,所以放到13的右侧节点上,如下图所示:
3.2.2.3、删除2
2小于8,在8的左边找。找到4,2小于4,在4的左边找,找到了2,将这个节点置为null即可。如下图
3.2.3、总结
可以发现利用二叉排序树的数据结构,增删改查都很方便,效率相对均衡。
二、树的示意图
三、树的常用术语
1、节点
没得解释,上图中任何一个圆圈都代表一个节点。
2、根节点
最顶层节点,也就是上图中的A节点。
3、父节点
所有非叶子节点都是父节点。比如:ABCD,他们都有对应的子节点。
4、子节点
比如B和C是A的子节点,H是D的子节点。
5、叶子节点
没有子节点的节点
6、节点的权
节点的值。
7、路径
从root节点找到该节点的路线。比如要找到F,那就是A->C->F这条查找线就称为路径。
8、层
上图画出来了。就是树的每一层。
9、子树
D->H称为根的子树(注意这里是子树,是一棵树,不是单个节点),同理,C->F、G也称为根节点的子树
10、树的高度
最大层数。
11、森林
多颗子树构成森林,比如:整棵树就是一个森林。
四、二叉树的概念
1、基础二叉树
每个节点最多只能有两个子节点。
注意“最多”,也就是说可以有两个,可以只有左子节点,也可以只有右子节点。如下图
2、满二叉树
该二叉树的所有叶子节点都在最后一层,并且节点总数=2n - 1,n为层数。
如下图就是满二叉树
再如下图就不是满二叉树,因为不符合所有叶子节点都在最后一层的特点,节点H的问题。
3、完全二叉树
该二叉树的所有叶子节点都在最后一层或倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
1.根据定义可知,满二叉树都是完全二叉树。
如图所示,两颗都是完全二叉树。
五、二叉树的遍历
0、demo
1、前序遍历
1.1、概念
先输出父节点,再遍历左子树,最后遍历右子树。
1.2、demo前序遍历
A B D H E C F G
1.3、核心代码
/**
* 前序遍历
*/
public void preOrder () {
// 先输出父节点
System.out.println(this);
// 遍历左侧节点
if (this.left != null) {
this.left.preOrder();
}
// 遍历右侧节点
if (this.right != null) {
this.right.preOrder();
}
}
2、中序遍历
2.1、概念
先遍历左子树,再输出父节点,最后遍历右子树。
2.2、demo中序遍历
D H B E A F C G
2.3、核心代码
/**
* 中序遍历
*/
public void middleOrder () {
// 先遍历左侧节点
if (this.left != null) {
this.left.middleOrder();
}
// 输出父节点
System.out.println(this);
// 最后遍历右侧节点
if (this.right != null) {
this.right.middleOrder();
}
}
3、后序遍历
3.1、概念
先遍历左子树,再遍历右子树,最后输出父节点。
3.2、demo后序遍历
H D E B F G C A
3.3、核心代码
/**
* 后序遍历
*/
public void postOrder () {
// 先遍历左侧节点
if (this.left != null) {
this.left.postOrder();
}
// 再遍历右侧节点
if (this.right != null) {
this.right.postOrder();
}
// 最后输出父节点
System.out.println(this);
}
4、小结
看父节点的顺序,就确定是前序、中序还是后序。
5、完整代码
package com.chentongwei.tree;
/**
* Description:
*
* @author TongWei.Chen 2019-12-24 15:56:45
*/
public class BinaryTreeDemo {
public static void main(String[] args) {
Node root = new Node(1, "A");
Node node2 = new Node(2, "B");
root.setLeft(node2);
Node node3 = new Node(3, "C");
root.setRight(node3);
Node node4 = new Node(4, "D");
node2.setLeft(node4);
Node node5 = new Node(5, "E");
node2.setRight(node5);
Node node6 = new Node(6, "F");
node3.setLeft(node6);
Node node7 = new Node(7, "G");
node3.setRight(node7);
Node node8 = new Node(8, "H");
node4.setRight(node8);
BinaryTree binaryTree = new BinaryTree();
binaryTree.setRoot(root);
System.out.println("------------------前序遍历------------------");
binaryTree.preNode(); // A B D H E C F G
System.out.println("------------------中序遍历------------------");
binaryTree.middleNode(); // D H B E A F C G
System.out.println("------------------后序遍历------------------");
binaryTree.postNode(); // H D E B F G C A
}
}
class BinaryTree {
private Node root;
public void setRoot(Node root) {
this.root = root;
}
public void preNode () {
if (this.root == null) {
System.out.println("根节点为空");
return;
}
this.root.preOrder();
}
public void middleNode () {
if (this.root == null) {
System.out.println("根节点为空");
return;
}
this.root.middleOrder();
}
public void postNode () {
if (this.root == null) {
System.out.println("根节点为空");
return;
}
this.root.postOrder();
}
}
class Node {
private int id;
private String value;
// 左侧节点
private Node left;
// 右侧节点
private Node right;
public Node(int id, String value) {
this.id = id;
this.value = value;
}
/**
* 前序遍历
*/
public void preOrder () {
// 先输出父节点
System.out.println(this);
// 遍历左侧节点
if (this.left != null) {
this.left.preOrder();
}
// 遍历右侧节点
if (this.right != null) {
this.right.preOrder();
}
}
/**
* 中序遍历
*/
public void middleOrder () {
// 先遍历左侧节点
if (this.left != null) {
this.left.middleOrder();
}
// 输出父节点
System.out.println(this);
// 最后遍历右侧节点
if (this.right != null) {
this.right.middleOrder();
}
}
/**
* 后序遍历
*/
public void postOrder () {
// 先遍历左侧节点
if (this.left != null) {
this.left.postOrder();
}
// 再遍历右侧节点
if (this.right != null) {
this.right.postOrder();
}
// 最后输出父节点
System.out.println(this);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Node{");
sb.append("id=").append(id);
sb.append(", value='").append(value).append('\'');
sb.append('}');
return sb.toString();
}
}
------------------前序遍历------------------
Node{id=1, value=‘A’}
Node{id=2, value=‘B’}
Node{id=4, value=‘D’}
Node{id=8, value=‘H’}
Node{id=5, value=‘E’}
Node{id=3, value=‘C’}
Node{id=6, value=‘F’}
Node{id=7, value=‘G’}
------------------中序遍历------------------
Node{id=4, value=‘D’}
Node{id=8, value=‘H’}
Node{id=2, value=‘B’}
Node{id=5, value=‘E’}
Node{id=1, value=‘A’}
Node{id=6, value=‘F’}
Node{id=3, value=‘C’}
Node{id=7, value=‘G’}
------------------后序遍历------------------
Node{id=8, value=‘H’}
Node{id=4, value=‘D’}
Node{id=5, value=‘E’}
Node{id=2, value=‘B’}
Node{id=6, value=‘F’}
Node{id=7, value=‘G’}
Node{id=3, value=‘C’}
Node{id=1, value=‘A’}
来源:CSDN
作者:【原】编程界的小学生
链接:https://blog.csdn.net/ctwctw/article/details/103667308