哈夫曼编码算法 java_我所知道的算法之哈夫曼编码

上一篇文章中提到数据结构:哈夫曼树,今天接着学习由哈夫曼提出编码方式,一种程序算法。简称:哈夫曼编码

一、什么是哈夫曼编码?

与哈夫曼树一样,会不会有小伙伴对哈夫曼编码很陌生,有疑惑

问题疑惑

1.哈夫曼编码是做什么的?有什么用?

2.为什么要使用哈夫曼编码?使用别的编码行不行?

哈夫曼编码是做什么的?有什么用?哈夫曼(Huffman)编码算法

它是基于二叉树构建编码压缩结构的,它是数据压缩中经典的一种算法:根据文本字符出现的频率,重新对字符进行编码

那么就会有小伙伴说:什么是数据压缩?什么是字符编码?字符编码压缩介绍

在我们的世界里,我们可以看到漂亮的图像、好听的声音、震撼的视频等等这些精彩内容。但是对于计算机说,它的世界里只有二进制的 0 和 1。

c735df96a751493b591f5fed1950d30f.png

因此在数字电子电路中,逻辑门的实现直接应用了二进制,因此现代的计算机和依赖计算机的设备里都用到二进制。

那么在二进位电脑系统中,存储的单位有:bit、kb、mb、gb

bit 电脑记忆体中最小的单位,每一bit 只代表0 或 1的数位讯号

b5dae86194705139a00b8b17061f74f5.png

Byte由八个 bits 所组成,可代表:字元(A~Z)、数字(0~9)、或符号(,.?!%&+-*/)

1ce4cb56528d26e6c5097a06b59c5b92.png

当记忆体容量过大时,位元组(byte)这个单位就不够用,因此就有千位元组的单位KB出现,以下乃个记忆体计算单位之间的相关性:1 Byte = 8 Bits

1 KB = 1024 Bytes

1 MB = 1024 KB

1 GB = 1024 MB

我们现在在计算机上看到的一切的图像、声音、视频等内容,都是由二进制的方式进行存储的

28e3e00f0b190fe38201a874c0385e0a.png

简单来讲,我们把信息转化为二进制的过程可以称之为编码

编码方式从长度上来分大致可以分为两个大类:定长编码表明:段与段之间长度相同,没说明是多长。

变长编码表明:段与段之间的长度不相同,不定义具体有多长。

请注意!这里并没有标明是多长!这会导致无法区分编码信息与文件内容的分离!造成乱码!自然语言的分隔问题民可使由之不可使知之 ——_出自《论语 第八章 泰伯篇》_

这么一串十个字要如何去分隔并解释呢?

断法解释一:民可使由之,不可使知之。

解释:你可以去驱使你的民众,但不可让他们知道为什么(不要去教他们知识)

断法解释二:民可,使由之;不可,使知之。

解释:民众可以做的,放手让他们去做;不会做的,教会他们如何去做(又或:不可以去做的,让他们明白为何不可以)

显然,以上的文字是以某种定长或变长的方式组合在一起的,但是关于它们如何分隔的信息则被丢弃了,于是在解释时就存在产生歧义可能了。

举个栗子,假如我们对 「pig」 进行自定义编码,使用定长编码,为了方便,采用了十进制,原理与二进制是一样的。

1e5cb00e0aedd3369516e850fdd13724.png

假设我们现在有文件,内容是:00080008

假如定长 2 位是唯一的编码方案,用它去解码就会得到:「pipi」

假如定长 4 位是唯一的编码方案,用它去解码就会得到:「ii」

11981300a33b47c8d47684eef4d9fb75.png

如果文件的内容并没有暗示它使用了何种编码!就会出现解释时存在产生歧义。

这就好比孔夫子写下“民可使由之不可使知之”时

并没有暗示它是5|5分隔(民可使由之|不可使知之)

还是暗示它是2|3|2|3分隔(民可|使由之|不可|使知之)。

其实当我们有字节这一基本单位后,我们就可以说得更具体:如定长一字节或者定长二字节。

比如说ASCII码它是最早也是最简单的一种字符编码方案:使用定长一字节来表示一个字符示例编码认识差异

其实现在我们的计算机的世界里编码总已经有很多种格式

比如常见的: ASCII 、 Unicode 、 GBK 、 GB2312 、 GB18030 、 UTF-8 、 UTF-16 等等。

77b5798e895e08965d2414d4dba3eb0e.pngASCII编码介绍

我们都知道在计算机的世界里只有二进制0和1,通过不同的排列组合,可以使用 0 和 1 就可以表示世界上所有的东西。

是不是有我们中国"太极"的感觉。——“太极生两仪,两仪生四象,四象生八卦

而在计算机中:一字节 = 八位二进制数(二进制有0和1两种状态)

因此一字节可以组合出二百五十六种状态。

如果这二百五十六种状态每一个都对应一个符号,就能通过一字节的数据表示二百五十六个字符。

美国人于是就制定了一套编码(其实就是个字典),描述英语中的字符和这八位二进制数的对应关系,这被称为 ASCII 码。

9ddfd25b9dfae1c78848881e9e57837d.png

随着时代的发展,不仅美国人要对他们的英文进行编码,我们中国人也要对汉字进行编码。

而早期的 ASCII 码的方案只有一个字节,对我们汉字文化而言是远远不够的编码的发展

这点可怜的空间拿到中国来,它能编码的汉字量也就小学一年级的水平。

这也就导致了不同的需求推动了发展,各种编码方案都出来了,彼此之间的转换也带来了诸多的问题。采用某种统一的标准就势在必行了,于是乎天上一声霹雳,Unicode登场!

Unicode早期是作为定长二字节方案出来的。它的理论编码空间一下达到了64k

不过对于可以只需要使用到一个字节的ASCII 码就可以表示的美国人来讲,让他们使用 Unicode ,多少还是不是很愿意的。

比如 「he」 两个字符,用 ASCII 只需要保存成 6865 ( 16 进制),现在则成了 00680065 ,前面多的那两个 0 (用作站位)。

