JAVA-数据结构与算法-堆排序和赫夫曼树

堆排序

  • 选择排序,不稳定排序
  • 每个节点的值,都大于或者等于其左右孩子节点的值,称为大顶堆,升序
  • 每个节点的值,都小于或者等于其左右孩子节点的值,称为小顶堆,降序
  • 前提条件,针对一个数组,将数组的坐标转换成顺序存储二叉树的格式,在进行大小顶堆化
  • 排序,arr.length / 2为非叶子节点的个数,-1是非叶子节点最大的坐标
public static void heapSort(int[] arr) {
    //调整为大顶堆
    for (int i = arr.length / 2 - 1; i >= 0 ; i--) {
        adjustHeap(arr,i,arr.length);
    }
    //将堆顶元素与末尾元素交换,将最大的元素沉到数组末端
    //每次交换后,都要重新进行调整
    int temp = 0;
    for (int i = arr.length - 1; i > 0 ; i--) {
        temp = arr[i];
        arr[i] = arr[0];
        arr[0] = temp;
        //因为每次交换时,只调整了堆顶和末尾的元素
        //所以只需要将堆顶这个非叶子节点进行调整即可
        //而且,每次交换完,都确定一个最大值,即每次调整的数组长度-1,也就是i
        adjustHeap(arr,0,i);
    }
    System.out.println(Arrays.toString(arr));
}
复制代码
  • 将数组调整成大顶堆
/**
 * 将i对应的非叶子节点的树,调整成大顶堆
 * @param arr 被调整数组
 * @param i 非叶子节点在数组的索引
 * @param length 对多少个元素进行调整
 */
public static void adjustHeap(int[] arr, int i, int length) {
    int temp = arr[i];
    //k指向i的左子节点
    for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
        if (k + 1 < length && arr[k] < arr[k+1]) {
            //兄弟节点之间的比较
            //左子节点小于右子节点的值
            //把左右子节点的最大值
            k++; //k指向右子节点
        }
        if (arr[k] > temp) {
            //如果子节点大于父节点,要交换
            arr[i] = arr[k]; //把大的值赋给当前节点
            i = k; //将k的值给i,循环结束找到最大值后,将父节点的值给子节点
        } else {
            break;
        }
    }
    //for循环结束后,已经将i作为父节点的最大值,放在了这个树最顶部
    //将temp换到被交换的叶子节点
    arr[i] = temp;
}
复制代码
  • 小顶堆
public static void adjustHeap(int[] arr, int i, int length) {
    int temp = arr[i];
    for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {
        if (k + 1 < length && arr[k] > arr[k+1]) {
            k++; //k指向右子节点
        }
        if (arr[k] < temp) {
            //如果子节点大于父节点,要交换
            arr[i] = arr[k]; //把大的值赋给当前节点
            i = k; //将k的值给i,循环结束找到最大值后,将父节点的值给子节点
        } else {
            break;
        }
    }
    arr[i] = temp;
}
复制代码

赫夫曼树

  • 给定n个权值,作为n个叶子节点,若该树的带权路径长度(wpl)达到最小,则为最优二叉树,也叫赫夫曼树,权值越大的节点离根越近
  • 路径,从一个节点,往下可以达到孩子或者孙子节点之间的通路
  • 路径长度,规定根节点层数为-1,到第L层节点的路径长度为L-1
  • 带权路径长度,给节点一个权重值,从根节点到该节点之间的路径长度与该节点的权的乘积
  • 树的带权路径长度,所有叶子节点的带权路径之和,记为weighted path length,权值越大的节点距离根节点越近,最小的就是赫夫曼树/最优二叉树
public static HuffmanNode createHuffmanTree(int[] arr) {
    //放入list
    ArrayList<HuffmanNode> nodes = new ArrayList<>();
    for (int i : arr) {
        nodes.add(new HuffmanNode(i));
    }
    while (nodes.size() > 1) {
        //排序
        Collections.sort(nodes);
        //取出根节点权重值最小的两棵树
        HuffmanNode left = nodes.get(0);
        HuffmanNode right = nodes.get(1);
        //构建新的二叉树
        HuffmanNode parent = new HuffmanNode(left.value + right.value);
        parent.left = left;
        parent.right = right;
        //从list中删除
        nodes.remove(left);
        nodes.remove(right);
        //将新构建的加入
        nodes.add(parent);
    }
    //返回赫夫曼树的头
    return nodes.get(0);
}
复制代码
  • 节点
