文章目录
前提须知
为了尽快的找到一个好实习,我不得不翻出来基础知识好好复习,并且从头到位把代码都敲了一遍!!!!。复习课程数据结构和算法:尚硅谷av54029771 ,这是尚硅谷的课程链接,我把知识总结全部做了笔记,我在下面的博客会写道,想要更全的可以私信我。本妹子超可又自恋。本文有点长,大家可以通过目录点到你需要看的地方。代码全部正确可以直接用哦!!
以下开始正文!!!!!
赫夫曼树
赫夫曼树基本介绍
- 给定n个权值作为n个叶子结点,构造一棵二叉树,若概述带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树。
- 赫夫曼树是地权路径最短的树,权值较大的结点离根比较近。
赫夫曼树几个重要概念和举例说明
-
路径和路径长度,在一棵树中,从一个结点往下可达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根节点到第L层结点的路径长度为L-1。
-
结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该节点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
-
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL,权值越大的结点离根节点越近的二叉树才是最优二叉树
-
WPL最小的就是赫夫曼树
构建赫夫曼树的步骤
- 从小到大进行排序,将每一个数据,每个数据都是一个结点,每个结点可以看成是一棵简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一个新的二叉树,该新的二叉树的根节点的权值是前面二叉树根节点权值的和
- 再将新的这颗二叉树,以根结点的权值大小再次排序,不断重复1-2-3-4步骤。直到数列中所有的数据都被处理,就得到一棵赫夫曼树。
赫夫曼编码
- 赫夫曼编码也翻译为 哈夫曼编码,又称为霍夫曼编码,是一种编码方式,属于一种程序算法
- 赫夫曼编码是赫夫曼在电讯通讯中的经典应用之一
- 赫夫曼编码广泛的用于数据文件压缩(解压),其压缩率通常在20%~90%之u见
- 赫夫曼编码是而可变字长编码(VLC)的一种。赫夫曼在1952年提出的一种编码方式,称之为最佳编码。
赫夫曼编码原理剖析
- 一般的前缀编码有歧义。
- 赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也完全不一样,但是wpl是一样的,都是最小的。最后生成的赫夫曼树编码的长度是一样的。
- 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,既不能匹配到重复的编码。
赫夫曼编码代码实现(字符串的压缩和解压)
- 创建赫夫曼树核心代码
/**
* 统计每一个出现的次数,把他们存在list中
* @param bytes
* @return
*/
private static List<Node> getNode(byte[] bytes){
//存放数组
List<Node> nodeList=new ArrayList<>();
//遍历bytes,统计里面每一个byte出现的次数。
Map<Byte,Integer> counts=new HashMap<>();
for(byte b:bytes){
Integer count=counts.get(b);
if(count==null){//判断是否有,如果没有加入。
counts.put(b,1);
}else{//如果有,把得到的值加1
counts.put(b,count+1);
}
}
//遍历Map
Iterator iterator=counts.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry= (Map.Entry) iterator.next();
Node node=new Node();
Byte data=(Byte) entry.getKey();
node.setData((int)data);
node.setWeight((Integer) entry.getValue());
nodeList.add(node);
}
return nodeList;
}
}
/**
* 创建赫夫曼树
* @param nodeList
*/
private static Node creatHuffmanTree(List<Node> nodeList){
//创建赫夫曼树
while (nodeList.size()>1){
//每次都要进行排序
Collections.sort(nodeList);
Node node1=nodeList.get(0);//获取最小的两个0
Node node2=nodeList.get(1);//获取最小的
Node node=new Node();//创建新节点
node.setWeight(node1.getWeight()+node2.getWeight());//新节点将权值相加
node.setLeft(node1);//得到左右结点
node.setRight(node2);
nodeList.remove(1);
nodeList.remove(0);
nodeList.add(node);//新的到的结点加入表中
}
return nodeList.get(0);
}
/**
* 生成赫夫曼编码
* @param root
* @return
*/
private static Map<Byte,String> getCode(Node root){
StringBuilder sb=new StringBuilder();
Map<Byte,String> codes=new HashMap<>();
codes=getCode(root,sb,"",codes);
return codes;
}
-
数据压缩
-
先将数组btye转成赫夫曼编码对应的二进制字符串
-
核心代码如下
private static byte[] zip(byte[] bytes,Map<Byte,String> codes){
StringBuilder sb=new StringBuilder();
//获取相应的字符串
for (Byte b:bytes){
sb.append(codes.get(b));
}
System.out.println(sb.toString());
int len=(sb.length()+7)/8;//这一步等于-》如果能整除8那么就是整除8的结果,如果不能就是整除的结果加1。
byte[] newByte=new byte[len];
int index=0;
for (int i=0;i<sb.length();){
if(i+8>sb.length()){
newByte[index++]=(byte)Integer.parseInt(sb.substring(i),2);//转换成二进制
sb.delete(0,sb.length());
}else {
newByte[index++]=(byte)Integer.parseInt(sb.substring(0,8),2);//转换成二进制
sb.delete(0,8);
}
}
//System.out.println(Arrays.toString(newByte));
return newByte;
}
- 数据解压核心代码
/**
* 将byte类型转换成二进制字符串
* @param bytes
* @return
*/
private static String byteToBitString(byte[] bytes){
StringBuilder sb=new StringBuilder();
for (byte b:bytes){
if(b==bytes[bytes.length-1]){
String str=Integer.toBinaryString(b);
sb.append(str);
break;
}
int temp=b;
temp|=256;
String str=Integer.toBinaryString(temp);
sb.append(str.substring(str.length()-8));
}
System.out.println(sb.toString());
return sb.toString();
}
private static byte[] decode(Map<Byte,String> huffmanCodes,String byteCode){
//存放得到的byte数据
List<Byte> node=new ArrayList<>();
//创建解码的map,就是把原来的哈夫曼编码反过来
Map<String,Byte> decode=new HashMap<>();
for(Map.Entry<Byte,String>entry:huffmanCodes.entrySet()){
decode.put(entry.getValue(),entry.getKey());
}
for (int i=0,j=1;i<byteCode.length()&&j<=byteCode.length();){
while (true){
if(j>byteCode.length())break;
String str=byteCode.substring(i,j++);
if(decode.get(str)!=null){
node.add(decode.get(str));
i=j-1;
break;
}
}
}
byte[] bytes=new byte[node.size()];
for (int i=0;i<node.size();i++){
bytes[i]=node.get(i);
}
System.out.println(Arrays.toString(bytes));
System.out.println(new String(bytes));
return null;
}
-
最佳实践-文件压缩
-
思路:读取文件->得到赫夫曼编码表->完成压缩
-
代码实现
/**
* 解压文件
* @param zipFile
* @param dstFile
* @throws IOException
* @throws ClassNotFoundException
*/
private static void unZip(String zipFile,String dstFile) throws IOException, ClassNotFoundException {
//创建文件输入流,和对象输入流
InputStream is=new FileInputStream(zipFile);
ObjectInputStream ooi=new ObjectInputStream(is);
//获取其中的赫夫曼编码后的字节
byte[] huffmanCode=(byte[])ooi.readObject();
// System.out.println("读出的huffmanCode"); 没问题
//System.out.println(Arrays.toString(huffmanCode));
//获取其中的哈夫曼编码
Map<Byte,String> codes=(Map<Byte,String>)ooi.readObject();
closeAll(ooi,is);
String byteCodes=byteToBitString(huffmanCode);
//获取目标
byte[] newByte=decode(codes,byteCodes);
//创建输出流
OutputStream os=new FileOutputStream(dstFile);
os.write(newByte);
}
/**
* 压缩文件
* @param zipFile
* @throws IOException
*/
private static void zipFileFunction(String zipFile) throws IOException {
//获取文件输入流
InputStream is=new FileInputStream(zipFile);
OutputStream os=null;
//创建一个和源文件大小一样的byte[]
byte[] bytes=new byte[is.available()];
//直接压缩文件
is.read(bytes);
List<Node> nodeList=getNode(bytes);
//创建赫夫曼树,返回根节点
Node root=creatHuffmanTree(nodeList);
//对应的赫夫曼编码
Map<Byte,String> codes=getCode(root);
byte[] huffmanCodeByte=zip(bytes,codes);
closeAll(is);
//创建输出流
os=new FileOutputStream("img.zip");
ObjectOutputStream oos=new ObjectOutputStream(os);
//把得到的赫夫曼编码后的字节写进去
oos.writeObject(huffmanCodeByte);
//把赫夫曼编码写进去,这样才能解压。
oos.writeObject(codes);
closeAll(oos,os);
}
//关闭流操作
public static void closeAll(Closeable...cos) throws IOException {
for(Closeable c:cos){
c.close();
}
}
图
图的基本介绍
- 前面有线性表和树
- 线性表觉先于一个直接前驱和 一个后继的关系
- 树也只有一个直接前驱也就是父节点
- 当我们需要表示多对多的关系时候,我们用到了图
-
图的说明
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点职期间的链接称为边。结点也可以称为顶点。 -
图的常用概念
1) 顶点(vertex)
2)边(edge)
3)路径
- 无向图、有向图
- 带权图
图的表示方式
- 图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)
- 邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵的row和col表示的是1-n的点。
- 邻接表
1) 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在的,会造成空间的一些损失
2)邻接表的实现只关心存在的边,不关心不存在的边。因为没有空间浪费,邻接表由数组+链表组成。
图的创建代码
package com.data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Graph {
private List<Character> vertexList=new ArrayList<>();//存放顶点的表
private int[][]edge;//邻接矩阵
private boolean[] isView;
//构造器初始化邻接矩阵
public Graph(int n){
edge=new int[n][n];
isView=new boolean[n];
}
//添加顶点
private void addVertex(char vertex){
vertexList.add(vertex);
}
//邻接矩阵添置
private void contact(char vertex1,char vertex2,int isHasEdge){
int index1=vertexList.indexOf(vertex1);
int index2=vertexList.indexOf(vertex2);
edge[index1][index2]=isHasEdge;
edge[index2][index1]=isHasEdge;
}
//打印邻接矩阵表
private void print(){
for(int[] line:edge){
System.out.println(Arrays.toString(line));
}
}
public static void main(String[] args) {
Graph graph=new Graph(5);
String vertex="ABCDE";
//逐个添加顶点
for (int i=0;i<vertex.length();i++){
graph.addVertex(vertex.charAt(i));
}
graph.contact('A','B',1);
graph.contact('B','D',1);
graph.contact('A','C',1);
graph.contact('D','E',1);
graph.contact('B','C',1);
graph.print();
//graph.DFS(0);
System.out.println();
}
}
图的遍历
- 图的遍历介绍
- 所谓图的遍历,即使对结点的访问。一个图有那么多个结点,如果遍历这些结点,需要特定策略:
- 深度优先
- 广度优先
图的深度优先(DFS)
图的深度优先遍历思想
- 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问他的第一个邻接结点,可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
- 我们可以看到,这样访问策略是优先往纵向挖深深入,而不是对一个结点的所有邻接结点进行横向访问。
- 显然,深度优先搜索是一个递归过程。
深度优先遍历的算法步骤
- 访问初始结点v,并标记结点v已经被访问
- 查找结点v的第一个邻接结点w
- 若w存在,则继续执行4,如果不存在,则返回1,将从v的下一个结点继续。
- 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行1,2,3)
- 查找结点v的w邻接结点的下一个邻接结点,转到步骤3
图的深度优先代码实现
//核心代码(用回溯)
//深度优先遍历,回溯
private void DFS(int n){
//默认遍历邻接结点
for (int i=n;i!=-1;i=getNextNeighbor(n)){
if(judgeHasFirst(vertexList.get(i))){
i=getFirstNeighbor(i);
DFS(i);
}
}
}
//判断是否有下一个,并且输出当前的值
private boolean judgeHasFirst(char vertex){
int index=vertexList.indexOf(vertex);
if(!isView[index]){
System.out.print(vertexList.get(index)+"->");
isView[index]=true;
// return true;
}
if(getFirstNeighbor(index)!=-1)return true;
return false;
}
//找到该结点的第一个值
private int getFirstNeighbor(int index){
for(int j=index+1;j<edge[index].length;j++){
if(edge[index][j]>0){
if(isView[j]==false)return j;
else return -1;
}
}
return -1;
}
//找到该结点的下一个
private int getNextNeighbor(int index){
for(int j=index+1;j<edge[index].length;j++){
if(edge[index][j]>0&&isView[j]==false){
return j;
}
}
return -1;
}
广度优先遍历(BFS)
广度优先遍历基本思想
图的广度 优先搜索,类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。
广度优先遍历算法步骤
- 访问初始结点v并标记结点v为已访问
- 结点v入队列
- 当队列非空时,继续执行,否则算法结束
- 出队列,取得对头结点u
- 查找结点u的第一个邻接结点w
- 若结点u的邻接结点w不存在,则转到步骤3,否则循环执行以下三个步骤
1. 若结点w尚未被访问,则访问结点w并标记为已经访问
2. 结点w入队列
3. 查找结点u的w个邻接结点后的下一个邻接结点w,转到步骤6
广度优先代码实现
//广度优先遍历
private void BFS(int n){
List<Character> queue=new ArrayList<>();
//先将头顶点放入数组,并将该结点为访问过
queue.add(vertexList.get(n));
isView[n]=true;
int next=getNextNeighbor(n);
while (!queue.isEmpty()){//如果队列不空,则一直进行判断
//开始放下一个,如果没有被访问过的话
if(next!=-1&&(!isView[next])){
//进队列并且状态设置为被访问过
queue.add(vertexList.get(next));
isView[next]=true;
}//如果没有下一个,那么弹出第一个
if(getNextNeighbor(n)==-1){
//弹出队首
System.out.print(queue.remove(0)+"->");
//获得下一个顶点的index
if(!queue.isEmpty()){
n=vertexList.indexOf(queue.get(0));
}
}//每一轮都要向后移
next=getNextNeighbor(n);
}
}
小结
以上对于赫夫曼树、图的创建、遍历简单介绍和总结,这是一场面试前的突击,要想拿到好的offer一定得基础过硬,因为各种大厂都是很看重我们的基础滴。今后我会将我整理的,学习到的分享到博客来,以及即将迎接的各种面试经历,希望在这观看的你和我都不断努力,争取拿到高薪!!!如有问题,欢迎留言。