文章目录
- 10.1 树存在的必要性
- 10.2 树的常用术语
- 10.3 二叉树的定义
- 10.5 二叉树的基本操作(手动添加结点)
- 10.4 二叉树的遍历(前序, 中序, 后序)
- 10.5 二叉树的查找
- 10.5 二叉树的删除
10.1 树存在的必要性
- [数组存储方式的优缺点]
- 优点: 通过下标直接访问目标数据, 速度快. 对于有序数组, 还可使用二分查找提高检索速度
- 缺点: 如果要检索具体某个值, 或者插入值(按一定顺序)会整体移动, 效率较低.
- [链表存储方式的优缺点]
- 优点:在一定程度上对数组存储方式有所优化。比如插入一个数值节点,只需要将插入节点链接到链表中即可,其删除效率也较高。
- 缺点:在进行检索时,效率仍然较低。比如检索某个值, 需要从头节点开始遍历。
从上面数组和链表的特点我们可以知道: 数组查找快, 删除插入慢; 链表查找慢,删除插入快, 在数组和链表中, 查找和增删不可兼得.
- [树存储的特点]
- 而树这种数据结构, 在查找速度快的同时又兼顾了增删的效率,可以说是结合了数组和链表的优点。
- 树存储方式的核心特点为:能提高数据存储、读取的效率。比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入、删除、修改的速度。
10.2 树的常用术语
树这种数据结构有较多的常用术语,这些术语我们需要熟练记住。
- 这些常用术语包括:
- 节点
- 根节点
- 父节点
- 子节点
- 叶子节点(没有子节点的节点)
- 节点的权(节点值)
- 路径(从 root 节点找到该节点的路线)
- 层
- 子树
- 树的高度(最大层数)
- 森林(多颗子树构成森林)
10.3 二叉树的定义
二叉树
- 树有很多种, 每个节点最多只能有两棵子树, 而且子树有左右顺序之分, 这树叫二叉树.
满二叉树
- 二叉树的所有叶子节点都在最后一层,并且结点总数= 2n -1(n为层数), ,则我们称其为满二叉树。
完全二叉树
- 二叉树满足以下特点
- 只有最下面两层结点度<2
- 并且最下一层结点
连续
集中在靠左的若干位置上
- 这样的二叉树叫做完全二叉树.
每一层都是要按从左往右,依此填满,最后一层可以不满, 如下图.
注意: 满二叉树一定是完全二叉树, 完全二叉树不一定是满二叉树.
下面几节, 关于二叉树的操作,都是对如下图所示的二叉树进行的:
10.5 二叉树的基本操作(手动添加结点)
- 程序的组成:
[结点类]: 左右结点域, 数据, 构造方法, toString(), 左右节点的添加方法
[二叉树操作类]: 构造方法(指定二叉树的根节点), 测试方法(手动添加每个父节点的左右结点)
- 结点内部类: 包含
左结点,右结点指针域, 数据域, get和set方法, 构造方法(初始化结点数据域), toString()(自定义输出数据域)
- 因为本栗子追求最简单的演示, 所有删去了toString(), 数据域的get,set方法等未用到的方法,在之后的复杂栗子中, 会做进一步的展开.
public static class BinaryTreeNode{
//二叉树结点的内容: 数据域, 左右结点指针域
private BinaryTreeNode leftNode;
private BinaryTreeNode rightNode;
private int id;
private String name;
//设置结点的左孩子节点
public void setLeftNode(BinaryTreeNode leftNode) {
this.leftNode = leftNode;
}
//设置结点的右孩子节点
public void setRightNode(BinaryTreeNode rightNode) {
this.rightNode = rightNode;
}
//构造方法, 初始化结点的数据域
public BinaryTreeNode(int id, String name) {
this.id = id;
this.name = name;
}
}
- 二叉树操作类和测试方法: 包含 二叉树的构造方法(指定二叉树的根节点), 测试方法
//对结点的操纵
///添加新节点到二叉树
private BinaryTreeNode root;
public BasicBinaryTree(BinaryTreeNode node){
this.root = node; //指定根节点
}
public static void main(String[] args) {
//创建若干个结点
BinaryTreeNode node1 = new BinaryTreeNode(1, "宋江");
BinaryTreeNode node2 = new BinaryTreeNode(2, "吴用");
BinaryTreeNode node3 = new BinaryTreeNode(3, "卢俊义");
BinaryTreeNode node4 = new BinaryTreeNode(4, "公孙胜");
BinaryTreeNode node5 = new BinaryTreeNode(5, "关胜");
//创建二叉树, 并指定根节点
BasicBinaryTree tree = new BasicBinaryTree(node1);
//手动添加结点
node1.setLeftNode(node2);
node1.setRightNode(node3);
node3.setLeftNode(node4);
node3.setRightNode(node5);
}
运行结果(通过DEBUG展示)
10.4 二叉树的遍历(前序, 中序, 后序)
【案例需求】
使用前序、中序、后序分别遍历二叉树。
【二叉树三种遍历的定义】
- 前序遍历
- 先遍历根节点, 再遍历左子树, 最后遍历右子树;
- 中序遍历
- 先遍历左子树, 再遍历根节点, 最后遍历右子树;
- 后序遍历
- 先遍历左子树, 再遍历右子树, 最后遍历根节点;
根据这三种遍历顺序的定义,我们可以得知两个结论:
- 左子树总是在右子树的前面遍历;
- 前、中、后的遍历顺序指的是遍历根节点的顺序。
【实现思路】
上面说过,二叉树的遍历包括:前序遍历、中序遍历、后序遍历。
其中前序遍历的实现思路如下:
- 首先输出当前节点;
- 如果左子节点不为空,就对左子节点递归前序遍历;
- 如果右子节点不为空,就对右子节点递归前序遍历。
中序遍历的实现思路如下:
- 首先判断左子节点是否为空,如果不为空,就对左子节点递归中序遍历;
- 然后输出当前节点;
- 最后判断右子节点是否为空,如果不为空,就对右子节点递归中序遍历。
后序遍历的实现思路如下:
- 首先判断左子节点是否为空,如果不为空,就对左子节点递归后序遍历;
- 然后判断右子节点是否为空,如果不为空,就对右子节点递归后序遍历;
- 最后输出当前节点。
【代码实现】
- 程序结构:
- 结点类(TreeNode):
- 左右指针域
- 设置父结点的左右孩子结点的set方法
- 初始化结点数据的构造方法
- 输出结点信息的toString方法
- 具体实现对结点的前中后序遍历
- 二叉树遍历类(BinaryTreeTraverse)
- 设置父节点root的构造方法
- 使用结点类的前中后序遍历方法
- 测试方法
结点类具体实现:
public class BinaryTreeNode {
/**
* 二叉树结点类
* 1. 左右节点指针域
* 2. 结点相关的数据域
* 3. 设置结点的左右节点的set方法
* 4. 初始化结点数据的构造方法
* 5. 输出结点数据的toString()
*/
private BinaryTreeNode leftNode;
private BinaryTreeNode rightNode;
private int id;
private String name;
//带参构造, 初始化结点的数据
public BinaryTreeNode(int id, String name){
this.id = id;
this.name = name;
}
// 设置结点的左孩子结点
public void setLeftNode(BinaryTreeNode node){
this.leftNode = node;
}
//设置结点的右孩子节点
public void setRightNode(BinaryTreeNode node){
this.rightNode = node;
}
//重写输出结点数据信息的toString()
public String toString(){
return "id : "+id+", name: "+name;
}
/遍历二叉树, 我们在结点类中作具体实现
//1. 前序遍历
public void preOrder(){
//输出父节点
System.out.println(this);
//遍历输出左子树
if(this.leftNode != null){
this.leftNode.preOrder();
}
//遍历输出右子树
if(this.rightNode != null){
this.rightNode.preOrder();
}
}
//2. 中序遍历
public void midOrder(){
//遍历左子树
if( this.leftNode != null){
this.leftNode.midOrder();
}
//输出父节点
System.out.println(this);
//遍历右子树
if(this.rightNode != null){
this.rightNode.midOrder();
}
}
//3. 后序遍历
public void postOrder(){
//遍历左子树
if( this.leftNode != null){
this.leftNode.postOrder();
}
//遍历右子树
if( this.rightNode != null){
this.rightNode.postOrder();
}
System.out.println(this);
}
}
二叉树遍历类具体实现
public class BinaryTreeTraverse {
/**
* 二叉树的遍历
* 1. 借助构造方法指定二叉树的根节点
* 2. 使用结点类提供的遍历方法进行遍历
*/
private BinaryTreeNode root;
public BinaryTreeTraverse(BinaryTreeNode node) {
this.root = node;
}
//通过判断父节点是否为空, 我们来决定二叉树遍历的开始和结束
public void preOrderTraverse(){
if(root != null){
root.preOrder();
}else{
System.out.println("二叉树为空, 前序遍历失败!");
}
}
public void midOrderTraverse(){
if(root != null){
root.midOrder();
}else{
System.out.println("二叉树为空, 中序遍历失败!");
}
}
public void postOrderTraverse(){
if(root != null){
root.postOrder();
}else{
System.out.println("二叉树为空, 后序遍历失败");
}
}
//测试方法(单独做成一个测试类也是可行的)
public static void main(String[] args) {
//建立二叉树结点
BinaryTreeNode node1 = new BinaryTreeNode(1, "宋江");
BinaryTreeNode node2 = new BinaryTreeNode(2, "吴用");
BinaryTreeNode node3 = new BinaryTreeNode(3, "卢俊义");
BinaryTreeNode node4 = new BinaryTreeNode(4, "公孙胜");
BinaryTreeNode node5 = new BinaryTreeNode(5, "关俊");
//新建一棵二叉树
BinaryTreeTraverse tree = new BinaryTreeTraverse(node1);
//结点添加到二叉树
node1.setLeftNode(node2);
node1.setRightNode(node3);
node3.setLeftNode(node4);
node3.setRightNode(node5);
//二叉树的遍历
System.out.println("前序遍历二叉树的结果为: ");
tree.preOrderTraverse();
System.out.println("=========================");
System.out.println("中序遍历二叉树的结果为: ");
tree.midOrderTraverse();
System.out.println("=========================");
System.out.println("后序遍历二叉树的结果为: ");
tree.postOrderTraverse();
System.out.println("=========================");
}
10.5 二叉树的查找
【案例需求】
分别使用前序, 中序, 后序查找二叉树中指定编号节点.
【思路分析】
前序遍历查找的思路如下
- 首先判断当前节点(this)的编号是否等于目标编号,如果相等, 则直接返回当前节点;
- 如果不相等, 再判断左子节点是否为空, 如果不为空, 则递归前序查找左子树;
- 如果2中左子树递归前序查找的结果不为空, 说明找到了, 返回即可;
- 如果没有找到, 则判断当前节点的右子树是否为空, 如果不为空, 就递归前序查找右子树, 把结果存放到res中;
- 返回查询结果res;
核心代码:
- 结点类中具体实现的查找方法(这里以中序查找为例)
public BinaryTreeNodeSe midOrderSearchById(int id) {
//找到了, 直接返回
if (this.id == id) return this;
//左子树非空, 递归左子树查询
BinaryTreeNodeSe res = null;
if (this.leftNode != null) {
res = this.leftNode.midOrderSearchById(id);
}
//左子树查到了(res不为空), 直接返回即可, 下面的递归右子树直接略过
if (res != null) return res;
if (this.rightNode != null) {
res = this.rightNode.midOrderSearchById(id);
}
return res;
}
- 二叉树类中使用结点类查找方法的方法(仍以中序查找为例)
// 2. 中序遍历查找
public void midOrderSe(int id){
//第一层判断, 二叉树是否为空
if( root != null){
BinaryTreeNodeSe res = root.midOrderSearchById(id);
// 第二层判断, 是否查到对应id 的结点
if(res != null){
System.out.println("找到了id= "+id+"的二叉树结点, 具体的信息如下: ");
System.out.println(res);
}else{
System.out.println("二叉树上没有id="+id+"的结点");
}
}else{
System.out.println("二叉树为空, 查找失败! ");
}
}
10.5 二叉树的删除
【案例需求】
根据节点编号删除指定节点:
如果节点为叶子节点,直接删除;
如果节点为父节点,则把此节点和节点的整个子树都删除。
【思路分析】
除了二叉树的根节点之外,其余的节点都是有父节点的。由于二叉树的每个节点关系都是单向的,即每个节点记录的都是自己的左、右子节点的信息,所以是无法通过目标节点自己来删除自己的,而是需要借助目标节点的父节点来删除目标节点。
因此二叉树的节点删除思路
如下:
- 如果二叉树的根节点就是目标节点,那么直接将根节点置空即可;
- 否则,如果当前根节点的左子节点不为空且是目标节点,就将左子节点置空 this.left = null 并返回;
- 如果第 2 步没删除,若当前根节点的右子节点不为空且是目标节点,就将右子节点置空 this.right = null 并返回;
- 如果第 3 步没删除,就遍历左子树递归查找删除;
- 如果第 4 步也没删除,就遍历右子树递归查找删除;
【代码实现】
- 结点类中具体实现的结点删除方法
//二叉树结点的删除
public void delNodeById(int id){
//根节点的删除交给二叉树类完成
//1. 如果左子树非空,并且找到了id相同的结点, 删除并返回
if(this.leftNode != null && this.leftNode.id == id){
this.leftNode = null;
return;
}
//2. 如果1未得到执行, 那么弱右子树非空, 并且找到了id相同的结点, 删除并返回
if(this.rightNode != null && this.rightNode.id == id) {
this.rightNode = null;
return;
}
//3. 如果1,2均为获得执行, 而左子树又非空的话, 递归遍历左子树
if(this.leftNode != null)
this.leftNode.delNodeById(id);
//3. 如果1,2均为获得执行, 并且3也没有找到合适的结点
// 若右子树又非空的话, 递归遍历右子树
if(this.rightNode != null)
this.rightNode.delNodeById(id);
}
- 二叉树类中使用结点类中删除方法的方法
//结点删除的方法
public void nodeDelById(int id){
if( root != null){
if(root.id == id){
root = null;
System.out.println("id = "+id+"的结点已删除");
}else{
root.delNodeById(id);
System.out.println("id = "+id+"的结点已删除");
}
}else{
System.out.println("二叉树已经为空, 查找失败");
}
}
Java实现二叉树结点删除的完整代码示例
- 参考资料:
- 韩顺平Java数据结构课程
- 简单二叉树