写点什么

线索化二叉树的作用

  • 2021 年 11 月 12 日
  • 本文字数:6591 字

    阅读完需:约 22 分钟

3、代码实现


四、先看一个问题


五、线索化二叉树


六、线索化二叉树代码实例


1、线索化二叉树


2、测试


3、控制台输出


七、遍历线索化二叉树


1、代码实例


2、控制台输出


八、大顶堆和小顶堆图解说明


1、堆排序基本介绍


2、大顶堆举例说明


3、小顶堆距离说明


4、一般升序采用大顶堆,降序采用小顶堆


九、堆排序思路和步骤解析


1、将无序二叉树调整为大顶堆


2、将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再讲堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。


3、重新调整结构,使其继续满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。


十、堆排序代码实例


1、堆排序代码


2、堆排序控制台输出


3、堆排序性能测试


4、性能测试控制台输出


十一、赫夫曼树


1、基本介绍


2、赫夫曼树几个重要概念和举例说明


3、赫夫曼树创建思路


4、赫夫曼树代码实例




一、前言




数组的搜索比较方便,可以直接用下标,但删除和插入就比较麻烦;


链表与之相反,删除和插入元素很快,但查找比较慢;


此时,二叉树应运而生,二叉树既有链表的好处,也有数组的好处,在处理大批量的动态数据时比较好用,是一种折中的选择。


文件系统和数据库系统一般都是采用树(特别是 B 树)的数据结构数据,主要为排序和检索的效率。


二叉树是一种最基本最典型的排序树,用于教学和研究树的特性,本身很少在实际中进行应用,因为缺点太明显,就像冒泡排序一样,因为效率问题并不实用,但也是我们必须会的。


二、二叉树缺点




1、顺序存储可能会浪费空间(在非完全二叉树的时候),但是读取某个指定的结点的时候效率比较高 O(0);


2、链式存储相对于二叉树,浪费空间较少,但是读取某个结点的时候效率偏低 O(nlogn)。



满二叉树:


在一颗二叉树中,如果所有分支结点都有左子结点和右子结点,并且叶结点都集中在二叉树的最底层,这样的二叉树称为满二叉树。



完全二叉树:


若二叉树中最多只有最下面两层的结点,而且相差只有 1 级,并且最下面一层的叶结点都依次排列在该层的最左边位置,则这样的二叉树称为完全二叉树。


三、遍历与结点删除




二叉树是一种非常重要的数据结构,非常多的数据结构都是基于二叉树的基础演变而来的。对于二叉树有深度遍历和广度遍历,深度遍历有前序、中序以及后序三种遍历方法,广度遍历即我们寻常所说的层次遍历。由于树的定义本身就是递归定义,因此采用递归的方法实现树的三种遍历。


对于一段代码来说,可读性有时候要比代码本身的效率要重要的多。

1、四种基本的遍历思想

  • 前序遍历:根结点 -->左子树-->右子树;

  • 中序遍历:左子树 -->根结点-->右子树;

  • 后续遍历:左子树 -->右子树-->根结点;

  • 层次遍历:仅仅需按成次遍历即可;

2、自定义二叉树

3、代码实现

(1)girlNode


package com.guor.tree;


public class GirlNode {


private int no;


private String name;


private GirlNode left; //默认 null


private GirlNode right; //默认 null


//1、如果 leftType == 0 表示指向的是左子树,如果 leftType == 1 则表示指向的是前驱结点


//2、如果 rightType == 0 表示指向的是右子树,如果 rightType == 1 则表示指向的是后继结点


private int leftType;


private int rightType;


public int getLeftType() {


return leftType;


}


public void setLeftType(int leftType) {


this.leftType = leftType;


}


public int getRightType() {


return rightType;


}


public void setRightType(int rightType) {


this.rightType = rightType;


}


public GirlNode(int no, String name) {


this.no = no;


this.name = name;


}


public int getNo() {


return no;


}


public void setNo(int no) {


this.no = no;


}


public String getName() {


return name;


}


public void setName(String name) {


this.name = name;


}


public GirlNode getLeft() {


return left;


}


public void setLeft(GirlNode left) {


this.left = left;


}


public GirlNode getRight() {


return right;


}


public void setRight(GirlNode right) {


this.right = right;


}


@Override


public String toString() {


return "GirlNode [no=" + no + ", name=" + name + "]";


}


//前序遍历


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);//输出父节点


}


//递归删除结点


//1.如果删除的节点是叶子节点,则删除该节点


//2.如果删除的节点是非叶子节点,则删除该子树


