算法训练营|【算法训练营】 - ⑤ Trie Tree、桶排序、排序总结


【算法训练营】 - ⑤ Trie Tree、桶排序、排序总结

  • Trie Tree
  • 桶排序
  • 排序总结
    • 排序算法的稳定性
    • 排序总结表
  • 常见的坑
  • 工程上对排序的改进

https://www.bilibili.com/video/BV1Ef4y1T7Qi
https://github.com/algorithmzuo
Trie Tree 前缀树
  1. 单个字符串中,字符从前到后的加到一棵多叉树上
  2. 字符放在路上,节点上有专属的数据项(常见的是pass和end值)
  3. 所有样本都这样添加,如果没有路就新建,如有路就复用
  4. 沿途节点的pass值增加1,每个字符串结束时来到的节点end值增加1
可以完成前缀相关的查询
例子: 设计一种结构
使用户可以:
  1. void insert(String str) 添加某个字符串,可以重复添加,每次算1个
  2. int search(String str) 查询某个字符串在结构中还有几个
  3. void delete(String str) 删掉某个字符串,可以重复删除,每次算1个
  4. int prefixNumber(String str) 查询有多少个字符串,是以str做前缀的
【算法训练营|【算法训练营】 - ⑤ Trie Tree、桶排序、排序总结】ps: 前缀树路的实现方式
  1. 固定数组实现
  2. 哈希表实现
