第十二章 哈夫曼树和哈夫曼编码

文章目录

  • 第十二章 哈夫曼树和哈夫曼编码
  • 一、哈夫曼树
    • 1.基本术语
    • 2.构建思路
    • 3.代码实现
  • 三、哈夫曼编码
    • 1.引入
    • 2.介绍
    • 3.代码实现哈夫曼编码综合案例

一、哈夫曼树

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近

1.基本术语

WPL 最小的二叉树是赫夫曼树

  • 路径和路径长度:
    在一棵树中,从上一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L - 1
  • 结点的权及带权路径长度:
    若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
  • 树的带权路径长度:
    树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(Weight Path Length)

2.构建思路

假设有 n 个权值,则构造出的哈夫曼树有 n 个叶子结点。n 个权值分别设为 w1、w2、w3、…、wn

  1. 将 w1、w2、w3、…、wn 看成是有 n 棵树的森林(每棵树仅有一个结点)
  2. 在森林中选出根结点的权值最小的两棵树进行合并,作为一棵新树的左、右子树,且新树的根结点权值为其左右子树根结点的权值之和
  3. 从森林中删除选取的两棵树,并将新树加入森林
  4. 重复 2 和 3,直到森林中只剩一棵树为止,该树即为所求的哈夫曼树

以 {5,6,7,8,15} 为例

  1. 创建森林,森林包括 5 棵树,这 5 棵树的权值分别是5,6,7,8,15
  2. 在森林中,选择根结点权值最小的两棵树(5 和 6)进行合并,将它们作为一棵新树的左右子结点,新树的根结点的权值为 11,将 5 和 6 从森林删除,添加新树 11
  3. 在森林中,选择根结点权值最小的两棵树(7 和 8)进行合并,将它们作为一棵新树的左右子结点,新树的根结点的权值为 15,将 7 和 8 从森林删除,添加新树 15
  4. 在森林中,选择根结点权值最小的两棵树(11 和 15)进行合并,将它们作为一棵新树的左右子结点,新树的根结点的权值为 26,将 11 和 15 从森林删除,添加新树 26
  5. 在森林中,选择根结点权值最小的两棵树(15 和 26)进行合并,将它们作为一棵新树的左右子结点,新树的根结点的权值为 41,将 15 和 26 从森林删除,添加新树 41

此时森林中只剩一棵树 41,该树即为所求的哈夫曼树

3.代码实现

package com.sisyphus.huffmantree;import java.util.ArrayList;
import java.util.Collections;/*** @Description: 哈夫曼树$* @Param: $* @return: $* @Author: Sisyphus* @Date: 7/24$*/
public class HuffmanTree {public static void main(String[] args) {int arr[] = {13,7,8,3,29,6,1};createHuffmanTree(arr);}//创建哈夫曼树的方法public static Node createHuffmanTree(int[] arr){//第一步为了操作方便//1.遍历 arr 数组//2.将 arr 的每个元素构成 Node//3.将 Node 放入到 ArrayList 中ArrayList<Node> nodes = new ArrayList<>();for(int value : arr){nodes.add(new Node(value));}while(nodes.size() > 1){//排序,Nodes实现了 Comparable 接口Collections.sort(nodes);System.out.println("nodes = " + nodes);//取出根结点权值最小的两棵二叉树//(1)取出权值最小的结点(二叉树)Node leftNode = nodes.get(0);//(2)取出权值第二小的结点(二叉树)Node rightNode = nodes.get(1);//(3)构建一棵新的二叉树Node parent = new Node(leftNode.value + rightNode.value);parent.left = leftNode;parent.right = rightNode;//(4)从数组中删除处理过的二叉树nodes.remove(leftNode);nodes.remove(rightNode);//(5)将 parent 加入 nodesnodes.add(parent);}System.out.println("nodes = " + nodes);//返回哈夫曼树的根结点return nodes.get(0);}
}//创建结点类
//为了让 Node 对象支持排序,让 Node 实现 Comparable 接口
class Node implements Comparable<Node>{int value;  //结点权值Node left;  //指向左子结点Node right; //指向右子结点public Node(int value){this.value = value;}@Overridepublic String toString() {return "Node{" +"value=" + value +'}';}@Overridepublic int compareTo(Node o) {//表示从小到大排序return this.value - o.value;}
}

