【Java数据结构与算法】第十二章 哈夫曼树和哈夫曼编码
第十二章 哈夫曼树和哈夫曼编码
文章目录
- 第十二章 哈夫曼树和哈夫曼编码
- 一、哈夫曼树
- 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
- 将 w1、w2、w3、…、wn 看成是有 n 棵树的森林(每棵树仅有一个结点)
- 在森林中选出根结点的权值最小的两棵树进行合并,作为一棵新树的左、右子树,且新树的根结点权值为其左右子树根结点的权值之和
- 从森林中删除选取的两棵树,并将新树加入森林
- 重复 2 和 3,直到森林中只剩一棵树为止,该树即为所求的哈夫曼树
以 {5,6,7,8,15} 为例
- 创建森林,森林包括 5 棵树,这 5 棵树的权值分别是5,6,7,8,15
- 在森林中,选择根结点权值最小的两棵树(5 和 6)进行合并,将它们作为一棵新树的左右子结点,新树的根结点的权值为 11,将 5 和 6 从森林删除,添加新树 11
- 在森林中,选择根结点权值最小的两棵树(7 和 8)进行合并,将它们作为一棵新树的左右子结点,新树的根结点的权值为 15,将 7 和 8 从森林删除,添加新树 15
- 在森林中,选择根结点权值最小的两棵树(11 和 15)进行合并,将它们作为一棵新树的左右子结点,新树的根结点的权值为 26,将 11 和 15 从森林删除,添加新树 26
- 在森林中,选择根结点权值最小的两棵树(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)实现了两个重要目标:
- 任何一个字符编码都不是其他字符编码的前缀
- 信息编码的总长度最小
哈夫曼编码并非一套固定的编码,而是根据给定信息中各个字符出现的频次,动态生成最优的编码
使用需要传送的字符构造字符集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数据结构与算法】第十二章 哈夫曼树和哈夫曼编码相关推荐
- Android版数据结构与算法汇总十二章
Android版数据结构与算法(一):基础简介 https://www.cnblogs.com/leipDao/p/9140726.html Android版数据结构与算法(二):基于数组的实现Arr ...
- Java 数据结构和算法(十五):无权无向图
Java数据结构和算法(十五)--无权无向图 前面我们介绍了树这种数据结构,树是由n(n>0)个有限节点通过连接它们的边组成一个具有层次关系的集合,把它叫做"树"是因为它看起 ...
- 12_JavaScript数据结构与算法(十二)二叉树
JavaScript 数据结构与算法(十二)二叉树 二叉树 二叉树的概念 如果树中的每一个节点最多只能由两个子节点,这样的树就称为二叉树: 二叉树的组成 二叉树可以为空,也就是没有节点: 若二叉树不为 ...
- Java数据结构和算法(十)——二叉树
接下来我们将会介绍另外一种数据结构--树.二叉树是树这种数据结构的一员,后面我们还会介绍红黑树,2-3-4树等数据结构.那么为什么要使用树?它有什么优点? 前面我们介绍数组的数据结构,我们知道对于有序 ...
- Java数据结构与算法(第四章栈和队列)
2019独角兽企业重金招聘Python工程师标准>>> 本章涉及的三种数据存储类型:栈.队列和优先级队列. 不同类型的结构 程序员的工具 数组是已经介绍过的数据存储结构,和其他结构( ...
- 【Java数据结构与算法】第十一章 顺序存储二叉树、线索二叉树和堆
第十一章 顺序存储二叉树.线索化二叉树.大顶堆.小顶堆和堆排序 文章目录 第十一章 顺序存储二叉树.线索化二叉树.大顶堆.小顶堆和堆排序 一.顺序存储二叉树 1.介绍 2.代码实现 二.线索二叉树 1 ...
- 【Java数据结构与算法】第十七章 二分查找(非递归)和分治算法(汉诺塔)
第十七章 二分查找(非递归)和分治算法(汉诺塔) 文章目录 第十七章 二分查找(非递归)和分治算法(汉诺塔) 一.二分查找 1.思路 2.代码实现 二.分治算法(汉诺塔) 1.概述 2.汉诺塔 一.二 ...
- 【Java数据结构与算法】第七章 冒泡排序、选择排序、插入排序和希尔排序
第七章 冒泡排序.选择排序.插入排序和希尔排序 文章目录 第七章 冒泡排序.选择排序.插入排序和希尔排序 一.冒泡排序 1.基本介绍 2.代码实现 二.选择排序 1.基本介绍 2.代码实现 三.插入排 ...
- 斗地主AI算法——第十二章の主动出牌(1)
本章开始,我们介绍主动出牌的算法,和被动出牌类似,我们第一步把主要架子搭起来. 首先清空出牌序列 clsHandCardData.ClearPutCardList(); 主动出牌的策略按照优先级大体可 ...
- 【Java数据结构与算法】第六章 算法的时间复杂度、算法的空间复杂度和排序算法的介绍
第六章 算法的时间复杂度.算法的空间复杂度和排序算法的介绍 文章目录 第六章 算法的时间复杂度.算法的空间复杂度和排序算法的介绍 一.算法的时间复杂度 1.时间频度 2.时间复杂度 3.常见的时间复杂 ...
最新文章
- 2.1.1 物理层的基本概念
- python3视频教程-Python3深度学习视频学习路线
- Redis中的发布与订阅
- 160 - 45 Dope2112.2
- win10桌面搜索不能用的问题
- 如果你是测试在职,我给你几条快速成长的建议!供所有做软件测试的参考...
- tomcat端口修改以及jvm启动参数设置
- arm poky linux,opencv移植在4412和imx6(yocto 3.14.28 arm-poky-linux-gnueabi )上
- 恒温箱温度计算机控制系统仿真,实验用恒温箱控制系统设计及其模型建立
- [Ubuntu] 二、安卓模拟器
- unity摄像头实物识别_“千万别让女朋友擦倒车摄像头,太tm可怕了哈哈哈哈哈!”...
- 前向断言/前向预查/正向断言/正向预查(lookahead assertions)
- 模拟斗地主洗牌发牌,并对已发好的拍进行排序(红桃A,方块A, 黑桃2.......)
- Java学习day11--IO流总结
- 一万年太久只争朝夕:从灯泡的寿命谈截尾样本的基础知识
- 计算机音乐大学排名,2019音乐类大学排行榜_2019年世界十大权威大学排名报告发布,中国891所高...
- 深度学习与人脸识别系列(3)__利用caffe训练深度学习模型
- 红外线体温枪制作方案
- 有了自动驾驶和共享无人车,未来出行将会是什么样的体验?
- python怎么算一元二次方程_python如何解一元二次方程
热门文章
- mysql 命令 示例,mysql语句大全
- java 线程 wait 一定要同步_java中使用wait就得使用同步锁,而且2个线程必须都使用同步代码块,否则就会异常...
- hdfs存储与数据同步
- nginx 反向代理之 proxy_redirect
- grpc,protoc, protoc-gen-go,rust
- 返回一个二维整数数组中最大子数组的和(二人结对)
- Spring 7大功能模块的作用[转]
- 18号是什么php,19年1月18号CSS浮动float
- (29)System Verilog设计SPI接收
- (35)FPGA面试技能提升篇(AD、DA、时钟芯片)