public void delNode(int no) {


//思路


/*


  • 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点.


  1. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将 this.left = null; 并且就返回(结束递归删除)

  2. 如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将 this.right= null ;并且就返回(结束递归删除)

  3. 如果第 2 和第 3 步没有删除结点,那么我们就需要向左子树进行递归删除

  4. 如果第 4 步也没有删除结点,则应当向右子树进行递归删除.


*/


//2. 如果当前结点的左子结点不为空,并且左子结点 就是要删除结点,就将 this.left = null; 并且就返回(结束递归删除)


if(this.left != null && this.left.no == no) {


this.left = null;


return;


}


//3.如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将 this.right= null ;并且就返回(结束递归删除)


if(this.right != null && this.right.no == no) {


this.right = null;


return;


}


//4.我们就需要向左子树进行递归删除


if(this.left != null) {


this.left.delNode(no);


}


//5.则应当向右子树进行递归删除


if(this.right != null) {


this.right.delNode(no);


}


}


}


(2)二叉树测试?


package com.guor.tree;


public class BinaryTreeTest {


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.setLeft(node4);


node3.setRight(node5);


binaryTree.setRoot(root);


System.out.println("前序遍历");


binaryTree.preOrder();


System.out.println("中序遍历");


binaryTree.midOrder();


System.out.println("后序遍历");


binaryTree.postOrder();


binaryTree.delNode(3);


System.out.println("删除结点 3,前序遍历");


binaryTree.preOrder();


}


}


//定义 BinaryTree 二叉树


class BinaryTree {


private HeroNode root;


public HeroNode getRoot() {


return root;


}


public void setRoot(HeroNode root) {


this.root = 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("二叉树为空,无法遍历");


}


}


//删除结点


public void delNode(int no) {


if(root != null) {


//如果只有一个 root 结点, 这里立即判断 root 是不是就是要删除结点


if(root.getNo() == no) {


root = null;


} else {


//递归删除


root.delNode(no);


}


}else{


System.out.println("空树,不能删除~");


}


}


}


(3)控制台输出



四、先看一个问题




将数列{1,3,6,8,10,14}构建成一颗二叉树。



问题分析:


  1. 当我们对上面的二叉树进行中序遍历时,数列为{8,3,10,1,6,14}。

  2. 但是 6,8,10,14 这几个节点的左右指针,并没有完全的利用上。

  3. 如果我们希望充分的利用各个节点的左右指针,让各个节点可以指向自己的前后节点,要怎么办?

  4. 解决方案 --> 线索化二叉树?


五、线索化二叉树




1、n 个节点的二叉树链表总含有 n+1(公式 2n-(n-1)=n+1)个空指针域。利用二叉树表中的空指针域,存放指向该节点在某种遍历次序下的前驱和后继节点的指针(这种附加的指针称为“线索”)


2、这种加上了线索的二叉树称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。


3、一个节点的前一个节点,称为前驱节点


4、一个节点的后一个节点,称为后继节点



说明:当线索化二叉树后,Node 节点的属性 left 和 right,有如下情况:


  1. left 指向的是左子树,也可能指向的前驱节点,比如①节点 left 指向的左子树,而⑩节点的 left 指向的就是前驱节点。

  2. right 指向的是右子树,也可能是指向后继节点,比如①节点 right 指向的是右子树,而⑩节点的 right 指向的是后继节点。


六、线索化二叉树代码实例



1、线索化二叉树

package com.guor.tree;


//定义 ThreadBinaryTree,实现了线索化功能的二叉树


public class ThreadedBinaryTree {


private HeroNode root;


//为了实现线索化,需要创建指向当前结点的前驱结点的指针


//在递归进行线索化时,pre 总是保留前一个结点


private HeroNode pre = null;


public HeroNode getRoot() {


return root;


}


public void set


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


Root(HeroNode root) {


this.root = root;


}


//重载 threadedNodes 方法


public void threadedNodes(){


this.threadedNodes(root);


}


/**


  • 编写对二叉树进行中序线索化的方法

  • @param node 当前需要线索化的结点


*/


public void threadedNodes(HeroNode node){


//如果 node==null,不能线索化


if(node == null){


return;


}


//1、先线索化左子树


threadedNodes(node.getLeft());


//2、线索化当前结点


//处理当前结点的前驱结点


//以 8 为例来理解


//8 结点的.left = null,8 结点的.leftType = 1


if(node.getLeft() == null){


//让当前结点的左指针指向前驱结点


node.setLeft(pre);


//修改当前结点的左指针的类型,指向前驱结点


node.setLeftType(1);


}


//处理后继结点


if(pre != null && pre.getRight() == null){


//让当前结点的右指针指向当前结点


pre.setRight(node);


//修改当前结点的右指针的类型=


pre.setRightType(1);


}


//每处理一个结点后,让当前结点是下一个结点的前驱结点


pre = node;


//3、线索化右子树


threadedNodes(node.getRight());


}


//前序遍历


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("二叉树为空,无法遍历");


}


}