package class08; import java.util.HashMap; // 该程序完全正确 public class Code02_TrieTree {public static class Node1 { public int pass; public int end; public Node1[] nexts; // char tmp = 'b'(tmp - 'a') public Node1() { pass = 0; end = 0; // 0a // 1b // 2c // .... // 25z // nexts[i] == nulli方向的路不存在 // nexts[i] != nulli方向的路存在 nexts = new Node1[26]; } }public static class Trie1 { private Node1 root; public Trie1() { root = new Node1(); }public void insert(String word) { if (word == null) { return; } char[] str = word.toCharArray(); Node1 node = root; node.pass++; int path = 0; for (int i = 0; i < str.length; i++) { // 从左往右遍历字符 path = str[i] - 'a'; // 由字符,对应成走向哪条路 if (node.nexts[path] == null) { node.nexts[path] = new Node1(); } node = node.nexts[path]; node.pass++; } node.end++; }public void delete(String word) { if (search(word) != 0) { char[] chs = word.toCharArray(); Node1 node = root; node.pass--; int path = 0; for (int i = 0; i < chs.length; i++) { path = chs[i] - 'a'; if (--node.nexts[path].pass == 0) { node.nexts[path] = null; return; } node = node.nexts[path]; } node.end--; } }// word这个单词之前加入过几次 public int search(String word) { if (word == null) { return 0; } char[] chs = word.toCharArray(); Node1 node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = chs[i] - 'a'; if (node.nexts[index] == null) { return 0; } node = node.nexts[index]; } return node.end; }// 所有加入的字符串中,有几个是以pre这个字符串作为前缀的 public int prefixNumber(String pre) { if (pre == null) { return 0; } char[] chs = pre.toCharArray(); Node1 node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = chs[i] - 'a'; if (node.nexts[index] == null) { return 0; } node = node.nexts[index]; } return node.pass; } }public static class Node2 { public int pass; public int end; public HashMap nexts; public Node2() { pass = 0; end = 0; nexts = new HashMap<>(); } }public static class Trie2 { private Node2 root; public Trie2() { root = new Node2(); }public void insert(String word) { if (word == null) { return; } char[] chs = word.toCharArray(); Node2 node = root; node.pass++; int index = 0; for (int i = 0; i < chs.length; i++) { index = (int) chs[i]; if (!node.nexts.containsKey(index)) { node.nexts.put(index, new Node2()); } node = node.nexts.get(index); node.pass++; } node.end++; }public void delete(String word) { if (search(word) != 0) { char[] chs = word.toCharArray(); Node2 node = root; node.pass--; int index = 0; for (int i = 0; i < chs.length; i++) { index = (int) chs[i]; if (--node.nexts.get(index).pass == 0) { node.nexts.remove(index); return; } node = node.nexts.get(index); } node.end--; } }// word这个单词之前加入过几次 public int search(String word) { if (word == null) { return 0; } char[] chs = word.toCharArray(); Node2 node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = (int) chs[i]; if (!node.nexts.containsKey(index)) { return 0; } node = node.nexts.get(index); } return node.end; }// 所有加入的字符串中,有几个是以pre这个字符串作为前缀的 public int prefixNumber(String pre) { if (pre == null) { return 0; } char[] chs = pre.toCharArray(); Node2 node = root; int index = 0; for (int i = 0; i < chs.length; i++) { index = (int) chs[i]; if (!node.nexts.containsKey(index)) { return 0; } node = node.nexts.get(index); } return node.pass; } }public static class Right {private HashMap box; public Right() { box = new HashMap<>(); }public void insert(String word) { if (!box.containsKey(word)) { box.put(word, 1); } else { box.put(word, box.get(word) + 1); } }public void delete(String word) { if (box.containsKey(word)) { if (box.get(word) == 1) { box.remove(word); } else { box.put(word, box.get(word) - 1); } } }public int search(String word) { if (!box.containsKey(word)) { return 0; } else { return box.get(word); } }public int prefixNumber(String pre) { int count = 0; for (String cur : box.keySet()) { if (cur.startsWith(pre)) { count += box.get(cur); } } return count; } }// for test public static String generateRandomString(int strLen) { char[] ans = new char[(int) (Math.random() * strLen) + 1]; for (int i = 0; i < ans.length; i++) { int value = https://www.it610.com/article/(int) (Math.random() * 6); ans[i] = (char) (97 + value); } return String.valueOf(ans); }// for test public static String[] generateRandomStringArray(int arrLen, int strLen) { String[] ans = new String[(int) (Math.random() * arrLen) + 1]; for (int i = 0; i < ans.length; i++) { ans[i] = generateRandomString(strLen); } return ans; }public static void main(String[] args) { int arrLen = 100; int strLen = 20; int testTimes = 100000; for (int i = 0; i < testTimes; i++) { String[] arr = generateRandomStringArray(arrLen, strLen); Trie1 trie1 = new Trie1(); Trie2 trie2 = new Trie2(); Right right = new Right(); for (int j = 0; j < arr.length; j++) { double decide = Math.random(); if (decide < 0.25) { trie1.insert(arr[j]); trie2.insert(arr[j]); right.insert(arr[j]); } else if (decide < 0.5) { trie1.delete(arr[j]); trie2.delete(arr[j]); right.delete(arr[j]); } else if (decide < 0.75) { int ans1 = trie1.search(arr[j]); int ans2 = trie2.search(arr[j]); int ans3 = right.search(arr[j]); if (ans1 != ans2 || ans2 != ans3) { System.out.println("Oops!"); } } else { int ans1 = trie1.prefixNumber(arr[j]); int ans2 = trie2.prefixNumber(arr[j]); int ans3 = right.prefixNumber(arr[j]); if (ans1 != ans2 || ans2 != ans3) { System.out.println("Oops!"); } } } } System.out.println("finish!"); }}

桶排序 不基于比较的排序
桶排序思想下的排序: 计数排序 & 基数排序
  1. 桶排序思想下的排序都是不基于比较的排序
  2. 时间复杂度为O(N),额外空间负载度O(M)
  3. 应用范围有限,需要样本的数据状况满足桶的划分
计数排序和基数排序
  1. 一般来讲,计数排序要求,样本是整数,且范围比较窄
  2. 一般来讲,基数排序要求,样本是10进制的正整数
