树
- 每个结点有零个或多个子结点;没有父结点的结点称为根结点;每一个非根结点有且只有一个父结点;除了根结点外,每个子结点可以分为多个不相交的子树。
1、二叉树
-
树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树。
-
二叉树的子节点分为左节点和右节点。
-
如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树。
- 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
1.1、二叉树的操作
1.1.1、二叉树的遍历
- 前序遍历:先输出父节点,再遍历左子树和右子树。
- 中序遍历: 先遍历左子树,再输出父节点,再遍历右子树。
- 后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点。
1.1.2、二叉树的查找
- 三种查找方式:
- 前序查找:
- 先判断当前节点的no值是否等于要查找的值。
- 如果相等,则返回当前节点。
- 如果不相等,则判断当前节点的左子节点知否为空,如果不为空,则递归前序查找。
- 如果左递归前序查找找到节点,则返回该节点,否则继续判断当前节点的右子节点是否为空,如果不为空,则继续向右递归查找。
- 中序查找:
- 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找。
- 如果找到,则返回当前节点,如果没有找到,就和当前节点比较,如果是则返回当前节点,否则继续向右递归中序查找。
- 如果右递归中序查找,找到就返回。
- 后序查找:
- 判断当前节点的左子节点是否为空,如果不为空,则递归后序查找。
- 如果找到,则返回当前节点,如果没有找到,就**判断当前节点的右子节点是否为空,如果不为空,则右递归进行后序查找,**如果找到则返回。
- 和当前节点进行比较,如果相等,则返回,否则返回null。
- 前序查找:
1.1.3、二叉树的删除
规定一个要求:
-
如果删除的节点是叶子节点,则删除该节点。
-
如果删除的节点是非叶子节点,则删除该子树。
思路分析:
- 因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点。
- 如果当前结点的左子结点不为空,并且左子结点就是要删除结点,就将this.left = null; 并且就返回 (结束递归删除)。
- 如果当前结点的右子结点不为空,并且右子结点就是要删除结点,就将this.right= null ;并且就返回 (结束递归删除)。
- 如果第 2 和第 3 步没有删除结点,那么我们就需要向左子树进行递归删除。
- 如果第 4 步也没有删除结点,则应当向右子树进行递归删除。
代码分析(三个操作合成一起)
package com.tian.binaryTree;
import org.w3c.dom.Node;
//二叉树的遍历
public class BinaryTree01 {
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, "赛罗");
binaryTree.setRoot(root);
//先手动创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
System.out.println("================二叉树的遍历=====================");
System.out.println("前序遍历的结果是:");
binaryTree.preOrder();
System.out.println("后序遍历的结果是:");
binaryTree.lastOrder();
System.out.println("中序遍历的结果是:");
binaryTree.middleOrder();
System.out.println("================二叉树的查找=====================");
System.out.println("前序遍历查找的结果是");
HeroNode resNode = binaryTree.preOrderSearch(3);
if (resNode != null) {
System.out.println("找到了,姓名:" + resNode.getName() + " -- 编号:" + resNode.getNo());
} else {
System.out.println("没有找到!");
}
System.out.println("中序遍历查找的结果是");
HeroNode resNode2 = binaryTree.middleOrderSearch(3);
if (resNode2 != null) {
System.out.println("找到了,姓名:" + resNode2.getName() + " -- 编号:" + resNode2.getNo());
} else {
System.out.println("没有找到!");
}
System.out.println("后序遍历查找的结果是");
HeroNode resNode3 = binaryTree.lastOrderSearch(3);
if (resNode3 != null) {
System.out.println("找到了,姓名:" + resNode3.getName() + " -- 编号:" + resNode3.getNo());
} else {
System.out.println("没有找到!");
}
System.out.println("================二叉树的删除=====================");
System.out.println("删除前,前序遍历的结果是:");
binaryTree.preOrder();
binaryTree.delNode(3);
System.out.println("删除后,前序遍历的结果是:");
binaryTree.preOrder();
}
}
//创建一个二叉树
class BinaryTree {
private HeroNode 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 middleOrder() {
if (this.root != null) {
this.root.middleOrder();
} else {
System.out.println("当前二叉树为空!");
}
}
//后序遍历
public void lastOrder() {
if (this.root != null) {
this.root.lastOrder();
} else {
System.out.println("当前二叉树为空!");
}
}
//前序查找
public HeroNode preOrderSearch(int no) {
if (root != null) {
return root.preOrderSearch(no);
} else {
return null;
}
}
//中序查找
public HeroNode middleOrderSearch(int no) {
if (root != null) {
return root.middleOrderSearch(no);
} else {
return null;
}
}
//后序查找
public HeroNode lastOrderSearch(int no) {
if (root != null) {
return root.lastOrderSearch(no);
} else {
return null;
}
}
//删除节点
public void delNode(int no) {
if (root != null) {
//如果只有一个root节点,这里立即判断root是不是就是要删除的节点
if (root.getNo() == no) {
root = null;
} else {
//递归删除
root.delNode(no);
}
} else {
System.out.println("树为空,不能进行删除!");
}
}
}
//创建节点
class HeroNode{
private int no;
private String name;
private HeroNode left; //左子节点
private HeroNode right; //右子节点
public HeroNode(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 HeroNode getLeft() {
return left;
}
public void setLeft(HeroNode left) {
this.left = left;
}
public HeroNode getRight() {
return right;
}
public void setRight(HeroNode right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode{" +
"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 middleOrder(){
//先递归遍历左子树
if (this.left != null){
this.left.middleOrder();
}
//再输出父节点
System.out.println(this);
//最后遍历右子树
if (this.right != null){
this.right.middleOrder();
}
}
//编写后序遍历的方法
public void lastOrder(){
//先递归遍历左子树
if (this.left != null){
this.left.lastOrder();
}
//再递归遍历右子树
if (this.right != null){
this.right.lastOrder();
}
//最后输出节点信息
System.out.println(this);
}
//前序查找
public HeroNode preOrderSearch(int no) {
System.out.println("进行前序遍历");
//判断当前节点是不是要查找的值
if (this.no == no) {
return this;
}
/*
1.如果不是,则判断当前节点的左子节点是否为空。如果不为空,则进行递归查找
2.如果左递归前序查找找到节点,则返回
*/
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.preOrderSearch(no);
}
//如果左子树找到
if (resNode != null){
return resNode;
}
/*
1.如果左递归没有找到,则继续进行判断
2.判断当前节点的右子节点是否为空,如果不为空,则继续向右进行递归判断
*/
if (this.right != null) {
resNode = this.right.preOrderSearch(no);
}
return resNode;
}
//中序查找
public HeroNode middleOrderSearch(int no) {
System.out.println("进行中序遍历");
//先判断当前节点的左子节点是否为空,如果不为空,则进行递归中序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.middleOrderSearch(no);
}
//如果找到则返回,如果没有找到则和当前节点进行比较,如果是当前节点就返回
if (resNode != null) {
return resNode;
}
if (this.no == no) {
return this;
}
//如果没有找到则继续向右递归中序查找
if (this.right != null) {
resNode = this.right.middleOrderSearch(no);
}
return resNode;
}
//后序查找
public HeroNode lastOrderSearch(int no) {
System.out.println("进行后序遍历");
//先判断当前节点的左子节点是否为空,如果不为空,则进行递归后序查找
HeroNode resNode = null;
if (this.left != null) {
resNode = this.left.lastOrderSearch(no);
}
//说明左子树找到
if (resNode != null) {
return resNode;
}
//如果左子树没有找到,则向右子树递归进行后序遍历查找
if (this.right != null) {
resNode = this.right.lastOrderSearch(no);
}
if (resNode != null) {
return resNode;
}
//如果左右子树都没有找到,就比较当前节点
if (this.no == no) {
return this;
}
return resNode;
}
//递归删除节点
/*
1.如果要删除的节点是叶子节点,则删除该节点
2.如果要删除的节点是非叶子节点,则删除该子树
*/
public void delNode(int no) {
//如果当前结点的左子结点不为空,并且左子结点就是要删除结点,就将节点置空
if (this.left != null && this.left.no == no) {
this.left = null;
return;
}
//如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将节点置空
if (this.right != null && this.right.no == no){
this.right = null;
return;
}
//向左子树进行递归删除
if (this.left != null) {
this.left.delNode(no);
}
//向右子树进行递归删除
if (this.right != null) {
this.right.delNode(no);
}
}
}
1.2、顺序存储二叉树
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组。
-
特点
- 顺序二叉树通常只考虑完全二叉树。
- 树中节点对应于数组中的下标:
- 第 n 个元素的左子节点为 2 * n + 1。
- 第 n 个元素的右子节点为 2 * n + 2。
- 第 n 个元素的父节点为 (n-1) / 2。
- n : 表示二叉树中的第几个元素(按 0 开始编号)。
-
顺序存储二叉树遍历
package com.tian.binaryTree;
//给你一个数组 {1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。
public class ArrBinaryTree01 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7};
//创建一个ArrBinaryTree
ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
System.out.println("前序遍历的结果为:");
arrBinaryTree.preOrder();
System.out.println("中序遍历的结果为:");
arrBinaryTree.middleOrder();
System.out.println("后序遍历的结果为:");
arrBinaryTree.lastOrder();
}
}
//实现顺序存储二叉树遍历
class ArrBinaryTree {
private int[] arr; //存储数据结点的数组
public ArrBinaryTree(int[] arr) {
this.arr = arr;
}
public void preOrder() {
this.preOrder(0);
System.out.println();
}
/**
* 完成顺序存储二叉树的前序遍历
* @param index 表示数组的下标
*/
public void preOrder(int index) {
//如果数组为空,或者数组的长度为0
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能进行遍历!");
}
//输出当前这个元素
System.out.print(arr[index] + " ");
//向左遍历递归
if ((index * 2 + 1) < arr.length) {
preOrder(index * 2 + 1);
}
//向右遍历递归
if ((index * 2 + 2) < arr.length) {
preOrder(index * 2 + 2);
}
}
public void middleOrder() {
this.middleOrder(0);
System.out.println();
}
//中序遍历
public void middleOrder(int index) {
//如果数组为空,或者数组的长度为0
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能进行遍历!");
}
//向左遍历递归
if ((index * 2 + 1) < arr.length) {
middleOrder(index * 2 + 1);
}
//输出当前这个元素
System.out.print(arr[index] + " ");
//向右遍历递归
if ((index * 2 + 2) < arr.length) {
middleOrder(index * 2 + 2);
}
}
public void lastOrder() {
this.lastOrder(0);
System.out.println();
}
//后序遍历
public void lastOrder(int index) {
//如果数组为空,或者数组的长度为0
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能进行遍历!");
}
//向左遍历递归
if ((index * 2 + 1) < arr.length) {
lastOrder(index * 2 + 1);
}
//向右遍历递归
if ((index * 2 + 2) < arr.length) {
lastOrder(index * 2 + 2);
}
//输出当前这个元素
System.out.print(arr[index] + " ");
}
}
1.3、线索化二叉树
1.3.1、问题提出
问题分析:
-
当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 } 。
-
但是 6, 8, 10, 14 这几个节点的 左右指针,并没有完全的利用上。
-
如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
-
解决方案-线索二叉树。
1.3.2、定义
- n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")。
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
- 一个结点的前一个结点,称为前驱结点。
- 一个结点的后一个结点,称为后继结点。
1.3.3、案列
将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}。
思路分析:
代码
package com.tian.binaryTree;
public class ThreadedBinaryTree01 {
public static void main(String[] args) {
HeroNode02 root = new HeroNode02(1, "迪迦");
HeroNode02 node2 = new HeroNode02(3, "戴拿");
HeroNode02 node3 = new HeroNode02(6, "盖亚");
HeroNode02 node4 = new HeroNode02(8, "阿古茹");
HeroNode02 node5 = new HeroNode02(10, "赛文");
HeroNode02 node6 = new HeroNode02(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号节点
HeroNode02 left = node5.getLeft();
System.out.println("10号节点的前序节点是:" + left);
System.out.println("=======================================================");
//遍历
System.out.println("使用线索化的方式来遍历线索化二叉树");
threadedBinaryTree.threadedList();
}
}
//创建一个二叉树,实现线索化功能的二叉树
class ThreadedBinaryTree {
private HeroNode02 root;
/*
为了实现线索化,需要创建一个指向当前节点的前驱节点的指针
在递归进行线索化时,pre总是保留前一个节点
*/
private HeroNode02 pre = null;
public void setRoot(HeroNode02 root) {
this.root = root;
}
//遍历线索化二叉树
public void threadedList() {
//定义一个变量,存储当前遍历的节点,从root开始
HeroNode02 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();
}
}
//编写对二叉树进行中序线索化的方法
public void threadedNodes() {
this.threadedNodes(root);
}
/**
*
* @param node 就是当前需要线索化的节点
*/
public void threadedNodes(HeroNode02 node) {
//如果node为空,就不能进行线索化
if (node == null) {
return;
}
//一、先线索化左子树
threadedNodes(node.getLeft());
//二、线索化当前节点
//1、处理当前节点的前驱节点
if (node.getLeft() == null) {
//就让当前节点的左指针指向前驱节点
node.setLeft(pre);
//修改当前节点的左指针的类型,表明已经指向了前驱节点
node.setLeftType(1);
}
//2、处理后继节点
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightType(1);
}
//每处理完一个节点之后,让当前节点是下一个节点的前驱节点
pre = node;
//三、再线索化右子树
threadedNodes(node.getRight());
}
//前序遍历
public void preOrder(){
if (this.root != null){
this.root.preOrder();
} else {
System.out.println("当前二叉树为空!");
}
}
//中序遍历
public void middleOrder() {
if (this.root != null) {
this.root.middleOrder();
} else {
System.out.println("当前二叉树为空!");
}
}
//后序遍历
public void lastOrder() {
if (this.root != null) {
this.root.lastOrder();
} else {
System.out.println("当前二叉树为空!");
}
}
//前序查找
public HeroNode02 preOrderSearch(int no) {
if (root != null) {
return root.preOrderSearch(no);
} else {
return null;
}
}
//中序查找
public HeroNode02 middleOrderSearch(int no) {
if (root != null) {
return root.middleOrderSearch(no);
} else {
return null;
}
}
//后序查找
public HeroNode02 lastOrderSearch(int no) {
if (root != null) {
return root.lastOrderSearch(no);
} else {
return null;
}
}
//删除节点
public void delNode(int no) {
if (root != null) {
//如果只有一个root节点,这里立即判断root是不是就是要删除的节点
if (root.getNo() == no) {
root = null;
} else {
//递归删除
root.delNode(no);
}
} else {
System.out.println("树为空,不能进行删除!");
}
}
}
//创建节点
class HeroNode02{
private int no;
private String name;
private HeroNode02 left; //左子节点
private HeroNode02 right; //右子节点
/*
1.如果leftType == 0,表示指向的是左子树,如果是1则表示指向前驱节点
2.如果rightType == 0,表示指向的是右子树,如果是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 HeroNode02(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 HeroNode02 getLeft() {
return left;
}
public void setLeft(HeroNode02 left) {
this.left = left;
}
public HeroNode02 getRight() {
return right;
}
public void setRight(HeroNode02 right) {
this.right = right;
}
@Override
public String toString() {
return "HeroNode{" +
"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 middleOrder(){
//先递归遍历左子树
if (this.left != null){
this.left.middleOrder();
}
//再输出父节点
System.out.println(this);
//最后遍历右子树
if (this.right != null){
this.right.middleOrder();
}
}
//编写后序遍历的方法
public void lastOrder(){
//先递归遍历左子树
if (this.left != null){
this.left.lastOrder();
}
//再递归遍历右子树
if (this.right != null){
this.right.lastOrder();
}
//最后输出节点信息
System.out.println(this);
}
//前序查找
public HeroNode02 preOrderSearch(int no) {
System.out.println("进行前序遍历");
//判断当前节点是不是要查找的值
if (this.no == no) {
return this;
}
/*
1.如果不是,则判断当前节点的左子节点是否为空。如果不为空,则进行递归查找
2.如果左递归前序查找找到节点,则返回
*/
HeroNode02 resNode = null;
if (this.left != null) {
resNode = this.left.preOrderSearch(no);
}
//如果左子树找到
if (resNode != null){
return resNode;
}
/*
1.如果左递归没有找到,则继续进行判断
2.判断当前节点的右子节点是否为空,如果不为空,则继续向右进行递归判断
*/
if (this.right != null) {
resNode = this.right.preOrderSearch(no);
}
return resNode;
}
//中序查找
public HeroNode02 middleOrderSearch(int no) {
System.out.println("进行中序遍历");
//先判断当前节点的左子节点是否为空,如果不为空,则进行递归中序查找
HeroNode02 resNode = null;
if (this.left != null) {
resNode = this.left.middleOrderSearch(no);
}
//如果找到则返回,如果没有找到则和当前节点进行比较,如果是当前节点就返回
if (resNode != null) {
return resNode;
}
if (this.no == no) {
return this;
}
//如果没有找到则继续向右递归中序查找
if (this.right != null) {
resNode = this.right.middleOrderSearch(no);
}
return resNode;
}
//后序查找
public HeroNode02 lastOrderSearch(int no) {
System.out.println("进行后序遍历");
//先判断当前节点的左子节点是否为空,如果不为空,则进行递归后序查找
HeroNode02 resNode = null;
if (this.left != null) {
resNode = this.left.lastOrderSearch(no);
}
//说明左子树找到
if (resNode != null) {
return resNode;
}
//如果左子树没有找到,则向右子树递归进行后序遍历查找
if (this.right != null) {
resNode = this.right.lastOrderSearch(no);
}
if (resNode != null) {
return resNode;
}
//如果左右子树都没有找到,就比较当前节点
if (this.no == no) {
return this;
}
return resNode;
}
//递归删除节点
/*
1.如果要删除的节点是叶子节点,则删除该节点
2.如果要删除的节点是非叶子节点,则删除该子树
*/
public void delNode(int no) {
//如果当前结点的左子结点不为空,并且左子结点就是要删除结点,就将节点置空
if (this.left != null && this.left.no == no) {
this.left = null;
return;
}
//如果当前结点的右子结点不为空,并且右子结点 就是要删除结点,就将节点置空
if (this.right != null && this.right.no == no){
this.right = null;
return;
}
//向左子树进行递归删除
if (this.left != null) {
this.left.delNode(no);
}
//向右子树进行递归删除
if (this.right != null) {
this.right.delNode(no);
}
}
}
2、堆排序
2.1、基本介绍
-
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序。
-
堆是具有以下性质的完全二叉树:
- 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆。
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
- 注意 : 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
-
大顶堆举例说明
- 大顶堆的特点:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] // i 对应第几个节点,i从0开始编号。
-
小顶堆举例说明
- 小顶堆特点:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] // i 对应第几个节点,i从0开始编号。
- 一般升序采用大顶堆,降序采用小顶堆。
2.2、基本思想
- 将待排序序列构造成一个大顶堆(根据需要进行选择)。
- 此时,整个序列的最大值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值。
- 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n 个元素的次小值。如此反复执行,便能得到一个有序序列了。
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
2.3、步骤图解
要求:给你一个数组 {4,6,8,5,9} , 要求使用堆排序法,将数组升序排序。
-
步骤一:构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
-
原始的数组 [4, 6, 8, 5, 9]
-
1、假设给定无序序列结构如下
-
- 2、此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,也就是下面的 6 结点),从左至右,从下至上进行调整。
- 3、找到第二个非叶节点 4,由于[4,9,8]中 9 元素最大,4 和 9 交换。
- 4、这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中 6 最大,交换 4 和 6。
-
此时,我们就将一个无序序列构造成了一个大顶堆。
-
步骤二:将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
- 1、将堆顶元素 9 和末尾元素 4 进行交换。
- 2、重新调整结构,使其继续满足堆定义。
- 3、再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8。
- 4、后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序。
2.4、思路总结
- 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
2.5、代码实现
package com.tian.binaryTree;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
int[] arr = {4, 6, 8, 5, 9};
heapSort(arr);
}
//编写一个堆排序的方法
public static void heapSort(int arr[]) {
int temp = 0;
System.out.println("堆排序");
//分步完成
/*
adjustHeap(arr, 1, arr.length);
System.out.println("第一次:" + Arrays.toString(arr)); //4, 9, 8, 5, 6
adjustHeap(arr, 0, arr.length);
System.out.println("第二次:" + Arrays.toString(arr)); //9, 6, 8, 5, 4
*/
//一次完成
//将无序序列构建成一个堆,根据升序降序需求选择大顶推或小顶堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, arr.length);
}
/*
2).将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
3).重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,
反复执行调整+交换步骤, 直到整个序列有序。
*/
for (int j = arr.length - 1; j > 0; j--) {
//交换
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
adjustHeap(arr, 0, j);
}
System.out.println("数组为:" + Arrays.toString(arr));
}
//将一个数组(二叉树)调整成一个大顶堆
/**
* 功能:完成将以 i 对应的非叶子节点的树调整成大顶堆
* 举例:int[] arr = {4, 6, 8, 5, 9}; i = 1 ==>
* adjustHeap调整之后 ==> {4, 9, 8, 5, 6};
* 如果我们再次调用adjustHeap传入的是 i = 0 ==>
* 得到{4, 9, 8, 5, 6} ==>{9, 6, 8, 5, 4}
* @param arr 表示待调整的数组
* @param i 表示非叶子节点在数组中的索引
* @param length 表示对多少个元素进行调整,length是在逐渐减少的
*/
public static void adjustHeap(int arr[], int i, int length) {
int temp = arr[i]; //先取出当前元素的值,保存在临时变量中
//说明
//1、k = i * 2 + 1,k是i节点的左子节点
for (int k = i * 2 + 1; k < length; k = i * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k + 1]) { //说明左子节点的值小于右子节点的值
k++; //指向右子节点
}
if (arr[k] > temp) { //如果子节点大于父节点
arr[i] = arr[k]; //把比较大的值赋值给当前节点
i = k; //i 指向 k,继续循环比较
} else {
break;
}
}
//当循环结束之后,我们已经将以i为父节点的数的最大值放在了最顶端
arr[i] = temp; //将temp值放到调整后的位置
}
}
3、赫夫曼树
3.1、基本介绍
- 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(Huffman Tree)。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
3.2、赫夫曼树几个重要概念
-
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。
通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1。
-
结点的权:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
-
结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
-
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
-
WPL最小的就是赫夫曼树。
3.3、赫夫曼树创建思路图解
给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树。
步骤:
-
从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树。
-
取出根节点权值最小的两颗二叉树。
-
组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和。
-
再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树。
3.4、代码实现
package com.tian.huffmanTree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
Node root = creatHuffmanTree(arr);
//测试
preOrder(root);
}
//前序遍历
public static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("空树不能遍历");
}
}
//创建赫夫曼树
public static Node creatHuffmanTree(int[] arr) {
//第一步
/*
1、遍历arr数组
2、将arr的每个元素都构成Node
3、将Node放入到ArrayLi中
*/
List<Node> nodes = new ArrayList<>();
for (int value : arr) {
nodes.add(new Node(value));
}
while (nodes.size() > 1) {
//排序 从小到大
Collections.sort(nodes);
System.out.println(nodes);
//取出根节点权值最小的两棵二叉树(单个节点也能被看成二叉树)
//(1) 取出权值最小的节点
Node leftNode = nodes.get(0);
//(2) 取出权值第二小的节点
Node rightNode = nodes.get(1);
//(3) 构建一颗新的二叉树
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//(4) 从ArrayList中删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//(5) 将parent加入到nodes中
nodes.add(parent);
}
//返回赫夫曼树的root节点
return nodes.get(0);
}
}
//创建节点类
class Node implements Comparable<Node>{
int value; //节点的权值
Node left; //指向左子节点
Node right; //指向右子节点
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
@Override
public int compareTo(Node o) {
//表示从小到大排序
return this.value - o.value;
}
}
4、赫夫曼编码
4.1、基本介绍
- 赫夫曼编码是一种编码方式, 属于一种程序算法。
- 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
- 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间。
- 赫夫曼码是可变字长编码(VLC)的一种。Huffman 于 1952 年提出一种编码方法,称之为最佳编码。
4.2、原理剖析
-
通信领域中信息的三种处理方式
-
定长编码
- 变长编码
- 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码。
- 赫夫曼编码
-
赫夫曼编码的实现步骤
- 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值。
-
根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为 0 向右的路径为 1 , 编码如下:
- o: 1000,u: 10010,d: 100110,y: 100111,i: 101,a : 110,k: 1110,e: 1111,j: 0000,v: 0001,l: 001,(空格): 01
-
按照上面的赫夫曼编码,我们的"i like like like java do you like a java"字符串对应的编码为 (注意这里我们使用的无损压缩)
- 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
- 通过赫夫曼编码处理 长度为 133。
-
原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%。
-
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性赫夫曼编码是无损处理方案。
-
注意:这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wp是一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样。
4.3、代码实现
package com.tian.huffmanCode;
import java.util.*;
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
byte[] contentBytes = content.getBytes();
byte[] huffmanCodeBytes = huffmanZip(contentBytes);
System.out.println("压缩之后的结果是:" + Arrays.toString(huffmanCodeBytes) + " 长度为:" + huffmanCodeBytes.length);
byte[] sourceBytes = decode(huffmanCodes, huffmanCodeBytes);
System.out.println("原来的字符串是:" + new String(sourceBytes));
/*
List<Node> nodes = getNodes(contentBytes);
System.out.println(nodes);
System.out.println("赫夫曼树");
Node root = creatHuffmanTree(nodes);
System.out.println("前序遍历");
root.preOrder();
//测试赫夫曼编码
Map<Byte, String> codes = getCodes(root);
System.out.println("生成的赫夫曼编码表" + codes);
byte[] zip = zip(contentBytes, huffmanCodes);
System.out.println("赫夫曼编码为:" + Arrays.toString(zip));
*/
}
//完成数据的解压
/*
1、将huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
重新转成赫夫曼编码对应的二进制的字符串
2、赫夫曼编码对应的二进制的字符串,按照赫夫曼编码转成原来的字符串 "i like like like java do you like a java"
*/
//编写一个方法,完成对压缩数据的解码
/**
*
* @param huffmanCodes 赫夫曼编码表Map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 就是原来的字符串对应的数组
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
//1、先得到huffmanBytes对应的二进制的字符串
StringBuilder stringBuilder = new StringBuilder();
//将byte数组转成二进制的字符串
for (int i = 0; i < huffmanBytes.length; i++) {
//先判断是不是最后一个字符
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
}
//把字符串按照指定的赫夫曼编码进行解码
//把赫夫曼编码表进行调换,也就是将Map中的key和value进行调换
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建一个集合,用于存放byte
List<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuilder.length();) {
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
//递增的取出 key
String key = stringBuilder.substring(i, i + count);//i 不动,让count移动,直到匹配到一个字符
b = map.get(key);
if (b == null) { //说明没有匹配到
count++;
} else { //匹配到
flag = false;
}
}
list.add(b);
i += count; //让i直接移动到count
}
//当for循环结束之后,list中就存放了所有的字符
//把list中的数据放入到byte[]中,并且返回
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 将一个byte转成一个二进制的字符串
* @param flag 标识是否需要补高位,如果是最后一个字节则无需补高位
* @param b 传入的byte
* @return 返回的是该b对应的二进制的字符串,(注意是:按二进制的补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {
//使用变量保存b
int temp = b; //将b转成int
//如果是正数我们还需要补高位
if (flag) {
temp |= 256; //按位与256 1 0000 0000 | 0000 0001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
if (flag) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
//封装操作
/**
*
* @param bytes 原始的字符串对应的字节数组
* @return 是经过赫夫曼编码处理之后的字节数组(压缩过后的字节数组)
*/
private static byte[] huffmanZip(byte[] bytes) {
List<Node> nodes = getNodes(bytes);
//根据nodes创建的赫夫曼树
Node root = creatHuffmanTree(nodes);
//对应的赫夫曼编码
Map<Byte, String> codes = getCodes(root);
//根据生成的赫夫曼编码,压缩得到压缩过后的赫夫曼编码字节数组
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
return huffmanCodeBytes;
}
//通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的 byte[]
/**
*
* @param bytes 这是原始的字符串对应的byte[]
* @param huffmanCodes 生成的赫夫曼编码Map
* @return 返回赫夫曼编码处理后的byte[]
*
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
/*
1、利用赫夫曼编码表将bytes数组转成赫夫曼编码对应的字符串,也就是将需要编码
的字符串"i like like like java do you like a java"转成使用赫夫曼编码的形式
*/
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes数组,根据某一个字符对应的二进制数在赫夫曼编码表中查找其对应的赫夫曼编码,
// 并将他们拼接成一个字符串,其中b就是某一个字符对应的二进制数,get()得到的就是其对应的赫夫曼编码
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
/*
得到的字符串为"10101000101111111100100010111111110010001011111111001001010011011
10001110000011011101000111100101000101111111100110001001010011011100"将其转为byte[]
*/
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//创建存储压缩后的 byte数组
byte[] huffmanCodeBytes = new byte[len];
int index = 0; //记录是第几个byte
for (int i = 0; i < stringBuilder.length(); i += 8) {
String strByte;
if (i + 8 > stringBuilder.length()) {
strByte = stringBuilder.substring(i);
} else {
strByte = stringBuilder.substring(i, i + 8);
}
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanCodeBytes;
}
//生成赫夫曼树对应的赫夫曼编码
//思路:
//1、将赫夫曼编码表存放在Map<Byte, String>中
//生成的赫夫曼编码表:{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011,
// 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
static Map<Byte, String> huffmanCodes = new HashMap<>();
//2、在生成的赫夫曼表中,需要去路径拼接起来,形成一个完整的路径
static StringBuilder stringBuilder = new StringBuilder();
//调用方便,重载
private static Map<Byte, String> getCodes(Node root) {
if (root == null) {
return null;
}
//处理root的左子树
getCodes(root.left, "0", stringBuilder);
//处理root的右子树
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
/**
*功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中
* @param node 需要传入的节点
* @param code 路径:左子节点是0,右子节点是1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringBuilder2中
stringBuilder2.append(code);
if (node != null) { //如果node为空则不进行处理
//判断当前节点是叶子节点还是非叶子节点
if (node.data == null) { //非叶子节点
//进行递归处理
//向左递归
getCodes(node.left, "0", stringBuilder2);
//向右递归
getCodes(node.right, "1", stringBuilder2);
} else { //是叶子节点
//就表示找到了某个叶子节点,也就是构建完成了该叶子节点的路径
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
//前序遍历
private static void preOrder(Node root) {
if (root != null) {
root.preOrder();
} else {
System.out.println("赫夫曼树为空");
}
}
public static List<Node> getNodes(byte[] bytes) {
//创建一个ArrayList
List<Node> nodes = new ArrayList<>();
//遍历bytes,统计每一个byte出现的次数 使用Map
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) { //表示是一次,map还没有这个数据
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
//把每一个键值对转成一个Node对象,并加入到nodes集合中
//遍历map
for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
//通过List创建对应的赫夫曼树
private static Node creatHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
//进行排序
Collections.sort(nodes);
//取出最小的元素
Node leftNode = nodes.get(0);
//取出第二小的元素
Node rightNode = nodes.get(1);
//将其构建成一颗二叉树,该节点没有data,只有权值
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//将已经处理完的节点从nodes中删除
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
//nodes最后的节点就是赫夫曼树的根节点
return nodes.get(0);
}
}
//创建Node,有数据和权值
class Node implements Comparable<Node>{
Byte data; //存放数据(字符)本身
int weight; //权值,表示字符出现的次数
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight; //从小到大排序
}
}
5、二叉排序树
5.1、背景
- 给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加
解决方案
- 1、数组
- 数组未排序, 优点:直接在数组尾添加;速度快。 缺点:查找速度慢。
- 数组排序,优点:可以使用二分查找,查找速度快;缺点:为了保证数组有序,在添加新数据时,找到插入位置后,后面的数据需整体移动,速度慢。
- 2、链式存储-链表
- 不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。
- 3、二叉排序树
5.2、基本介绍
-
二叉排序树:BST(Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
-
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点。
-
数列 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
5.3、二叉排序树的创建
- 一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为
5.4、二叉排序树的删除
-
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
- 删除叶子节点 (比如:2, 5, 9, 12)。
- 删除只有一颗子树的节点(比如:1)。
- 删除有两颗子树的节点(比如:7, 3,10 )。
-
思路分析
-
删除叶子节点
- 思路:
- 需要先去找到要删除的结点 targetNode。
- 找到 targetNode 的 父结点 parent。
- 确定 targetNode 是 parent 的左子结点还是右子结点。
- 根据前面的情况来对应删除。
- 左子结点 parent.left = null。
- 右子结点 parent.right = null。
-
删除只有一颗子树的节点
-
思路:
-
需要先去找到要删除的结点 targetNode。
-
找到 targetNode 的 父结点 parent。
-
确定 targetNode 的子结点是左子结点还是右子结点。
-
确定 targetNode 是 parent 的左子结点还是右子结点。
-
如果 targetNode 有左子结点
-
如果 targetNode 是 parent 的左子结点。
parent.left = targetNode.left;
-
如果 targetNode 是 parent 的右子结点。
parent.right = targetNode.left;
-
-
如果 targetNode 有右子结点
-
如果 targetNode 是 parent 的左子结点
parent.left = targetNode.right;
-
如果 targetNode 是 parent 的右子结点
parent.right = targetNode.right;
-
-
-
删除有两颗子树的节点
- 思路:
- 需要先去找到要删除的结点 targetNode。
- 找到 targetNode 的 父结点 parent。
- 从 targetNode 的右子树找到最小的结点 或者 在左子树中找到最大的节点。
- 用一个临时变量temp,将最小结点的值保存。
- 删除该最小结点。
- targetNode.value = temp;
-
5.5、代码实现
package com.tian.binarySortTree;
public class BinarySortTree01 {
public static void main(String[] args) {
int[] arr = {7, 3, 10, 12, 5, 1, 9, 2};
BinarySortTree binarySortTree = new BinarySortTree();
//循环添加节点
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//中序遍历
System.out.println("中序遍历二叉排序树");
binarySortTree.middleOrder();
//删除节点
System.out.println("======删除节点后======");
binarySortTree.delNode(12);
binarySortTree.delNode(5);
binarySortTree.delNode(10);
binarySortTree.delNode(2);
binarySortTree.delNode(3);
binarySortTree.delNode(9);
binarySortTree.delNode(1);
binarySortTree.delNode(7);
binarySortTree.middleOrder();
}
}
//创建二叉排序树
class BinarySortTree {
private Node root;
//查找要删除的节点
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
//查找父节点
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 1、返回的是以node为跟节点的二叉排序树的最小节点的值
* 2、删除node为根节点的二叉排序树的最小节点
*
* @param node 传入的节点(作为新二叉树的根节点)
* @return 返回的是以node为跟节点的二叉排序树的最小节点的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
//循环的查找左子节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
//这时target就指向了最小节点
//删除该最小节点
delNode(target.value);
return target.value;
}
//删除节点
public void delNode(int value) {
if (root == null) {
return;
} else {
//先找到待删除的节点
Node targetNode = search(value);
//如果没有找到待删除的节点
if (targetNode == null) {
return;
}
//如果当前二叉排序树只有一个节点
if (root.left == null && root.right == null) {
root = null;
return;
}
//找到targetNode的父节点
Node parent = searchParent(value);
//1、如果要删除的节点是叶子节点
if (targetNode.left == null && targetNode.right == null) {
//判断targetNode是父节点的左子节点,还是右子节点
if (parent.left != null && parent.left.value == value) {
parent.left = null;
} else if (parent.right != null && parent.right.value == value) {
parent.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) { //2、删除有两颗子树的节点
int minVal = delRightTreeMin(targetNode.right);
targetNode.value = minVal;
} else { //3、删除只有一颗子树的节点
//如果要删除的节点有左子节点
if (targetNode.left != null) {
if (parent != null) {
//如果targetNode是parent的左子节点
if (parent.left.value == value) {
parent.left = targetNode.left;
} else { //targetNode是parent的右子节点
parent.right = targetNode.left;
}
} else {
root = targetNode.left;
}
} else { //如果要删除的节点有右子节点
if (parent != null) {
//如果targetNode是parent的左子节点
if (parent.left.value == value) {
parent.left = targetNode.right;
} else { //targetNode是parent的右子节点
parent.right = targetNode.right;
}
} else {
root = targetNode.right;
}
}
}
}
}
//添加节点的方法
public void add(Node node) {
if (root == null) {
root = node;
} else {
root.add(node);
}
}
//中序遍历
public void middleOrder() {
if (root != null) {
root.middleOrder();
} else {
System.out.println("二叉排序树为空!");
}
}
}
//创建节点
class Node {
int value;
Node right;
Node left;
public Node(int value) {
this.value = value;
}
/**
* 查找要删除的节点
* @param value 希望要删除节点的值
* @return 如果找到该节点则返回该节点,否则返回null
*/
public Node search(int value) {
if (value == this.value) {
return this;
} else if (value < this.value){ //待删除节点的值小于当前节点,向左子树递归查找
//如果左子树为空
if (this.left == null) {
return null;
}
return this.left.search(value);
} else { //待删除节点的值不小于当前节点,向右子树递归查找
//如果右子树为空
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除节点的父节点
* @param value 要删除节点的值
* @return 返回的是要删除节点的父节点,没有就返回空
*/
public Node searchParent(int value) {
//如果当前节点就是要删除节点的父节点,就返回
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {
return this;
} else {
//如果查找的值小于当前节点的值,并且当前节点的左子节点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value);
} else if (value >= this.value && this.right != null) {
return this.right.searchParent(value);
} else {
return null; //没有找到父节点
}
}
}
//添加节点的方法
//递归的添加节点,注意需要满足二叉排序树的要求
public void add(Node node) {
if (node == null) {
return;
}
//判断传入的节点的值,和当前子树的根节点的值得关系
if (node.value < this.value) {
//如果当前节点左子节点为空
if (this.left == null) {
this.left = node;
} else {
//递归向左子树添加
this.left.add(node);
}
} else { //大于等于
if (this.right == null) {
this.right = node;
} else {
//递归向右子树添加
this.right.add(node);
}
}
}
//中序遍历
public void middleOrder() {
if (this.left != null) {
this.left.middleOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.middleOrder();
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
6、平衡二叉树
6.1、背景
- 给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在。
-
BST 存在的问题分析
- 左子树全部为空,从形式上看,更像一个单链表。
- 插入速度没有影响。
- 查询速度明显降低(因为需要依次比较), 不能发挥 BST的优势,因为每次还需要比较左子树,其查询速度比单链表还慢 。
-
解决方案 - 平衡二叉树(AVL)
6.2、基本介绍
- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树,可以保证查询效率较高。
- 具有以下特点:
- 它是一 棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
6.3、单旋转
6.3.1、左旋转
-
要求:给你一个数列,创建出对应的平衡二叉树.数列 {4,3,6,5,7,8}。
-
思路分析:
-
创建一个新的节点 newNode (以4这个值创建),创建一个新的节点,值等于当前根节点的值。
-
把新节点的左子树设置了当前节点的左子树。
newNode.left = left;
-
把新节点的右子树设置为当前节点的右子树的左子树。
newNode.right =right.left;
-
把当前节点的值换为右子节点的值。
value=right.value; -
把当前节点的右子树设置成右子树的右子树。
right=right.right;
-
把当前节点的左子树设置为新节点
left=newLeft;
-
-
代码实现
//左旋转的方法 private void leftRotate() { //创建新的节点,以当前根节点的值作为其值 Node newNode = new Node(value); //把新的节点的左子树设置成当前节点的左子树 newNode.left = left; //把新的节点的右子树设置成当前节点的右子树的左子树 newNode.right = right.left; //把当前节点的值替换成右子节点的值 value = right.value; //把当前节点的右子节点设置成当前节点右子树的右子节点 right = right.right; //把当前节点的左子节点设置成新的节点 left = newNode; }
6.3.2、右旋转
-
要求:给你一个数列,创建出对应的平衡二叉树.数列 {10,12, 8, 9, 7, 6}。
-
思路分析:
-
创建一个新的节点 newNode (以10这个值创建),创建一个新的节点,值等于当前根节点的值。
-
把新节点的右子树设置了当前节点的右子树。
newNode.right = right;
-
把新节点的左子树设置为当前节点的左子树的右子树。
newNode.left =left.right;
-
把当前节点的值换为左子节点的值。
value=left.value; -
把当前节点的左子树设置成左子树的左子树。
left=left.left;
-
把当前节点的右子树设置为新节点。
right=newLeft;
-
-
代码实现
//右旋转 private void rightRotate() { //创建一个新的节点 newNode (以10这个值创建),创建一个新的节点,值等于当前根节点的值。 Node newNode = new Node(value); //把新节点的右子树设置了当前节点的右子树。 newNode.right = right; //把新节点的左子树设置为当前节点的左子树的右子树。 newNode.left = left.right; //把当前节点的值换为左子节点的值。 value = left.value; //把当前节点的左子树设置成左子树的左子树。 left = left.left; //把当前节点的右子树设置为新节点。 right = newNode; }
6.3.3、双旋转
-
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列:
-
int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL 树。
-
int[] arr = {2,1,6,5,7,3}; 运行原来的代码可以看到,并没有转成 AVL 树。
-
-
思路分析:
- 当符合右旋转的条件时。
- 如果它的左子树的右子树高度大于它的左子树的高度。
- 先对当前这个结点的左节点进行左旋转。
- 在对当前结点进行右旋转的操作即可。
6.4、代码实现
-
代码实现AVL 树的汇总代码
package com.tian.avlTree; public class AVLTreeTest01 { public static void main(String[] args) { //int[] arr = {4, 3, 6, 5, 7, 8}; //int[] arr = {10, 12, 8, 9, 7, 6}; int[] arr = {10, 11, 7, 6, 8, 9}; //创建一个AVLTree AVLTree avlTree = new AVLTree(); //添加节点 for (int i = 0; i < arr.length; i++) { avlTree.add(new Node(arr[i])); } //遍历 System.out.println("中序遍历"); avlTree.middleOrder(); System.out.println(); System.out.println("=====在平衡处理后====="); System.out.println("树的高度为:" + avlTree.getRoot().height()); System.out.println("树的左子树的高度为:" + avlTree.getRoot().leftHeight()); System.out.println("树的右子树的高度为:" + avlTree.getRoot().rightHeight()); System.out.println("根节点为:" + avlTree.getRoot()); } } //创建AVLTree class AVLTree { private Node root; public Node getRoot() { return root; } //查找要删除的节点 public Node search(int value) { if (root == null) { return null; } else { return root.search(value); } } //查找父节点 public Node searchParent(int value) { if (root == null) { return null; } else { return root.searchParent(value); } } /** * 1、返回的是以node为跟节点的二叉排序树的最小节点的值 * 2、删除node为根节点的二叉排序树的最小节点 * * @param node 传入的节点(作为新二叉树的根节点) * @return 返回的是以node为跟节点的二叉排序树的最小节点的值 */ public int delRightTreeMin(Node node) { Node target = node; //循环的查找左子节点,就会找到最小值 while (target.left != null) { target = target.left; } //这时target就指向了最小节点 //删除该最小节点 delNode(target.value); return target.value; } //删除节点 public void delNode(int value) { if (root == null) { return; } else { //先找到待删除的节点 Node targetNode = search(value); //如果没有找到待删除的节点 if (targetNode == null) { return; } //如果当前二叉排序树只有一个节点 if (root.left == null && root.right == null) { root = null; return; } //找到targetNode的父节点 Node parent = searchParent(value); //1、如果要删除的节点是叶子节点 if (targetNode.left == null && targetNode.right == null) { //判断targetNode是父节点的左子节点,还是右子节点 if (parent.left != null && parent.left.value == value) { parent.left = null; } else if (parent.right != null && parent.right.value == value) { parent.right = null; } } else if (targetNode.left != null && targetNode.right != null) { //2、删除有两颗子树的节点 int minVal = delRightTreeMin(targetNode.right); targetNode.value = minVal; } else { //3、删除只有一颗子树的节点 //如果要删除的节点有左子节点 if (targetNode.left != null) { if (parent != null) { //如果targetNode是parent的左子节点 if (parent.left.value == value) { parent.left = targetNode.left; } else { //targetNode是parent的右子节点 parent.right = targetNode.left; } } else { root = targetNode.left; } } else { //如果要删除的节点有右子节点 if (parent != null) { //如果targetNode是parent的左子节点 if (parent.left.value == value) { parent.left = targetNode.right; } else { //targetNode是parent的右子节点 parent.right = targetNode.right; } } else { root = targetNode.right; } } } } } //添加节点的方法 public void add(Node node) { if (root == null) { root = node; } else { root.add(node); } } //中序遍历 public void middleOrder() { if (root != null) { root.middleOrder(); } else { System.out.println("二叉排序树为空!"); } } } //创建节点 class Node { int value; Node right; Node left; public Node(int value) { this.value = value; } //返回左子树的高度 public int leftHeight() { if (left == null) { return 0; } return left.height(); } //返回右子树的高度 public int rightHeight() { if (right == null) { return 0; } return right.height(); } //返回当前节点的高度(以该节点为根节点的数的高度) public int height() { return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1; } //左旋转的方法 private void leftRotate() { //创建新的节点,以当前根节点的值作为其值 Node newNode = new Node(value); //把新的节点的左子树设置成当前节点的左子树 newNode.left = left; //把新的节点的右子树设置成当前节点的右子树的左子树 newNode.right = right.left; //把当前节点的值替换成右子节点的值 value = right.value; //把当前节点的右子节点设置成当前节点右子树的右子节点 right = right.right; //把当前节点的左子节点设置成新的节点 left = newNode; } //右旋转 private void rightRotate() { //创建一个新的节点 newNode (以10这个值创建),创建一个新的节点,值等于当前根节点的值。 Node newNode = new Node(value); //把新节点的右子树设置了当前节点的右子树。 newNode.right = right; //把新节点的左子树设置为当前节点的左子树的右子树。 newNode.left = left.right; //把当前节点的值换为左子节点的值。 value = left.value; //把当前节点的左子树设置成左子树的左子树。 left = left.left; //把当前节点的右子树设置为新节点。 right = newNode; } /** * 查找要删除的节点 * @param value 希望要删除节点的值 * @return 如果找到该节点则返回该节点,否则返回null */ public Node search(int value) { if (value == this.value) { return this; } else if (value < this.value){ //待删除节点的值小于当前节点,向左子树递归查找 //如果左子树为空 if (this.left == null) { return null; } return this.left.search(value); } else { //待删除节点的值不小于当前节点,向右子树递归查找 //如果右子树为空 if (this.right == null) { return null; } return this.right.search(value); } } /** * 查找要删除节点的父节点 * @param value 要删除节点的值 * @return 返回的是要删除节点的父节点,没有就返回空 */ public Node searchParent(int value) { //如果当前节点就是要删除节点的父节点,就返回 if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) { return this; } else { //如果查找的值小于当前节点的值,并且当前节点的左子节点不为空 if (value < this.value && this.left != null) { return this.left.searchParent(value); } else if (value >= this.value && this.right != null) { return this.right.searchParent(value); } else { return null; //没有找到父节点 } } } //添加节点的方法 //递归的添加节点,注意需要满足二叉排序树的要求 public void add(Node node) { if (node == null) { return; } //判断传入的节点的值,和当前子树的根节点的值得关系 if (node.value < this.value) { //如果当前节点左子节点为空 if (this.left == null) { this.left = node; } else { //递归向左子树添加 this.left.add(node); } } else { //大于等于 if (this.right == null) { this.right = node; } else { //递归向右子树添加 this.right.add(node); } } //当添加完一个节点之后,如果:(右子树的高度 - 左子树的高度)> 1,左旋转 if (rightHeight() - leftHeight() > 1) { //如果它的右子树的左子树的高度大于它的右子树的右子树的高度 if (right != null && right.leftHeight() > right.rightHeight()) { //先对右子节点进行右旋转 right.rightRotate(); //再对当前节点进行左旋转 leftRotate(); } else { //直接左旋转即可 leftRotate(); } return; } //当添加完一个节点之后,如果:(左子树的高度 - 右子树的高度)> 1,右旋转 if (leftHeight() - rightHeight() > 1) { //如果它的左子树的右子树的高度大于它的左子树高度 if (left != null && left.rightHeight() > left.leftHeight()) { //先对当前节点的左子节点 --> 左旋转 left.leftRotate(); //再对当前节点进行右旋转 rightRotate(); } else { //直接右旋转即可 rightRotate(); } } } //中序遍历 public void middleOrder() { if (this.left != null) { this.left.middleOrder(); } System.out.println(this); if (this.right != null) { this.right.middleOrder(); } } @Override public String toString() { return "Node{" + "value=" + value + '}'; } }
7、多路查找树
7.1、二叉树与 B 树
7.1.1、二叉树的问题分析
二叉树的操作效率较高,但是也存在问题
- 二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如 1 亿), 就存在如下问题:
- 在构建二叉树时,需要多次进行 i/o 操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响。
- 节点海量,也会造成二叉树的高度很大,会降低操作速度。
7.1.2、多叉树
-
在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)。
-
2-3 树,2-3-4 树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
7.1.3、B 树的基本介绍
B 树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数来提升效率。
- 如图 B 树通过重新组织节点, 降低了树的高度。
- 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为 4k),这样每个节点只需要一次 I/O 就可以完全载入。
- 将树的度 M 设置为 1024,在 600 亿个元素中最多只需要 4 次 I/O 操作就可以读取到想要的元素, B 树(B+)广泛应用于文件存储系统以及数据库系统中。
7.2、2-3 树
-
2-3 树是最简单的 B 树结构, 具有如下特点:
- 2-3 树的所有叶子节点都在同一层(只要是 B 树都满足这个条件)。
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点。
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点。
- 2-3 树是由二节点和三节点构成的树。
-
将数列{16, 24, 12, 32, 14, 26, 34, 10, 8, 28, 38, 20} 构建成 2-3 树,并保证数据插入的大小顺序。(演示一下构建 2-3树的过程)
-
插入规则:
- 2-3 树的所有叶子节点都在同一层(只要是 B 树都满足这个条件)。
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点。
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点。
- 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面 3 个条件。
- 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则。
7.3、B 树、B+树和 B*树
7.3.1、B 树说明
- B 树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4。
- B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
- 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据。
- 搜索有可能在非叶子结点结束。
- 其搜索性能等价于在关键字全集内做一次二分查找。
7.3.2、B+树的介绍
B+树是 B 树的变体,也是一种多路搜索树。
- B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。
- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。
- 不可能在非叶子结点命中。
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层。
- B 树和 B+树各有自己的应用场景,不能说 B+树完全比 B 树好,反之亦然。
- 更适合文件索引系统。
7.3.3、B*树的介绍
B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针。
- B*树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为 2/3,而 B+树的块的最低使用率为的1/2。
- 从第 1 个特点我们可以看出,B*树分配新结点的概率比 B+树要低,空间使用率更高。