//删除结点


public void delNode(int no) {


if(root != null) {


//如果只有一个 root 结点, 这里立即判断 root 是不是就是要删除结点


if(root.getNo() == no) {


root = null;


} else {


//递归删除


root.delNode(no);


}


}else{


System.out.println("空树,不能删除~");


}


}


}

2、测试

package com.guor.tree;


public class ThreadedBinaryTreeTest {


public static void main(String[] args) {


//测试中序线索二叉树的功能


HeroNode root = new HeroNode(1,"比比东");


HeroNode node2 = new HeroNode(3,"云韵");


HeroNode node3 = new HeroNode(6,"纳兰嫣然");


HeroNode node4 = new HeroNode(8,"雅妃");


HeroNode node5 = new HeroNode(10,"千仞雪");


HeroNode node6 = new HeroNode(14,"美杜莎");


//二叉树,后面我们要递归创建,现在简单处理使用手动创建


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 号节点测试


HeroNode leftNode = node5.getLeft();


System.out.println("10 号结点的前驱结点是:"+leftNode);//应该是 3 号


HeroNode rightNode = node5.getRight();


System.out.println("10 号结点的后继结点是:"+rightNode);//应该是 1 号


}


}

3、控制台输出


七、遍历线索化二叉树




说明:对前面的中序线索化的二叉树,进行遍历


分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用心得方式遍历线索化二叉树,各个结点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。遍历的次序应当和中序遍历保持一致。

1、代码实例

/**


  • 遍历线索化二叉树的方法


*/


public void threadedList(){


//定义一个变量,存储当前遍历的结点,从 root 开始


HeroNode node = root;


while (node != null){


//循环找到 leftType == 1 的结点,第一个找到就是 8 结点


//后面随着遍历而变化,因为当 leftType==1 时,说明该结点是按照线索化处理后的有效结点


while(node.getLeftType() == 0){


node = node.getLeft();


}


//打印当前结点


System.out.println(node);


//如果当前结点的右指针指向的是后继结点,就一直输出


while(node.getRightType() == 1){


//获取当前结点的后继结点


node = node.getRight();


System.out.println(node);


}


//替换这个遍历的结点


node = node.getRight();


}


}

2、控制台输出


八、大顶堆和小顶堆图解说明



1、堆排序基本介绍

  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为线性对数阶,它也是不稳定排序。

  2. 堆是具有以下特性的完全二叉树:每个结点的值都大于或等于其左右子结点的值,称为大顶堆,注意:没有要求结点的左子结点值和右子结点值的大小关系。

  3. 每个结点的值都小于或等于其左右子结点的值,称为小顶堆。

2、大顶堆举例说明


我们对堆中的结点按层进行编号,映射到数组中就是下面这一个样子



大顶堆特点:


arr[i]>=ar[2*i+1]&&arr[i]>=arr[2*i+2]//i 对应第几个结点,i 从 0 开始编号?

3、小顶堆距离说明


小顶堆特点:


arr[i]<=arr[2*i+1]&&arr[i]<=arr[2*i+2]//i 对应第几个结点,i 从 0 开始编号

4、一般升序采用大顶堆,降序采用小顶堆

九、堆排序基本思想


  1. 将待排序序列构成一个大顶堆

  2. 此时,整个序列的最大值就是堆顶的根节点

  3. 将其与末尾元素进行交换,此时末尾就为最大值

  4. 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。


九、堆排序思路和步骤解析



1、将无序二叉树调整为大顶堆

(1)原始的数组[4,6,8,5,9]



(2)此时从最后一个非叶子结点开始(第一个非叶子结点 arr.length/2-1=5/2-1=1,也就是 6 结点),从左至右,从上至下进行调整。



(3)再找到第二个非叶子结点,由于[4,9,8]中 9 最大,所以 4 和 9 交换。



4、此时,交换之后导致[4,5,6]结构混乱了,继续调整,交换 4 和 6。



此时,我们就讲一个无序结构的二叉树调整为了一个大顶堆。

2、将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再讲堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

3、重新调整结构,使其继续满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。


再将堆顶的 8 与末尾元素 5 交换,得到第二大元素 8


然后继续进行调整,交换,最后变成:



简单总结一下堆排序的基本思路:


  1. 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;

  2. 将堆顶元素与末尾元素交换,将最大元素交换至数组末端;

  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。


十、堆排序代码实例



1、堆排序代码

评论

发布
暂无评论
线索化二叉树的作用