三、哈夫曼编码

1.引入

从狭义上来讲,把人类能看懂的各种信息,转换成计算机能够识别的二进制形式,被称为编码

编码的方式可以有很多种,我们大家最熟悉的编码方式就属 ASCII 码

在ASCII码当中,把每一个字符表示成特定的8位二进制数,比如:

显然,ASCII码是一种等长编码,也就是任何字符的编码长度都相等

等长编码的优点很明显,因为每个字符对应的二级制编码长度相等,所以很容易设计,也很方便读写。但是计算机的存储空间以及网络传输的带宽是有限的,等长编码最大的缺点就是编码结果太长,会占用过多资源

假如一段信息当中,只有 A,B,C,D,E,F 这6个字符,如果使用不定长编码,比如:

如此一来,给定的信息 “ABEFCDAED”,就可以编码成二进制的 “0 00 10 11 01 1 0 10 1”,编码的总长度只有 14

但是这样的编码设计会带来歧义,A 的编码是 0,B 的编码是 00,那么二进制 000 既可能是 AB,又可能是 BA,还可能是 AAA。因此,不定长编码是不能随意设计的,如果一个字符的编码恰好是另一个字符编码的前缀,就会产生歧义

哈夫曼编码也是不定长编码,并且哈夫曼编码可以保证编码不存在二义性

2.介绍

哈夫曼编码(Huffman Coding)实现了两个重要目标:

  1. 任何一个字符编码都不是其他字符编码的前缀
  2. 信息编码的总长度最小

哈夫曼编码并非一套固定的编码,而是根据给定信息中各个字符出现的频次,动态生成最优的编码

使用需要传送的字符构造字符集C = {c1, c2, … cn},并根据字符出现的频率构建概率集W = {w1, w2, … wn}。哈夫曼编码的流程如下:

  • 将字符集 C 作为叶子结点
  • 将频率集 W 作为叶子结点的权值
  • 使用 C 和 W 构造哈夫曼树
  • 哈夫曼树的每一个结点包括左、右两个分支,二进制的每一位有 0、1 两种状态,我们可以把这两者对应起来,结点的左分支当做 0,结点的右分支当做 1

哈夫曼树的根结点到每一个叶子结点的路径就是一段二进制编码

上述过程借助哈夫曼树所生成的二进制编码,就是哈夫曼编码

需要注意,哈夫曼树根据排序方法不同,对应的哈夫曼编码也不完全一样,但是 WPL 一定是一样的

这样生成的编码有没有前缀问题带来的歧义呢?

因为每一个字符对应的都是哈夫曼树的叶子结点,从根结点到这些叶子结点的路径并没有包含关系,最终得到的二进制编码自然也不会是彼此的前缀

这样生成的编码能保证总长度最小吗?

哈夫曼树的重要特性,就是所有叶子结点的(权重 X 路径长度)之和最小

放在信息编码的场景下,叶子结点的权重对应字符出现的频次,结点的路径长度对应字符的编码长度

所有字符的(频次 X 编码长度)之和最小,自然就说明总的编码长度最小

3.代码实现哈夫曼编码综合案例

功能如下:

  • 生成字符串对应的哈夫曼编码
  • 对字符串压缩
  • 解压压缩后的字符串
  • 压缩文件
  • 解压文件