基本上可以说是毫无意义,用 Unicode 编码的文本,原来可能只有 1KB 大小,现在要变成 2KB ,体积成倍的往上涨。

可是更糟糕的事还在后头,我们中文可是字符集里面的大头。1.“茴字有四种写法”——上大人孔乙己

2.据说有些新近整理的汉语字典收录的汉字数量已经高达10万级别!

如果还是定长的方案,眼瞅着就要奔着四字节而去了,容量与效率的矛盾在这时候开始激化。容量与效率的矛盾所谓容量,这里指用几个字节表示一个字符,显然用的字节越多,编码空间越大,能表示更多不同的字符,也即容量越大。

所谓效率,当表示一个字符用的字节越多,所占用的存储空间也就越大,换句话说,存储(乃至检索)的效率降低了。

ebd5e949d44a1aa6ddf480be3b4f05e9.png

如果说效率是阴,那么容量就是阳。

(_我没还没忘记自小学语文老师就开始教导的,写作文要遥相呼应_)莫尔斯电码图

看如图所示你就会发现,字母e只用了一个点(dot)来编码

e8ab29f8edcbaf785984ae24a9179a0d.png

其它字母可能觉得不公平,为啥我们就要录入那么多个点和划(dash)才行呢?这里面其实是有统计规律支撑的。e出现的概率是最大的z你能想到什么?zoo大概很多人能想到,厉害一点可能还能想到zebra(斑马),Zuckerberg(扎克伯格),别翻字典!你还能想到更多不?e你能想到什么?含有的e的单词则多了去了。zebra中不就有个e吗,Zuckerberg中还两个e呢

所以我们希望频率越高的词,编码越短,这样最终才能最大化压缩存储文本数据的空间。

为什么要使用哈夫曼编码?使用别的编码行不行?

上面提到常见的信息处理方式:定长编码与变长编码

假设当前有一句话:i like like like java do you like a java(共40个字符,包括空格)那么按照定长编码处理,那么会变成什么呢?

ba71cd3661495f12705ef8458d908b28.png

我们发现最后存在计算机中(二进制)长度是:三百五十九那么按照变长编码处理,那么会变成什么呢?

2c67ec448c2cb45f8ba45b3dcab15f14.png

假设我们将这串10010110100...发给别人,但是没有说明解码的方式,这时就会出现解释时存在产生歧义。

比如说可能翻译成:"a a a aa a ",所以我们需要说明字符编码不能是其他字符编码的前缀,即不能匹配重复的编码。那么按照哈夫曼处理,那么会变成什么呢?

abf9ef248bef3849a1a929ad174d6970.png

b50d388c064059b5bb8e7750d1a07201.png

1231a8625aa2c16650e10920505487a4.png

c65a41967dd42cdab4ea1ac6d55fde98.png

相比定长编码二进制的长度:三百五十九,哈夫曼二进制长度为:一百三十三

那么相比定长编码二进制的长度优化了多少呢?:(359-133)/359=62.9%

我们再将哈夫曼二进制转码对应的byte字符

8ec8e1f915b044e159b81ac3a4bb1855.png

那么相比原字符长度优化了多少呢?:(40-17)/40 = 57.5%

一下将原字符40位变成17位, 这样的情况下,是不是我们想要的

二、解析哈夫曼编码执行思路与过程

上面我们采用"i like like like java do you like a java",做例子分析,那么我们现在分析分析哈夫曼编码思路将字符串里的字符进行统计个数并转为Byte[]数组

根据字符的出现次数构建一颗哈夫曼树、次数为权值

根据哈夫曼树进行自定义哈夫曼编码实现

将原字符的所有哈夫曼编码构建成编码串

根据编码串进行补码->反码->原码构建新字符

三、统计字符出现次数并构建哈弗曼树

图解思路分析获取传输的字符串:"i like like like java do you like a java"

获取各个字符对应的个数:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9

按照上面的字符出现的次数构建成一颗哈夫曼树,出现的个数作为权值

构建哈夫曼树的步骤思路

1.将数列进行从小到大排序

2.新数列的每个数看成最简单根节点的二叉树

3.取出权值最小的两颗二叉树组成新的二叉树,为两最小的树之和

4.将新二叉树以跟节点的权重值再次从小到大排序,不断重复步骤

b50d388c064059b5bb8e7750d1a07201.png

构建哈夫曼树的代码思路Node{data:存放数据,weight:权重,left和right}

得到"i like like like java do you like a java"对应byte[]数组

编写方法,将准备构建赫夫曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = .....

通过List创建对应的哈夫曼树

体现获取各个字符对应的个数:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9

比如说字符:a 应该是Node[data='a', weight= 5],data为什么会是97而不是a?

因为底层:字母等于ASCII数字

构建哈夫曼树的代码实现

那么第一步:创建节点Node{data:存放数据,weight:权重,left和right}class Nodedata implements Comparable{

Byte data; //存放数据(字符)本身,比如'a' =>97 '' =>32

int weight; //权值,表示字符出现的次数

Nodedata left;

Nodedata right;

public Nodedata(Byte data, int weight) {

this.data = data;

this.weight = weight;

}

@Override

public int compareTo(Nodedata o) {

return this.weight - o.weight;

}

@Override

public String toString() {

return "Nodedata[" +"data=" + data + ", weight=" + weight +']';

}

}

第二步:得到"i like like like java do you like a java"对应byte[]数组public static void main(String[] args) {

//得到`"i like like like java do you like a java"`对应byte[]数组

String content = "i like like like java do you like a java";

byte[] contentBytes = content.getBytes();

System.out.println(contentBytes.length);

}

第三步:编写方法,将准备构建赫夫曼树的Node节点放到Listprivate static List getNodes(byte[] bytes){

//1.创建一个ArrayList

List nodeslist = new ArrayList();

//2.遍历bytes,统计每一个byte出现的次数->map[key,value]

Map map = new HashMap<>();

for(byte b :bytes){

Integer items = map.get(b);

if(items == null){

map.put(b,1);

}else{

map.put(b, items + 1);

}

}

//3.把每个键值对转为Node 对象,并放入nodeslist集合里

for(Map.Entry temp :map.entrySet()){

nodeslist.add(new Nodedata(temp.getKey(),temp.getValue()));

}

return nodeslist;

}

