写在前面
堆排序
- 选择排序,不稳定排序
- 每个节点的值,都大于或者等于其左右孩子节点的值,称为
大顶堆
,升序
- 每个节点的值,都小于或者等于其左右孩子节点的值,称为
小顶堆
,降序
- 前提条件,针对一个数组,将数组的坐标转换成
顺序存储二叉树
的格式,在进行大小顶堆化
- 排序,
arr.length / 2
为非叶子节点的个数,-1
是非叶子节点最大的坐标
public static void heapSort(int[] arr) {
//调整为大顶堆
for (int i = arr.length / 2 - 1
adjustHeap(arr,i,arr.length)
}
//将堆顶元素与末尾元素交换,将最大的元素沉到数组末端
//每次交换后,都要重新进行调整
int temp = 0
for (int i = arr.length - 1
temp = arr[i]
arr[i] = arr[0]
arr[0] = temp
//因为每次交换时,只调整了堆顶和末尾的元素
//所以只需要将堆顶这个非叶子节点进行调整即可
//而且,每次交换完,都确定一个最大值,即每次调整的数组长度-1,也就是i
adjustHeap(arr,0,i)
}
System.out.println(Arrays.toString(arr))
}
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
if (k + 1 < length && arr[k] < arr[k+1]) {
k++;
}
if (arr[k] > temp) {
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp;
}
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i]
for (int k = i * 2 + 1
if (k + 1 < length && arr[k] > arr[k+1]) {
k++
}
if (arr[k] < temp) {
//如果子节点大于父节点,要交换
arr[i] = arr[k]
i = k
} else {
break
}
}
arr[i] = temp
}
赫夫曼树
- 给定n个权值,作为n个叶子节点,若该树的
带权路径长度(wpl)
达到最小,则为最优二叉树,也叫赫夫曼树,权值越大的节点离根越近
- 路径,从一个节点,往下可以达到孩子或者孙子节点之间的通路
- 路径长度,规定根节点层数为-1,到第L层节点的路径长度为
L-1
- 带权路径长度,给节点一个权重值,从根节点到该节点之间的路径长度与该节点的权的乘积
- 树的带权路径长度,所有叶子节点的带权路径之和,记为
weighted path length
,权值越大的节点距离根节点越近,最小的就是赫夫曼树/最优二叉树
public static HuffmanNode createHuffmanTree(int[] arr) {
ArrayList<HuffmanNode> nodes = new ArrayList<>();
for (int i : arr) {
nodes.add(new HuffmanNode(i));
}
while (nodes.size() > 1) {
Collections.sort(nodes);
HuffmanNode left = nodes.get(0);
HuffmanNode right = nodes.get(1);
HuffmanNode parent = new HuffmanNode(left.value + right.value);
parent.left = left;
parent.right = right;
nodes.remove(left);
nodes.remove(right);
nodes.add(parent);
}
return nodes.get(0);
}
class HuffmanNode implements Comparable<HuffmanNode> {
int value;
HuffmanNode left;
HuffmanNode right;
public HuffmanNode(int value) {
this.value = value;
}
@Override
public String toString() {
return "HuffmanNode{" +
"value=" + value +
'}';
}
@Override
public int compareTo(HuffmanNode o) {
return this.value - o.value;
}
public static void preOrderList(HuffmanNode node) {
System.out.println(node);
if (node.left != null) {
preOrderList(node.left);
}
if (node.right != null) {
preOrderList(node.right);
}
}
}
赫夫曼编码
- 算法,数据文件压缩,可变字长编码(VLC)的一种
- 变长编码,统计每个字符出现的次数,字数越多,对应的二进制位越少,但是会有多义性
- 赫夫曼编码,无损压缩,将字符出现的次数构建成一颗赫夫曼树,次数作为权值;向左为0,向右为1,按照根节点到叶子节点的路径作为这个字符的编码,避免多义性
- 借助了赫夫曼树的特点
权值越大离根节点越近
,那么字符出现次数越多,编码长度越小
- 如果赫夫曼树中有多个相同的权重值,会有可能导致形成的树的结构不一样,但是wpl是一样的,这样生成的赫夫曼编码不一样,但是压缩后的大小/长度是一样的
压缩
private static byte[] huffmanZip(byte[] bytes) {
List<HuffmanCodeNode> nodes = getNodes(bytes);
HuffmanCodeNode node = createHuffmanTree(nodes);
Map<Byte, String> huffmanCodes = getHuffmanCodes(node);
return zip(bytes, huffmanCodes);
}
public static List<HuffmanCodeNode> getNodes(byte[] bytes) {
ArrayList<HuffmanCodeNode> nodes = new ArrayList<>()
//遍历bytes统计每个出现的次数
HashMap<Byte, Integer> map = new HashMap<>()
Integer count = 0
//存入字符-字符个数
for (byte b : bytes) {
count = map.get(b)
if (count == null) {
map.put(b, 1)
} else {
map.put(b, count + 1)
}
}
//把map转换成node对象,node对象包括`字符`和`权重(次数)`
map.forEach((b, val) -> nodes.add(new HuffmanCodeNode(b,val)))
return nodes
}
public static HuffmanCodeNode createHuffmanTree(List<HuffmanCodeNode> nodes) {
while (nodes.size() > 1) {
Collections.sort(nodes)
HuffmanCodeNode left = nodes.get(0)
HuffmanCodeNode right = nodes.get(1)
//创建新的二叉树节点,没有字符,只有值
HuffmanCodeNode parent = new HuffmanCodeNode(null, left.weight + right.weight)
parent.left = left
parent.right = right
nodes.remove(left)
nodes.remove(right)
nodes.add(parent)
}
return nodes.get(0)
}
}
static Map<Byte, String> huffmanCodes = new HashMap<>();
static StringBuilder stringBuilder = new StringBuilder();
public static Map<Byte, String> getHuffmanCodes(HuffmanCodeNode node) {
if (node == null) {
return null;
}
getHuffmanCodes(node.left, "0", stringBuilder);
getHuffmanCodes(node.right, "1", stringBuilder);
return huffmanCodes;
}
public static void getHuffmanCodes(HuffmanCodeNode node, String code, StringBuilder stringBuilder) {
StringBuilder stringCode = new StringBuilder(stringBuilder);
stringCode.append(code);
if (node != null) {
if (node.data == null) {
getHuffmanCodes(node.left, "0",stringCode);
getHuffmanCodes(node.right,"1",stringCode);
} else {
huffmanCodes.put(node.data, stringCode.toString());
}
}
}
- 生成对应赫夫曼编码字节数组,因为字节数组转换成二进制字符串的时候,末尾如果是0开头的,开头将会被舍去,所以要另外用
endString
记录
//存放结尾的编码
static String endString = ""
/**
* 将一个字符串对应的byte数组,通过赫夫曼编码表,返回赫夫曼编码压缩后的byte数组
* @param bytes 原始字符数组
* @param huffmanCodes 经过赫夫曼编码处理后的字符编码
* @return 原始字符编码数组
* java的数字都是以补码的形式出现的,byte要转为数字,也要把补码转换成原码
* 正数三码合一
* 负数补码 = 原码保持符号为不变按位取反 + 1
* byte[] 一个字节存8位带符号数的二进制 需要-> -1 反码 ->保留符号为,取反转换成原码->十进制
* 10101000(补码) => 10101000 - 1 => 10100111 取反 => 11011000 => -88
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
StringBuilder stringBuilder = new StringBuilder()
//获取字符对应的赫夫曼编码,并拼接
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b))
}
//转换成byte数组
//如果不能被8整除,加上7,一定能被8整除;如果能被8整除,加上7,多出来的部分也不会影响结果
//int len = (stringBuilder.length() + 7) / 8
int len = stringBuilder.length() % 8 == 0 ? stringBuilder.length() / 8 : stringBuilder.length() / 8 + 1
if (stringBuilder.length() - (len - 1) * 8 != 0) {
//处理末尾
endString = stringBuilder.substring((len - 1) * 8, stringBuilder.length())
}
//创建存储压缩后的byte数组
byte[] huffmanCodeBytes = new byte[len]
String strByte
//记录第几个byte
int index = 0
for (int i = 0
if (i + 8 > stringBuilder.length() - 1) {
//不够8位
strByte = stringBuilder.substring(i)
} else {
strByte = stringBuilder.substring(i, i + 8)
}
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte,2)
index ++
}
return huffmanCodeBytes
}
解压
byte[] source = decode(huffmanCodes, res)
- 完成对压缩数据的解码,本质就是将压缩完成的字节数组和对应的赫夫曼编码表传入,解码成原来的字节数组
/**
* @param huffmanCodes 赫夫曼编码表
* @param huffmanBytes 赫夫曼编码得到的字节数组,被解压的数组
* @return
*/
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
StringBuilder stringBuilder = new StringBuilder()
//将byte数组转换成字符串
for (int i = 0
boolean flag = i == huffmanBytes.length - 1
stringBuilder.append(byteToBitString(!flag,huffmanBytes[i]))
}
//按照编码表解码
//要将编码表调转
HashMap<String, Byte> map = new HashMap<>()
huffmanCodes.forEach((b, s) -> map.put(s,b))
//创建集合存放byte
ArrayList<Byte> list = new ArrayList<>()
int count
for (int i = 0
//扫描对应的二进制字符串
count = 1
boolean flag = true
Byte b = null
while (flag) {
//取出一位
//让count移动,直到取到一个存在的字符
String key = stringBuilder.substring(i, i + count)
b = map.get(key)
if (b == null) {
count ++
} else {
//匹配到
flag = false
}
}
list.add(b)
//i移动到count的位置
//i += count
}
//循环结束后,存放所有的字符
byte[] bytes = new byte[list.size()]
for (int i = 0
bytes[i] = list.get(i)
}
return bytes
}
private static String byteToBitString(boolean flag, byte b) {
int temp = b;
if (flag) {
temp |= 256;
}
String s = Integer.toBinaryString(temp);
if (flag) {
return s.substring(s.length() - 8);
} else {
return endString;
}
}
针对文件
/**
* @param srcFile 来源
* @param desFile 目标
*/
private static void zipFile(String srcFile, String desFile) {
FileInputStream fis = null
FileOutputStream ops = null
ObjectOutputStream oos = null
try {
fis = new FileInputStream(srcFile)
//创建与原文件大小一样的数组
byte[] bytes = new byte[fis.available()]
//读取
fis.read(bytes)
//编码
byte[] huffmanBytes = huffmanZip(bytes)
//创建输出流,存放压缩文件
ops = new FileOutputStream(desFile)
//对象输出流
oos = new ObjectOutputStream(ops)
//以对象流的方式写入 赫夫曼编码 和 文件压缩字节,方便恢复源文件的方式时使用
oos.writeObject(huffmanBytes)
oos.writeObject(huffmanCodes)
oos.writeObject(endString)
} catch (Exception e) {
e.printStackTrace()
} finally {
try {
if (ops != null) {
ops.close()
}
if (fis != null) {
fis.close()
}
if (oos != null) {
oos.close()
}
} catch (IOException e) {
e.printStackTrace()
}
}
}
private static void unzipFile(String zipFile, String desFile) {
FileInputStream fis = null
ObjectInputStream ois = null
FileOutputStream ops = null
try {
fis = new FileInputStream(zipFile)
ois = new ObjectInputStream(fis)
//读取赫夫曼字节数组,和编码表,按存入顺序读取
byte[] huffmanBytes = (byte[]) ois.readObject()
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject()
String endString = (String) ois.readObject()
System.out.println(endString)
//解码
byte[] bytes = decode(huffmanCodes, huffmanBytes)
//写入
ops = new FileOutputStream(desFile)
ops.write(bytes)
} catch (Exception e) {
e.printStackTrace()
} finally {
try {
if (ops != null) {
ops.close()
}
if (ois != null) {
ois.close()
}
if (fis != null) {
fis.close()
}
} catch (IOException e) {
e.printStackTrace()
}
}
}
小结
- 不是对任何文件都很有效,如果文件重复的元素很多,压缩率很高
- 重要的是获取映射关系和顺序,
字符与次数的映射
->对应的赫夫曼树
,字符与编码的映射
->赫夫曼编码表
;按照原顺序存储成二进制字符串
->以字节为单位存到字节数组中
,解压时将赫夫曼编码的键值对调
->编码与字符的映射
,在转换后的编码表中,根据获取的二进制字符串顺序读取相应的字符