package com.sisyphus.huffmancode;import java.io.*;
import java.util.*;/*** @Description: 哈夫曼编码$* @Param: $* @return: $* @Author: Sisyphus* @Date: 7/24$*/
public class HuffmanCode {public static void main(String[] args) {//测试压缩字符串String str = "The relationship between Java and JavaScript is like Zhou Yang and Zhou Yangqing.Neither of them has any similarities";//获取原始字符串的字节数组byte[] contentBytes = str.getBytes();System.out.println("压缩前的长度为:" + contentBytes.length);byte[] huffmanCodesBytes = huffmanzip(contentBytes);System.out.println("压缩后的结果为:" + Arrays.toString(huffmanCodesBytes));System.out.println("压缩后的长度为:" + huffmanCodesBytes.length);byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);System.out.println("原来的字符串:" + new String(sourceBytes));//测试压缩文件String srcFile = "C:\\Users\\admin\\Desktop\\src.png";String dstFile = "C:\\Users\\admin\\Desktop\\dst.zip";zipFile(srcFile,dstFile);File zip = new File("C:\\Users\\admin\\Desktop\\dst.zip");if (zip.exists()){System.out.println("文件压缩成功!");}//测试解压文件String zipFile = "C:\\Users\\admin\\Desktop\\dst.zip";String dstFile1 = "C:\\Users\\admin\\Desktop\\src1.png";unZip(zipFile,dstFile1);File src1 = new File("C:\\Users\\admin\\Desktop\\src1.png");if (src1.exists()){System.out.println("文件解压成功!");}}//编写一个方法,完成对压缩文件的解压/**** @param zipFile   准备解压的文件* @param dstFile   将文件解压到哪个路径*/public static void unZip(String zipFile,String dstFile){//定义文件输入流InputStream is = null;//定义一个对象输入流ObjectInputStream ois = null;//定义文件输出流OutputStream os = null;try {//创建文件输入流is = new FileInputStream(zipFile);//创建一个和 is 关联的对象输入流ois = new ObjectInputStream(is);//读取 byte 数组 huffmanBytesbyte[] huffmanBytes = (byte[])ois.readObject();//读取哈夫曼编码表Map<Byte,String> huffmanCodes = (Map<Byte, String>)ois.readObject();//解码byte[] bytes = decode(huffmanCodes,huffmanBytes);//将 bytes 数组写入到目标文件os = new FileOutputStream(dstFile);//写数据到 dstFileos.write(bytes);} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}finally {try {os.close();ois.close();is.close();} catch (IOException e) {e.printStackTrace();}}}//编写方法,将一个文件进行压缩/**** @param srcFile   你传入的希望压缩的文件的全路径* @param dstFile   我们压缩后将压缩文件放到哪个目录*/public static void zipFile(String srcFile,String dstFile){//创建输出流OutputStream os = null;ObjectOutputStream oos = null;//创建文件的输入流FileInputStream is = null;try {is = new FileInputStream(srcFile);//创建一个和源文件大小一样的 byte[]byte[] b = new byte[is.available()];//读取文件is.read(b);//直接堆源文件压缩byte[] huffmanBytes = huffmanzip(b);//创建文件的输出流,存放压缩文件os = new FileOutputStream(dstFile);//创建一个和文件输出流关联的 ObjectOutputStreamoos = new ObjectOutputStream(os);//把哈夫曼编码后的字节数组写入压缩文件oos.writeObject(huffmanBytes);//先把//这里我们以对象流的方式写入哈夫曼编码,是为了以后我们解压的时候恢复源文件使用oos.writeObject(huffmanCodes);} catch (IOException e) {e.printStackTrace();}finally {try {oos.close();os.close();is.close();} catch (IOException e) {e.printStackTrace();}}}//完成数据的解压//思路//1.先转成哈夫曼编码对应的二进制字符串//2.对照哈夫曼编码转换为字符串/*** 将一个 byte 转成一个二进制的字符串* @param flag  如果是 true 则需要补高位,如果是 false 则不补* @param b     传入的 byte* @return      是该 b 对应的二进制的字符串,(注意是按补码返回的)*/private static String byteToBitString(boolean flag,byte b){//使用变量保存 bint temp = b;   //将 b 转成 int//如果是正数,我们还存在补高位的问题if (flag) {temp |= 256;    //按位或 256(1 0000 0000) | 1(0000 0001) => 1 0000 0001}String str = Integer.toBinaryString(temp);  //返回的是 temp 对应的二进制的补码if (flag) {return str.substring(str.length() - 8);}else{return str;}}//编写一个方法,完成对压缩数据的解码/**** @param huffmanCodes  哈夫曼编码 map* @param huffmanBytes  哈夫曼编码得到的字节数组* @return              就是原来的字符串对应的数组*/private static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){//1.先得到 huffmanBytes 对应的二进制的字符串StringBuilder stringBuilder = new StringBuilder();//将 byte 数组转成二进制的字符串for (int i = 0; i < huffmanBytes.length; i++) {byte b = huffmanBytes[i];//判断是不是最后一个字节boolean flag = (i == huffmanBytes.length - 1);stringBuilder.append(byteToBitString(!flag,b));}//把字符串按照指定的哈夫曼编码进行解码//把哈夫曼编码进行调换,因为需要反向查询Map<String,Byte> map = new HashMap<>();for (Map.Entry<Byte,String> entry : huffmanCodes.entrySet()) {map.put(entry.getValue(),entry.getKey());}//创建一个集合,存放 byteArrayList<Byte> list = new ArrayList<>();//i 可以理解成就是索引,扫描 stringBuilderfor (int i = 0; i < stringBuilder.length();) {int count = 1;  //小的计数器boolean flag = true;Byte b = null;while (flag) {//递增地取出字节数组中的 ’1‘ 或者 ’0‘String key = stringBuilder.substring(i,i+count);//i 不动,让 count 移动,指定匹配到一个字符b = map.get(key);if (b == null){ //说明没有匹配到count++;}else{//匹配到了flag = false;}}list.add(b);i += count; //i 直接移动到 count,左闭右开}//for 循环结束后 list 就存放了所有字符//把 list 中的数据放入到 byte[] 并返回byte b[] = new byte[list.size()];for (int i = 0; i < b.length; i++) {b[i] = list.get(i);}return b;}//使用一个方法,将所有的方法封装起来,便于我们调用/**** @param bytes 原始的字符串对应的字节数组* @return      经过哈夫曼编码处理后的字节数组(压缩后的数组)*/private static byte[] huffmanzip(byte[] bytes){List<Node> nodes = getNodes(bytes);//创建哈夫曼树Node huffmanTreeRoot = createHuffmanTree(nodes);//根据哈夫曼树创建对应的哈夫曼编码Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);//根据生成的哈夫曼编码亚索,得到压缩后的哈夫曼编码字节数组byte[] huffmanCodeBytes = zip(bytes,huffmanCodes);return huffmanCodeBytes;}//编写一个方法,将字符串对应的 byte[] 数组,通过生成的哈夫曼编码表,返回一个哈夫曼编码压缩后的 byte[]/**** @param bytes         原始的字符串对应的 byte[]* @param huffmanCodes  生成的哈夫曼编码 map* @return              返回哈夫曼编码处理后的 byte[],即 8 位对应一个 byte,存放在 bute[] 数组中,需要注意的是 byte 存放的是二进制数的补码*/private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){//1.利用 huffmanCodes 将 bytes 转成哈夫曼编码对应的字符串StringBuilder stringBuilder = new StringBuilder();//遍历 bytes 数组for (byte b : bytes){stringBuilder.append(huffmanCodes.get(b));}//统计返回 bytep[] huffmanCodeBytes 长度//一句话搞定 int len = (stringBuilder.length() + 7) / 8;int len;if (stringBuilder.length() % 8 == 0){len = stringBuilder.length() / 8;}else{len = stringBuilder.length() / 8 + 1;}//创建存储压缩后的 byte 数组byte[] huffmanCodeBytes = new byte[len];int index = 0;//记录是第几个 butefor (int i = 0; i < stringBuilder.length(); i += 8) {   //因为是每 8 位对应一个 byte,所以步长 +8String strByte;if (i + 8 >stringBuilder.length()){ //不够 8 位strByte = stringBuilder.substring(i);}else {strByte = stringBuilder.substring(i,i + 8);}//将 strByte 转成一个 byte,放入到 huffmanCodeByteshuffmanCodeBytes[index] = (byte) Integer.parseInt(strByte,2);index++;}return huffmanCodeBytes;}//生成哈夫曼树对应的哈夫曼编码//思路://1.将哈夫曼编码表存放在 Map<Byte,String>static Map<Byte,String> huffmanCodes = new HashMap<>();//2.在生成哈夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径static StringBuilder stringBuilder = new StringBuilder();//为了调用方便,我们重载 getCodesprivate static Map<Byte,String> getCodes(Node root){if (root == null){return null;}//处理 root 的左子树getCodes(root.left,"0",stringBuilder);//处理 root 的右子树getCodes(root.right,"1",stringBuilder);return huffmanCodes;}/*** 得到传入的 node 结点的所有叶子结点的哈夫曼编码,并放入到 huffmanCodes 集合* @param node  传入结点* @param code  路径:左子结点 0,右子结点 1* @param stringBuilder 用于拼接路径*/private static void getCodes(Node node, String code, StringBuilder stringBuilder){StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);//将 code 加入到 stringBuilder2stringBuilder2.append(code);if (node != null){  //如果 node === null 不处理//判断当前 node 是叶子结点还是非叶子节点if (node.data == null){ //非叶子节点//递归处理//向左递归getCodes(node.left,"0",stringBuilder2);//向右递归getCodes(node.right,"1",stringBuilder2);}else{  //说明是一个叶子结点//就表示找到某个叶子节点了huffmanCodes.put(node.data,stringBuilder2.toString());}}}//前序遍历的方法private static void preOrder(Node root){if (root != null){root.preOrder();}else{System.out.println("哈夫曼树为空,无法遍历");}}/**** @param bytes 接收字节数组* @return      返回的是 List 形式*/private static List<Node> getNodes(byte[] bytes){//1.创建一个 ArrayListArrayList<Node> nodes = new ArrayList<Node>();//遍历 bytes,统计每一个 byte 出现的次数 -> map[key,value]HashMap<Byte,Integer> counts = new HashMap<>();for (byte b : bytes) {Integer count = counts.get(b);if (count == null){ ///Map 还没有这个字符数据,第一次加入counts.put(b,1);}else{counts.put(b,count + 1);}}//把每一个键值对转成一个 Node 对象,并加入到 nodes 集合//遍历 mapfor (Map.Entry<Byte, Integer> entry : counts.entrySet()) {nodes.add(new Node(entry.getKey(),entry.getValue()));}return nodes;}//通过 List 创建对应的哈夫曼树private static Node createHuffmanTree(List<Node> nodes){while(nodes.size() > 1){//排序,从小到大Collections.sort(nodes);//取出第一棵最小的二叉树Node leftNode = nodes.get(0);//取出第二棵最小的二叉树Node rightNode = nodes.get(1);//创建一棵新的二叉树,它的根结点没有 data,只有权值Node parent = new Node(null,leftNode.weight + rightNode.weight);parent.left = leftNode;parent.right = rightNode;//将已经处理的两棵二叉树从 nodes 删除nodes.remove(leftNode);nodes.remove(rightNode);//将新的二叉树加入到 nodesnodes.add(parent);}//最后的结点就是哈夫曼树的根结点return nodes.get(0);}
}//创建 Node
class Node implements Comparable<Node>{Byte data;  //存放数据(字符)本身,比如 'a' => 97int weight; //权值,表示字符出现的次数Node left;Node right;public Node(Byte data, int weight) {this.data = data;this.weight = weight;}@Overridepublic int compareTo(Node o) {//从小到大排序return this.weight - o.weight;}@Overridepublic String toString() {return "Node{" +"data=" + data +", weight=" + weight +'}';}//前序遍历public void preOrder(){System.out.println(this);if (this.left != null){this.left.preOrder();}if (this.right != null){this.right.preOrder();}}
}

【Java数据结构与算法】第十二章 哈夫曼树和哈夫曼编码相关推荐