第四步:通过List创建对应的哈夫曼树private static Nodedata createHaffman(List nodedataList){

while(nodedataList.size() >1){

//排序从小到大

Collections.sort(nodedataList);

/**

* 操作思路

* 1.取出两个权值最小的节点二叉树

* 2.根据两个权值最小的二叉树构建成一个新的二叉树

* 3.删除原先两个权值最小的节点二叉树

* 4.将新的二叉树放入队列,并构建新的队列

* 5.新的队列进行从小到大排序

*/

//取出第一个权值最小的二叉树

Nodedata leftNode = nodedataList.get(0);

//取出第二个权值最小的二叉树

Nodedata rightNode = nodedataList.get(1);

//根据两个权值最小的二叉树构建成一个新的二叉树 同时构建连接

Nodedata parentNode = new Nodedata(null,leftNode.weight + rightNode.weight);

parentNode.left = leftNode;

parentNode.right = rightNode;

//删除原先两个权值最小的节点二叉树

nodedataList.remove(leftNode);

nodedataList.remove(rightNode);

//将新的二叉树放入队列,并构建新的队列

nodedataList.add(parentNode);

}

return nodedataList.get(0);

}

测试检查是否满足代码思路

检查是否Byte[]长度:40public static void main(String[] args) {

//得到`"i like like like java do you like a java"`对应byte[]数组

String content = "i like like like java do you like a java";

byte[] contentBytes = content.getBytes();

System.out.println("byte[]数组长度为:"+contentBytes.length);

}

运行结果如下:

byte[]数组长度为:40

检查字符个数:d:1 y:1 u:1 j:2 v:2 0:2 l:4 k:4 e:4 i:5 a:5 (空格):9public static void main(String[] args) {

//将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = .....

List datalist = getNodes(contentBytes);

for(Nodedata data : datalist){

System.out.println(data);

}

}

运行结果如下:

Nodedata[data=32, weight=9]

Nodedata[data=97, weight=5]

Nodedata[data=100, weight=1]

Nodedata[data=101, weight=4]

Nodedata[data=117, weight=1]

Nodedata[data=118, weight=2]

Nodedata[data=105, weight=5]

Nodedata[data=121, weight=1]

Nodedata[data=106, weight=2]

Nodedata[data=107, weight=4]

Nodedata[data=108, weight=4]

Nodedata[data=111, weight=2]

ecd3d654aca1bc8770a3316db230f476.png

Nodedata 节点添加前序遍历,添加哈弗曼树方法class Nodedata implements Comparable{

//省略实体类代码

//前序遍历方法

public void preOrder(){

System.out.println(this);

if(this.left != null){

this.left.preOrder();

}

if(this.right != null){

this.right.preOrder();

}

}

}private static void preOrder(Nodedata root){

if(root != null){

root.preOrder();

}else{

System.out.println("哈弗曼树为空!");

}

}

检查前序遍历哈弗曼树,查看是否图一致:40->17->8->4->4->2->2->9...

b50d388c064059b5bb8e7750d1a07201.pngpublic static void main(String[] args) {

//获取哈弗曼树的根节点

Nodedata root = createHaffman(datalist);

//遍历哈弗曼树

preOrder(root);

}

运行结果如下:

Nodedata[data=null, weight=40]

Nodedata[data=null, weight=17]

Nodedata[data=null, weight=8]

Nodedata[data=108, weight=4]

Nodedata[data=null, weight=4]

Nodedata[data=106, weight=2]

Nodedata[data=111, weight=2]

Nodedata[data=32, weight=9]

Nodedata[data=null, weight=23]

Nodedata[data=null, weight=10]

Nodedata[data=97, weight=5]

Nodedata[data=105, weight=5]

Nodedata[data=null, weight=13]

Nodedata[data=null, weight=5]

Nodedata[data=null, weight=2]

Nodedata[data=100, weight=1]

Nodedata[data=117, weight=1]

Nodedata[data=null, weight=3]

Nodedata[data=121, weight=1]

Nodedata[data=118, weight=2]

Nodedata[data=null, weight=8]

Nodedata[data=101, weight=4]

Nodedata[data=107, weight=4]

那么我们前面分析了如何将字符转为哈弗曼树的思路与代码

下面看看如何将根据哈夫曼树进行构成自定义哈夫曼编码。

四、根据哈夫曼树自定义哈夫曼编码根据哈夫曼树我们来自定义编码规则:向左为0、向右为1

这样根据上面如图所示,定义的字符编码如下:[o: 0011 u: 11001 d: 11000 y: 11010 i: 101 a: 100 k: 1111 e: 1110 j: 0010 V: 11011 I: 000 (空格):01]

1231a8625aa2c16650e10920505487a4.png

有没有发现哈弗曼树的节点编码它是前缀编码:不是是其他字符编码的前缀,即匹配不到重复的编码。

根据哈夫曼树自定义哈夫曼编码代码思路分析使用StringBuilder记录所经过的路径:'a'的路径是:1->1->0

使用Key:Value 键值的方式存放对应的字符:路径;比如:(a:1000)、(j:0010)、(o:0011)

字符特点:data !=null(属于叶子节点)

假设查询字符v的路径,发现左递归不符合后还需要进行右递归判断

根据哈夫曼树自定义哈夫曼编码代码实现//将哈夫曼编码以Key:Vale形式存放

//比如说32->01 97->100 100->11000 等等[形式]

static Map huffmanCodes = new HashMap();

//2.在生成赫夫曼编码表示,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径

static StringBuilder stringBuilder = new StringBuilder();

/**

* 1.功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合 * @param node 传入节点

* @param code 路径:左节点是0 右节点是1

* @param stringBuilder 用于拼接路径

*/

