网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容 |
如果右边没有就找找左边 |
一、二叉树
为什么需要树 |
- 数组的优点是,通过下标访问,速度快,有序数组,可以通过二分查找提高检索效率,但是如果要检索某个值,或者从中间插入一个值,需要移动整体,效率就很低了
- 链式存储优点是,一定程度上优化数组存储方式,插入和删除效率提高,不用整体移动了,但是检索时效率却异常的低,比如检索特定的一个值,需要遍历整个链表
- 树,就是提高数据存储和读取的效率,比如二叉排序树,既可以保证数据检索速度,又保证数据插入,删除,修改速度。
树的概念 |
- 下图中有一个错误,H、E、F、G没有子节点,称为叶子节点,图中将H写成了A
二叉树概念 |
- 二叉树就是每个节点最多有两个子节点,而形成的树形数据结构
- 如果二叉树的所有叶子节点(没有子节点的节点称为叶子节点)都在最后一层,并且节点总数=2^n-1(n为层数),则称为满二叉树。
- 如果二叉树的所有叶子节点都在倒数第一或倒数第二层,最后一层的叶子节点在左边连续,倒数第二层叶子节点在右边连续,称为完全二叉树
二叉树遍历:前序、中序、后序遍历 |
以这颗树为例
- 前序遍历:先输出父节点,再遍历左子树和右子树
- 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
- 根据父节点的顺序即可判断是那种遍历,父节点最先遍历就是前序,父节点在左子树后面遍历就是中序,父节点最后遍历就是后序
- 通过遍历,我们就可以写出查找,删除的代码
- 运行效果
- 代码
public class Test {
public static void main(String[] args) {
BinaryTreeNode[] binaryTreeNodes = new BinaryTreeNode[7];
for(int i = 0 ;i<binaryTreeNodes.length;i++){
binaryTreeNodes[i] = new BinaryTreeNode(i,i+1);
}
//人为构建二叉树
binaryTreeNodes[0].setLeftNode(binaryTreeNodes[1]);
binaryTreeNodes[0].setRightNode(binaryTreeNodes[2]);
binaryTreeNodes[1].setLeftNode(binaryTreeNodes[3]);
binaryTreeNodes[1].setRightNode(binaryTreeNodes[4]);
binaryTreeNodes[2].setLeftNode(binaryTreeNodes[5]);
binaryTreeNodes[2].setRightNode(binaryTreeNodes[6]);
BinaryTree binaryTree = new BinaryTree(binaryTreeNodes[0]);
System.out.println("前序遍历================");
binaryTree.preOrder();
System.out.println("中序遍历================");
binaryTree.midOrder();
System.out.println("后序遍历================");
binaryTree.postOrder();
System.out.println("前序查找id为5节点"+binaryTree.preFind(5));
System.out.println("中序查找id为5节点"+binaryTree.midFind(5));
System.out.println("后序查找id为5节点"+binaryTree.postFind(5));
System.out.println("删除id为5节点");
binaryTree.deleteTreeById(5);
binaryTree.preOrder();
System.out.println("删除id为2的节点,包括它的所有子节点");
binaryTree.deleteTreeById(2);
binaryTree.preOrder();
}
}
/**
* 二叉树节点
*/
class BinaryTreeNode{
private int id;
private Object data;
private BinaryTreeNode leftNode;
private BinaryTreeNode rightNode;
public BinaryTreeNode(int id, Object data) {
this.id = id;
this.data = data;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public BinaryTreeNode getLeftNode() {
return leftNode;
}
public void setLeftNode(BinaryTreeNode leftNode) {
this.leftNode = leftNode;
}
public BinaryTreeNode getRightNode() {
return rightNode;
}
public void setRightNode(BinaryTreeNode rightNode) {
this.rightNode = rightNode;
}
@Override
public String toString() {
return "BinaryTreeNode{" +
"id=" + id +
", data=" + data +
'}';
}
/**
* 前序遍历,先输出当前节点,然后遍历左子树,然后遍历右子树
*/
public void preOrder(){
//1. 先输出当前节点
System.out.println(this+" ");
//2. 如果左子树存在,遍历左子树
if(this.leftNode!=null){
this.leftNode.preOrder();
}
//3. 如果右子树存在,遍历右子树
if(this.rightNode!=null){
this.rightNode.preOrder();
}
}
/**
* 前序查找,先判断当前节点是否是查找的节点,不是遍历左子树,左子树没找到遍历右子树,都没找到返回null
*/
public BinaryTreeNode preFind(Integer id){
//1. 先判断当前节点
if(id == this.id) return this;
BinaryTreeNode node = null;//用来保存结果
//2. 如果左子树存在,遍历左子树
if(this.leftNode!=null){
node = this.leftNode.preFind(id);
}
if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
return node;
}
//3. 如果右子树存在,遍历右子树
if(this.rightNode!=null){
node = this.rightNode.preFind(id);
}
return node;
}
/**
* 中序遍历,先遍历左子树,然后输出当前节点,然后遍历右子树
*/
public void midOrder(){
//1. 如果左子树存在,遍历左子树
if(this.leftNode!=null){
this.leftNode.midOrder();
}
//2. 然后输出当前节点
System.out.println(this+" ");
//3. 如果右子树存在,遍历右子树
if(this.rightNode!=null){
this.rightNode.midOrder();
}
}
/**
* 中序查找
*/
public BinaryTreeNode midFind(Integer id){
BinaryTreeNode node = null;//用来保存结果
//1. 如果左子树存在,遍历左子树
if(this.leftNode!=null){
node = this.leftNode.preFind(id);
}
if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
return node;
}
//2. 没有左子树了,判断是否和自己一致
if(id == this.id) return this;
//3. 如果右子树存在,遍历右子树
if(this.rightNode!=null){
node = this.rightNode.preFind(id);
}
return node;
}
/**
* 后序遍历,先遍历左子树,然后遍历右子树,最后输出当前节点
*/
public void postOrder(){
//1. 如果左子树存在,遍历左子树
if(this.leftNode!=null){
this.leftNode.postOrder();
}
//2. 如果右子树存在,遍历右子树
if(this.rightNode!=null){
this.rightNode.postOrder();
}
//3. 然后输出当前节点
System.out.println(this+" ");
}
/**
* 后序查找
*/
public BinaryTreeNode postFind(Integer id){
BinaryTreeNode node = null;//用来保存结果
//1. 如果左子树存在,遍历左子树
if(this.leftNode!=null){
node = this.leftNode.preFind(id);
}
if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
return node;
}
//2. 如果右子树存在,遍历右子树
if(this.rightNode!=null){
node = this.rightNode.preFind(id);
}
if(node != null){//如果node不为null,那么表示在右子树找到了,直接返回,无需执行下面找自己的代码
return node;
}
//3. 判断是否和自己一致
if(id == this.id) return this;
return node;
}
/**
* 根据id删除以它为根的子树,就是比如我们要删除5这个结点,那么我们删除它本身以及它所有子节点
* 因为树是单向数据结构,我们如果找到这个结点本身,无法回到它的父节点断开引用链接,所以我们判断条件为子节点的id是否相匹配
* 也就是说,这里无法判断root是否是要删除的节点,也没办法删除它
*/
public void deleteTreeById(Integer id){
//1、 判断左节点是否就是要删除的节点,如果是直接断开链接
if(this.leftNode != null && this.leftNode.id == id) {
this.leftNode = null;
return;
}
//2、 判断右节点是否是要删除结点
if(this.rightNode != null &&this.rightNode.id == id){
this.rightNode = null;
return;
}
//3、如果左右结点都不是要删除的节点,那么递归
if(this.leftNode != null ) this.leftNode.deleteTreeById(id);
if(this.rightNode != null ) this.rightNode.deleteTreeById(id);
}
}
/**
* 二叉树
*/
class BinaryTree{
private BinaryTreeNode root;//根节点,只要有根节点,就可以找到整棵树
public BinaryTree(BinaryTreeNode root) {
this.root = root;
}
/**
* 判断二叉树是否为空
* @return true 二叉树为空,false 二叉树不为空
*/
public boolean isNull(){
if(root == null){
System.out.println("二叉树为空!!!");
return true;
}
return false;
}
//前序遍历
public void preOrder(){
if(isNull()) return;//如果二叉树为空,不执行遍历
root.preOrder();
}
//中序遍历
public void midOrder(){
if(isNull()) return;
root.midOrder();
}
//后序遍历
public void postOrder(){
if(isNull()) return;
root.postOrder();
}
//前序查找
public BinaryTreeNode preFind(Integer id){
if(isNull()) return null;//如果二叉树为空,不执行遍历
return root.preFind(id);
}
//中序查找
public BinaryTreeNode midFind(Integer id){
if(isNull()) return null;
return root.midFind(id);
}
//后序查找
public BinaryTreeNode postFind(Integer id){
if(isNull()) return null;
return root.postFind(id);
}
/**
* 根据id删除子树
*/
public void deleteTreeById(Integer id){
if (isNull()) return;
if(root.getId() == id){//如果根节点就是要删除的节点,那么直接置为空
root = null;
}else{//否则进行root的子树删除
root.deleteTreeById(id);
}
}
}
1. 顺序存储二叉树
顺序存储二叉树 |
- 以数组存放树的节点,遍历数组时,依旧按照前序,中序和后序的顺序遍历
- 顺序二叉树通常只考虑完全二叉树
- 第n个元素的左子节点为2n+1,右子节点为2n+2,父节点为(n-1)/2. —n表示二叉树的第几个元素,从0开始
- 有啥用?
- 八大排序算法的堆排序,就会用到,包括类似的场景都可以用,很好用,后面会讲到
- 代码
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5, 6, 7 };
//创建一个 ArrBinaryTree
ArrBinaryTree arrBinaryTree = new ArrBinaryTree(arr);
arrBinaryTree.preOrder(); // 1,2,4,5,3,6,7
}
}
//编写一个ArrayBinaryTree, 实现顺序存储二叉树遍历
class ArrBinaryTree {
private int[] arr;//存储数据结点的数组
public ArrBinaryTree(int[] arr) {
this.arr = arr;
}
//重载preOrder
public void preOrder() {
this.preOrder(0);
}
//编写一个方法,完成顺序存储二叉树的前序遍历
/**
*
* @param index 数组的下标
*/
public void preOrder(int index) {
//如果数组为空,或者 arr.length = 0
if(arr == null || arr.length == 0) {
System.out.println("数组为空,不能按照二叉树的前序遍历");
}
//输出当前这个元素
System.out.println(arr[index]);
//向左递归遍历
if((index * 2 + 1) < arr.length) {
preOrder(2 * index + 1 );
}
//向右递归遍历
if((index * 2 + 2) < arr.length) {
preOrder(2 * index + 2);
}
}
}
2. 线索化二叉树
线索化二叉树 |
- 解决了指针浪费问题
- 让空闲的指针指向自己的前后节点
- 代码
- 首先我们再节点中添加两个属性,分别代表左、右指针的状态,看它是指向子树,还是前驱或后继
- 然后,我们再二叉树中,创建一个属性,代表当前节点的上一个节点
- 编写方法并测试(注意,线索化二叉树,需要使用新的遍历方式,线性遍历,效率更高,结果和非线索化二叉树遍历结果一样)
//为了实现线索化,需要创建要给指向当前结点的前驱结点的指针
//在递归进行线索化时,pre 总是保留前一个结点
private BinaryTreeNode pre = null;
//重载一把threadedNodes方法
public void threadedNodes() {
this.threadedNodes(root);
}
//编写对二叉树进行中序线索化的方法
/**
*
* @param node 就是当前需要线索化的结点
*/
public void threadedNodes(BinaryTreeNode node) {
//如果node==null, 不能线索化
if(node == null) {
return;
}
//(一)先线索化左子树
threadedNodes(node.getLeftNode());
//(二)线索化当前结点[有难度]
//处理当前结点的前驱结点
//以8结点来理解
//8结点的.left = null , 8结点的.leftType = 1
if(node.getLeftNode() == null) {
//让当前结点的左指针指向前驱结点
node.setLeftNode(pre);
//修改当前结点的左指针的类型,指向前驱结点
node.setLeftType(1);
}
//处理后继结点
if (pre != null && pre.getRightNode() == null) {
//让前驱结点的右指针指向当前结点
pre.setRightNode(node);
//修改前驱结点的右指针类型
pre.setRightType(1);
}
//!!! 每处理一个结点后,让当前结点是下一个结点的前驱结点
pre = node;
//(三)在线索化右子树
threadedNodes(node.getRightNode());
}
//遍历线索化二叉树的方法
public void threadedList() {
//定义一个变量,存储当前遍历的结点,从root开始
BinaryTreeNode node = root;
while(node != null) {
//循环的找到leftType == 1的结点,第一个找到就是8结点
//后面随着遍历而变化,因为当leftType==1时,说明该结点是按照线索化
//处理后的有效结点
while(node.getLeftType() == 0) {
node = node.getLeftNode();
}
//打印当前这个结点
System.out.println(node+"left指向"+node.getLeftNode());
//如果当前结点的右指针指向的是后继结点,就一直输出
while(node.getRightType() == 1) {
//获取到当前结点的后继结点
node = node.getRightNode();
System.out.println(node+"right指向"+node.getRightNode());
}
//替换这个遍历的结点
node = node.getRightNode();
}
}
3. 二叉排序树(BST)
- BST(Binary Sort(Search) Tree),二叉排序树就是任何一个非叶子结点,要求左子结点的值比当前结点的值小,右子结点的值比当前结点的值大。如果有相同的值,可以将该结点放在左或右子结点
二叉树的构建、添加和删除 |
- 构建
- 比如将{7,3,10,12,5,1,9}构建成排序二叉树,依次遍历数据,第一个直接就是根结点,剩下的按照规则,小的在左边,大的在右边,依次添加即可
- 添加
- 添加就是从根结点开始往下找,如果他比根结点大,就比较右边,如果右子结点没值,直接添加,如果有,就和右子结点比较,依次类推
- 删除
- 删除叶子结点,比如2,5,9,12,直接删除即可
- 删除有一颗子树的节点,比如1,直接让它的子树来到它的位置,然后删掉自己
- 删除有两颗子树的节点,比如7,3,10,让它的右子树来到它的位置,然后删掉自己,然后让左子树按排序树规则添加到右子树,比如删除7,让10和它的子树到它的位置,然后3和它的子树添加到9的左子树
- 删除结点时,如果当前结点的子结点,就是要删除的结点,那么开始执行逻辑
- 首先判断要删除的结点时1,2,3种情况的那种,然后进行相应操作
代码 |
- 运行效果
- 代码
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {7,3,10,12,5,1,9};
System.out.println("将数组"+ Arrays.toString(arr)+"构建成二叉排序树=================");
BinarySortTree binarySortTree = new BinarySortTree(arr);
System.out.println("二叉排序树中序遍历结果为======================================");
binarySortTree.midOrder();
System.out.println("添加一个结点2");binarySortTree.add(2);binarySortTree.midOrder();
System.out.println("删除结点2");binarySortTree.deleteNode(2);binarySortTree.midOrder();
System.out.println("删除结点5");binarySortTree.deleteNode(5);binarySortTree.midOrder();
System.out.println("删除结点9");binarySortTree.deleteNode(9);binarySortTree.midOrder();
System.out.println("删除结点12");binarySortTree.deleteNode(12);binarySortTree.midOrder();
System.out.println("删除结点7");binarySortTree.deleteNode(7);binarySortTree.midOrder();
System.out.println("删除结点3");binarySortTree.deleteNode(3);binarySortTree.midOrder();
System.out.println("删除结点10");binarySortTree.deleteNode(10);binarySortTree.midOrder();
System.out.println("删除结点1");binarySortTree.deleteNode(1);binarySortTree.midOrder();
}
}
/**
* 二叉排序树结点
*/
class BinarySortTreeNode{
private int data;
private BinarySortTreeNode left;
private BinarySortTreeNode right;
public BinarySortTreeNode(int data) {
this.data = data;
}
/**
* 二叉排序树添加结点方法
* @param node 一个结点
*/
public void add(BinarySortTreeNode node){
if(node == null) return;//如果结点为null,不进行添加
//如果要添加结点比当前结点小,那么添加到左子结点
if(node.getData()<this.data){
if(this.left==null){//如果左子结点为空
this.left = node;//添加到左子结点
}else{//如果不为空
this.left.add(node);//进行递归,让左子树添加
}
}else if(node.getData()>this.data){//如果要添加结点比当前结点大,那么添加到右子结点
if(this.right==null){//如果右子结点为空,将结点添加
this.right = node;
}else {//不为空
this.right.add(node);//递归到右子树添加
}
}else{//如果要添加结点等于当前结点,添加到左右都可以
if(this.left==null){
this.left = node;
}else if(this.right == null){
this.left = node;
}
//如果左右子节点都有了,那么放在右子树
else{
this.right.add(node);
}
}
}
/**
* 中序遍历,先遍历左子树,然后输出当前节点,然后遍历右子树
*/
public void midOrder(){
//1. 如果左子树存在,遍历左子树
if(this.left!=null){
this.left.midOrder();
}
//2. 然后输出当前节点
System.out.print(this+" ");
//3. 如果右子树存在,遍历右子树
if(this.right!=null){
this.right.midOrder();
}
}
/**
* 中序查找指定数据的父节点
*/
public BinarySortTreeNode midFindParent(Integer id){
BinarySortTreeNode node = null;//用来保存结果
//1. 如果左子树存在,判断左子结点是否是要找的值,不是继续遍历左子树
if(this.left!=null&&this.left.getData()!=id){
node = this.left.midFindParent(id);
}
if(node != null){//如果node不为null,那么表示在左子树找到了,直接返回,无需执行下面找右子树的代码
return node;
}
//2. 没有左子树了,判断是否和自己一致
if(this.left!=null){
if(id==this.left.data) return this;
}
if(this.right!=null){
if(id==this.right.data) return this;
}
//3. 如果右子树存在,遍历右子树
if(this.right!=null&&this.right.getData()!=id){
node = this.right.midFindParent(id);
}
return node;
}
public int getData() { return data; }
public void setData(int data) { this.data = data; }
public BinarySortTreeNode getLeft() { return left; }
public void setLeft(BinarySortTreeNode left) { this.left = left; }
public BinarySortTreeNode getRight() { return right; }
public void setRight(BinarySortTreeNode right) { this.right = right; }
@Override
public String toString() { return "Node{" + "data=" + data + '}'; }
}
/**
* 二叉排序树
*/
class BinarySortTree{
private BinarySortTreeNode root;
public BinarySortTreeNode getRoot() { return root; }
public BinarySortTree(){}
//构造方法,通过数组直接构造排序二叉树
public BinarySortTree(int[] arr){ this.addArrayToBinarySortTree(arr); }
/**
* 添加结点方法
* @param node 一个结点
*/
public void add(BinarySortTreeNode node){
if(node == null) throw new RuntimeException("要添加结点为空!!!");
if(root == null) root = node;
else root.add(node);
}
/**
* 重载直接将一个数据添加到排序二叉树
* @param data
*/
public void add(int data){
add(new BinarySortTreeNode(data));
}
/**
* 查找指定结点的父节点
* @param data
* @return
*/
public BinarySortTreeNode findParentByData(int data){
if(root.getData() == data) return null;
return root.midFindParent(data);
}
/**
* 删除根结点
*/
private void deleteRoot(){
if(root.getLeft()==null&&root.getRight()==null){
root=null;
}else if(root.getLeft()!=null&&root.getRight()!=null){
BinarySortTreeNode left = root.getLeft();
root = root.getRight();
add(left);
}else if(root.getLeft()==null&&root.getRight()!=null){
root=root.getRight();
}else if(root.getLeft()!=null&&root.getRight()==null){
root=root.getLeft();
}
}
/**
* 删除非根结点
* @param node 要删除结点
* @param nodeParent 要删除结点的父结点
* @param flag 要删除结点是父结点的左子结点还是右子结点,左为true
*/
private void deleteNode(BinarySortTreeNode node,BinarySortTreeNode nodeParent,Boolean flag){
if (node.getLeft()==null&&node.getRight()==null){//如果要删除结点是叶子结点,直接删除
if(flag) nodeParent.setLeft(null);//如果它是父结点的左子结点,直接让父结点的left置为null
else nodeParent.setRight(null);//如果是右子结点,让right置为null
}else if(node.getLeft()!=null && node.getRight()==null){//如果要删除结点是只有右子树的结点
//让右子树到它的位置
if(flag) nodeParent.setLeft(node.getLeft());
else nodeParent.setRight(node.getRight());
}else if(node.getRight()!=null&&node.getLeft()==null){//如果要删除结点是只有左子树的结点
//让左子树到它的位置
if(flag)nodeParent.setLeft(node.getRight());
else nodeParent.setRight(node.getRight());
}else{//如果要删除结点拥有左右两个子树
//让右子树到它的位置,然后左子树按规则添加到右子树中
BinarySortTreeNode left = node.getLeft();//先记录左子树
if(flag){
nodeParent.setLeft(node.getRight());//然后让它的右子树到它原来的位置
node.getRight().add(left);//然后将左子树按规则添加到右子树
}else{
nodeParent.setRight(node.getRight());
node.getRight().add(left);
}
}
}
/**
* 删除指定单个结点,如果有一样的只删除一个
* @param data
*/
public void deleteNode(int data){
if(root == null) throw new RuntimeException("二叉排序树为空");
BinarySortTreeNode nodeParent = null;//要删除节点的父节点
BinarySortTreeNode node = null;//要删除的节点
boolean flag = true;//要删除节点是父节点左子结点True,还是右子结点False
if(root.getData()==data){//如果是根结点
deleteRoot();//删除根结点
return;
}
//获取要删除结点的父结点
nodeParent = findParentByData(data);
//获取要删除结点,并用flag记录结点时父结点的左子结点还是右子结点
if(nodeParent.getLeft()!=null&&nodeParent.getLeft().getData()==data){
node = nodeParent.getLeft();
flag = true;
}else{
node = nodeParent.getRight();
flag = false;
}
if(node==null){
System.out.println("没有找到要删除的节点");
return;
}else{
deleteNode(node,nodeParent,flag);
}
}
/**
* 中序遍历
*/
public void midOrder(){
if(root == null) System.out.println("二叉排序树为空!!!");
else {
root.midOrder();
System.out.println();
}
}
/**
* 通过数组添加元素到二叉树
* @param arr
*/
public void addArrayToBinarySortTree(int[] arr){
for(int i : arr){
BinarySortTreeNode binarySortTreeNode = new BinarySortTreeNode(i);
add(binarySortTreeNode);
}
}
}
4. 平衡二叉树(AVT)
平衡二叉树,就是更完善的二叉排序树,代码也是在二叉排序基础上添加了一些逻辑 |
- 平衡二叉树(AVL),又名平衡二叉搜索树,保证查询效率较高
- 它是一课空树或它的
左右两个子树的高度差的绝对值小于等于1
,并且左右两个子树都是一课平衡二叉树。满足这些特点就是平衡二叉树。(下图就是一课树,右子树高度比左子树高2,不满足,所以进行左旋转操作,后面会讲)
- 常用实现算法有红黑树、AVL算法、替罪羊树、Treap、伸展树等
为什么要有平衡二叉树 |
- 当我们要将一个递增的序列转换成二叉排序树时,那么会变成一个效率很低的单链表,因为需要判断左右子树
- 而平衡二叉树可以解决此类问题
左旋转(以右子树比左子树高度,高1以上,比如2,就需要左旋转,反之右旋转) |
右旋转 |
双旋转 |
- 某些序列,无法通过左旋转,或右旋转,转变为平衡二叉树,此时需要双旋转
代码,只贴出和二叉排序树不同的代码块 |
- 运行效果
- 代码(以下指出添加的代码,和修改的代码)
/**
* 返回左子树高度
*/
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;
}
/**
* 以当前结点为根结点进行左旋转
*/
public void leftRotate(){
//1.创建新结点,保存当前根结点
BinarySortTreeNode binarySortTreeNode = new BinarySortTreeNode(data);
//2.左子树变为新结点左子树
binarySortTreeNode.setLeft(left);
//3.右子树的左子树变为新结点的右子树
binarySortTreeNode.setRight(right.getLeft());
//4.让右子树上移成为根结点
data = right.getData();
right = right.getRight();
//5.新结点作为左子树
left = binarySortTreeNode;
}
/**
* 以当前结点为根结点进行右旋转
*/
public void rightRotate(){
//1.创建新结点,保存当前根结点
BinarySortTreeNode binarySortTreeNode = new BinarySortTreeNode(data);
//2.右子树变为新结点右子树
binarySortTreeNode.setRight(right);
//3.左子树的右子树变为新结点的左子树
binarySortTreeNode.setLeft(left.getRight());
//4.让左子树上移成为根结点
data = left.getData();
left = left.getLeft();
//5.新结点作为右子树
right = binarySortTreeNode;
}
/**
* 二叉排序树添加结点方法
* @param node 一个结点
*/
public void add(BinarySortTreeNode node){
if(node == null) return;//如果结点为null,不进行添加
//如果要添加结点比当前结点小,那么添加到左子结点
if(node.getData()<this.data){
if(this.left==null){//如果左子结点为空
this.left = node;//添加到左子结点
}else{//如果不为空
this.left.add(node);//进行递归,让左子树添加
}
}else if(node.getData()>this.data){//如果要添加结点比当前结点大,那么添加到右子结点
if(this.right==null){//如果右子结点为空,将结点添加
this.right = node;
}else {//不为空
this.right.add(node);//递归到右子树添加
}
}else{//如果要添加结点等于当前结点,添加到左右都可以
if(this.left==null){
this.left = node;
}else if(this.right == null){
this.left = 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();
}
return;
}
}
二、赫夫曼树
- 给定n个权值作为n个
叶子节点
,构造一棵二叉树,若该树的带权路径长度(wpl)
达到最小,称为最优二叉树
,又称哈夫曼树(Huffman Tree
),或霍夫曼树或赫夫曼树
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
- 13这个结点,权值是13,根结点层数为1的情况下,此结点的路径长度是2
- 这并不是一课赫夫曼树,请继续往下看
路径和路径长度 |
- 一棵树中,一个结点往下可以到达的孩子或孙子结点之间的通路,称为路径。
- 通路中分支的数目称为路径长度,若根节点层数为1,则根节点到第L层结点的路径长度为L-1
- 根结点层数为1,13这个结点层数为3,3-1=2,那么13这个结点路径长度为2
结点的权和带权路径长度 |
- 权:树中结点赋予一个具有某种含义的数值,称为该结点的权
- 结点带权路径长度:根节点到该结点之间的路径长度与该结点的权的乘积
- 依然以上面的图片为例,13这个
结点
的带权路径长度
就是路径长度
*权
=2 *13=26
树的带权路径长度 |
- 所有叶子结点的带权路径长度之和,即为WPL(weighted path length),权值越大的结点离根结点越近的二叉树才是最优二叉树。
- WPL最小的就是赫夫曼树
- 树的带权路径长度wpl是评价一课二叉树是否为赫夫曼树的指标,wpl最小的就是最赫夫曼树
树的wpl
就是所有叶子节点的带权路径长度相加
后的结果- 上图可以看出,中间的树,满足
权值越大的叶子结点离根节点越近(层数越靠近根结点层数)
的条件,那么它就是最优二叉树
1. 创建赫夫曼树
如何创建赫夫曼树 |
- 假定数列{13,7,8,3,29,6,1}要转换成一棵赫夫曼树
- 先从小到大排序,将每一个数据(就是一个结点),看成是一颗最简单的二叉树
- 取出根结点权值最小的两颗二叉树(第一次取,就是1和3这两颗)
- 组成一棵新的二叉树,此时新二叉树的根结点权值是前面两颗二叉树根结点的权值之和(第一次就是1和3合并,根结点权值为1+3=4,那么此时就有了一颗{1,4,3}的二叉树)
- 最后再将新二叉树,以根结点的权值大小再次排序(第一次就是{1,4,3}这颗和{6}这颗),然后不断重复3-4-5的步骤,直到数列中,所有数据都被处理,就得到了赫夫曼树
代码 |
- 运行效果
- 代码
import java.util.ArrayList;
import java.util.Collections;
public class Test {
public static void main(String[] args) {
int arr[] = {13,7,8,3,29,6,1};
HuffmanTreeNode huffmanTree = createHuffmanTree(arr);
System.out.println("====前序遍历结果====");
preOrder(huffmanTree);
}
/**
* 创建赫夫曼树
* @param arr 序列
*/
public static HuffmanTreeNode createHuffmanTree(int[] arr){
//1.将arr中每个元素构建成结点,放入一个集合中
ArrayList<HuffmanTreeNode> huffmanTreeNodes = new ArrayList<>();
for(int value:arr){
huffmanTreeNodes.add(new HuffmanTreeNode(value));
}
//不断重复 取结点---构成新二叉树----重新排序,直到集合中只剩下一个结点
while(huffmanTreeNodes.size()>1){
//2.排序,使用Collections接口的sort方法,对集合排序,要求集合中元素,必须实现Comparable接口,并从写compareTo方法,至于从小到大排,还是从大到小排,取决于compareTo方法的实现
Collections.sort(huffmanTreeNodes);
//3. 取出根结点最小的两颗二叉树,构建成新二叉树,然后删除最小两颗二叉树,将新二叉树放集合中,然后排序
HuffmanTreeNode leftNode = huffmanTreeNodes.get(0);//取出集合第一个,也就是最小的
HuffmanTreeNode rightNode = huffmanTreeNodes.get(1);//取出第二个,也就是第二小的
HuffmanTreeNode parent = new HuffmanTreeNode(leftNode, rightNode);//然后构建一个新二叉树
huffmanTreeNodes.remove(leftNode);//删除
huffmanTreeNodes.remove(rightNode);//删除
huffmanTreeNodes.add(parent);//将新二叉树放进集合
Collections.sort(huffmanTreeNodes);//排序
}
return huffmanTreeNodes.get(0);
}
/**
* 前序遍历赫夫曼树
*/
public static void preOrder(HuffmanTreeNode node){
if(node == null){
System.out.println("赫夫曼树为空树");
return;
}
node.preOrder();
}
}
/**
* 赫夫曼树结点类
* 继承Comparable接口,用来比较结点之间的权值,需要实现compareTo方法
*/
class HuffmanTreeNode implements Comparable<HuffmanTreeNode> {
private int value;//结点权值
private HuffmanTreeNode left;//左子结点
private HuffmanTreeNode right;//右子结点
/**
* 根据权值直接构建结点
* @param value
*/
public HuffmanTreeNode(int value) {
this.value = value;
}
/**
* 根据左右结点,构建新的二叉树,权值为左右结点的权值之和
* @param left
* @param right
*/
public HuffmanTreeNode(HuffmanTreeNode left,HuffmanTreeNode right){
this.left = left;
this.right = right;
this.value = left.getValue() + right.getValue();
}
@Override
public int compareTo(HuffmanTreeNode o) {
//从小到大排序,至于为什么,百度一下Comparable接口
return this.value - o.value;
//从大到小排
//return -(this.value - o.value);
}
/**
* 前序遍历,先输出当前节点,然后遍历左子树,然后遍历右子树
*/
public void preOrder(){
//1. 先输出当前节点
System.out.println(this+" ");
//2. 如果左子树存在,遍历左子树
if(this.left!=null){
this.left.preOrder();
}
//3. 如果右子树存在,遍历右子树
if(this.right!=null){
this.right.preOrder();
}
}
@Override
public String toString() {
return "HuffmanTreeNode{" +
"value=" + value +
'}';
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public HuffmanTreeNode getLeft() {
return left;
}
public void setLeft(HuffmanTreeNode left) {
this.left = left;
}
public HuffmanTreeNode getRight() {
return right;
}
public void setRight(HuffmanTreeNode right) {
this.right = right;
}
}
2. 赫夫曼编码
- 又称哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,一种程序算法
- 是赫夫曼树在电讯通讯中的经典应用之一
- 广泛应用于数据文件压缩,压缩率在20%~90%之间
- 是可变字长编码(VLC的一种),Huffman于1952年提出一种编码方法,称为最佳编码
首先看一下定长编码方式,就是将字符转换成Ascii码,然后取到对应2进制编码,传输 |
变长编码,就是统计每个字符出现的次数,次数越多,编码越小,然后按照编码,将其组成一串2进制编码,但是因为编码的前缀重复性,比如编码1和编码10,计算机不知道什么时候按1解析,什么时候按10解析,也就是说,不能匹配到重复编码 |
赫夫曼编码,将字符出现次数,作为权值,出现的越多,权值越大,根据权值构建赫夫曼树 |
- 根据权值创建赫夫曼树,向左的路径为0,向右的路径为1
- 然后根据创建好的赫夫曼树,给各字符规定编码,可以发现,每个字符的编码,都不是其它字符编码的前缀,比如o的编码是1000,u的编码10010,如果o的编码是100,那么确实是10010的前缀,但是此编码,不会出现这种情况,那么赫夫曼编码,满足前缀编码的特性,不会造成匹配多义性
- 额外的,如果出现权值相同的情况,比如有4个权值为4的二叉树,那么它们的位置并不固定,这样生成的赫夫曼树不一样,编码也完全不一样,但是wpl是一样的,所以如果出现这种情况也无伤大雅,压缩最后的结果长度,还是一样的
实现思路和代码 |
- 实现思路
- 赫夫曼树的结点,除了存放权值、左右子树,还要存放数据
- 得到"i like like like java do you like a java"对应的byte数组
- 生成结点,将结点放到List
- 创建赫夫曼树
- 获取赫夫曼编码,
- 获取字符串对应二进制编码,也就是压缩操作(压缩对应的byte数组,返回的也是byte数组)
- 解码,就是解压操作
- 运行效果
- 代码
import java.util.*;
public class Test {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
//> > 2. 得到"i like like like java do you like a java"对应的byte数组
byte[] strBytes = str.getBytes();
System.out.println("源字符串和源字节数组==========================");
System.out.println(str);
System.out.println("压缩前字节数组"+Arrays.toString(strBytes));
//获取压缩后字节数组和赫夫曼树编码表
List<Object> list = HuffmanZip(strBytes);
byte[] zip = (byte[]) list.get(0);//获取压缩后的字节数组
HashMap<Byte, String> byteStringHashMap =(HashMap<Byte, String>) list.get(1);//获取赫夫曼编码表
//> > 7. 解码,就是解压操作
byte[] bytes = huffmanUnZip(zip, byteStringHashMap);//获取解压后字节数组
System.out.println("最终解压效果"+new String(bytes));
}
/**
* 将字节数组压缩,返回压缩编码后的字节数组
* @param strBytes 初始的字节数组
* @return 压缩后的字节数组
*/
public static List<Object> HuffmanZip(byte[] strBytes){
//> > 3. 生成结点,将结点放到List
//> > 4. 创建赫夫曼树
HuffmanTreeNode huffmanTree = createHuffmanTree(strBytes);
//> > 5. 获取赫夫曼编码
HashMap<Byte, String> byteStringHashMap = HuffmanCode(huffmanTree);
System.out.println("每个字符对应的编码为==============================");
for(Map.Entry<Byte,String> entry:byteStringHashMap.entrySet()){
System.out.println(entry.getKey()+" "+entry.getValue());
}
//> > 6. 获取字符串对应二进制编码,也就是压缩操作(压缩对应的byte数组,返回的也是byte数组)
byte[] zip = zip(strBytes, byteStringHashMap);
System.out.println("编码后的字符数组======================================");
System.out.println(Arrays.toString(zip));
//封装结果集,将压缩后字节数组,和赫夫曼编码表封装返回
ArrayList<Object> objects = new ArrayList<>();
objects.add(zip);
objects.add(byteStringHashMap);
return objects;
}
/**
* 将压缩后的字节数组,解压
* @param zip 压缩后字节数组
* @param byteStringHashMap 赫夫曼编码表
* @return 解压后的字节数组
*/
public static byte[] huffmanUnZip(byte[] zip,HashMap<Byte, String> byteStringHashMap){
//字节数组解压成二进制编码
String s = byteToString(zip);
System.out.println("解压后获取的二进制编码"+s);
//将字符串按照赫夫曼编码,进行解码
HashMap<String, Byte> stringByteHashMap = new HashMap<>();//先将赫夫曼编码表进行反转,就是反向查询,比如32->10 转换成 10->32
for(Map.Entry<Byte,String> entry:byteStringHashMap.entrySet()){
stringByteHashMap.put(entry.getValue(), entry.getKey());
}
System.out.println("将赫夫曼编码表进行反转==============================");
for(Map.Entry<String,Byte> entry:stringByteHashMap.entrySet()){
System.out.println(entry.getKey()+" "+entry.getValue());
}
//通过反转后的赫夫曼编码表,将二进制串解析为正常字符,保存到list中
ArrayList<Byte> list = new ArrayList<>();
for(int i = 0;i<s.length();){//依次遍历二进制码,注意这里没有i++,因为i的增长在代码中控制
int count = 1;//用来记录当前判断了多少个二进制码,因为赫夫曼编码的长度都不一样
boolean flag = true;//while循环的条件
Byte b = null;//用来保存通过二进制码找到的对应值
while(flag){
String key = s.substring(i,i+count);//每次都从当前索引位置,截取count个二进制码
b = stringByteHashMap.get(key);//匹配map表中的值
if(b==null){//如果没有匹配到,count++
count++;
}else{//如果匹配到了,结束while循环,进行下一个字符匹配
flag = false;
}
}
list.add(b);//将匹配到的值加入list
i+=count;//让i从匹配完成后的位置开始
}
//将list中数据放在byte数组中返回
byte b[] = new byte[list.size()];
for(int i =0 ;i< b.length;i++){
b[i] = list.get(i);
}
return b;
}
/**
* > > 4. 创建赫夫曼树
* @param bytes 字节数组,存储了字符串的对应字节,我们需要统计每个byte出现次数,当做权值,构建赫夫曼树
*/
public static HuffmanTreeNode createHuffmanTree(byte[] bytes){
//1.将arr中每个元素构建成结点,放入一个集合中
ArrayList<HuffmanTreeNode> huffmanTreeNodes = new ArrayList<>();
//存储每一个byte出现的次数,获取权值
HashMap<Byte, Integer> counts = new HashMap<>();
for(byte data:bytes){
Integer count = counts.get(data);//从map中获取byte对应的值
if(count == null){//如果当前map还没有统计过当前byte的出现次数,那么进行第一次存储
counts.put(data,1);
}else{
counts.put(data,count+1);//如果当前map已经统计过,那么次数加1
}
}//此时每个字符的权值已经获取完毕
//把每一个key-value队,转成一个结点对象加入到集合中
for(Map.Entry<Byte,Integer> entry:counts.entrySet()){
huffmanTreeNodes.add(new HuffmanTreeNode(entry.getKey(),entry.getValue()));
}
//不断重复 取结点---构成新二叉树----重新排序,直到集合中只剩下一个结点
while(huffmanTreeNodes.size()>1){
//2.排序,使用Collections接口的sort方法,对集合排序,要求集合中元素,必须实现Comparable接口,并从写compareTo方法,至于从小到大排,还是从大到小排,取决于compareTo方法的实现
Collections.sort(huffmanTreeNodes);
//3. 取出根结点最小的两颗二叉树,构建成新二叉树,然后删除最小两颗二叉树,将新二叉树放集合中,然后排序
HuffmanTreeNode leftNode = huffmanTreeNodes.get(0);//取出集合第一个,也就是最小的
HuffmanTreeNode rightNode = huffmanTreeNodes.get(1);//取出第二个,也就是第二小的
HuffmanTreeNode parent = new HuffmanTreeNode(leftNode, rightNode);//然后构建一个新二叉树
huffmanTreeNodes.remove(leftNode);//删除
huffmanTreeNodes.remove(rightNode);//删除
huffmanTreeNodes.add(parent);//将新二叉树放进集合
Collections.sort(huffmanTreeNodes);//排序
}
return huffmanTreeNodes.get(0);
}
/**
* > > 5. 获取赫夫曼编码
* 生成赫夫曼树对应编码
* @param node
*/
public static HashMap<Byte,String> HuffmanCode(HuffmanTreeNode node){
if(node == null) throw new RuntimeException("赫夫曼树为空!!!");
//根据路径拼接每个叶子节点的赫夫曼编码
StringBuilder stringBuilder = new StringBuilder();
//将赫夫曼编码表放在Map中,比如空格,32 对应编码为01
HashMap<Byte, String> huffmanCodes = new HashMap<>();
getCodes(node,"",stringBuilder,huffmanCodes);
return huffmanCodes;
}
/**
* > > 5. 获取赫夫曼编码
* 将结点的所有叶子节点的赫夫曼编码得到,并放入map中
* @param node 赫夫曼树的结点
* @param code 路径编码,左路径是0,右路径是1
* @param stringBuilder 拼接路径
* @param huffmanCodes 最终结果存放到这个map
*/
private static void getCodes(HuffmanTreeNode node,String code,StringBuilder stringBuilder,HashMap<Byte,String> huffmanCodes){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);//将code路径拼接
if(node != null){//如果node!=null,说明还可以遍历,这个结点不是一个无效结点
//判断当前结点是否是叶子结点,如果不是叶子结点,它的data是null
if(node.getData() == null){//如果是非叶子结点
//向左递归
getCodes(node.getLeft(),"0",stringBuilder2,huffmanCodes);
//向右递归
getCodes(node.getRight(),"1",stringBuilder2,huffmanCodes);
}else{//如果是叶子结点
//将其添加到hashMap中
huffmanCodes.put(node.getData(),stringBuilder2.toString());
}
}
}
/**
* > > 6. 获取字符串对应二进制编码,也就是压缩操作
* @param bytes 压缩前字节数组
* @param huffmanCodes 赫夫曼树编码表
* @return 压缩后的字节数组
*/
public static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
//用于拼接压缩后的二进制
StringBuilder stringBuilder = new StringBuilder();
for(byte b : bytes){//遍历byte数组
//为每个byte查询相应编码,然后拼接到stringBuilder中
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println("压缩后的二进制编码"+stringBuilder.toString());
//将二进制码,转换成byte[],如果不转,反而变大了,起不到压缩效果
//获取byte[]长度,因为每个byte是8位,所以我们将stringBuilder/8就可以了
int len;
// if(stringBuilder.length()%8 == 0){//如果取余8==0,说明通过length/8获取的长度,刚好存放
// len = stringBuilder.length()/8;
// }else{//如果不等于0,说明/8后,还有余下的没地方存放,那么我们多加一个长度,用来存放这些多出来的
// len = stringBuilder.length()/8+1;
// }
//下面这句的效果,等同于上面注释的代码效果
len = (stringBuilder.length()+7)/8;
//创建,存储压缩后的byte数组
byte[] by = new byte[len];
int index = 0;//by数组下标
for(int i = 0;i<stringBuilder.length();i+=8){//字节数组,一个元素可以存储8为,所以步长为8
String strByte;
if(i+8>stringBuilder.length())//如果不够8为了,直接将当前下标到末尾的二进制代码取出来
strByte = stringBuilder.substring(i);
else//如果还够8位,直接从当前下标取8位
strByte = stringBuilder.substring(i,i+8);
//将取出的字串,放在字节数组中
by[index] = (byte)Integer.parseInt(strByte,2);
index++;//下标自增
}
return by;
}
/**
* > > 7. 解码,就是解压操作
* 将字符数组,转换成对应二进制字符串
* @return 按补码形式返回
*/
public static String byteToString(byte[] zip){
StringBuilder stringBuilder = new StringBuilder();//保存最终结果
boolean flag = true;//用于标记一个字节是否需要补高位
for(int i = 0;i<zip.length;i++){
int intNum = zip[i];//将byte转成一个int值
flag = (i != zip.length-1);//如果是最后一个字节,无需补高位
if(flag){//如果是true,我们需要补高位
intNum |= 256;//按位与,举个例子:256的二进制是 1 0000 0000 ,1的二进制是0000 0001 | 1 0000 0000 = 1 0000 0001
}
String s = Integer.toBinaryString(intNum);//返回i对应的二进制补码
//如果是true,我们取字符串后8位二进制
if(flag){
stringBuilder.append(s.substring(s.length()-8));
}else{//如果不是,无需补高位,那么直接连接即可
stringBuilder.append(s);
}
}
return stringBuilder.toString();
}
/**
* 前序遍历赫夫曼树
*/
public static void preOrder(HuffmanTreeNode node){
if(node == null){
System.out.println("赫夫曼树为空树");
return;
}
node.preOrder();
}
}
/**
* 赫夫曼树结点类
* 继承Comparable接口,用来比较结点之间的权值,需要实现compareTo方法
*/
class HuffmanTreeNode implements Comparable<HuffmanTreeNode> {
private Byte data;//存放数据本身,比如'a' = 97
private int value;//结点权值
private HuffmanTreeNode left;//左子结点
private HuffmanTreeNode right;//右子结点
/**
* 根据权值和数据直接构建结点
* @param value
*/
public HuffmanTreeNode(Byte data, int value) {
this.data = data;
this.value = value;
}
/**
* 根据左右结点,构建新的二叉树,权值为左右结点的权值之和
* @param left
* @param right
*/
public HuffmanTreeNode(HuffmanTreeNode left,HuffmanTreeNode right){
this.left = left;
this.right = right;
this.value = left.getValue() + right.getValue();
}
@Override
public int compareTo(HuffmanTreeNode o) {
//从小到大排序,至于为什么,百度一下Comparable接口
return this.value - o.value;
//从大到小排
//return -(this.value - o.value);
}
/**
* 前序遍历,先输出当前节点,然后遍历左子树,然后遍历右子树
*/
public void preOrder(){
//1. 先输出当前节点
System.out.println(this+" ");
//2. 如果左子树存在,遍历左子树
if(this.left!=null){
this.left.preOrder();
}
//3. 如果右子树存在,遍历右子树
if(this.right!=null){
this.right.preOrder();
}
}
@Override
public String toString() {
return "HuffmanTreeNode{" +
"data=" + data +
", value=" + value +
'}';
}
public Byte getData() {return data;}
public void setData(Byte data) {this.data = data;}
public int getValue() {return value;}
public void setValue(int value) {this.value = value;}
public HuffmanTreeNode getLeft() {return left; }
public void setLeft(HuffmanTreeNode left) {this.left = left;}
public HuffmanTreeNode getRight() {return right; }
public void setRight(HuffmanTreeNode right) {this.right = right;}
}
3. 使用赫夫曼编码压缩解压文件
这里只是介绍如何压缩文件与解压,解压文件时,需要根据不同文件类型来响应式解码,比如高清图片,中文字符,都需要特殊处理,与赫夫曼树编码无关,这里不做介绍 |
- 运行效果
- 代码(只贴出了修改过和新增的代码)
public static void main(String[] args) {
String srcFile = "E://a.txt";
String dstFile = "E://a.zip";
zipFile(srcFile,dstFile);
System.out.println("压缩文件完成");
String zipFile = "E://a.zip";
String dstFile2 = "E://unZip_a.txt";
unZipFile(zipFile,dstFile2);
}
/**
* 压缩文件
* @param srcFile 要压缩的文件路径
* @param dstFile 压缩后保存路径
*/
public static void zipFile(String srcFile,String dstFile){
//创建文件输入流
FileInputStream is = null;
//创建文件输出流
OutputStream os = null;
//创建一个和文件输出流关联的对象输出流,这样我们可以利用对象流将一个对象写入压缩文件(一起输出),比如我们将赫夫曼编码表输出出去,那么日后想要解压,就可以直接拿到编码表解压
ObjectOutputStream objectOutputStream=null;
try {
is = new FileInputStream(srcFile);//获取指定文件输入流
byte[] b = new byte[is.available()];//创建对应字节数组
is.read(b);//将文件内容读取到字节数组中
//压缩,获取赫夫曼编码表和压缩后字节数组
List<Object> list = HuffmanZip(b);
byte[] zip = (byte[]) list.get(0);//获取压缩后的字节数组
HashMap<Byte, String> byteStringHashMap =(HashMap<Byte, String>) list.get(1);//获取赫夫曼编码表
//创建文件输出流,存放压缩后文件
os = new FileOutputStream(dstFile);
//创建一个和文件输出流关联的ObjectOutputStream
objectOutputStream = new ObjectOutputStream(os);
//首先将压缩后字节数组写入输出流,这样就完成了压缩文件的创建
objectOutputStream.writeObject(zip);
//以对象流的方式,将赫夫曼编码写入压缩文件,恢复源文件时使用
objectOutputStream.writeObject(byteStringHashMap);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();//关闭流
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void unZipFile(String zipFile,String dstFile){
//创建文件输入流
InputStream is = null;
//定义和文件输入流关联的对象输入流
ObjectInputStream ois = null;
//文件输出流
OutputStream os = null;
try {
is = new FileInputStream(zipFile);
ois = new ObjectInputStream(is);
//获取压缩后文件
byte[] zip = (byte[])ois.readObject();
//获取赫夫曼编码表
HashMap<Byte,String> huffmanCodes = (HashMap<Byte,String>)ois.readObject();
//解压,获取解压后字节数组
byte[] unZip = huffmanUnZip(zip, huffmanCodes);
//创建文件输出流
os = new FileOutputStream(dstFile);
//将解压后字节数组,写入输出流,完成解压操作,恢复源文件
os.write(unZip);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
三、多叉树,多路查找树
1. 2-3树
- 最简单的B树结构,特点如下(就是每个结点可以保存两个数据,最多可以有3个子结点,但是需要满足如下规则)
- 2-3树的所有叶子结点都在同一层.(只要是B树都满足这个条件)
- 有两个子结点的结点叫二结点,二结点要么没有子结点,要么有两个子结点
- 有三个子结点的结点叫三结点,三结点要么没有子结点,要么有3个结点
- 2-3树是由二结点和三结点构成的树
另外还有一些2-3-4树等等,和2-3树差不多,就是每个结点最多存3个元素,最多有4个子结点 |
2. B树,B+树,B*树
- B-tree,B树,Balanced-tree,平衡的意思,有人翻译成B-树,容易让人误认为叫B减树,或者是B杠树,和B树不一样。其实就是B树
- B+树
- B*树