开篇问题:如何根据年龄给100万用户数据排序
以下几种排序就比较适用这种数据量比较大的场景。
核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
如果每个桶的数据分布不均匀,可以在数据比较多的桶里继续划分数据。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
容器版
public static void bucketSort2(int[] arr){ int max = arr[0]; int min = arr[0]; for (int i = 0; i < arr.length; i++) { max = Math.max(max,arr[i]); min = Math.min(min,arr[i]); } //计算桶的数量 int bucketNum = (max - min) / arr.length + 1; List<List<Integer>> bucketArr = new ArrayList<>(bucketNum); for (int i = 0; i < bucketNum; i++) { bucketArr.add(new ArrayList<>()); } //将每个元素放入桶内 for (int i = 0; i < arr.length; i++) { int index = (arr[i] - min) / arr.length; bucketArr.get(index).add(arr[i]); } //对每个桶进行排序 for (int i = 0; i < bucketArr.size(); i++) { Collections.sort(bucketArr.get(i)); } //取出数据存入数组中 int index = 0; for (int i = 0; i < bucketArr.size(); i++) { for (int j = 0; j < bucketArr.get(i).size(); j++) { arr[index] = bucketArr.get(i).get(j); index++; } } }
二维数组版(支持自动扩容)
public static void bucketSort(int[] arr,int bucketSize){ if (arr.length < 2){ return; } int minValue = arr[0]; int maxValue = arr[0]; for (int i = 0; i < arr.length; i++) { if (arr[i] > maxValue){ maxValue = arr[i]; } if (arr[i] < minValue){ minValue = arr[i]; } } //桶数量 int bucketCount = (maxValue - minValue) / bucketSize + 1; int[][] buckets = new int[bucketCount][bucketSize]; int[] indexArr = new int[bucketCount]; //将数值分配到各个桶中 for (int i = 0; i < arr.length; i++) { int bucketIndex = (arr[i] - minValue) / bucketSize; if (indexArr[bucketIndex] == buckets[bucketIndex].length){ //扩容 ensureCapacity(buckets,bucketIndex); } buckets[bucketIndex][indexArr[bucketIndex]++] = arr[i]; } //对每个桶进行排序,使用快速排序 int k = 0; for (int i = 0; i < buckets.length; i++) { if (indexArr[i] == 0){ continue; } //快速排序 quickSort(buckets[i],0,indexArr[i] - 1); //赋值到原数组 for (int j = 0; j < indexArr[i]; j++) { arr[k++] = buckets[i][j]; } } } /** * 数组扩容 * @param buckets 数组 * @param bucketIndex 下标 */ private static void ensureCapacity(int[][] buckets,int bucketIndex){ int[] tempArr = buckets[bucketIndex]; int[] newArr = new int[tempArr.length * 2]; for (int i = 0; i < tempArr.length; i++) { newArr[i] = tempArr[i]; } buckets[bucketIndex] = newArr; } /** * 快速排序 * @param arr 数组 * @param p 分区点左边 * @param r 分区点右边 */ private static void quickSort(int[] arr,int p,int r){ if (p >= r){ return; } //分区点 int q = partition(arr,p,r); //往左边继续分区 quickSort(arr,p,q - 1); //往右遍继续分区 quickSort(arr,q + 1,r); } private static int partition(int[] arr, int p,int r){ //分区点 int pivot = arr[r]; int i = p; for (int j = p; j < r; j++) { //小于分区点的放左边 if (arr[j] <= pivot){ swap(arr,i,j); i++; } } swap(arr,i,r); return i; } private static void swap(int[] arr,int i,int j){ if (i == j){ return; } int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; }
我们都经历过高考,高考查分数系统你还记得吗?我们查分数的时候,系统会显示我们的成绩以及所在省的排名。如果你所在的省有50万考生,如何通过成绩快速排序得出名次呢?
考生的满分是900分,最小是0分,这个数据的范围很小,所以我们可以分成901个桶,对应分数从0分到900分。根据考生的成绩,我们将这50万考生划分到这901个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序。我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了50万考生的排序。因为只涉及扫描遍历操作,所以时间复杂度是O(n)。
当要排序的n个数据,所处的范围并不大的时候,比如最大值是k,我们就可以把数据划分成k个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。
我们对C[6]数组顺序求和,C[6]存储的数据就变成了下面这样子。C[k]里存储小于等于分数k的考生个数。通过这样,就能直接知道每个数的排名了。
最后将这些数据依次放入数组R中,即排序成功。
从后到前依次扫描数组A。比如,当扫描到3时,我们可以从数组C中取出下标为3的值7,也就是说,到目前为止,包括自己在内,分数小于等于3的考生有7个,也就是说3是数组R中的第7个元素(也就是数组R中下标为6的位置)。
当3放入到数组R中后,小于等于3的元素就只剩下了6个了,所以相应的C[3]要减1,变成6。
代码实现:
public static void countingSort(int[] arr){ int min = arr[0]; int max = arr[0]; for (int i = 0; i < arr.length; i++) { max = Math.max(max,arr[i]); min = Math.min(min,arr[i]); } int[] bucket = new int[max - min + 1]; for (int i = 0; i < arr.length; i++) { //减去min是为了防止array[i]小于0, // 数组索引不能小于0,相当于做了个偏移。++是为了记录次数 bucket[arr[i] - min]++; } int index = 0; for (int i = 0; i < bucket.length; i++) { while (bucket[i]-- > 0){ arr[index++] = i + min; } } }
假设我们有10万个手机号码,希望将这10万个手机号码从小到大排序,你有什么比较快速的排序方法呢?
桶排序、计数排序能派上用场吗?手机号码有11位,范围太大,显然不适合用这两种排序算法。
这种情况下就要用到基数排序:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
手机号码稍微有点长,画图比较不容易看清楚,可以用字符串排序来模拟一下
可以借助相同的处理思路,先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,以此类推,最后按照第一位重新排序。经过11次排序之后,手机号码就都有序了。
根据每一位来排序,用桶排序或者计数排序,时间复杂度可以做到O(n)。如果要排序的数据有k位,那我们就需要k次桶排序或者计数排序,总的时间复杂度是O(k*n)。当k不大的时候,比如手机号码排序的例子,k最大就是11,所以基数排序的时间复杂度就近似于O(n)。
代码实现:
public static void randixSort(int[] arr){ int max = arr[0]; //找到最大值 for (int i = 0; i < arr.length; i++) { if (arr[i] > max){ max = arr[i]; } } // 从个位开始,对数组arr按“指数”进行排序 for (int exp = 1;max / exp > 0;exp *= 10){ countingSort(arr,exp); } } private static void countingSort(int[] arr,int exp){ if (arr.length <= 1){ return; } //计算每个元素的个数 int[] c = new int[10]; for (int i = 0; i < arr.length; i++) { c[(arr[i] / exp) % 10]++; } //计算排序后的位置 for (int i = 1; i < c.length; i++) { c[i] += c[i - 1]; } //临时数组temp,存储排序之后的结果 int[] temp = new int[arr.length]; for (int i = arr.length - 1; i >= 0; i--) { temp[c[(arr[i] / exp) % 10] - 1] = arr[i]; c[(arr[i] / exp) % 10]--; } //将临时数组的值赋值到原数组中 for (int i = 0; i < arr.length; i++) { arr[i] = temp[i]; } }
问题解答
最后我们来看下一开始的问题,如何根据年龄给100万用户排序?
我们假设人大年龄为150,最小1岁,我们可以遍历这100万个用户将其年龄放入150个有序桶中,然后按顺序取出即可。
喜欢本文的话,可以关注一下公众号,每天定时更新一篇学习日记,让我们一起成长!