private static void getCodes(Nodedata node,String code,StringBuilder stringBuilder){

StringBuilder str = new StringBuilder(stringBuilder);

//将code加入到str 总

str.append(code);

if(node!=null){

//字符特点:`data !=null `(属于叶子节点)

if(node.data ==null){//(属于非叶子节点)

//向左递归

getCodes(node.left,"0",str);

//向右递归

getCodes(node.right,"1",str);

}else{

//`使用Key:Value 键值`的方式存放对应的`字符:路径;`比如:`(a:110)、(j:0000)、(o:1000)`

huffmanCodes.put(node.data,str.toString());

}

}

}

我们优化一下代码,实现传入根节点就可以返回哈夫曼编码private static Map getCodes(Nodedata node){

//根节点为空则不做处理

if(node == null){

return null;

}

//向左递归

getCodes(node.left,"0",stringBuilder);

//向右递归

getCodes(node.right,"1",stringBuilder);

return huffmanCodes;

}

我们进行运行测试看看,是否欲思路一致生成编码public static void main(String[] args) {

//将哈夫曼树生成哈夫曼编码

//getCodes(root,"",stringBuilder);

//执行优化代码

getCodes(root);

}

运行结果如下:

生成的哈夫曼编码表:{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}

注意:不同的排序方式会生成不同的哈弗曼树,所造成的哈夫曼编码也不一样,但WPL是一致

五、将原字符的所有哈夫曼编码构建成编码串

我们将"i like like like java do you like a java"字符串,即生成对应的哈夫曼编码数据(如下)

c65a41967dd42cdab4ea1ac6d55fde98.png

将原字符串转为编码串思路分析将字符串"i like like like java do you like a java"转为对应的Byte[]数组

使用StringBuilder追加存储byte字符的对应字符路径

将原字符串转为编码串思路分析代码实现private static void zip(byte[] bytes, Map huffmanCodes) {

//1.利用huffmanCodes将bytes 转成赫夫曼编码对应的字符串

StringBuilder stringBuilder = new StringBuilder();

//遍历bytes数组

for(byte b: bytes) {

stringBuilder.append(huffmanCodes.get(b));

}

System.out.printf("stringBuilder=" + stringBuilder.toString());

System.out.print("\n stringBuilder的长度=" + stringBuilder.length());

}

代码测试public static void main(String[] args) {

//得到`"i like like like java do you like a java"`对应byte[]数组

String content = "i like like like java do you like a java";

byte[] contentBytes = content.getBytes();

System.out.println("byte[]数组长度为:"+contentBytes.length);

//将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = .....

List datalist = getNodes(contentBytes);

//获取哈弗曼树的根节点

Nodedata root = createHaffman(datalist);

//将哈夫曼树生成哈夫曼编码Key:Value

getCodes(root);

//将Byte[]数组转为自定义的哈夫曼编码字符路径

zip(contentBytes,huffmanCodes);

}

运行结果如下:

byte[]数组长度为:40

stringBuilder=1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

stringBuilder的长度=133

我们将"i like like like java do you like a java"

转为编码串相比之前定长编码优化了多少呢?:(359-133)/359=62.9%

c65a41967dd42cdab4ea1ac6d55fde98.png

六、将编码串进行补码->反码->原码构建新字符

那么如何将长度一百三十三的二进制串转为新的字符呢?

在讲编码串进行补码->反码->原码构建新字符前,先解释了解了解不同的进制

相同的数字有不同的样子在计算机的世界中就分两种人:认识二进制和不认识二进制

上面我们介绍到我们看到的漂亮的图像、好听的声音、震撼的视频等等这些精彩内容。但是对于计算机说,它的世界里只有二进制的 0 和 1。

9b33245ea918bd454f49cb9b73e438f8.png所有数字在计算机底层都以二进制形式存在

对于整数有不同表达方式:二进制、十进制、八进制、十六进制

18874bc5b3a9b3e347f7784fab050612.png

6c7e14ab2a717d61241d0a2327f09ef1.png

ec3ddb90df439b13f4a2e29c6b717ea5.png

二进制与十进制的说明

我们使用Byte字符的二进制做演示,首先我们直观一点举例正数说明

34881144e17150aab92a22f58ef6f838.png

d9f6611ef5978b1c5716ccc1d23ceef2.png

652720fbec3c3e58f0e07bc838344f99.png

180cf5ad39e8363cbce9c27b3543c801.png

dd53adb5a43e2717b3cd8ff97647ac49.png

以此类推....那么十进制转二进制该怎么做呢?来看看规律

e6190a697f1f6b6fc16f76fb6c4638d1.png

4163c95db93cc2fd3613fec02957a556.png

cf4a38c1df6e45426a36650184de59dc.png

有没有发现?从右开始往左是2^0(二的零次方)、2^1(二的一次方)、2^2(二的平方)、2^3(二的三次方)

15589c7c10a6b1add17cf8c562274056.png

根据前面知识点,你知道:01101110的十进制是多少么?

73eeb1d9c848043da0ca74caa26e616b.png

那么我们说Byte表示 -128 ~127 ,那么0和1 是怎么表示正数和负数的呢?

078cc4ff1504773f3bf20467fb885600.png

我们现在知道二进制的最高位:0 正数 1-负数 ,那么对于整数还要提提三个码:原码、反码、补码

64be0bf24b5d437e431a6489133473dc.png

那么我们思考一下:在计算机中:-14 是什么样的呢?

接下来看看负数的解析图:十进制:14、二进制:00001110

e80b65a7b90f485416adfa1c9e9ce631.png

那么我们现在反推一下:10111011 转为十进制是多少呢?我们刚刚提到计算机底层都以补码方式存储

我们刚刚观察知道最高位: 0 - 正数、1-负数

而负数则是补码的形式,那么思路反推:补码->反码->原码

905a8a5a56dec34f7ac77652aed75123.png

接下来我相信大家都了解了什么是原码、反码、补码了

上面我们将字符串"i like like like java do you like a java" 转为了自定义的哈弗曼编码串

1392343b971bc6198dfbec16442e0d30.png