//重写排序方法
class HuffmanNode implements Comparable<HuffmanNode> {
    int value;
    HuffmanNode left;
    HuffmanNode right;
    public HuffmanNode(int value) {
        this.value = value;
    }
    @Override
    public String toString() {
        return "HuffmanNode{" +
                "value=" + value +
                '}';
    }
    @Override
    public int compareTo(HuffmanNode o) {
        //升序
        return this.value - o.value;
    }
    //前序遍历
    public static void preOrderList(HuffmanNode node) {
        System.out.println(node);
        if (node.left != null) {
            preOrderList(node.left);
        }
        if (node.right != null) {
            preOrderList(node.right);
        }
    }
}
复制代码

赫夫曼编码

  • 算法,数据文件压缩,可变字长编码(VLC)的一种
  • 变长编码,统计每个字符出现的次数,字数越多,对应的二进制位越少,但是会有多义性
  • 赫夫曼编码,无损压缩,将字符出现的次数构建成一颗赫夫曼树,次数作为权值;向左为0,向右为1,按照根节点到叶子节点的路径作为这个字符的编码,避免多义性
  • 借助了赫夫曼树的特点权值越大离根节点越近,那么字符出现次数越多,编码长度越小
  • 如果赫夫曼树中有多个相同的权重值,会有可能导致形成的树的结构不一样,但是wpl是一样的,这样生成的赫夫曼编码不一样,但是压缩后的大小/长度是一样的

压缩

  • 效果,将一个字节数组转换成赫夫曼编码字节数组
/**
 * 返回赫夫曼编码字节数组
 * @param bytes 原始字节数组
 * @return
 */
private static byte[] huffmanZip(byte[] bytes) {
    List<HuffmanCodeNode> nodes = getNodes(bytes);
    HuffmanCodeNode node = createHuffmanTree(nodes);
    Map<Byte, String> huffmanCodes = getHuffmanCodes(node);
    return zip(bytes, huffmanCodes);
}
复制代码
  • 先将bytes转换成list,便于生成赫夫曼树
public static List<HuffmanCodeNode> getNodes(byte[] bytes) {
    ArrayList<HuffmanCodeNode> nodes = new ArrayList<>();
    //遍历bytes统计每个出现的次数
    HashMap<Byte, Integer> map = new HashMap<>();
    Integer count = 0;
    //存入字符-字符个数
    for (byte b : bytes) {
        count = map.get(b);
        if (count == null) {
            map.put(b, 1);
        } else {
            map.put(b, count + 1);
        }
    }
    //把map转换成node对象,node对象包括`字符`和`权重(次数)`
    map.forEach((b, val) -> nodes.add(new HuffmanCodeNode(b,val)));
    return nodes;
}
复制代码
  • 生成赫夫曼树,获取根节点
public static HuffmanCodeNode createHuffmanTree(List<HuffmanCodeNode> nodes) {
        while (nodes.size() > 1) {
            Collections.sort(nodes);
            HuffmanCodeNode left = nodes.get(0);
            HuffmanCodeNode right = nodes.get(1);
            //创建新的二叉树节点,没有字符,只有值
            HuffmanCodeNode parent = new HuffmanCodeNode(null, left.weight + right.weight);
            parent.left = left;
            parent.right = right;
            nodes.remove(left);
            nodes.remove(right);
            nodes.add(parent);
        }
        return nodes.get(0);
    }
}
复制代码
  • 生成赫夫曼编码表
//编码表,放在map<Byte, String>
static Map<Byte, String> huffmanCodes = new HashMap<>();
//负责拼接编码
static StringBuilder stringBuilder = new StringBuilder();
//重载,调用时直接传入根节点
public static Map<Byte, String> getHuffmanCodes(HuffmanCodeNode node) {
    if (node == null) {
        return null;
    }
    getHuffmanCodes(node.left, "0", stringBuilder);
    getHuffmanCodes(node.right, "1", stringBuilder);
    return huffmanCodes;
}
/**
 * 将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入的huffmanCodes
 * @param node 传入根节点
 * @param code 路径 左子节点为0 右子节点为1
 * @param stringBuilder 拼接路径
 */
