二叉树
为什么需要树?
- 数组的查找效率高,但是插入效率低。
- 链表的插入效率高,查找效率低。
需要一种数据结构来平衡查找与插入效率,使得查找速度和插入速度都能得到提升,因此有了树这种数据结构。
满二叉树
如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n为层数,则我们称为满二叉树。
完全二叉树
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树
二叉树的遍历
前序遍历
先遍历父节点,再遍历左子节点,最后遍历右子节点
中序遍历
先遍历左子节点,再遍历父节点,最后遍历右子节点
后序遍历
先遍历左子节点,再遍历右子节点,最后遍历父节点
可以看出,前中后的区别在于父节点遍历的时机
如下案例:
简单实现二叉树的代码如下:
package test.tree;
/**
*@ClassName BinaryTreeTest
*@Description 二叉树代码测试
*@Author tangjian
*@Date 2022/3/22 8:05 下午
*@Version 1.0
**/
public class BinaryTreeTest {
public static void main(String[] args) {
StuTreeNode root = new StuTreeNode(1,"学生1");
StuTreeNode stuTreeNode1 = new StuTreeNode(2,"学生2");
StuTreeNode stuTreeNode2 = new StuTreeNode(4,"学生4");
StuTreeNode stuTreeNode3 = new StuTreeNode(6,"学生6");
StuTreeNode stuTreeNode4 = new StuTreeNode(8,"学生8");
root.leftNode = stuTreeNode1;
root.rightNode = stuTreeNode2;
stuTreeNode2.leftNode = stuTreeNode3;
stuTreeNode2.rightNode = stuTreeNode4;
StuBinaryTree binaryTree = new StuBinaryTree();
binaryTree.root = root;
System.out.println("前序打印为:");
binaryTree.prePrint();
System.out.println("中序打印为:");
binaryTree.middlePrint();
System.out.println("后序打印为:");
binaryTree.afterPrint();
System.out.println("前序查找树中的学生8");
binaryTree.preSearch(9);
System.out.println("中序查找树中的学生8");
binaryTree.middleSearch(8);
System.out.println("后序查找树中的学生8");
binaryTree.afterSearch(8);
System.out.println("删除节点 8,然后前序打印 ");
binaryTree.deleteNode(8);
binaryTree.prePrint();
}
}
class StuBinaryTree {
StuTreeNode root;
public void prePrint() { //前序遍历打印
if (root == null) {
System.out.println("当前为空");
}
root.prePrint();
}
public void middlePrint() { //中序遍历打印
if (root == null) {
System.out.println("当前为空");
}
root.middlePrint();
}
public void afterPrint() { //后=序遍历打印
if (root == null) {
System.out.println("当前为空");
}
root.afterPrint();
}
public void preSearch(int id) { //前序查找数据元素
if (root == null) {
System.out.println("当前为空");
}
root.preSearch(id);
}
public void middleSearch(int id) { //中序查找数据元素
if (root == null) {
System.out.println("当前为空");
}
root.middleSearch(id);
}
public void afterSearch(int id) { //中序查找数据元素
if (root == null) {
System.out.println("当前为空");
}
root.afterSearch(id);
}
//删除节点
public void deleteNode(int id) {
if (root == null) {
System.out.println("当前为空");
}
if (root.id == id) {
System.out.println("已删除整个二叉树");
root = null;
}
root.deleteNode(id);
}
}
class StuTreeNode { //学生二叉树节点
int id;
String name;
StuTreeNode leftNode; //左节点
StuTreeNode rightNode; //右节点
public StuTreeNode(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "StuTreeNode{" +
"id=" + id +
", name=" + name +
'}';
}
public void prePrint() { //前序遍历打印
System.out.println(this); //先遍历当前自己
if (this.leftNode != null) {
this.leftNode.prePrint();
}
if (this.rightNode != null) {
this.rightNode.prePrint();
}
}
public void middlePrint() { //中序遍历打印
if (this.leftNode != null) {
this.leftNode.middlePrint();
}
System.out.println(this); //先遍历左子树再打印自己
if (this.rightNode != null) {
this.rightNode.middlePrint();
}
}
public void afterPrint() { //后=序遍历打印
if (this.leftNode != null) {
this.leftNode.afterPrint();
}
if (this.rightNode != null) {
this.rightNode.afterPrint();
}
System.out.println(this); //先遍历左子树和右子树后再打印自己
}
public void preSearch(int id) { //前序查找数据元素
if (this.id == id) {
System.out.println(this); //先遍历当前自己
return;
}
if (this.leftNode != null) {
this.leftNode.preSearch(id);
}
if (this.rightNode != null) {
this.rightNode.preSearch(id);
}
}
public void middleSearch(int id) { //中序查找数据元素
if (this.leftNode != null) {
this.leftNode.middleSearch(id);
}
if (this.id == id) {
System.out.println(this); //先遍历当前自己
return;
}
if (this.rightNode != null) {
this.rightNode.middleSearch(id);
}
}
public void afterSearch(int id) { //中序查找数据元素
if (this.leftNode != null) {
this.leftNode.afterSearch(id);
}
if (this.rightNode != null) {
this.rightNode.afterSearch(id);
}
if (this.id == id) {
System.out.println(this); //先遍历当前自己
return;
}
}
//前序删除节点
public void deleteNode(int id) {
//从要删除的父节点来进行删除,如果要删除的节点是左子节点,则将左子节点置位null,如果为右子节点,则将右子节点置位null
if (this.leftNode != null && this.leftNode.id == id) {
this.leftNode = null;
System.out.println("已删除该节点 ");
return;
}
if (this.rightNode != null && this.rightNode.id == id) {
this.rightNode = null;
System.out.println("已删除该节点");
return;
}
if (this.leftNode != null) {
this.leftNode.deleteNode(id);
}
if (this.rightNode != null) {
this.rightNode.deleteNode(id);
}
}
}
顺序存储二叉树
基本说明
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,如下图所示:
特点
- 顺序二叉树通常只考虑完全二叉树
- 第 n个元素的左子节点为 2 × n + 1
- 第 n个元素的右子节点为 2 × n + 2
- 第 n个元素的父节点为(n-1) ÷2
- 其中n 表示二叉树中的第几个元素(从0开始编号)
遍历过程和二叉树的遍历类似,只不过递归的条件有所不同
分析 以上二叉树的前,中,后序遍历应该为:
前序:1,2,4,5,3,6,7
中序: 4,2,5,1,6,3,7
后序:4,5,2,6,7,3,1
代码实现如下:
package test.tree;
/**
*@ClassName OrderBinaryTreeTest
*@Description 顺序二叉树代码测试,将数组转成二叉树
* - 第 n个元素的**左**子节点为 **2 × n + 1**
* - 第 n个元素的**右**子节点为 **2 × n + 2**
* - 第 n个元素的父节点为(n-1) ÷2
* - 其中**n** 表示二叉树中的第几个元素(从0开始编号)
*
*@Author tangjian
*@Date 2022/3/24 8:25 下午
*@Version 1.0
**/
public class OrderBinaryTreeTest {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
OrderBinaryTree binaryTree = new OrderBinaryTree(arr);
System.out.println("前序打印该顺序二叉树");
//前序打印为
binaryTree.prePrint(0);
//中序打印为
System.out.println("中序打印该顺序二叉树");
binaryTree.middlePrint(0);
//后序打印为
System.out.println("后序打印该顺序二叉树");
binaryTree.afterPrint(0);
}
}
class OrderBinaryTree {
int[] treeArr;
public OrderBinaryTree(int[] treeArr) {
this.treeArr = treeArr;
}
//前序遍历整个数组
public void prePrint(int index) {
if (treeArr == null && treeArr.length == 0) {
System.out.println("当前顺序二叉树为null");
}
System.out.println(treeArr[index]);
if ((2 * index + 1) < treeArr.length) { //2 × n + 1 遍历左子节点
prePrint(2 * index + 1);
}
if ((2 * index + 2) < treeArr.length) { //2 × n + 1 遍历左子节点
prePrint(2 * index + 2);
}
}
//中序遍历整个数组
public void middlePrint(int index) {
if (treeArr == null && treeArr.length == 0) {
System.out.println("当前顺序二叉树为null");
}
if ((2 * index + 1) < treeArr.length) { //2 × n + 1 遍历左子节点
middlePrint(2 * index + 1);
}
System.out.println(treeArr[index]);
if ((2 * index + 2) < treeArr.length) { //2 × n + 1 遍历左子节点
middlePrint(2 * index + 2);
}
}
//后序遍历整个数组
public void afterPrint(int index) {
if (treeArr == null && treeArr.length == 0) {
System.out.println("当前顺序二叉树为null");
}
if ((2 * index + 1) < treeArr.length) { //2 × n + 1 遍历左子节点
afterPrint(2 * index + 1);
}
if ((2 * index + 2) < treeArr.length) { //2 × n + 1 遍历左子节点
afterPrint(2 * index + 2);
}
System.out.println(treeArr[index]);
}
}
哈夫曼树
基本介绍
-
给定 n个权值作为 n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)
-
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近
重要概念
-
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为 L-1
-
结点的权及带权路径长度
1.若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
2.结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积(W×L)
-
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和(W1×L1+W2×L2…),记为WPL(weighted pathlength) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
-
WPL最小的就是哈夫曼树
最优二叉树图解(哈夫曼树)
1、先将数组进行排序
2、然后将数组的前两个最小的元素取出组成左子树和右子树,然后将两者的权值相加组成一个新的二叉树,然后将相加后的权值添加到数组中;
3、然后再次排序,重复上述的1,2操作,直到数组中只剩下最后一个元素
代码实现:
package test.tree;
import java.util.Collections;
import java.util.LinkedList;
/**
*@ClassName HafumanBinaryTreeTest
*@Description 哈夫曼树 -- 案例思路代码整理实现
* 1.
*@Author tangjian
*@Date 2022/3/28 10:30 上午
*@Version 1.0
**/
public class HafumanBinaryTreeTest {
public static void main(String[] args) {
int[] arr = {16,18,3,4,8,25};
Node root = createHfmTree(arr);
root.prePrint();
}
public static Node createHfmTree(int[] arr) {
LinkedList<Node> linkedList = new LinkedList<>();
for (int i : arr) {
linkedList.add(new Node(i));
}
while (linkedList.size() > 1) {
//先排序
Collections.sort(linkedList);
//取出前两个节点
Node left = linkedList.getFirst();
linkedList.removeFirst();
Node right = linkedList.getFirst();
linkedList.removeFirst();
Node newNode = new Node(left.value + right.value);
newNode.left = left;
newNode.right = right;
linkedList.add(newNode);
}
return linkedList.getFirst();
}
}
class Node implements Comparable<Node> {
Node left; //左子节点
Node right; //右子节点
int value; //权值
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
return this.value - o.value;
}
public void prePrint() { //前序遍历打印
System.out.println(this); //先遍历当前自己
if (this.left != null) {
this.left.prePrint();
}
if (this.right != null) {
this.right.prePrint();
}
}
}
哈夫曼编码
java中的ACSII码
将哈夫曼树的左子树的路径设置为0,将哈夫曼树的右子树的路劲设置为1
代码实现如下:
package test.tree;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
/**
*@ClassName HafumanCodeTest
*@Description 哈夫曼编码生成
* 1. 将字符串转成byte数组,统计每个Code的value权重和字节c
* 2. 转成哈夫曼树
* 3. 然后递归获取哈夫曼编码
*@Author tangjian
*@Date 2022/3/31 5:10 下午
*@Version 1.0
**/
public class HafumanCodeTest {
public static Map<Byte, String> hfmCodeMap = new HashMap<>();
public static void main(String[] args) {
Map<Byte, Integer> fine = tranToByteArray("i am am am fine");
System.out.println("字符串fine为:");
System.out.println(fine);
Code code = toCreateHfmTree(fine);
System.out.println("哈夫曼树的前序遍历为:");
code.prePrint();
HashMap<Byte, String> map = new HashMap<>();
getHfmCode(code, "", new StringBuffer(), map);
System.out.println("该字符串对应的哈夫曼编码集为:");
System.out.println(map);
}
public static Map<Byte, Integer> tranToByteArray(String source) {
byte[] bytes = source.getBytes();
Map<Byte, Integer> map = new HashMap<>();
for (byte b : bytes) {
if (map.containsKey(b)) {
map.put(b, map.get(b) + 1);
} else {
map.put(b, 1);
}
}
return map;
}
public static Code toCreateHfmTree(Map<Byte, Integer> map) {
LinkedList<Code> codes = new LinkedList<>();
Iterator<Map.Entry<Byte, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Byte, Integer> next = iterator.next();
codes.add(new Code(next.getKey(), next.getValue()));
}
while (codes.size() > 1) {
//先排序
Collections.sort(codes);
//取出前两个节点
Code left = codes.getFirst();
codes.removeFirst();
Code right = codes.getFirst();
codes.removeFirst();
Code newCode = new Code(null, left.value + right.value);
newCode.left = left;
newCode.right = right;
codes.add(newCode);
}
return codes.getFirst();
}
//递归获取哈夫曼编码集
public static void getHfmCode(Code code, String dir, StringBuffer str, Map<Byte, String> map) {
StringBuffer strBuf = new StringBuffer(str);
strBuf.append(dir);
if (code != null) {
if (code.c == null) {
getHfmCode(code.left, "0", strBuf, map);
getHfmCode(code.right, "1", strBuf, map);
} else {
map.put(code.c, strBuf.toString());
}
}
}
}
class Code implements Comparable<Code> {
Code left; //左子节点
Code right; //右子节点
int value; //权值
Byte c;
public Code(Byte c, int value) {
this.value = value;
this.c = c;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
", c=" + c +
'}';
}
@Override
public int compareTo(Code o) {
return this.value - o.value;
}
public void prePrint() { //前序遍历打印
System.out.println(this); //先遍历当前自己
if (this.left != null) {
this.left.prePrint();
}
if (this.right != null) {
this.right.prePrint();
}
}
}
打印如下:
字符串fine为:
{32=4, 97=3, 101=1, 102=1, 105=2, 109=3, 110=1}
哈夫曼树的前序遍历为:
Node{value=15, c=null}
Node{value=6, c=null}
Node{value=3, c=109}
Node{value=3, c=null}
Node{value=1, c=110}
Node{value=2, c=105}
Node{value=9, c=null}
Node{value=4, c=32}
Node{value=5, c=null}
Node{value=2, c=null}
Node{value=1, c=101}
Node{value=1, c=102}
Node{value=3, c=97}
该字符串对应的哈夫曼编码集为:
{32=10, 97=111, 101=1100, 102=1101, 105=011, 109=00, 110=010}