接下来我们创建Byte[] huffmanCodeBytes,处理哈夫曼编码串,以八位为一组对应一个Byte 放入huffmanCodeBytes中

893db4d688979b58ae0e0694505aba20.png

04a97c1d12e60fbc58a279f92f9c0ac8.png

转为新字符代码的思路分析每八位对应一个Byte,所以133 % 8,使用循环每次i + 8

因为每次 i + 8 所以需要使用变量index 记录对应的第几位Byte

使用(byte)Integer.parInt(string,radix)转码

转为新字符代码的代码实现private static byte[] zip(byte[] bytes, Map huffmanCodes) {

//1.利用huffmanCodes将bytes 转成赫夫曼编码对应的字符串

StringBuilder stringBuilder = new StringBuilder();

//遍历bytes数组

for(byte b: bytes) {

stringBuilder.append(huffmanCodes.get(b));

}

//2.1010100010111111...思路:补码->反码->原码->转成Byte

int len;

if(stringBuilder.length() %8 == 0){

len = stringBuilder.length() /8;

}else{

len = stringBuilder.length() /8 + 1;

}

//3.创建存储压缩后的byte数组

byte[] huffmanCodeBytes = new byte[len];

int index = 0;//记录是第几个byte

for (int i = 0; i < stringBuilder.length(); i += 8) { //因为是每8位对应一个byte,所以步长+8

String strByte;

if(i+8 > stringBuilder.length()) {//不够8位

strByte = stringBuilder .substring(i);

}else {

strByte = stringBuilder .substring(i, i + 8);

}

//将strByte转成一个byte ,放入到huffmanCodeBytes

huffmanCodeBytes[index] = (byte)Integer .parseInt(strByte, 2);

index++;

}

return huffmanCodeBytes;

}public static void main(String[] args) {

//得到`"i like like like java do you like a java"`对应byte[]数组

String content = "i like like like java do you like a java";

byte[] contentBytes = content.getBytes();

System.out.println("byte[]数组长度为:"+contentBytes.length);

//将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = .....

List datalist = getNodes(contentBytes);

//获取哈弗曼树的根节点

Nodedata root = createHaffman(datalist);

//将哈夫曼树生成哈夫曼编码

getCodes(root);

byte[] huffmanCodeBy =zip(contentBytes,huffmanCodes);

System.out.println(Arrays.toString(huffmanCodeBy));

}

运行结果如下:

byte[]数组长度为:40