public static void getHuffmanCodes(HuffmanCodeNode node, String code, StringBuilder stringBuilder) {
    //生成一个新的StringBuilder,因为每次遇到非叶子节点,都会进入递归,相当于进行了分叉
    //所以,每次进入递归,都要再次生成一个新的,否则会重复拼接
    StringBuilder stringCode = new StringBuilder(stringBuilder);
    stringCode.append(code);
    if (node != null) {
        //判断当前是什么节点
        if (node.data == null) {
            //非叶子节点,递归处理
            //左
            getHuffmanCodes(node.left, "0",stringCode);
            //右
            getHuffmanCodes(node.right,"1",stringCode);
        } else {
            //找到某个叶子节点
            huffmanCodes.put(node.data, stringCode.toString());
        }
    }
}
复制代码
  • 生成对应赫夫曼编码字节数组,因为字节数组转换成二进制字符串的时候,末尾如果是0开头的,开头将会被舍去,所以要另外用endString记录
//存放结尾的编码
static String endString = "";
/**
 * 将一个字符串对应的byte数组,通过赫夫曼编码表,返回赫夫曼编码压缩后的byte数组
 * @param bytes 原始字符数组
 * @param huffmanCodes 经过赫夫曼编码处理后的字符编码
 * @return 原始字符编码数组
 * java的数字都是以补码的形式出现的,byte要转为数字,也要把补码转换成原码
 * 正数三码合一
 * 负数补码 = 原码保持符号为不变按位取反 + 1
 * byte[] 一个字节存8位带符号数的二进制 需要-> -1 反码 ->保留符号为,取反转换成原码->十进制
 * 10101000(补码) => 10101000 - 1 => 10100111 取反 => 11011000 => -88
 */
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
    StringBuilder stringBuilder = new StringBuilder();
    //获取字符对应的赫夫曼编码,并拼接
    for (byte b : bytes) {
        stringBuilder.append(huffmanCodes.get(b));
    }
    //转换成byte数组
    //如果不能被8整除,加上7,一定能被8整除;如果能被8整除,加上7,多出来的部分也不会影响结果
    //int len = (stringBuilder.length() + 7) / 8;
    int len = stringBuilder.length() % 8 == 0 ? stringBuilder.length() / 8 : stringBuilder.length() / 8 + 1;
    if (stringBuilder.length() - (len - 1) * 8 != 0) {
        //处理末尾
        endString = stringBuilder.substring((len - 1) * 8, stringBuilder.length());
    }
    //创建存储压缩后的byte数组
    byte[] huffmanCodeBytes = new byte[len];
    String strByte;
    //记录第几个byte
    int index = 0;
    for (int i = 0; i < stringBuilder.length(); i += 8) {
        if (i + 8 > stringBuilder.length() - 1) {
            //不够8位
            strByte = stringBuilder.substring(i);
        } else {
            strByte = stringBuilder.substring(i, i + 8);
        }
        huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte,2);
        index ++;
    }
    return huffmanCodeBytes;
}
复制代码

解压

  • 调用
byte[] source = decode(huffmanCodes, res);
复制代码
  • 完成对压缩数据的解码,本质就是将压缩完成的字节数组和对应的赫夫曼编码表传入,解码成原来的字节数组