一旦要求稍有升级,改写代价增加是显而易见的
计数排序实现
package class08; import java.util.Arrays; public class Code03_CountSort { // only for 0~200 value public static void countSort(int[] arr) { if (arr == null || arr.length < 2) { return; } int max = Integer.MIN_VALUE; for (int i = 0; i < arr.length; i++) { max = Math.max(max, arr[i]); } int[] bucket = new int[max + 1]; for (int i = 0; i < arr.length; i++) { bucket[arr[i]]++; } int i = 0; for (int j = 0; j < bucket.length; j++) { while (bucket[j]-- > 0) { arr[i++] = j; } } } // for test public static void comparator(int[] arr) { Arrays.sort(arr); } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = https://www.it610.com/article/150; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); countSort(arr1); comparator(arr2); if (!isEqual(arr1, arr2)) { succeed = false; printArray(arr1); printArray(arr2); break; } } System.out.println(succeed ?"Nice!" : "Fucking fucked!"); int[] arr = generateRandomArray(maxSize, maxValue); printArray(arr); countSort(arr); printArray(arr); }}

基数排序实现
package class08; import java.util.Arrays; public class Code04_RadixSort { // only for no-negative value public static void radixSort(int[] arr) { if (arr == null || arr.length < 2) { return; } radixSort(arr, 0, arr.length - 1, maxbits(arr)); } public static int maxbits(int[] arr) { int max = Integer.MIN_VALUE; for (int i = 0; i < arr.length; i++) { max = Math.max(max, arr[i]); } int res = 0; while (max != 0) { res++; max /= 10; } return res; } // arr[L..R]排序,最大值的十进制位数digit public static void radixSort(int[] arr, int L, int R, int digit) { final int radix = 10; int i = 0, j = 0; // 有多少个数准备多少个辅助空间 int[] help = new int[R - L + 1]; for (int d = 1; d <= digit; d++) { // 有多少位就进出几次 // 10个空间 // count[0] 当前位(d位)是0的数字有多少个 // count[1] 当前位(d位)是(0和1)的数字有多少个 // count[2] 当前位(d位)是(0、1和2)的数字有多少个 // count[i] 当前位(d位)是(0~i)的数字有多少个 int[] count = new int[radix]; // count[0..9] for (i = L; i <= R; i++) { // 10313 // 20919 j = getDigit(arr[i], d); count[j]++; } for (i = 1; i < radix; i++) { count[i] = count[i] + count[i - 1]; } for (i = R; i >= L; i--) { j = getDigit(arr[i], d); help[count[j] - 1] = arr[i]; count[j]--; } for (i = L, j = 0; i <= R; i++, j++) { arr[i] = help[j]; } } } public static int getDigit(int x, int d) { return ((x / ((int) Math.pow(10, d - 1))) % 10); } // for test public static void comparator(int[] arr) { Arrays.sort(arr); } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = https://www.it610.com/article/100000; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); radixSort(arr1); comparator(arr2); if (!isEqual(arr1, arr2)) { succeed = false; printArray(arr1); printArray(arr2); break; } } System.out.println(succeed ?"Nice!" : "Fucking fucked!"); int[] arr = generateRandomArray(maxSize, maxValue); printArray(arr); radixSort(arr); printArray(arr); }}

排序总结 排序算法的稳定性 稳定性是指同样大小的样本再排序之后不会改变相对次序
对基础类型来说,稳定性毫无意义
对非基础类型来说,稳定性有重要意义
有些排序算法可以实现成稳定的,而有些排序算法无论如何都实现不成稳定的
排序总结表
时间复杂度 额外空间复杂度 稳定性
基于比较的排序
选择排序 O(N^2) O(1)
冒泡排序 O(N^2) O(1)
插入排序 O(N^2) O(1)
归并排序 O(N * logN) O(N)
随机快排 O(N * logN) O(logN)
堆排序 O(N * logN) O(1)
不基于比较的排序
计数排序 O(N) O(M)
基数排序 O(N) O(N)
排序算法总结
  1. 不基于比较的排序,对样本数据有严格要求,不易改写
  2. 基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
  3. 基于比较的排序,时间复杂度的极限是O(N*logN)
  4. 时间复杂度O(N*logN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。
  5. 为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并
常见的坑
  1. 归并排序的额外空间复杂度可以变成O(1),“归并排序内部缓存法",但是将变得不再稳定。
  2. “原地归并排序"是垃圾贴,会让时间复杂度变成O(N^2)
  3. 快速排序稳定性改进, 见论文“01 stable sort"(很难,不要看),但是会对样本数据要求更多。
  4. 面试官压薪题:在整型数组中,请把奇数放在数组左边,偶数放在数组右边,要求所有奇数之间原始的相对次序不变,所有偶数之间原始相对次序不变。时间复杂度做到O(N),额外空间复杂度做到O(1) 【碰到之后的做法:问面试官:01划分,为什么快排不改成稳定的呢?】
工程上对排序的改进
  1. 稳定性的考虑
  2. 充分利用O(N*logN)和O(N^2)排序各自的优势(数量小直接插入排序,量大快排堆排)

    推荐阅读