堆排序
基础概念:
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn),它也是不稳定排序
- 堆是具有以下性质的完全二叉树:注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系
- 每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
- 一般升序采用大顶堆,降序采用小顶堆
基本思想:
堆排序的基本思想:
- 将待排序序列构造成一个大顶堆
- 循环完成:将每个最大值的根节点与末尾元素进行交换,并且在调整使其成为大顶堆
构造大顶堆的过程:从非叶子节点开始进行调整,逐渐调整到根节点,采用循环维护整个子树
代码说明:
package com.atguigu.tree;
import java.util.Arrays;
/**
* @ClassName HeapSortDemo
* @Author Jeri
* @Date 2022-02-23 21:52
* @Description 堆排序
*/
public class HeapSortDemo {
/*
* @Description 大顶堆调整方法
* @Date 2022/2/23 21:56
* @param arr 待排序数组
* @param i 数组中第i个下标的非叶子节点
* @param length 数组查询的长度
* @return [arr, i, length]
*/
public static void adjustHeap(int[] arr,int i,int length){
//取出当前非叶子结点的值
int temp = arr[i];
// k = 2*i + 1 是第i个节点对应的左子节点下标
for(int k = 2*i+ 1;k < length;k = 2*k + 1){
//进行一次比较 得到左右子节点的最大值
if(k + 1 < length && arr[k] < arr[k + 1]){
//满足条件 左子节点值 < 右子节点值
k++;
}
//进行一次比较 得到 最大值
if(arr[k] > temp){
//发生交换
arr[i] = arr[k];
// i 指向 k ,继续循环比较
//说明:方便在 i 很小 length 很大时 维护整个树
// 通过循环 可以将比较小的值 挪动到树的底部
i = k; //
}else{
//父节点值比较大
break;
}
//结束 for 循环时 认为以i为 父^n 结点的树的最大值,放在了顶部
//将 temp 临时保存的值放在调整之后的位置
arr[i] = temp;
}
}
/*
* @Description 堆排序方法
* @Date 2022/2/23 21:53
* @param arr 待排序数组
*/
public static void heapSort(int arr[]){
//用作交换的临时变量
int temp = 0;
System.out.println("开始进行堆排序:-------");
//将现在的待排序序列构建成一个大顶堆
for(int i = arr.length /2 - 1;i >= 0;i--){
adjustHeap(arr,i,arr.length);
}
// 逐步将每个最大值的根节点与末尾元素进行交换 使其在成为大顶堆
for(int j = arr.length - 1;j > 0;j--){
//实现交换
temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
//循环构建大顶堆
adjustHeap(arr,0,j);
}
}
public static void main(String[] args) {
int[] array = new int[]{12,34,3,5,90,234,11};
System.out.println("原数组:---------");
System.out.println(Arrays.toString(array));
heapSort(array);
System.out.println("堆排序(升序):---------");
System.out.println(Arrays.toString(array));
}
}
原数组:---------
[12, 34, 3, 5, 90, 234, 11]
开始进行堆排序:-------
堆排序(升序):---------
[3, 5, 11, 12, 34, 90, 234]
哈夫曼树
基本介绍:
给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近
重要概念:
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
- WPL 最小的就是哈夫曼树
思路分析:
- 从小到大进行排序, 将每一个数据(节点),每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,
不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗哈夫曼树
代码实现:
package com.atguigu.tree.huffmantree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @ClassName HuffmanTreeDemo
* @Author Jeri
* @Date 2022-02-24 10:27
* @Description 哈夫曼树
*/
//节点类
//为了让 Node 对象持续排序 Collections 集合排序
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 +
'}';
}
@Override
public int compareTo(Node obj){
//表示从小到大排序
return this.value - obj.value;
}
/*
* @Description //前序遍历
* @Date 2022/2/24 10:32
*/
public void preOrder(){
System.out.println(this);
if(this.left != null){
this.left.preOrder();
}
if(this.right != null){
this.right.preOrder();
}
}
}
public class HuffmanTreeDemo {
/*
* @Description 创建哈夫曼树
* @Date 2022/2/24 10:34
* @param arr 带创建数组 存储节点权值
* @return 创建好的哈夫曼树的头节点
*/
public static Node huffmanTree(int[] arr){
//遍历数组 得到每个Node 并放入集合中
List<Node> nodeList = new ArrayList<>();
for(int value:arr){
nodeList.add(new Node(value));
}
//循环处理
//循环条件 集合的长度 > 1
while (nodeList.size() > 1){
//进行排序 默认从小到达
Collections.sort(nodeList);
//取出根节点权值最小的两棵二叉树
Node leftNode = nodeList.get(0);
Node rightNode = nodeList.get(1);
//构建新的二叉树
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//从集合中删除处理掉的二叉树
nodeList.remove(leftNode);
nodeList.remove(rightNode);
//将 parent添加到集合中
nodeList.add(parent);
}
return nodeList.get(0);
}
public static void main(String[] args) {
int[] arr = new int[]{13,8,7,3};
Node root = huffmanTree(arr);
if(root == null){
System.out.println("空树,无法完成前序遍历");
}else{
System.out.println("前序遍历结果:-----");
root.preOrder();
}
}
}
前序遍历结果:-----
Node{value=31}
Node{value=13}
Node{value=18}
Node{value=8}
Node{value=10}
Node{value=3}
Node{value=7}
哈夫曼编码
基本介绍:
- 哈夫曼编码也翻译为 赫夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
- 哈夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
- 哈夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间
- 哈夫曼码是可变字长编码(VLC)的一种。Huffman 于 1952 年提出一种编码方法,称之为最佳编码
思路分析:
传输的字符串为:content = “i like like like java do you like a java”
统计信息如下:d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
步骤如下:
- 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
- 根据赫夫曼树,给各个字符,规定编码 (前缀编码),
- 按照上面的赫夫曼编码,将 content 字符串进行压缩
注意 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个
哈夫曼解码
步骤如下:
- 对哈夫曼字节数组 单个字节转换成比特
- 根据 哈夫曼字节数组 和 哈夫曼编码表 进行解码
整体代码如下:
package com.atguigu.huffmancodes;
import java.util.*;
/**
* @ClassName HfmEnodeDecodeDemo
* @Author Jeri
* @Date 2022-02-26 11:02
* @Description 哈夫曼编码与解码
*/
//创建节点类 存储数据和权值
class Node implements Comparable<Node>{
Byte data;//存放数据 a d ..
int weight;//存放数据对应的权值
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Node o) {
//按照权值从小到大排列
return this.weight - o.weight;
}
//前序遍历
public void preOrder(){
System.out.println(this);
if(this.left != null){
this.left.preOrder();
}
if(this.right != null){
this.right.preOrder();
}
}
}
public class HfmEnodeDecodeDemo {
// huffmanCodes : map 存储哈夫曼编码结果
public static Map<Byte, String> hfmCodesTable = new HashMap<>();
//临时字符串
public static StringBuilder stringBuilder = new StringBuilder();
// 用于判断最后一位的编码省略了几个0!!!(解压时用)
public static int zeroCount = 0;
//字节数组 转换为 节点类集合对象
public static List<Node> getNodeList(byte[] bytes){
//创建集合对象
ArrayList<Node> nodeList = new ArrayList<>();
//创建 map 存储 bytes数组元素及其出现数量
Map<Byte,Integer> counts = new HashMap<>();
for(Byte b:bytes){
Integer bCount = counts.get(b);
if(bCount == null){
//Map 还没有存储该Byte数据
counts.put(b,1);
}else{
//Map 已经存储该Byte数据
counts.put(b,bCount + 1);
}
}
//遍历 map 对象 创建节点集合
System.out.println("遍历 byte 数组");
for(Map.Entry<Byte,Integer> entry:counts.entrySet()){
System.out.println("byte = " + entry.getKey() + ",weight = " + entry.getValue());
nodeList.add(new Node(entry.getKey(),entry.getValue()));
}
return nodeList;
}
//创建哈夫曼树 并返回哈夫曼树的根节点
public static Node createHfmTree(List<Node> nodeList){
while (nodeList.size() > 1){
//集合排序 从小到大
Collections.sort(nodeList);
//取出前两个二叉树
Node leftNode = nodeList.get(0);
Node rightNode = nodeList.get(1);
//构建父节点二叉树
Node parent = new Node(null,leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//从集合中删除处理过的二叉树
nodeList.remove(leftNode);
nodeList.remove(rightNode);
//将构建完成新的二叉树加入集合中
nodeList.add(parent);
}
//nodeList 中剩下的一个数据 便是根节点
return nodeList.get(0);
}
//将 哈夫曼编码结果保存在 map 对象中
public static void gethfmEncode(Node node, String type, StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将 type 加入到 stringBuilder2
stringBuilder2.append(type);
//对当前节点 node 进行处理
//node == null 不做处理
if(node != null){
//判断当前节点是叶子节点还是非叶子节点
if(node.data == null){
//非叶子节点
//向左递归
gethfmEncode(node.left,"0",stringBuilder2);
//向右递归
gethfmEncode(node.right,"1",stringBuilder2);
}else{
//叶子节点
//加入哈夫曼编码表
hfmCodesTable.put(node.data,stringBuilder2.toString());
}
}
}
//重载 getHfmCodes 方法 方便调用
public static Map<Byte,String> gethfmEncode(Node root){
if(root == null){
return null;
}
//处理 root 左子树
gethfmEncode(root.left,"0",stringBuilder);
//处理 root 右子树
gethfmEncode(root.right,"1",stringBuilder);
return hfmCodesTable;
}
// 得到哈夫曼编码比特字符串
public static String gethfmBitString(byte[] bytes,Map<Byte,String> huffmanCodes){
StringBuilder temp = new StringBuilder();
//遍历 bytes 数组
for(Byte b:bytes){
temp.append(huffmanCodes.get(b));
}
return temp.toString();
}
//得到哈夫曼byte 数组
public static byte[] gethfmZip(String hfmBitString){
String temp = hfmBitString;
//创建存储压缩后的 bytes 数组
int length;
if(temp.length() % 8 == 0){
length = temp.length() / 8;
}else{
length = temp.length() / 8 + 1;
}
byte[] huffmanEncodeByteArray = new byte[length];
//记录 huffmanEncodeArray 的索引
int index = 0;
for(int i = 0;i < temp.length();i += 8){
//计算机底层存储 每8位bit 即1字节byte
String tempStr;
if(i + 8 > temp.length()){
//处理末尾最后一个字节
tempStr = temp.substring(i);
//末尾补零
for (int j = i; j < temp.length(); j++) {
zeroCount++;
}
}else{
//处理前面的字节
tempStr = temp.substring(i,i+8);
}
huffmanEncodeByteArray[index] = (byte)Integer.parseInt(tempStr,2);
index++;
}
return huffmanEncodeByteArray;
}
//对哈夫曼字节数组 单个字节转换成比特
public static String byteToString(boolean flag,byte b){
//flag 用来处理最后一个数据
int temp = b;
if(flag){
temp |= 256;
}
String str = Integer.toBinaryString(temp);
if (flag) {
return str.substring(str.length() - 8);
} else {
//补零
int length = zeroCount - str.length();
for (int i = 0; i < length; i++) {
str = 0 + str;
}
// System.out.println(str);
if (str.length() > 8) {
return str.substring(str.length() - 8);
} else {
return str;
}
}
}
//根据 哈夫曼字节数组 和 哈夫曼编码表 进行解码
public static byte[] gethfmDecodeByteArray(Map<Byte,String> huffmanCodes,byte[] huffmanByteArray){
StringBuilder huffmanBitString = new StringBuilder();
//将byte数组转成二进制的字符串
for(int i = 0; i < huffmanByteArray.length; i++) {
byte b = huffmanByteArray[i];
//判断是不是最后一个字节
boolean flag = (i == huffmanByteArray.length - 1);
huffmanBitString.append(byteToString(!flag, b));
}
System.out.println("解码哈夫曼比特数据长度:" + huffmanBitString.length());
System.out.println("解码哈夫曼比特数据:" + huffmanBitString.toString());
//创建 map 对象 倒叙存放哈夫曼编码表
//通过匹配比特字符串 来获得 byte 数值
Map<String,Byte> reverseHuffmanCodes = new HashMap<>();
for(Map.Entry<Byte,String> entry:huffmanCodes.entrySet()){
reverseHuffmanCodes.put(entry.getValue(),entry.getKey());
}
//匹配比特字符串 添加至集合对象
List<Byte> list = new ArrayList<>();
for(int i = 0;i < huffmanBitString.length(); ){
int count = 1;//比特字符长度计数器
boolean flag = true;
Byte b = null;
while (flag){
String key = huffmanBitString.substring(i,i + count);
b = reverseHuffmanCodes.get(key);
if(b == null){
count++;
}else {
flag = false;
}
}
i += count;
list.add(b);
}
byte[] decodeByteArray = new byte[list.size()];
for(int i = 0;i < list.size();i++){
decodeByteArray[i] = list.get(i);
}
return decodeByteArray;
}
public static void main(String[] args) {
//原始字符串
String content = "i like like like java do you like a java";
//得到字符串对应的 byte 数组
byte[] conByteArray = content.getBytes();
System.out.println("原始byte数组长度:" + conByteArray.length);
System.out.println("原始byte数组内容:" + Arrays.toString(conByteArray));
//得到 byte 数组对应的节点类集合
List<Node> nodeList = getNodeList(conByteArray);
//将节点类集合构造成哈夫曼树 并返回根节点
Node root = createHfmTree(nodeList);
//根据哈夫曼树的根节点 得到哈夫曼表
Map<Byte, String> hfmCodesTable = gethfmEncode(root);
System.out.println("查看哈夫曼编码表:---");
for(Map.Entry<Byte,String> entry:hfmCodesTable.entrySet()){
System.out.println("byte = " + entry.getKey() + ",encode = " + entry.getValue());
}
根据原始字节数组 哈夫曼编码表 得到哈夫曼编码Bit字符串
String hfmBitString = gethfmBitString(conByteArray, hfmCodesTable);
System.out.println("哈夫曼比特字符串长度:" + hfmBitString.length() + "位");
System.out.println("哈夫曼比特字符串:" + hfmBitString);
//计算机采用字节进行传输 得到哈夫曼byte数组
byte[] hfmByteArray = gethfmZip(hfmBitString);
System.out.println("哈夫曼字节数组长度:" + hfmByteArray.length);
System.out.println("哈夫曼字节数组:" + Arrays.toString(hfmByteArray));
System.out.println("哈夫曼编码完成--------");
System.out.println();
//得到哈夫曼解码字节数组
byte[] hfmDecodeByteArray = gethfmDecodeByteArray(hfmCodesTable, hfmByteArray);
//得到哈夫曼解码字符串
String decodeContent = new String(hfmDecodeByteArray);
System.out.println("哈夫曼解码完成--------");
System.out.println();
System.out.println("原始内容:" + content);
System.out.println("解码内容:" + decodeContent);
}
}
原始byte数组长度:40
原始byte数组内容:[105, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 32, 100, 111, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 97, 32, 106, 97, 118, 97]
遍历 byte 数组
byte = 32,weight = 9
byte = 97,weight = 5
byte = 100,weight = 1
byte = 101,weight = 4
byte = 117,weight = 1
byte = 118,weight = 2
byte = 105,weight = 5
byte = 121,weight = 1
byte = 106,weight = 2
byte = 107,weight = 4
byte = 108,weight = 4
byte = 111,weight = 2
查看哈夫曼编码表:---
byte = 32,encode = 01
byte = 97,encode = 100
byte = 100,encode = 11000
byte = 117,encode = 11001
byte = 101,encode = 1110
byte = 118,encode = 11011
byte = 105,encode = 101
byte = 121,encode = 11010
byte = 106,encode = 0010
byte = 107,encode = 1111
byte = 108,encode = 000
byte = 111,encode = 0011
哈夫曼比特字符串长度:133位
哈夫曼比特字符串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
哈夫曼字节数组长度:17
哈夫曼字节数组:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
哈夫曼编码完成--------
解码哈夫曼比特数据长度:133
解码哈夫曼比特数据:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
哈夫曼解码完成--------
原始内容:i like like like java do you like a java
解码内容:i like like like java do you like a java