/**
 * @param huffmanCodes 赫夫曼编码表
 * @param huffmanBytes 赫夫曼编码得到的字节数组,被解压的数组
 * @return
 */
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
    StringBuilder stringBuilder = new StringBuilder();
    //将byte数组转换成字符串
    for (int i = 0; i < huffmanBytes.length; i++) {
        boolean flag = i == huffmanBytes.length - 1;
        stringBuilder.append(byteToBitString(!flag,huffmanBytes[i]));
    }
    //按照编码表解码
    //要将编码表调转
    HashMap<String, Byte> map = new HashMap<>();
    huffmanCodes.forEach((b, s) -> map.put(s,b));
    //创建集合存放byte
    ArrayList<Byte> list = new ArrayList<>();
    int count;
    for (int i = 0; i < stringBuilder.length(); i += count) {
        //扫描对应的二进制字符串
        count = 1;
        boolean flag = true;
        Byte b = null;
        while (flag) {
            //取出一位
            //让count移动,直到取到一个存在的字符
            String key = stringBuilder.substring(i, i + count);
            b = map.get(key);
            if (b == null) {
                count ++;
            } else {
                //匹配到
                flag = false;
            }
        }
        list.add(b);
        //i移动到count的位置
        //i += count;
    }
    //循环结束后,存放所有的字符
    byte[] bytes = new byte[list.size()];
    for (int i = 0; i < bytes.length; i++) {
        bytes[i] = list.get(i);
    }
    return bytes;
}
复制代码
  • 将一个byte转换成二进制字符串
/**
 * 将一个byte转换成二进制字符串
 * @param flag 标志是否需要补高位,如果是true需要补高位,如果false不补;如果是最后一个字节不需要补高位
 * @param b 对应的是一个字节,二进制的字符串,是按补码的形式
 * @return
 */
private static String byteToBitString(boolean flag, byte b) {
    //使用变量保存b
    //将b转换成int
    int temp = b;
    if (flag) {
        //2^8
        temp |= 256; //按位或
    }
    //11111111111111111111111110101000
    //实际上是取后8位
    String s = Integer.toBinaryString(temp);
    //转换成b的补码,负数时,需要裁剪;正数需要补位
    if (flag) {
        return s.substring(s.length() - 8);
    } else {
        //处理末尾的时候,直接拼接保存好的endString
        return endString;
    }
}
复制代码

针对文件

  • 将文件进行压缩
/**
 * @param srcFile 来源
 * @param desFile 目标
 */
private static void zipFile(String srcFile, String desFile) {
    FileInputStream fis = null;
    FileOutputStream ops = null;
    ObjectOutputStream oos = null;
    try {
        fis = new FileInputStream(srcFile);
        //创建与原文件大小一样的数组
        byte[] bytes = new byte[fis.available()];
        //读取
        fis.read(bytes);
        //编码
        byte[] huffmanBytes = huffmanZip(bytes);
        //创建输出流,存放压缩文件
        ops = new FileOutputStream(desFile);
        //对象输出流
        oos = new ObjectOutputStream(ops);
        //以对象流的方式写入 赫夫曼编码 和 文件压缩字节,方便恢复源文件的方式时使用
        oos.writeObject(huffmanBytes);
        oos.writeObject(huffmanCodes);
        oos.writeObject(endString);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (ops != null) {
                ops.close();
            }
            if (fis != null) {
                fis.close();
            }
            if (oos != null) {
                oos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
复制代码
  • 将文件解压
private static void unzipFile(String zipFile, String desFile) {
    FileInputStream fis = null;
    ObjectInputStream ois = null;
    FileOutputStream ops = null;
    try {
        fis = new FileInputStream(zipFile);
        ois = new ObjectInputStream(fis);
        //读取赫夫曼字节数组,和编码表,按存入顺序读取
        byte[] huffmanBytes = (byte[]) ois.readObject();
        Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
        String endString = (String) ois.readObject();
        System.out.println(endString);
        //解码
        byte[] bytes = decode(huffmanCodes, huffmanBytes);
        //写入
        ops = new FileOutputStream(desFile);
        ops.write(bytes);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (ops != null) {
                ops.close();
            }
            if (ois != null) {
                ois.close();
            }
            if (fis != null) {
                fis.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
复制代码

小结

  • 不是对任何文件都很有效,如果文件重复的元素很多,压缩率很高
  • 重要的是获取映射关系和顺序,字符与次数的映射->对应的赫夫曼树字符与编码的映射->赫夫曼编码表;按照原顺序存储成二进制字符串->以字节为单位存到字节数组中,解压时将赫夫曼编码的键值对调->编码与字符的映射,在转换后的编码表中,根据获取的二进制字符串顺序读取相应的字符


作者:99永远差一分
链接:https://juejin.cn/post/7005377930175987742
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值