排序算法分类:
内部排序 数据在内存中进行排序, 外部排序 数据量很大,一次不能容纳全部的数据,在排序过程中需要访问外存。
比较排序: 冒泡排序、选择排序、插入排序、归并排序、堆排序、快速排序;
非比较排序:计数排序、基数排序、桶排序。
排序算法的稳定性:两个相同的数排序前后的相对顺序不变。
一、桶排序(Bucket sort)
桶排序(Bucket sort)是一种通过 分桶和合并 实现的排序算法,是计数排序的升级版。
核心思想是把数据分配到有序的桶中,对每个桶分别进行排序,然后依次取出每个桶里的数据组成序列。
1、设置固定数量的空桶;
2、把数据放到对应的桶中;
3、对每个不为空的桶中数据进行排序;
4、拼接不为空的桶中数据,得到结果。
基本思路:一个萝卜一个坑,也可以多个萝卜一个坑,对每个坑排序,再拿出来,整体就有序。
import java.util.Arrays;
import static java.lang.System.*;
public class BucketSort {
public static void bucketSort(int[] nums) {
int max = nums[0], min = nums[0];
for (int i = 0; i < nums.length; i++) {
if (nums[i] > max) max = nums[i];
if (nums[i] < min) min = nums[i];
}
int[] bucketArray = new int[max - min + 1];
for (int i = 0; i < nums.length; i++) {
// 减去 min 是为了防止 nums[i] 小于 0,相当于做了个偏移。
bucketArray[nums[i] - min]++;
}
int index = 0;
for (int i = 0; i < bucketArray.length; i++) {
for (int j = 0; j < bucketArray[i]; j++) {
nums[index++] = i + min;
}
}
}
public static void main(String[] args) {
int[] arr = new int[]{1, 3, 5, 0, -2, 9, 33, -20, 0};
bucketSort(arr);
out.println(Arrays.toString(arr));
}
}
★220. 存在重复元素 III
方法一:滑动窗口 + 有序集合
对于序列中每一个元素 x,考虑其左侧的至多 k 个元素,如果存在元素落在区间 [x - t, x + t] 中,那么返回 True。维护一个大小为 k 的滑动窗口,每次遍历到元素 x 时,滑动窗口中包含元素 x 前面的最多 k 个元素,检查窗口中是否存在元素落在区间 [x - t, x + t] 中。
找一个「有序集合」去维护长度为 k 的滑动窗口内的数,支持高效「查询」与「插入/删除」操作:
查询:应用「二分查找」,快速找到「有序集合」中的最接近 u 的数。
插入/删除:在往「有序集合」添加或删除元素时,能够在低于线性的复杂度内完成(维持有序特性)。
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int indexDiff, int valueDiff) {
int n = nums.length, k = indexDiff;
long t = (long)valueDiff;
TreeSet<Long> set = new TreeSet();
for (int i = 0; i < n; i++) {
Long ceiling = set.ceiling(nums[i] - t);
if (ceiling != null && ceiling <= nums[i] + t)
return true;
set.add((long)nums[i]);
if (i >= k) set.remove(nums[i - k] * 1L);
}
return false;
}
}
ceiling(E e) 方法 返回在这个集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回 null. 也就是说 右侧最接近的数。floor(E e)
方法二:桶
对于元素 x,其有效的区间为 [x - t, x + t]。
定义桶的大小是 t + 1, nums[i] / (t + 1) 决定放入几号桶,在一个桶里面的任意两个的绝对值差值都 <= t;两个元素属于相邻桶,需要校验这两个元素是否差值不超过 t;两个元素既不属于同一个桶,也不属于相邻桶,这两个元素不符合条件。
将 int 范围内的每一个整数 x 表示为 x = (t + 1) × a + b (0 ≤ b ≤ t) 的形式,这样 x 即归属于编号为 a 的桶。因为一个桶内至多只会有一个元素,所以使用哈希表实现即可。
利用桶的思想,用哈希表存储桶,key 表示桶,value 存储桶中一个元素。
遍历 nums,在滑动窗口中,如果 nums[i] 所属的桶已经存在或者它与前或后的桶的差值 <= t,返回 True。
维护滑动窗口,删除桶 nums[i - k] / (t + 1),这样就能保证遍历到第 i + 1 个元素时,全部桶中元素的索引最小值是 i - k + 1。
class Solution {
long size;
public boolean containsNearbyAlmostDuplicate(int[] nums, int indexDiff, int valueDiff) {
int n = nums.length;
size = valueDiff + 1L;
Map<Long, Integer> map = new HashMap();
for (int i = 0; i < n; i++) {
int x = nums[i];
long id = getId(x);
if (map.containsKey(id)) return true;
// 检查 注意:左边 x > 左桶中的数
if (map.containsKey(id - 1) && x - map.get(id - 1) <= valueDiff) return true;
if (map.containsKey(id + 1) && map.get(id + 1) - x <= valueDiff) return true;
map.put(id, x); // 桶:元素的值
// 滑动窗口 移除相应的桶
if (i >= indexDiff) map.remove(getId(nums[i - indexDiff]));
}
return false;
}
long getId(int x) { // nums 的下标转化成桶的编号
// java 整除向 0 取整, 正数向下,负数向上。
return x >= 0 ? x / size : (x + 1) / size - 1;
}
}
★164. 最大间距
nums 长度为 n,n - 1 个间隙,最大值为 max、最小值为 min。
1、排序后最大间距 (平均间距)
桶的大小:bucketSize = max(1, (max - min) / (n - 1)) // 最小为 1
数量:bucketsLen = (max - min) / bucket_size + 1
2、最大间距不可能在桶内产生,只能在桶间产生。桶内只需要记录最大值和最小值,不需要排序。用后一个桶的最小值减前一个桶的最大值,更新最大间距。
class Solution {
public int maximumGap(int[] nums) {
int n = nums.length - 1; // 间隙个数
if (n < 1) return 0;
int max = nums[0], min = nums[0];
for (int x : nums) {
if (x > max) max = x;
if (x < min) min = x;
}
int maxGap = 0;
// if (max == min) return 0; // 1,1,1,1
int bucketSize = Math.max(1, (max - min) / n);
int bucketsLen = (max - min) / bucketSize + 1;
// 只记录最值
int[] bucketMin = new int[bucketsLen];
int[] bucketMax = new int[bucketsLen];
Arrays.fill(bucketMax, -1);
Arrays.fill(bucketMin, Integer.MAX_VALUE);
for (int x : nums) {
int loc = (x - min) / bucketSize; // 桶号
bucketMax[loc] = Math.max(bucketMax[loc], x);
bucketMin[loc] = Math.min(bucketMin[loc], x);
}
// 第二个开始,当前桶的最小值 - 前一个桶的最大值
int prevMax = bucketMax[0];
for (int i = 1; i < bucketsLen; i++){
if (bucketMax[i] == -1) continue; // 跳过空桶
maxGap = Math.max(maxGap, bucketMin[i] - prevMax);
prevMax = bucketMax[i];
}
return maxGap;
}
}
class Solution {
public int maximumGap(int[] nums) {
int n = nums.length - 1;
if (n < 1) return 0;
int max = nums[0], min = nums[0];
for (int x : nums) {
if (x > max) max = x;
if (x < min) min = x;
}
// if (max - min == 0) return 0; // 1,1,1,1
int[] bucketMin = new int[n];
int[] bucketMax = new int[n];
Arrays.fill(bucketMax, -1);
Arrays.fill(bucketMin, Integer.MAX_VALUE);
// int interval = (int) Math.ceil((double)(max - min) / n);
int interval = (max - min + n - 1) / n; // 向上取整: + (n - 1)
for(int x : nums) {
int ID = (x - min) / interval;
if(x == min || x == max) continue;
// 一个桶中只记录最大值和最小值
bucketMax[ID] = Math.max(bucketMax[ID], x);
bucketMin[ID] = Math.min(bucketMin[ID], x);
}
int maxGap = 0, previousMax = min;
for(int i = 0; i < n; i++) {
if(bucketMax[i] == -1) continue;
maxGap = Math.max(bucketMin[i] - previousMax, maxGap);
previousMax = bucketMax[i];
}
//1, 100000
maxGap = Math.max(maxGap, max - previousMax);
return maxGap;
}
347. 前 K 个高频元素
451. 根据字符出现频率排序
692. 前K个高频单词
912. 排序数组
二、计数排序
计数排序与基数排序的思想都来自于桶排序,桶排序的大体思想为:若 N 个关键字均可以映射到 0 - M 的整数范围内,则可以建立 M + 1 大小的桶,然后依次遍历 N 个关键字,依次将他们放到自己对应的那个桶里面,最后从 0 开始依次将桶中的元素倒出即可。
1、用一个数组用来保存每一个数字出现的次数
2、根据每一个元素出现的次数,按照下标在原数组中排列,这样这组数据就有序
假设序列值均为整数,首先获得序列的最大值 max 和最小值 min,建立一个大小为(max - min + 1)的数组 count,遍历该序列 data,count[data[i] - min]++。count 中第 k 个位置上的元素即为 k + min 出现的次数,再根据次数复制到 data 中去,完成排序。
public static void countSort(int[] data) {
// 找到最小值,最大值
int max = data[0], min = data[0];
for (int i = 0; i < data.length; i++) {
if (data[i] > max) max = data[i];
if (data[i] < min) min = data[i];
}
int[] count = new int[max - min + 1];
for (int tmp : data) count[tmp - min]++; // 偏移量 min
// 把值复制到 data 中
for (int i = 0, k = 0; i < count.length; i++)
for (int j = 0; j < count[i]; j++) data[k++] = i + min;
}
计数排序是 N 的线性范围复杂度的排序,其时间复杂度为 O(N)。空间复杂度由于建了 M 个桶,因此其空间复杂度为 O(M)。
912. 排序数组
class Solution {
public int[] sortArray(int[] nums) {
int n = nums.length;
int max = nums[0], min = nums[0];
for (int x : nums) {
if (x > max) max = x;
if (x < min) min = x;
}
int size = max - min + 1;
int[] count = new int[size];
for (int x : nums) {
count[x - min]++;
}
int index = 0;
for (int i = 0; i < size; i++) {
while (count[i]-- > 0)
nums[index++] = i + min;
}
return nums;
}
}
1122. 数组的相对排序
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
int n = arr1.length;
int max = arr1[0], min = arr1[0];
for (int x : arr1) {
if (x > max) max = x;
if (x < min) min = x;
}
int size = max - min + 1;
int[] count = new int[size];
for (int x : arr1) count[x - min]++;
int[] ans = new int[n];
int index = 0;
for (int x : arr2) {
while (count[x - min]-- > 0) ans[index++] = x;
}
for (int i = 0; i < size; i++) {
while (count[i]-- > 0) ans[index++] = i + min;
}
return ans;
}
}
1051. 高度检查器
561. 数组拆分
274. H 指数
三、基数排序
一般用于多个关键字的排序(如扑克牌中的花色和面值大小),基数排序分为两种主位优先和次位优先两种,更广泛的是次位优先。所谓次位优先,先以次位关键字建立桶,将所有元素按照关键字依次扔进桶中,再依次从小到大从桶中放回到原数组。对于多个关键字依次从次到主重复上述操作即可完成排序。
基本思路:也称为基于关键字的排序,例如针对数值排序,个位、十位、百位就是关键字。针对日期数据的排序:年、月、日、时、分、秒就是关键字。
「基数排序」用到了「计数排序」。
以三位数的整数排序为例,将三位数的百位、十位、个位作为关键字,显然百位是主位。
public static void radixSort(int[] data) {
// 以三位数的整数为例
int N = 10;
// 创建桶
List<LinkedList<Integer>> buck = new ArrayList<LinkedList<Integer>>(N);
for (int i = 0; i < N; i++) {
buck.add(new LinkedList<Integer>());
}
for (int i = 0, k = 1; i < 3; i++, k *= 10) { // 从低位数第i位 0=个位 1=百位
int temp = 0;
for (int j = 0; j < data.length; j++) {
tmp = data[j] % (10 * k) / k; // 取出 i 位上的数
buck.get(tmp).add(data[j]);
}
// 往 data 中倒
for (int j = 0, m = 0; j < N; j++) {
while (buck.get(j).size() > 0) {
data[m++] = buck.get(j).remove();
}
}
}
}
复杂度分析:
时间复杂度为 O(D(N+M)),其中 D 为关键字的个数,M 为桶的个数,N 为序列长度。
2343. 裁剪数字后查询第 K 小的数字
912. 排序数组
public class Solution {
// 基数排序:低位优先
private static final int OFFSET = 50000;
public int[] sortArray(int[] nums) {
int len = nums.length;
// 预处理,让所有的数都大于等于 0,这样才可以使用基数排序
for (int i = 0; i < len; i++) {
nums[i] += OFFSET;
}
// 第 1 步:找出最大的数字
int max = nums[0];
for (int num : nums) {
if (num > max) {
max = num;
}
}
// 第 2 步:计算出最大的数字有几位,这个数值决定了我们要将整个数组看几遍
int maxLen = getMaxLen(max);
// 计数排序需要使用的计数数组和临时数组
int[] count = new int[10];
int[] temp = new int[len];
// 表征关键字的量:除数
// 1 表示按照个位关键字排序
// 10 表示按照十位关键字排序
// 100 表示按照百位关键字排序
// 1000 表示按照千位关键字排序
int divisor = 1;
// 有几位数,外层循环就得执行几次
for (int i = 0; i < maxLen; i++) {
// 每一步都使用计数排序,保证排序结果是稳定的
// 这一步需要额外空间保存结果集,因此把结果保存在 temp 中
countingSort(nums, temp, divisor, len, count);
// 交换 nums 和 temp 的引用,下一轮还是按照 nums 做计数排序
int[] t = nums;
nums = temp;
temp = t;
// divisor 自增,表示采用低位优先的基数排序
divisor *= 10;
}
int[] res = new int[len];
for (int i = 0; i < len; i++) {
res[i] = nums[i] - OFFSET;
}
return res;
}
private void countingSort(int[] nums, int[] res, int divisor, int len, int[] count) {
// 1、计算计数数组
for (int i = 0; i < len; i++) {
// 计算数位上的数是几,先取个位,再十位、百位
int remainder = (nums[i] / divisor) % 10;
count[remainder]++;
}
// 2、变成前缀和数组
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 3、从后向前赋值
for (int i = len - 1; i >= 0; i--) {
int remainder = (nums[i] / divisor) % 10;
int index = count[remainder] - 1;
res[index] = nums[i];
count[remainder]--;
}
// 4、count 数组需要设置为 0 ,以免干扰下一次排序使用
for (int i = 0; i < 10; i++) {
count[i] = 0;
}
}
/**
* 获取一个整数的最大位数
*/
private int getMaxLen(int num) {
int maxLen = 0;
while (num > 0) {
num /= 10;
maxLen++;
}
return maxLen;
}
}
164. 最大间距
桶排序比较适合用在外部排序中。外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
如何根据年龄给 100 万用户排序?
现在你有 10 个接口访问日志文件,每个日志文件大小约 300MB,每个文件里的日志都是按照时间戳从小到大排序的。你希望将这 10 个较小的日志文件,合并为 1 个日志文件,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述排序任务的机器内存只有 1GB,你有什么好的解决思路,能“快速”地将这 10 个日志文件合并吗?
有 10GB 的订单数据,希望按订单金额(假设金额都是正整数)进行排序,但是内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。
一年的全国高考考生人数为500 万,分数使用标准分,最低100 ,最高900 ,没有小数,要求对这 500 万元素的数组进行排序。
在一个文件中有 10G 个整数,乱序排列,要求找出中位数。内存限制为 2G。只写出思路即可(内存限制为 2G 意思是可以使用 2G 空间来运行程序,而不考虑本机上其他软件内存占用情况。) 关于中位数:数据排序后,位置在最中间的数值。即将数据分成两部分,一部分大于该数值,一部分小于该数值。中位数的位置:当样本数为奇数时,中位数 = (N+1)/2 ; 当样本数为偶数时,中位数为 N/2 与 1+N/2 的均值(那么 10G 个数的中位数,就第 5G 大的数与第 5G + 1 大的数的均值了)。
一个数该放在哪里,是由这个数本身的大小决定的,它不需要经过比较。也可以认为是哈希的思想:由数值映射地址。
因此这三种算法一定需要额外的空间才能完成排序任务,时间复杂度可以提升到 O(N),使用这三种排序一定要保证输入数组的每个元素都在一个合理的范围内。