  1. Android版数据结构与算法汇总十二章

    Android版数据结构与算法(一):基础简介 https://www.cnblogs.com/leipDao/p/9140726.html Android版数据结构与算法(二):基于数组的实现Arr ...

  2. Java 数据结构和算法(十五):无权无向图

    Java数据结构和算法(十五)--无权无向图 前面我们介绍了树这种数据结构,树是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合,把它叫做"树"是因为它看起 ...

  3. 12_JavaScript数据结构与算法(十二)二叉树

    JavaScript 数据结构与算法(十二)二叉树 二叉树 二叉树的概念 如果树中的每一个节点最多只能由两个子节点,这样的树就称为二叉树: 二叉树的组成 二叉树可以为空,也就是没有节点: 若二叉树不为 ...

  4. Java数据结构和算法(十)——二叉树

    接下来我们将会介绍另外一种数据结构--树.二叉树是树这种数据结构的一员,后面我们还会介绍红黑树,2-3-4树等数据结构.那么为什么要使用树?它有什么优点? 前面我们介绍数组的数据结构,我们知道对于有序 ...

  5. Java数据结构与算法(第四章栈和队列)

    2019独角兽企业重金招聘Python工程师标准>>> 本章涉及的三种数据存储类型:栈.队列和优先级队列. 不同类型的结构 程序员的工具 数组是已经介绍过的数据存储结构,和其他结构( ...

  6. 【Java数据结构与算法】第十一章 顺序存储二叉树、线索二叉树和堆

    第十一章 顺序存储二叉树.线索化二叉树.大顶堆.小顶堆和堆排序 文章目录 第十一章 顺序存储二叉树.线索化二叉树.大顶堆.小顶堆和堆排序 一.顺序存储二叉树 1.介绍 2.代码实现 二.线索二叉树 1 ...

  7. 【Java数据结构与算法】第十七章 二分查找(非递归)和分治算法(汉诺塔)

    第十七章 二分查找(非递归)和分治算法(汉诺塔) 文章目录 第十七章 二分查找(非递归)和分治算法(汉诺塔) 一.二分查找 1.思路 2.代码实现 二.分治算法(汉诺塔) 1.概述 2.汉诺塔 一.二 ...

  8. 【Java数据结构与算法】第七章 冒泡排序、选择排序、插入排序和希尔排序

    第七章 冒泡排序.选择排序.插入排序和希尔排序 文章目录 第七章 冒泡排序.选择排序.插入排序和希尔排序 一.冒泡排序 1.基本介绍 2.代码实现 二.选择排序 1.基本介绍 2.代码实现 三.插入排 ...

  9. 斗地主AI算法——第十二章の主动出牌(1)

    本章开始,我们介绍主动出牌的算法,和被动出牌类似,我们第一步把主要架子搭起来. 首先清空出牌序列 clsHandCardData.ClearPutCardList(); 主动出牌的策略按照优先级大体可 ...

  10. 【Java数据结构与算法】第六章 算法的时间复杂度、算法的空间复杂度和排序算法的介绍

    第六章 算法的时间复杂度.算法的空间复杂度和排序算法的介绍 文章目录 第六章 算法的时间复杂度.算法的空间复杂度和排序算法的介绍 一.算法的时间复杂度 1.时间频度 2.时间复杂度 3.常见的时间复杂 ...

最新文章

  1. 2.1.1 物理层的基本概念
  2. python3视频教程-Python3深度学习视频学习路线
  3. Redis中的发布与订阅
  4. 160 - 45 Dope2112.2
  5. win10桌面搜索不能用的问题
  6. 如果你是测试在职,我给你几条快速成长的建议!供所有做软件测试的参考...
  7. tomcat端口修改以及jvm启动参数设置
  8. arm poky linux,opencv移植在4412和imx6(yocto 3.14.28 arm-poky-linux-gnueabi )上
  9. 恒温箱温度计算机控制系统仿真,实验用恒温箱控制系统设计及其模型建立
  10. [Ubuntu] 二、安卓模拟器
  11. unity摄像头实物识别_“千万别让女朋友擦倒车摄像头,太tm可怕了哈哈哈哈哈!”...
  12. 前向断言/前向预查/正向断言/正向预查(lookahead assertions)
  13. 模拟斗地主洗牌发牌,并对已发好的拍进行排序(红桃A,方块A, 黑桃2.......)
  14. Java学习day11--IO流总结
  15. 一万年太久只争朝夕:从灯泡的寿命谈截尾样本的基础知识
  16. 计算机音乐大学排名,2019音乐类大学排行榜_2019年世界十大权威大学排名报告发布,中国891所高...
  17. 深度学习与人脸识别系列(3)__利用caffe训练深度学习模型
  18. 红外线体温枪制作方案
  19. 有了自动驾驶和共享无人车,未来出行将会是什么样的体验?
  20. python怎么算一元二次方程_python如何解一元二次方程

热门文章

  1. mysql 命令 示例,mysql语句大全
  2. java 线程 wait 一定要同步_java中使用wait就得使用同步锁,而且2个线程必须都使用同步代码块,否则就会异常...
  3. hdfs存储与数据同步
  4. nginx 反向代理之 proxy_redirect
  5. grpc,protoc, protoc-gen-go,rust
  6. 返回一个二维整数数组中最大子数组的和(二人结对)
  7. Spring 7大功能模块的作用[转]
  8. 18号是什么php,19年1月18号CSS浮动float
  9. (29)System Verilog设计SPI接收
  10. (35)FPGA面试技能提升篇(AD、DA、时钟芯片)