[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

b9c50cfc6759cfce3f155b5cced44833.png

那么相比原字符长度优化了多少呢?:(40-17)/40 = 57.5%

一下将原字符40位变成17位

七、封装直接返回新符号Byte[]方法

根据前面的分析,我们调用的方法比较多也比较臃肿,每次测试需要将字符串转为Byte数组,并统计个数

根据字符出现次数作为权重,构建哈弗曼树

根据哈弗曼树生成自定义哈弗曼编码

将原字符的所有哈夫曼编码构建成编码串

根据编码串进行补码->反码->原码构建新字符

现在呢,我们将封装这些方法方便我们调用,我们的目的是获取到新字符组//使用一个方法将需要的方法疯转起来,方便调用

private static byte[] haffmanZip(byte[] bytes){

//将准备构建哈弗曼树的Node节点放到List:Node[data=97, weight= 5],Node[data=32,weight = .....

List datalist = getNodes(bytes);

//根据字符出现次数作为权重,构建哈弗曼树

//获取哈弗曼树的根节点

Nodedata root = createHaffman(datalist);

//根据哈夫曼树进行自定义哈夫曼编码实现

Map huffmanCodes = getCodes(root);

//将原字符的所有哈夫曼编码构建成编码串

//根据编码串进行补码->反码->原码构建新字符

byte[] huffmanCodeBy =zip(bytes,huffmanCodes);

return huffmanCodeBy;

}public static void main(String[] args) {

String content = "i like like like java do you like a java";

byte[] contentBytes = content.getBytes();

System.out.println("byte[]数组长度为:"+contentBytes.length);

byte[] huffmanCodeBy =haffmanZip(contentBytes);

System.out.println(Arrays.toString(huffmanCodeBy));

System.out.println("压缩后的byte[]数组长度为:"+huffmanCodeBy.length);

}运行结果如下:

byte[]数组长度为:40

[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

压缩后的byte[]数组长度为:17

八、将新字符解码转为原字符串

我们已经将"i like like like java do you like a java"包括空格在内的四十个字符

压缩成[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]只有十七位的新字符。

那么假如我们将这串新字符发给对方

对方如何解码成"i like like like java do you like a java"呢?

f8743b9f909e821c94eeb3ca693b5f6c.png

[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]是怎么来的?将长度为133的编码串 % 8 再进行补码->反码->原码

那么我们现在获取到新字符后需要使用逆向思维获取原补码

我们可以使用Integer.toBinaryString()方法将byte转成二进制/**

*将一个byte转成-个二进制的字符串

* @param b

* @return

*/

private static String byteToBitString(byte b) {

//使用变量保存b

int temp = b;//将b转成int

String str = Integer.toBinaryString(temp);

return str;

}

我们使用 -1 进行测试这个方法看看//测试一把byteToBitString方法

System.out.println(byteToBitString((byte)-1));

运行结果如下:

str=11111111111111111111111111111111

很遗憾,我们发现返回的是int 的32位补码。注意是:补码

按理说我们应该是从133 % 8获取的新字符,现在应该也是转为8位的补码,而不是32位/**

*将一个byte转成-个二进制的字符串 * @param b

* @return

*/

private static String byteToBitString(byte b) {

//使用变量保存b

int temp = b;//将b转成int

String str = Integer.toBinaryString(temp);

return str.substring(str.length() -8);

}

那么现在就可以了吗?其实并不然还有一个补位的问题!比如说我现在输入 1

b8eae57a74ac623b4d1560cfeb1272f1.png

当是正数的时候,当前stu =1 则需要补位、否则是无法完成截取操作的/**

*将一个byte转成-个二进制的字符串 * @param b

* @return

*/

private static String byteToBitString(byte b) {

//使用变量保存b

int temp = b;//将b转成int

//如果是正数我们还存在补高位

temp |= 256;

String str = Integer.toBinaryString(temp);

return str.substring(str.length() -8);

}

运行结果如下:

00000001

???

可能会有小伙伴会问到 temp |=256 是什么意思???为什么要|=256

444eee60cd6c8c79998ee572faebc78c.png

这就涉及到知识点二进制的运算符了,两个二进制对应位为0时该位为0,否则为1

677e7170886e3bca42e526c6661754dc.png

这样我们输入十进制:1 的时候,才能进行截取长度,否则不满足/**

*将一个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;

}

String str = Integer.toBinaryString(temp);

if(flag){

return str.substring(str.length() -8);

}else{

return str;

}

}

根据八位为一组的思路,就会发现最后一个byte字节就无需补高位

将新字符解码转为原字符串思路分析找到记录原字符byte转为自定义哈夫曼编码Map

使用StringBuilder记录新字符组的补码

根据自定义哈夫曼编码Map反获取(编码:字符)组成新map

根据StringBuilder匹配新map获取对应字符

第一步:使用StringBuilder记录新字符组的补码/**

* @param huffmanCodes 哈夫曼编码表map

* @param huffmanBytes 哈夫曼得到的字节数组

* @return就是原来的字符串对应的数组

*/

private static byte[] decode(Map huffmanCodes,byte[] huffmanBytes){

//1.使用StringBuilder记录新字符组的补码

StringBuilder stringBuilder =new StringBuilder();

//循环记录

for (int i=0;i

byte b = huffmanBytes[i];

//最后一个字符无需补位

boolean flag = (i == huffmanBytes.length -1?true:false);

stringBuilder.append(byteToBitString(!flag,b));

}

System.out.println("新字符组的补码串:"+stringBuilder.toString());

System.out.println("新字符组的补码串长度:"+stringBuilder.length());

return null;

}public static void main(String[] args) {

/得到`"i like like like java do you like a java"`对应byte[]数组

String content = "i like like like java do you like a java";

byte[] contentBytes = content.getBytes();

System.out.println("原byte[]数组长度为:"+contentBytes.length);

byte[] huffmanCodeBy =haffmanZip(contentBytes);

System.out.println("n开始进行哈夫曼编码压缩==============================n");

System.out.println("压缩后的新字符数组:"+Arrays.toString(huffmanCodeBy));

System.out.println("压缩后的byte[]数组长度为:"+huffmanCodeBy.length);

decode(huffmanCodes,huffmanCodeBy);

}

运行结果如下:

原byte[]数组长度为:40

原字符数组经过自定义编码后的编码串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

原字符数组经过自定义编码后的编码串长度:133

开始进行哈夫曼编码压缩==============================

压缩后的新字符数组:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

压缩后的byte[]数组长度为:17

新字符组的补码串:1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

新字符组的补码串长度:133

第二步:找到记录原字符byte转为自定义哈夫曼编码Map、根据自定义哈夫曼编码Map反获取(编码:字符)组成新map/**

* * @param huffmanCodes 哈夫曼编码表map

* @param huffmanBytes 哈夫曼得到的字节数组

* @return就是原来的字符串对应的数组

*/

private static byte[] decode(Map huffmanCodes,byte[] huffmanBytes){

//1.使用StringBuilder记录新字符组的补码

StringBuilder stringBuilder =new StringBuilder();

//循环记录

for (int i=0;i

byte b = huffmanBytes[i];

//最后一个字符无需补位

boolean flag = (i == huffmanBytes.length -1?true:false);

stringBuilder.append(byteToBitString(!flag,b));

}

//找到记录原字符byte转为自定义哈夫曼编码Map、根据自定义哈夫曼编码Map反获取原字符

Map map =new HashMap();

for(Map.Entry item:huffmanCodes.entrySet()){

map.put(item.getValue(),item.getKey());

}

System.out.println("根据自定义哈夫曼编码Map反获取(编码:字符)组成新map = "+map);

return null;

}

运行结果如下:

根据自定义哈夫曼编码Map反获取(编码:字符)组成新map = {000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}

第三步:根据StringBuilder匹配新map获取对应字符

ffb7a4b36b4b7736893377a6cba0b906.png

8164548c1f512033b2bbd78606a04aab.png/**

* * @param huffmanCodes 哈夫曼编码表map

* @param huffmanBytes 哈夫曼得到的字节数组

* @return就是原来的字符串对应的数组

*/

private static byte[] decode(Map huffmanCodes,byte[] huffmanBytes){

//1.使用StringBuilder记录新字符组的补码

StringBuilder stringBuilder =new StringBuilder();

//循环记录

for (int i=0;i

byte b = huffmanBytes[i];

//最后一个字符无需补位

boolean flag = (i == huffmanBytes.length -1?true:false);

stringBuilder.append(byteToBitString(!flag,b));

}

//找到记录原字符byte转为自定义哈夫曼编码Map、根据自定义哈夫曼编码Map反获取原字符

Map map =new HashMap();

for(Map.Entry item:huffmanCodes.entrySet()){

map.put(item.getValue(),item.getKey());

}

//根据StringBuilder匹配新map获取对应字符

List list =new ArrayList<>();

for (int j=0;j

int count = 1;

boolean flag = true;

Byte b = null;

while(flag){

String key =stringBuilder.substring(j,j+count);

b = map.get(key);

if(b == null){

count++;

}else{

flag = false;

}

}

list.add(b);

j +=count;

}

//当for循环结束后,list就存放了字符串"i like like like java do you like a java"的所有字符

//把集合里的数据放入byte[]中返回 byte[] b = new byte[list.size()];

for (int k =0;k

b[k]=list.get(k);

}

return b;

}public static void main(String[] args) {

/得到`"i like like like java do you like a java"`对应byte[]数组

String content = "i like like like java do you like a java";

byte[] contentBytes = content.getBytes();

System.out.println("原byte[]数组长度为:"+contentBytes.length);

byte[] huffmanCodeBy =haffmanZip(contentBytes);

System.out.println("n开始进行哈夫曼编码压缩==============================n");

System.out.println("压缩后的新字符数组:"+Arrays.toString(huffmanCodeBy));

System.out.println("压缩后的byte[]数组长度为:"+huffmanCodeBy.length);

byte[] source = decode(huffmanCodes,huffmanCodeBy);

System.out.println("新字符数组经过根据新map获取对应字符:"+new String(source));

}

运行结果如下:

根据自定义哈夫曼编码Map反获取(编码:字符)组成新map = {000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}

新字符数组经过根据新map获取对应字符:i like like like java do you like a java

九、最佳实践文件压缩与解压

我们将这个图片文件,进行压缩实践看看

55bf2a2de3262aa9e8685f3e97b84389.png

思路:读取文件-->得到哈夫曼编码表-->完成压缩/**

*@paramsrcFile 希望压缩文件的全路径

*@param dstFile 压综后将文件放到哪个目录

*/

public static void zipFile(String srcFile, String dstFile) {

//创建输出流

OutputStream os = null;

//对象输出流

ObjectOutputStream oos;

//创建文件的输入流

FileInputStream is = null;

try {

//创建文件的输入流

is = new FileInputStream(srcFile);

//创建一个和源文 件大小一样的byte[ ]

byte[] b = new byte[is.available()];

//读取文件

is.read(b);

//直接对文件压缩

byte[] bytes = haffmanZip(b);

//创建文件的输出流,存放压缩文件

os = new FileOutputStream(dstFile);

//创建一个和文件流关联的ObjectOutputStream;

oos = new ObjectOutputStream(os);

//以对象流的方写入哈夫曼编码,为了方便恢复

oos.writeObject(huffmanCodes);

} catch (Exception e) {

System.out.println(e.getMessage());

} finally {

try {

is.close();

oos.close();

os.close();

}catch (Exception e) {

System.out.println(e.getMessage());

}

}

}public static void main(String[] args) {

//测试压编文件

String srcFile = "d://src. bmp";

String dstFile = "d://dst.zip";

zipFile(srcFile, dstFile);

System. out . println("压编文件ok~~");

}

1607fabbc6c32578b312fef3646f8067.png

会出现无法打开并解压的方式,为什么呢?

因为我们自定义属于自己的的压缩方式,所以解压也要用我们自己的方式去解压

源文件大小:598kb

压缩后的文件大小:76kb

那么接下来我们编写自己的解压方式把

思路:读取压缩文件(数据和哈弗曼编码表)-->完成解压//编写一个方法,完成对压缩文件的解压

/**

* @param zipFile 准备解压的文件

* @param dstFile 将文件解压到哪个路径

*/

public static void unZipFile(String zipFile, String dstFile) {

//定义文件输入流

InputStream is = null;

//对象输出流

ObjectInputStream ois = null;

//创建输出流

OutputStream os = null;

try {

//创建文件的输入流

is = new FileInputStream(zipFile);

//创建一个和文件流关联的ObjectOutputStream;

ois = new ObjectInputStream(is);

//读取byte数组 huffmanbytes

byte[] huffmanBytes = (byte[])ois.readObject();

//获取哈夫曼编码表

Map huffmanCodes = (Map)ois.readObject();

//解码

byte[] bytes =decode(huffmanCodes,huffmanBytes);

//将恢复的原byte数组写入文件

os = new FileOutputStream(dstFile);

os.write(bytes);

} catch (Exception e) {

System.out.println(e.getMessage());

} finally {

try {

os.close();

ois.close();

is.close();

}catch (Exception e) {

System.out.println(e.getMessage());

}

}

}public static void main(String[] args) {

//测试解压文件

String zipFile = "d://dst. zip";

String dstFile = "d://src2. bmp";

unZipFile(zipFile, dstFile) ;

}

989f4421599dad696e70ab9057429b51.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/288946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Windows之Wireshake之抓HTTP请求包(过滤目的IP)

1 问题 用Wireshake抓取HTTP请求包 2 解决办法 直接在在WireShare里面过滤条件输入http 然后在浏览器输入http的请求,我的效果如下 看吧,都是http请求 如果数据太多,我只需要过滤一个目的的ip,我们可以这样设置参数 ip.addr == 目的ip && http 如下图 然后我输…

html使用element ui_Kendo UI for jQuery使用教程:自定义小部件(二)

Kendo UI目前最新提供KendoUI for jQuery、KendoUI for Angular、KendoUI Support for React和KendoUI Support for Vue四个控件。Kendo UI for jQuery是创建现代Web应用程序的完整UI库。Kendo UI通过继承基本窗口小部件类为您提供创建自定义窗口小部件的选项。处理事件1. 将更…

ASP.NET MVC 个人学习笔记之 Controller传值

2019独角兽企业重金招聘Python工程师标准>>> //ControllerViewBag.Message "Modify this template to jump-start your ASP.NET MVC application."; // View<section class"featured"><div class"content-wrapper"><…

Dapr 和 Spring Cloud 对比分析

很多人都是使用SpringBoot 和 Spring Cloud来开发微服务。Dapr 也是开发微服务的框架&#xff0c;它和Spring Cloud有什么区别呢&#xff0c;其实这不是一个区别的问题&#xff0c;它是不同的时代需要不同的框架。Spring Cloud 是一种产品&#xff0c;提供了分布式应用程序所需…

【C语言简单说】七:自定义函数(2)

刚刚说了如何自定义函数。那只是最简单的形式&#xff0c;那我们现在开始来说函数的返回值。我们先贴上之前的代码&#xff1a; #include<stdio.h> #include<stdlib.h> //头文件下面 void dy(){printf("这里是dy函数\n");} //main函数上面 int main() …

jdk源码分析书籍 pdf_什么?Spring5 AOP 默认使用Cglib?从现象到源码深度分析

推荐阅读&#xff1a;阿里工作十年拿下P8&#xff0c;多亏了这些PDF陪我成长&#xff08;Spring全家桶源码解析Redis实战等&#xff09;​zhuanlan.zhihu.com从入门到熟悉&#xff0c;一步一步带你了解 MySQL 中的「索引」和「锁」​zhuanlan.zhihu.comSpring5 AOP 默认使用 Cg…

Nginx monitor

为什么80%的码农都做不了架构师&#xff1f;>>> 最近在初步研究了一下nginx的监控&#xff0c;主要是想监控一些和业务相关的信息&#xff0c;发现能用的方案不多&#xff0c;主要有如下&#xff1a; 1 监控解析nginx log&#xff1a;ngxtop 官方的解释是可以…

ASP.NET 6 中间件系列 - 执行顺序

这篇文章是 ASP.NET 6 中间件系列文章的第 3 部分&#xff0c;你还可以阅读第1部分和第2部分。我们通过中间件创建的管道是有执行顺序的&#xff0c;执行顺序与中间件的添加顺序是相同的&#xff0c;接下来我们讨论一下为什么要有执行顺序&#xff0c;以及它的重要性。示例项目…

OSChina 周四乱弹 ——程序员怎么撩外国妹子攻略

2019独角兽企业重金招聘Python工程师标准>>> Osc乱弹歌单&#xff08;2017&#xff09;请戳&#xff08;这里&#xff09; 【今日歌曲】 冬天之雪 &#xff1a;听歌听到苏菲玛索和刘欢演唱《玫瑰人生》。有网友评论&#xff1a;法语专业的刘欢老师等的就是这一刻。…

【C语言简单说】七:自定义函数(3)

前一节说了返回值的内容&#xff0c;那么这一节就说一下参数的内容 手打码了几章内容了。。。。困。&#xff3e;(&#xffe3;&#xff09;&#xffe3;)《(&#xffe3;&#xff09;&#xffe3;)&#xff3e;困.困.困. 相比看过前一节的知道我说自定义函数如果你要做一个运…

WireShare抓包在ssl协议里面提示(Level: Fatal, Description: Protocol Version)

1 问题 在后台访问部分链接的时候抓包&#xff0c;客户端发了client hello包&#xff0c;但是没有收到Server hello包&#xff0c; 提示&#xff1a;Alert(Level: Fatal, Description: Handshake Failure ) 包文如下 2 解决办法 是因为客户端的ssl协议版本和链接地址环境ss…

对于 APM 用户的一次真实调查分析(下)

一.前言 对 APM 用户的一次真实调查分析&#xff08;上&#xff09;中&#xff0c;我们主要聊到了现阶段国外 APM 行业对各个企业的渗透率、大部分使用 APM 工具的企业规模以及 APM 工具在用户心中的地位等问题&#xff0c;有兴趣的朋友可以点击链接观看。 我们本次继续顺着这个…

linux cpu核数查看_Linux日常必备的 8 个小技能

身为一个码农&#xff0c;日常工作中与我们打交道次数较多的操作系统除了Windows和Mac OS 之外&#xff0c;还有一个就是 Linux。今天偶尔有空翻越了之前码代码时期汇总的一些小技巧发现挺实用的&#xff0c;故分享给大家&#xff0c;希望能对大伙有一定的帮助。1. 如何查看系统…

NotificationManagerService使用详解与原理分析(一)

概况 Android在4.3的版本中(即API 18)加入了NotificationListenerService&#xff0c;根据SDK的描述(AndroidDeveloper)可以知道&#xff0c;当系统收到新的通知或者通知被删除时&#xff0c;会触发NotificationListenerService的回调方法。同时在Android 4.4 中新增了Notifica…

【C语言简单说】八:分支结构之if(1)

今天貌似更了很多章了&#xff0c;现在感觉累觉不爱。。。 ┐(—__—)┌ 你说我有啥米办法咧~&#xff08;要不叫别人替我更一下&#xff1f;&#xff09; 继续更。。。 这一节我们来说一下if语句&#xff1b;这个东西可是很常用的呀&#xff1b;在此之前我们来举个例子&…

ASP.NET 6 中间件系列 - 自定义中间件类

这篇文章是 ASP.NET 6 中间件系列文章的第2部分&#xff0c;点击这里可以阅读第1部分。在上一篇文章中&#xff0c;我们讨论了什么是中间件&#xff0c;它的作用是什么&#xff0c;以及在 ASP.NET 6 应用管道中添加中间件的简单方法。在这篇文章中&#xff0c;我们将在这些基础…

如何在IE浏览器里面定位到关键字的位置(页面代码)和这个关键字位置模块的请求

1 问题 比如用IE浏览器,打开一个页面,如何定位到关键字的具体位置,以及这个位置请求是什么?可能这个请求不是主页面的请求,因为我们知道页面html里面可以嵌套很多Frame(框架),把页面分割成很多块,然而每个Frame(框架)里面可以再嵌套一个url,有时候我们需要找到这个请求…

Java并发编程-原子性变量

image.png1. 原子性布尔 AtomicBoolean AtomicBoolean 类为我们提供了一个可以用原子方式进行读和写的布尔值&#xff0c;它还拥有一些先进的原子性操作&#xff0c;比如 compareAndSet()。AtomicBoolean 类位于 java.util.concurrent.atomic 包&#xff0c;完整类名是为 java.…

【C语言简单说】八:分支结构之if...else...(2)

上一节我们说了if的基本用法&#xff0c;这一小节我们来说明if…else…的用法 首先惯例举例子&#xff1a; 你今天早上饿了&#xff0c;打算去吃包子&#xff0c;可是没有包子了&#xff0c;你打算去吃米粉。 你昨天早上下雨了&#xff0c;带伞出门&#xff0c;结果没找到&a…

Java集合之LinkedList

上一篇写的是ArrayList&#xff0c;这一篇写一下LinkedList. 开宗明义&#xff0c;因为Vector已经被废弃了&#xff0c;所以list家族只剩下ArrayList和LinkedList两兄弟了&#xff0c;这里直接对比一下二位&#xff1a; ArrayList基于动态数组的实现&#xff0c;它长于随机访问…