数据结构与算法——二叉树遍历、查找、删除、顺序存储二叉树、线索化二叉树
一、树的存储方式
1.1 为什么需要树这种数据结构
1.1.1 数组存储方式分析:
优点:通过下标方式访问,速度快。对于有序数组,还可以使用二分查找提高检索效率
缺点:如果要检索某个集体之或者插入值会整体移动,效率低
ArrayList的底层也是一个数组,(transient Object[] elementData)此数组当空间不足的时候会自动进行扩容
既然到这里了,那我们可以先了解一下ArrayList底层源码分析
当创建对象时,使用的无参构造器,初始elementData容量为0(JDK7是10)
如果使用的是指定容量的capacity的构造器,则初始elementData的容量为capacity
建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
添加元素的时候,先判断是否要扩容,若扩容则调用grow方法,否则直接添加元素到指定的位置
如果使用的是无参构造器,第一次添加需要扩容的话,则扩容elementData为10,如果需要再次扩容的话,则扩容elementData为1.5倍
如果使用的是capacity构造器,需要扩容的话,则直接扩容elementData为1.5倍
1.1.2 链式存储方式的分析:
优点:在一定程度上对数组存储方式优化,比如插入一个数值节点,只需要将插入节点连接到链表中,删除同样效率较高
缺点:在进行检索时,效率仍然较低,比如检索某个值,需要从头节点遍历
LinkedList的底层是一个双向链表
LinkedList:双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next; //next变量记录下一个元素的位置
this.prev = prev; //prev变量记录前一个元素的位置
}
}
1.1.3二叉树存储方式的分析:
能提高数据存储,读取的效率,比如利用二叉排序树,即可保证数据的检索速度,同时也可以保证数据的插入、删除、修改的速度
怎么构建树?
下图中的数组的数据构建成树。我们可以将数字的第一个节点设置为树的最上面,然后剩余的元素和节点比较,比节点大的在右侧添加,小的在节点左侧添加
假如我们要查找12的话,怎么查找呢?
12会先和7比较,比7大,则向7右侧寻找;
12再和7右侧的10比较,则再向10右侧寻找;
12再和10右侧的12比较,发现相等,则返回。
1.2 树的概念和常用术语
1.3 二叉树
树有很多种,每个节点最多只能有两个子节点的形式成为二叉树;
二叉树的子节点分为左节点和右节点;
如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树;
也可以说满二叉树的节点总数= 2^n -1 , n 为层数
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
二、二叉树的遍历
2.1 前序遍历、中序遍历、后序遍历
前序遍历: 先输出父节点,再遍历左子树和右子树
先输出当前节点(初始时为根节点,即root节点),
如果左子节点不为空,则递归继续前序遍历
如果右子节点不为空,则递归继续前序遍历
按照图的输出顺序为:1 2 3 4
中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
如果当前节点的左子节点不为空,则递归继续中序遍历
先输出当前节点(初始时为根节点,即root节点)
如果当前节点的右子节点不为空,则递归继续中序遍历
按照图的输出顺序为 2 1 3 4
后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
如果当前节点的左子节点不为空,则递归继续后序遍历
如果当前节点的右子节点不为空,则递归继续后序遍历
先输出当前节点(初始时为根节点,即root节点)
按照图的输出顺序为 2 4 3 1
小结: 看输出父节点的顺序,就确定是前序,中序还是后序
假设我们再增加一个5号节点关胜
前序遍历: 1 2 3 5 4
中序遍历: 2 1 5 3 4
后序遍历: 2 5 4 3 1
2.2 代码实现
使用代码实现下图的二叉树
package org.example.suanfa;
import lombok.Data;
public class BinaryTreeDemo {
public static void main(String[] args) {
// 先创建一个二叉树
BinaryTree binaryTree = new BinaryTree();
// 创建需要的节点
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
binaryTree.setRoot(root);
System.out.println("前序遍历");
binaryTree.preOrder();
System.out.println("中序遍历");
binaryTree.midOrder();
System.out.println("后续遍历");
binaryTree.postOrder();
}
}
@Data
class BinaryTree {
private HeroNode root;
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("当前二叉树为空,无法便利");
}
}
// 中序遍历
public void midOrder() {
if (this.root != null) {
this.root.midOrder();
} else {
System.out.println("当前二叉树为空,无法便利");
}
}
// 后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
} else {
System.out.println("当前二叉树为空,无法便利");
}
}
}
@Data
class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
/**
* 编写前序遍历的方法
*/
public void preOrder() {
// 先输出父节点
System.out.println(this);
// 向左子树递归前序遍历
if (this.left != null) {
this.left.preOrder();
}
// 向右子树递归前序遍历
if (this.right != null) {
this.right.preOrder();
}
}
/**
* 中序遍历
*/
public void midOrder() {
// 向左子树递归前序遍历
if (this.left != null) {
this.left.midOrder();
}
System.out.println(this);
// 向右子树递归前序遍历
if (this.right != null) {
this.right.midOrder();
}
}
/**
* 后序遍历
*/
public void postOrder() {
// 向左子树递归前序遍历
if (this.left != null) {
this.left.postOrder();
}
// 向右子树递归前序遍历
if (this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
public HeroNode(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
三、二叉树的查找
要求:
请编写前序查找,中序查找,后续查找的方法
并分别使用三种查找方式,查找heroNo=5的节点
并分析各种查找方式比较了多少次
3.1 前序查找,中序查找,后续查找的分析
3.2 代码实现
public class BinaryTreeDemo {
public static void main(String[] args) {
// 先创建一个二叉树
BinaryTree binaryTree = new BinaryTree();
// 创建需要的节点
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
System.out.println("前序查找");
System.out.println(binaryTree.preOrderSearch(5));
System.out.println("中序查找");
System.out.println(binaryTree.midOrderSearch(5));
System.out.println("后续查找");
System.out.println(binaryTree.postOrderSearch(5));
}
}
@Data
class BinaryTree {
private HeroNode root;
// 前序查找
public HeroNode preOrderSearch(int no) {
if (root != null) {
return root.postOrderSearch(no);
} else {
return null;
}
}
// 中序查找
public HeroNode midOrderSearch(int no) {
if (root != null) {
return root.midOrderSearch(no);
} else {
return null;
}
}
// 后续查找
public HeroNode postOrderSearch(int no) {
if (root != null) {
return root.postOrderSearch(no);
} else {
return null;
}
}
}
@Data
class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
/**
* @param no 要查找的编号
* @return 对应的Node,没有找到返回null
*/
public HeroNode preOrderSearch(int no) {
if (this.no == no) {
return this;
}
HeroNode heroNode = null;
if (this.left != null) {
// 运行到这里肯定不是自己要找的,向左递归
heroNode = this.left.preOrderSearch(no);
}
if (heroNode != null) {
// 找到了
return heroNode;
}
// this.right != null,为了调用的时候没有空指针异常
if (this.right != null) {
// 运行到这里说明还没有找到
heroNode = this.right.preOrderSearch(no);
}
return heroNode;
}
// 中序遍历查找
public HeroNode midOrderSearch(int no) {
HeroNode heroNode = null;
// 向左
if (this.left != null) {
// 运行到这里肯定不是自己要找的,向左递归
heroNode = this.left.midOrderSearch(no);
}
if (heroNode != null) {
// 找到了
return heroNode;
}
// 中
if (this.no == no) {
return this;
}
// 向右
if (this.right != null) {
// 运行到这里说明还没有找到
heroNode = this.right.midOrderSearch(no);
}
return heroNode;
}
// 后续遍历查找
public HeroNode postOrderSearch(int no) {
HeroNode heroNode = null;
// 向左
if (this.left != null) {
// 运行到这里肯定不是自己要找的,向左递归
heroNode = this.left.postOrderSearch(no);
}
if (heroNode != null) {
// 找到了
return heroNode;
}
// 向右
if (this.right != null) {
// 运行到这里说明还没有找到
heroNode = this.right.postOrderSearch(no);
}
// 这个地方再写一个if判断heroNode是不是空也行
// 中
if (this.no == no) {
return this;
}
return heroNode;
}
}
四、二叉树的删除操作
4.1 图解
因为现在还没有学二叉排序树,那现在如果删除的不是子节点的话,我们就将整个子节点的树给删掉
4.2 代码实现
public class BinaryTreeDemo {
public static void main(String[] args) {
// 先创建一个二叉树
BinaryTree binaryTree = new BinaryTree();
// 创建需要的节点
HeroNode root = new HeroNode(1, "宋江");
HeroNode node2 = new HeroNode(2, "吴用");
HeroNode node3 = new HeroNode(3, "卢俊义");
HeroNode node4 = new HeroNode(4, "林冲");
HeroNode node5 = new HeroNode(5, "关胜");
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
binaryTree.delNode(5);
binaryTree.preOrder();
}
}
@Data
class BinaryTree {
private HeroNode root;
// 前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
} else {
System.out.println("当前二叉树为空,无法便利");
}
}
public boolean delNode(int no){
if(root !=null){
// 如果只有一个root节点,这里立即判断root是不是要删除的节点
if(root.getNo() == no){
root =null;
return true;
}
}else {
System.out.println("空树,不能删除");
return false;
}
// 运行到这里肯定是没有删除的
// boolean nodeBoolean = root.deleteNode(no);
return root.deleteNode(no) ;
}
}
@Data
class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
/**
* 编写前序遍历的方法
*/
public void preOrder() {
// 先输出父节点
System.out.println(this);
// 向左子树递归前序遍历
if (this.left != null) {
this.left.preOrder();
}
// 向右子树递归前序遍历
if (this.right != null) {
this.right.preOrder();
}
}
/**
* 递归删除节点
* 如果删除的节点是叶子节点,则删除改节点
* 如果删除的节点是非叶子节点,则删除该子树
* @param no
*/
public boolean deleteNode(int no){
// 2.如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将this.left=null,并且结束递归
if(this.left !=null && this.left.no == no){
this.left = null;
return true;
}
// 3.如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,就将this.left=null,并且结束递归
if(this.right !=null && this.right.no ==no){
this.right =null;
return true;
}
// 4.如果第2,3步没有删除节点,那我们就需要向左子树递归删除
boolean deleteBoolean = false; //true表示删除了
if(this.left !=null){
deleteBoolean = this.left.deleteNode(no);
if (deleteBoolean){
return true;
}
}
// 5.再向右子树递归删除
if(this.right !=null){
deleteBoolean = this.right.deleteNode(no);
if (deleteBoolean){
return true;
}
}
return deleteBoolean;
}
}
五、顺序存储二叉树
八大排序算法中的堆排序,就会使用到顺序存储二叉树
5.1 顺序存储二叉树基本说明
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也能转换成数组。
要求:
下图的二叉树的结点,要求以数组的方式来存放 ,如,arr : [1, 2, 3, 4, 5, 6, 6]
要求在遍历数组 arr时,仍然可以以前序遍历,中序遍历和后序遍历的方式完成结点的遍历
简单的说要就是,数据以数组的方式存储,但是读取的时候却是按照树的方式进行读取
5.2 顺序存储二叉树的特点
顺序二叉树通常只考虑完全二叉树
第n个元素的左子节点的下标为 2 * n + 1
假设n=1,此时对应的数据为2,左子节点对应的下标为2*1+1=3,数据刚好是4
第n个元素的右子节点的下标为 2 * n + 2
假设n=1,此时对应的数据为2,右子节点对应的下标为2*1+2=4,数据刚好是5
第n个元素的父节点的下标为 (n-1) / 2
假设n=1,此时对应的数据为2,父节点对应的下标为0,数据刚好是1
n : 表示二叉树中的第几个元素(按0开始编号如图所示,也可以理解为数组的下标)
5.3 代码实现
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7};
ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
// 从0号节点开始遍历
arrBinaryTree.preOrder(0);
System.out.println("*********************");
arrBinaryTree.midOrder(0);
System.out.println("*********************");
arrBinaryTree.postOrder(0);
}
}
//编写一个ArrBinaryTree,实现顺序存储二叉树遍历
class ArrBinaryTree {
private int[] arr; //崔楚数据节点的数组
// 构造器
public ArrBinaryTree(int[] arr) {
this.arr = arr;
}
/**
* 编写一个方法,完成顺序存储二叉树的前序遍历
*
* @param index 数组的下标
*/
public void preOrder(int index) {
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能按照二叉树的前序遍历");
return;
}
// 运行到这里说明数组中还有数据
// 输出当前的这个元素
System.out.println(arr[index]);
// 向左节点遍历,记得判断一下,可能存在数组下标越界的可能
if ((index * 2 + 1) < arr.length) {
preOrder(index * 2 + 1);
}
// 左递归完成后再向右节点遍历
if ((index * 2 + 2) < arr.length) {
preOrder(index * 2 + 2);
}
}
// 中序遍历
public void midOrder(int index){
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能按照二叉树的前序遍历");
return;
}
// 向左节点遍历,记得判断一下,可能存在数组下标越界的可能
if ((index * 2 + 1) < arr.length) {
midOrder(index * 2 + 1);
}
// 输出当前的这个元素
System.out.println(arr[index]);
// 再向右节点遍历
if ((index * 2 + 2) < arr.length) {
midOrder(index * 2 + 2);
}
}
// 后续遍历
public void postOrder(int index){
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能按照二叉树的前序遍历");
return;
}
// 向左节点遍历,记得判断一下,可能存在数组下标越界的可能
if ((index * 2 + 1) < arr.length) {
postOrder(index * 2 + 1);
}
// 再向右节点遍历
if ((index * 2 + 2) < arr.length) {
postOrder(index * 2 + 2);
}
// 输出当前的这个元素
System.out.println(arr[index]);
}
}
六、线索化二叉树
下图中有7个空指针域,空指针域的个数可以看6.1的计算方式
当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }
但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上.
如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
解决方案-线索二叉树
6.1 介绍
n个结点的二叉链表中含有n+1 【公式 2n-(n-1)=n+1】 个空指针域。
利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
公式来历:一共有n个结点,一个结点有2个指针,n个结点有2n个指针;n个结点的话会占用n-1个指针,则还剩下2n-(n-1)个空指针域
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。
根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
一个结点的前一个结点,称为前驱结点
一个结点的后一个结点,称为后继结点
6.2 图解
说明:线索二叉树最终形成的结果和遍历的顺序有直接的关系
说明: 当线索化二叉树后,Node节点的 属性 left 和 right ,有如下情况:
left 指向的是左子树,也可能是指向的前驱节点. 比如 ① 节点 left 指向的左子树, 而 ⑩ 节点的 left 指向的就是前驱节点.
right指向的是右子树,也可能是指向后继节点,比如 ① 节点right 指向的是右子树,而⑩ 节点的right 指向的是后继节点.
6.3 线索化二叉树代码实现
省略所有get、set、构造方法
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
// 测试一把中序线索二叉树
HeroNode root = new HeroNode(1, "tom");
HeroNode node2 = new HeroNode(3, "jack");
HeroNode node3 = new HeroNode(6, "smith");
HeroNode node4 = new HeroNode(8, "mary");
HeroNode node5 = new HeroNode(10, "king");
HeroNode node6 = new HeroNode(14, "dim");
// 二叉树,手动创建
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
// 测试线索化
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.setRoot(root);
threadedBinaryTree.threadedNodes();
// 以10结点做测试
System.out.println("left--->"+node5.getLeft());
System.out.println("right--->"+node5.getRight());
}
}
class ThreadedBinaryTree {
private HeroNode root;
// 为了实现线索化,需要创建当前结点的前驱结点指针,简单的来说,这个pre总是保留前一个结点地址
private HeroNode pre =null;
public void threadedNodes() {
this.threadedNodes(root);
}
/**
* 中序线索化的代码
* 对某个结点进行线索化
* @param node 当前需要线索化的结点
*/
public void threadedNodes(HeroNode node) {
// node ==null,不能线索化
if(node == null) {
return;
}
// 1. 先线索化左子树
threadedNodes(node.getLeft());
// 2. 当前结点线索化(有点难度)
// 处理前驱
if(node.getLeft() == null) {
// 如果当前结点的左指针是空,那就指向前驱指针
node.setLeft(pre);
// 修改当前结点的左指针的类型,指向前驱结点
node.setLeftType(1);
}
// 处理后继
// 这个处理后继很巧妙,此时不处理当前node节点的后继节点,只有当node到下一个节点的时候 才会去处理上一个节点的后继节点
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightType(1);
}
// !!!!后移指针
// 每处理一个结点后,让当前结点是下一个结点的前驱结点
pre = node;
// 3. 再线索化右子树
threadedNodes(node.getRight());
}
}
class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
// 说明:
// 如果leftType ==0 表示指向的是左子树,如果1则表示指向的前驱结点
// 如果rightType ==0 表示指向的是右子树,如果1表示指向后继结点
private int leftType;
private int rightType;
}