【数据结构(三)】赫夫曼树手之写文件的压缩解压&图的创建以及遍历

前提须知

为了尽快的找到一个好实习,我不得不翻出来基础知识好好复习,并且从头到位把代码都敲了一遍!!!!。复习课程数据结构和算法:尚硅谷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. 前面有线性表和树
  2. 线性表觉先于一个直接前驱和 一个后继的关系
  3. 树也只有一个直接前驱也就是父节点
  4. 当我们需要表示多对多的关系时候,我们用到了图
  • 图的说明
     图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点职期间的链接称为边。结点也可以称为顶点。

  • 图的常用概念

 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)

图的深度优先遍历思想
  • 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问他的第一个邻接结点,可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
  • 我们可以看到,这样访问策略是优先往纵向挖深深入,而不是对一个结点的所有邻接结点进行横向访问。
  • 显然,深度优先搜索是一个递归过程。
深度优先遍历的算法步骤
  1. 访问初始结点v,并标记结点v已经被访问
  2. 查找结点v的第一个邻接结点w
  3. 若w存在,则继续执行4,如果不存在,则返回1,将从v的下一个结点继续。
  4. 若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行1,2,3)
  5. 查找结点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)

广度优先遍历基本思想

图的广度 优先搜索,类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。

广度优先遍历算法步骤
  1. 访问初始结点v并标记结点v为已访问
  2. 结点v入队列
  3. 当队列非空时,继续执行,否则算法结束
  4. 出队列,取得对头结点u
  5. 查找结点u的第一个邻接结点w
  6. 若结点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一定得基础过硬,因为各种大厂都是很看重我们的基础滴。今后我会将我整理的,学习到的分享到博客来,以及即将迎接的各种面试经历,希望在这观看的你和我都不断努力,争取拿到高薪!!!如有问题,欢迎留言。

发布了37 篇原创文章 · 获赞 14 · 访问量 9787

猜你喜欢

转载自blog.csdn.net/qmqm33/article/details/104366189