前缀树、计数排序、基数排序、排序总结

125 阅读2分钟

前缀树

  1. 单个字符串中,字符从前到后的加到一棵多叉树上。
  2. 字符放在路上,节点上有专属的数据项(常见的是pass和end值)。
  3. 所有样本都这样添加,如果没有路就新建,如有路就复用。沿途节点的pass值增加1,每个字符串结束时来到的节点end值增加1。
  4. 可以完成前缀相关的查询。
// 1)void insert(String str)            添加某个字符串,可以重复添加,每次算1个
// 2)int search(String str)             查询某个字符串在结构中还有几个
// 3)  void delete(String str)           删掉某个字符串,可以重复删除,每次算1个
// 4)int prefixNumber(String str)       查询有多少个字符串,是以str做前缀的
public static class Node2 {
   public int pass;
   public int end;
   public HashMap<Integer, Node2> 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;
         // 删除不仅要减去pass值,还要把节点删掉
         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 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;
      }
   }
}

基数排序

// 只非负数有效
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++) { // 有多少位就进出几次
      int[] count = new int[radix]; // count[0..9]
      for (i = L; i <= R; i++) {
         // 获得当前位(依次比较个位 十位 百位)上的数是几
         j = getDigit(arr[i], d);
         count[j]++;
      }
      // count[0] 当前位是0的数字有多少个
      // count[1] 当前位是(0和1)的数字有多少个
      // count[2] 当前位是(0、1和2)的数字有多少个
      // count[i] 当前位是(0~i)的数字有多少个
      for (i = 1; i < radix; i++) {
         count[i] = count[i] + count[i - 1];
      }
      // 从右向左遍历,这样的话当前位相等的数,左边的依然在左边,实现队列的功能
      for (i = R; i >= L; i--) {
         // 获得当前位(依次比较个位 十位 百位)上的数是几
         j = getDigit(arr[i], d);
         // 比如当前位的数是4,而当前位小于等于4的数有三个,那么这个数应该在数组下标2的位置
         // 下一次,如果当前位的数还是4,那么当于等于4的数有两个,那么这个数应该在数组下标1的位置
         help[count[j] - 1] = arr[i];
         count[j]--;
      }
      for (i = L, j = 0; i <= R; i++, j++) {
         arr[i] = help[j];
      }
   }
}
   // 从右向左,第一次进出时,砍掉0位,取最后一位;第二次进出时,砍掉1位,去最后一位
public static int getDigit(int x, int d) {
   return ((x / ((int) Math.pow(10, d - 1))) % 10);
}

排序总结

  1. 不基于比较的排序,对样本数据有严格要求,不易改写
  2. 基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
  3. 基于比较的排序,时间复杂度的极限是O(N*logN)
  4. 时间复杂度O(N*logN)、额外空间复杂度低于O(N)、且稳定的基于比较的排序是不存在的。
  5. 为了绝对的速度选快排(常数时间最短)、为了省空间选堆排、为了稳定性选归并

image.png