树结构的应用
一、堆排序
详情查看:排序算法
二、赫夫曼树
源码: 构建赫夫曼树
1,基本介绍
- 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
结点的路径长度为:层数 – 1;
结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积;
树的带权路径长度WPL(weighted path length) :规定为所有叶子结点的带权路径长度之和。
2,构建思路:
1) 将集合从小到大进行排序 。其中每个数据都是一个节点 ,每个节点可以看成是一颗最简单的二叉树 2) 取出根节点权值最小的两颗二叉树 3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和 4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3 的步骤,直到数列中,所有的数 据都被处理,就得到一颗赫夫曼树
3,代码实现
/** * 构建赫夫曼树 */ public static Node createHuffman(int[] arr) { List<Node> list = new ArrayList<>(arr.length); for (int value : arr) { list.add(new Node(value)); } while (list.size() > 1) { Collections.sort(list); //先排序,从小到大 Node leftNode = list.get(0); Node rigthNode = list.get(1); Node parent = new Node(leftNode.no + rigthNode.no); parent.left = leftNode; parent.rigth = rigthNode; list.remove(leftNode); list.remove(rigthNode); list.add(parent); } return list.get(0); } static class Node implements Comparable<Node>{ int no; Node left; Node rigth; public Node(int no) { this.no = no; } @Override public String toString() { return "Node[" + "no=" + no + ']'; } @Override public int compareTo(Node node) { return this.no - node.no; } }
View Code
三、赫夫曼编解码
源码: 赫夫曼压缩
1,基本介绍
- 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法
- 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
- 赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
2,定长编码与变长编码
a)定长编码
- 通信领域中信息的处理方式:定长编码,比如我需要发送如下字符串:
i like like like java do you like a java // 共40个字符(包括空格)
- 上述字符串对应的 ASCII 码为:
105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97 //对应Ascii码 01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制
- 按照二进制来传递信息,总的长度是 359 (包括空格)
b)变长编码
- 通信领域中信息的处理方式:变长编码,比如我需要发送如下字符串:
i like like like java do you like a java // 共40个字符(包括空格)
- 统计上述字符串出现的各字符出现的次数
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
- 按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9 次, 编码为0 ,其它依次类推
0= , 1=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d
- 按照上面给各个字符规定的编码,则我们在传输数据时,编码就是:
10010110100...
问题:字符编码为其他编码的前缀,比如:100到底是按照1=a, 10=i, 100=k中的哪一个进行解码?(赫夫曼编码进行解决)
3,赫夫曼编码原理
-
通信领域中信息的处理方式:赫夫曼编码
-
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码
-
比如我们处理如下字符串
i like like like java do you like a java // 共40个字符(包括空格)
- 统计各字符
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
- 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值,根据赫夫曼编码表确定具体字符的编码
- 根据赫夫曼树,给各个字符的编码 :向左的路径为 0 ;向右的路径为1
o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a: 110 k: 1110 e: 1111 j: 0000 v: 0001 l: 001 : 01
- 按照上面的赫夫曼编码,我们的”i like like like java do you like a java” 字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
- 编码后长度为 133 ,原来长度是 359 , 压缩了 (359-133) / 359 = 62.9% ,此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀,不会造成匹配的多义性
4,赫夫曼编码思路和实现
- 统计字节数组中各个数据的权重
- 将字节数组按照上面的权重值创建赫夫曼树
- 根据上面创建的赫夫曼树获得每个数值对应的可变长编码值(往左走为 0 ,往右走为 1)
- 以每个数值新的编码重新对字符数组进行编码,即可得到赫夫曼编码后的字节数组
/** * 整合 : 将原始的数据转换为赫夫曼数组 */ private static byte[] encodeToHuffmanBytes(byte[] contentBytes) { //第一步:构建node结点集合,将每个字符(byte)出现的次数统计并封装到list集合中 List<Node> nodes = getNodes(contentBytes); //第二步:构建赫夫曼树 Node root = creatHuffmanTree(nodes); //第三步:获取赫夫曼树中每个结点对应的字符(byte),出现对应的路径。其中key为字符(byte),value为路径(0,1组装) Map<Byte, String> nodePath = getNodePath(root); //第四步:根据字符路径集合,获取赫夫曼编码的字符串 String huffmanCode = createHuffmanCode(contentBytes, nodePath); System.out.println(huffmanCode); //第五步:将赫夫曼编码字符串,转换为压缩后的赫夫曼数组 return convertToHuffmanBytes(huffmanCode); } /** * 将赫夫曼编码数据转换为压缩之后的数组 * * @param code 赫夫曼编码的字符串 * @return 赫夫曼压缩数组 */ private static byte[] convertToHuffmanBytes(String code) { //获取转换后数组长度 int length = (code.length() + 7) / 8; byte[] bytes = new byte[length+1]; for (int i = 0, index = 0; i < code.length(); i += 8, index++) { if (code.length() > i + 8) { //将2进制转成10进制 并强转为byte bytes[index] = (byte) Integer.parseInt(code.substring(i, i + 8), 2); } else { //最后一个byte存储 倒数第二个字节的位数(防止出现0110->6->110) 出现位数少1的情况 最后无法匹配而报错 String substring = code.substring(i); bytes[index] = (byte) Integer.parseInt(substring, 2); bytes[index + 1] = (byte) substring.length(); } } return bytes; } /** * 获取赫夫曼编码的字符串 */ private static String createHuffmanCode(byte[] contentBytes, Map<Byte, String> nodePath) { StringBuilder stringBuilder = new StringBuilder(); for (byte key : contentBytes) { stringBuilder.append(nodePath.get(key)); } return stringBuilder.toString(); } /** * 根据根节点获取对应的编码集合 */ private static Map<Byte, String> getNodePath(Node node) { return getNodePath(node, "", new StringBuilder()); } static Map<Byte, String> huffmanCodeMap = new HashMap<>(); /** * 获取对应的节点和对应的编码 * * @param node 需要获取的节点 * @param code 当前节点相对上一个节点的编码 * @param stringBuilder 父节点的路径 * @return map集合:key为字符byte,value为路径 */ private static Map<Byte, String> getNodePath(Node node, String code, StringBuilder stringBuilder) { StringBuilder sb = new StringBuilder(stringBuilder); sb.append(code); if (node != null) { //非叶子结点 if (node.data == null) { //左子树 getNodePath(node.left, "0", sb); //右子数 getNodePath(node.right, "1", sb); } else { //叶子结点 huffmanCodeMap.put(node.data, sb.toString()); } } return huffmanCodeMap; } /** * 获取节点集合 */ private static List<Node> getNodes(byte[] contentBytes) { //先统计bytes中每个字节的次数 Map<Byte, Integer> map = new HashMap<>(); for (byte key : contentBytes) { map.put(key, map.get(key) == null ? 1 : map.get(key) + 1); } List<Node> list = new ArrayList<>(); for (Map.Entry<Byte, Integer> entry : map.entrySet()) { list.add(new Node(entry.getKey(), entry.getValue())); } return list; } /** * 生成赫夫曼树 */ private static Node creatHuffmanTree(List<Node> nodes) { while (nodes.size() > 1) { Collections.sort(nodes); Node leftNode = nodes.get(0); Node rightNode = nodes.get(1); Node parent = new Node(null, leftNode.count + rightNode.count); parent.left = leftNode; parent.right = rightNode; nodes.remove(leftNode); nodes.remove(rightNode); nodes.add(parent); } return nodes.get(0); } public static void preOrder(Node root) { if (root == null) { System.out.println("当前二叉树为空,不能遍历!"); return; } root.preOrder(); } /** * 结点 */ static class Node implements Comparable<Node> { //字节 i l i ... Byte data; //出现的次数 Integer count; //左子树 Node left; //右子数 Node right; //前序遍历 public void preOrder() { // System.out.println((this.data == null?null:(char)Integer.parseInt(this.data.toString()))+"\t"+this.count); System.out.println(this); if (this.left != null) { this.left.preOrder(); } if (this.right != null) { this.right.preOrder(); } } public Node(Byte data, Integer count) { this.data = data; this.count = count; } @Override public String toString() { return "Node[" + "data=" + data + ", count=" + count + ']'; } @Override public int compareTo(Node o) { return this.count - o.count; } }
View Code
5,赫夫曼解码思路和实现
-
先将压缩后的字节数组转换为对应的赫夫曼编码的10010..组成的字符串
- 遍历字节数组,将每个字节转成二进制
- 判断当前二进制长度是否大于8,如果大于8则截取后8位追加到编码字符串
- 如果二进制长度小于8,则判断是否为最后一个字节,如果是则直接加入追加,如果不是则将高位补0
-
再将赫夫曼编码字符串转成原始的字节数组
- 将原始的map(key为字节,value为二进制),进行翻转为新的map(key为二进制,value为字节)
- 遍历赫夫曼编码字符串的每个字符,如果能匹配到新的map中的key,则存储其value为字节
- 组装上一步中所有的字节为数组,则为原始字节数组返回
/** * 转换成原始的字符串 */ private static byte[] decodeToSourceStr(byte[] bytes, Map<Byte, String> huffmanMap) { //先将数组转换为对应的赫夫曼编码的10010..组成的字符串 String huffmancode = decodeBytesToHuffmancode(bytes); System.out.println(huffmancode); //将赫夫曼编码转换为对应的字符串 return decodeHuffmancodeToSourceStr(huffmancode, huffmanMap); } /** * 将赫夫曼编码进行解码为原始字节数组 * * @param huffmancode 赫夫曼编码 * @param huffmanMap 记录原始字节与编码的map集合 * @return 原始字符串 */ private static byte[] decodeHuffmancodeToSourceStr(String huffmancode, Map<Byte, String> huffmanMap) { //将map反转。原来是32->010,98->001... 转换为001->98,010->32... Map<String, Byte> map = new HashMap<>(); for (Map.Entry<Byte, String> entry : huffmanMap.entrySet()) { map.put(entry.getValue(), entry.getKey()); } //原始字节集合 List<Byte> byteList = new ArrayList<>(); for (int i = 0; i < huffmancode.length(); ) { StringBuilder stringBuilder = new StringBuilder(); //将huffman编码组成的字符村1000100011...,进行遍历。 //当指针指向一定的值存储在反转红藕的map集合中时,停止 while (map.get(stringBuilder.toString()) == null) { if (i >= huffmancode.length()) { break; } stringBuilder.append(huffmancode.charAt(i)); i++; } //获取每个字符,并组装成对应的字符串 Byte aByte = map.get(stringBuilder.toString()); byteList.add(aByte); } byte[] bytes = new byte[byteList.size()]; for (int i = 0; i < byteList.size(); i++) { bytes[i] = byteList.get(i); } return bytes; } /** * 将压缩后的数组转换为赫夫曼编码 * * @param bytes 压缩后的字节数组 * @return 赫夫曼编码 */ private static String decodeBytesToHuffmancode(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { byte b = bytes[i]; //将当前byte转换为二进制字符串 String binaryString = Integer.toBinaryString(b); //判断当前二进制是否长度大于8,如果大于8则截取 if (binaryString.length() > 8) { sb.append(binaryString.substring(binaryString.length() - 8)); } else { //判断是否为倒数第二个字节,如果为倒数第二个字节 则其二进制的长度应该为倒数第一个数的长度。 if (i == bytes.length - 2) { byte last = bytes[i + 1]; int length = binaryString.length(); StringBuilder builder = new StringBuilder(); //将少于倒数第一个数值长度 的二进制 前面用0 补齐 for (int j = 0; j < last - length; j++) { builder.append(0); } builder.append(binaryString); sb.append(builder); break; } else { //如果不是则补齐前面的0 int length = binaryString.length(); StringBuilder builder = new StringBuilder(); for (int j = 0; j < 8 - length; j++) { builder.append(0); } builder.append(binaryString); sb.append(builder); } } } return sb.toString(); }
View Code
6,赫夫曼压缩文件
/** * 压缩文件 * * @param srcFile 原始文件路径 * @param destFile 压缩后文件路径 */ private static void zipFile(String srcFile, String destFile) { InputStream is = null; OutputStream os = null; //对象输出流 ObjectOutputStream oos = null; try { is = new FileInputStream(srcFile); //创建原始字节数组 byte[] bytes = new byte[is.available()]; //读取文件 is.read(bytes); //压缩 byte[] zip = zip(bytes); os = new FileOutputStream(destFile); //关联 oos = new ObjectOutputStream(os); //将压缩之后的数组 和 赫夫曼编码对应关系 写出 oos.writeObject(zip); oos.writeObject(huffmanCodeMap); } catch (Exception e) { e.printStackTrace(); } finally { try { oos.close(); os.close(); is.close(); } catch (IOException e) { e.printStackTrace(); } } }
View Code
7,赫夫曼解压文件
/** * 解压文件 * * @param srcFile 压缩之后的文件 * @param destFile 解压后的文件 */ private static void unzipFile(String srcFile, String destFile) { InputStream is = null; ObjectInputStream ois = null; OutputStream os = null; try { is = new FileInputStream(srcFile); ois = new ObjectInputStream(is); //读取压缩之后的 文件 和 对应的赫夫曼编码 byte[] zipBytes = (byte[]) ois.readObject(); Map<Byte, String> huffmanMap = (Map<Byte, String>) ois.readObject(); //解压 byte[] srcBytes = unzip(zipBytes, huffmanMap); //输出 os = new FileOutputStream(destFile); os.write(srcBytes); } catch (Exception e) { e.printStackTrace(); }finally { try { os.close(); ois.close(); is.close(); } catch (IOException e) { e.printStackTrace(); } } }
View Code
四、二叉排序树
源码:二叉排序树
1,介绍
- 二叉排序树BST(Binary Sort Tree):对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
- 特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
- 二叉排序树的中序遍历为有序数列
2,添加子节点
a)思路
1)添加结点为node。如果node.value<this.value,则需要操作左子树 如果this.left ==null 则this.left=node 如果this.left != null 则递归调用this.left.add(node) 2)如果node.value>=this.value,则需要操作右子树 如果this.right ==null 则this.right =node 如果this.right != null 则递归调用this.right .add(node)
b)代码实现
/** * 添加结点 */ public void add(Node node) { if (node == null) { return; } //如果当前节点值大于添加的值 if (this.value > node.value) { //如果左节点不为空就向左递,否则就赋值给左节点 if (this.left != null) { this.left.add(node); } else { this.left = node; } } else { if (this.right != null) { this.right.add(node); } else { this.right = node; } } }
View Code
3,删除子节点
a)思路
查找需要删除的当前节点target和父节点parent 1)叶子结点直接删除 2)有且仅有一个子节点(左或右) 当前结点有左子树,当前节点是父节点的左节点 parent.left = target.left 当前结点有左子树,当前节点是父节点的右节点 parent.right = target.left 当前结点有右子树,当前节点是父节点的左节点 parent.left = target.right 当前结点有右子树,当前节点是父节点的右节点 parent.right = target.right 3)有两个子节点 查找当前右子树的最左边结点使用临时变量temp,赋值target.value = temp.value 删除temp
b)代码实现
public void del(int value) { Node target = root.search(value); if (target == null) { return; } Node parent = root.searchParent(value); //1)叶子结点 if (target.left == null && target.right == null) { //当前父节点为空,目标结点是叶子结点(目标结点是根节点) if (parent == null) { root = null; return; } if (parent.left == target) { parent.left = null; } if (parent.right == target) { parent.right = null; } } //2)存在两个叶子结点 else if (target.left != null && target.right != null) { //找到右子树的最左结点,并删除它 Node endNode = findLeft(target.right); del(endNode.value); //如果parent为null 则删除结点为根节点 if (parent == null) { root = endNode; } else { if (parent.left == target) { parent.left = endNode; } if (parent.right == target) { parent.right = endNode; } } endNode.left = target.left; endNode.right = target.right; } //3)存在一个叶子结点 else { if (target.left != null) { //当前删除为根节点,设置目标结点的左节点为根节点 if (parent == null) { root = target.left; return; } if (parent.left == target) { parent.left = target.left; } else { parent.right = target.left; } } if (target.right != null) { if (parent == null) { root = target.right; return; } if (parent.left == target) { parent.left = target.right; } else { parent.right = target.right; } } } } /** * 查询目标结点的父节点 */ public Node searchParent(int val) { if (this.value > val && this.left != null) { //左节点为需要寻找的结点 if (this.left.value == val) { return this; } return this.left.searchParent(val); } if (this.value < val && this.right != null) { if (this.right.value == val) { return this; } return this.right.searchParent(val); } return null; } /** * 查找当前结点 */ public Node search(int val) { if (this.value == val) { return this; } //如果查找的值小于当前值 并且 左子节点不为空 递归查找 if (this.value > val && this.left != null) { return this.left.search(val); } if (this.value < val && this.right != null) { return this.right.search(val); } return null; }
View Code
五、平衡二叉树(AVL树)
源码:平衡二叉树
1,二叉排序树的问题
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在
- 左子树全部为空,从形式上看,更像一个单链表
- 插入速度没有影响
- 查询速度明显降低(因为需要依次比较),不能发挥BST 的优势,因为每次还需要比较左子,其查询速度比单链表还慢
2,介绍
- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。
- 平衡二叉树具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
- 平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
- 注意:平衡二叉树一定是二叉排序树
3,树的高度
/** * 左子树高度 */ public int leftHeight() { return this.left == null ? 0 : this.left.height(); } /** * 右子树高度 */ public int rightHeight() { return this.right == null ? 0 : this.right.height(); } /** * 二叉树的总高度 */ public int height() { //取左子树和右子树的最大值为总高度。左子树递归查找,如果为null则是0,不为空则获取原先高度+1 return Math.max(this.left == null ? 0 : this.left.height(), this.right == null ? 0 : this.right.height()) + 1; }
4,左旋转
a)思路
b)代码实现
/** * 左旋转(以当前结点为根节点) */ private void leftRotate() { //1)创建新节点值为当前根节点的值 Node newNode = new Node(value); //2)新结点的左结点为根节点的左结点 newNode.left = this.left; //3)新结点的右结点为根节点的右结点的左结点 newNode.right = this.right.left; //4)当前结点值为当前结点的右结点值 this.value = this.right.value; //5)当前结点的左结点指向新结点 this.left = newNode; //6)当前结点的右结点指向原来右子树的右结点 this.right = this.right.right; }
5,右旋转
a)思路
b)代码实现
/** * 右旋转 */ public void rightRotate() { //1)创建新结点值为当前根节点的值 Node newNode = new Node(value); //2)新结点的右子树为当前右子树 newNode.right = this.right; //3)新结点的左子树为当前根节点的左子树的右子树 newNode.left = this.left.right; //4)当前结点值为当前结点的左子树的值 this.value = this.left.value; //5)当前结点的右子树为新结点 this.right = newNode; //6)当前结点的左子树为当前结点左子树的左子树 this.left = this.left.left; }
6,双旋转
int[] arr = { 10, 11, 7, 6, 8, 9 };// 运行原来的代码可以看到,并没有转成 AVL 树。 int[] arr = {2,1,6,5,7,3};// 运行原来的代码可以看到,并没有转成 AVL 树。
解决思路:
- 当符合右旋转的条件时
- 如果它的左子树的右子树高度大于它的左子树的高度
- 先对当前这个结点的左节点进行左旋转
- 再对当前结点进行右旋转的操作即可
代码实现:
/** * 添加结点 */ public void add(Node node) { if (node == null) { return; } //如果当前节点值大于添加的值 if (this.value > node.value) { //如果左节点不为空就向左递,否则就赋值给左节点 if (this.left != null) { this.left.add(node); } else { this.left = node; } } else { if (this.right != null) { this.right.add(node); } else { this.right = node; } } if (rightHeight() - leftHeight() > 1) { if (right.leftHeight() > right.rightHeight()) { right.rightRotate(); } leftRotate(); } if (leftHeight() - rightHeight() > 1) { if (left.rightHeight() > left.leftHeight()) { left.leftRotate(); } rightRotate(); } }
View Code
六、红黑树