一、堆排序
詳情檢視:排序演算法
二、赫夫曼樹
原始碼: 構建赫夫曼樹
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; } }
三、赫夫曼編解碼
原始碼: 赫夫曼壓縮
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; } }
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(); }
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(); } } }
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(); } } }
四、二叉排序樹
原始碼:二叉排序樹
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; } } }
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; }
五、平衡二叉樹(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(); } }
六、紅黑樹