一、概述
1. 基于比较的排序算法
算法 | 最好 | 最坏 | 平均 | 空间 | 稳定 | 思想 | 注意事项 |
---|---|---|---|---|---|---|---|
冒泡 | O(n) | O(n^2) | O(n^2) | O(1) | Y | 比较 | 最好情况需要额外判断 |
选择 | O(n^2) | O(n^2) | O(n^2) | O(1) | N | 比较 | 交换次数一般少于冒泡 |
堆 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | N | 选择 | 堆排序的辅助性较强,理解前先理解堆的数据结构 |
插入 | O(n) | O(n^2) | O(n^2) | O(1) | Y | 比较 | 插入排序对于近乎有序的数据处理速度比较快,复杂度有所下降,可以提前结束 |
希尔 | O(nlogn) | O(n^2) | O(nlogn) | O(1) | N | 插入 | gap序列的构造有多种方式,不同方式处理的数据复杂度可能不同 |
归并 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | Y | 分治 | 需要额外的O(n)的存储空间 |
快速 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | N | 分治 | 快排可能存在最坏情况,需要把枢轴值选取得尽量随机化来缓解最坏情况下的时间复杂度 |
2. 非比较排序算法
非比较排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
计数排序 | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(d*(n+k)) | O(n+k) | 稳定 |
其中,n是数组长度,k是桶长度,d是基数位数。
3. 稳定 vs 不稳定
4. Java中的排序
Arrays.sort
JDK 7 ~ 13中的排序实现
排序目标 | 条件 | 采用算法 |
---|---|---|
int[] long[] float[] double[] | size < 47 | 混合插入排序 (pair) |
size < 286 | 双基准点快排 | |
有序度低 | 双基准点快排 | |
有序度高 | 归并排序 | |
byte[] | size <= 29 | 插入排序 |
size > 29 | 计数排序 | |
char[] short[] | size < 47 | 插入排序 |
size < 286 | 双基准点快排 | |
有序度低 | 双基准点快排 | |
有序度高 | 归并排序 | |
size > 3200 | 计数排序 | |
Object[] | -Djava.util.Arrays.useLegacyMergeSort=true | 传统归并排序 |
TimSort(归并 + 插入) |
JDK 14~20中的排序实现
排序目标 | 条件 | 采用算法 |
---|---|---|
int[] long[] float[] double[] | size < 44 并位于最左侧 | 插入排序 |
size < 65 并不是最左侧 | 混合插入排序 (pin) | |
有序度低 | 双基准点快排 | |
递归次数超过 384 | 堆排序 | |
对于整个数组或非最左侧 size > 4096,有序度高 | 归并排序 | |
byte[] | size <= 64 | 插入排序 |
size > 64 | 计数排序 | |
char[] short[] | size < 44 | 插入排序 |
再大 | 双基准点快排 | |
递归次数超过 384 | 计数排序 | |
size > 1750 | 计数排序 | |
Object[] | -Djava.util.Arrays.useLegacyMergeSort=true | 传统归并排序 |
TimSort |
- 其中,TimSort是用归并 + 二分插入的混合排序算法
- 值得注意的是从Java 8 开始支持Arrays.parallelSort并行排序
- 根据最新的提交记录来看JDK 21可能会引入基数排序等优化
二、外部排序
1. 冒泡排序
要点:
- 每轮冒泡不断地比较相邻的两个元素,如果它们是逆序的,则交换它们的位置
- 下一轮冒泡,可以调整未排序的右边界,减少不必要的比较
以数组3、2、1的冒泡排序为例,第一轮冒泡
第二轮冒泡
未排序区域内就剩一个元素,结束
优化手段:每次循环时,若能确定更合适的右边界,则可以减少冒泡轮数(x记录最后一次交换的位置)
以数组3、2、1、4、5为例,第一轮结束后记录的x,即为右边界
递归实现
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class BubbleSort {
private static void bubble(int[] a) {
helper(a, a.length - 1);
}
private static void helper(int[] a, int j) {
if(j == 0) {
return;
}
int x = 0; // 记录最后一次交换的位置
for(int i = 0; i < j; i++) {
if(a[i] > a[i + 1]) {
int t = a[i];
a[i] = a[i + 1];
a[i + 1] = t;
x = i;
}
}
helper(a, x);
}
public static void main(String[] args) {
int[] a = {6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(a));
bubble(a);
System.out.println(Arrays.toString(a));
}
}
非递归实现
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class BubbleSort {
private static void bubble(int[] a) {
int j = a.length - 1;
while(true) {
int x = 0;
for(int i = 0; i < j; i++) {
if(a[i] > a[i + 1]) {
int t = a[i];
a[i] = a[i + 1];
a[i + 1] = t;
x = i;
}
}
j = x; // 更新右边界
if(j == 0) {
// 没有发生交换,已经有序
break;
}
}
}
public static void main(String[] args) {
int[] a = {6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(a));
bubble(a);
System.out.println(Arrays.toString(a));
}
}
2. 选择排序
要点:
- 每一轮选择,找出最大(最小)的元素,并把它交换到合适的位置
以下面的数组选择最大值为例
非递归实现
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class SelectionSort {
public static void sort(int[] a) {
// 1. 选择轮数 a.length - 1
// 2. 交换的索引位置 初始a.length - 1,每次递减
for(int right = a.length - 1; right >= 0; right--) {
int max = right;
for(int i = 0; i < right; i++) {
if(a[i] > a[max]) {
max = i;
}
}
if(max != right) {
int t = a[max];
a[max] = a[right];
a[right] = t;
}
}
}
public static void main(String[] args) {
int[] a = {6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
3. 堆排序
要点:
- 建立大顶堆
- 每次将堆顶元素(最大值)交换到末尾,调整堆顶元素,让它重新符合大顶堆特性
建堆
交换,下潜调整
代码实现
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class HeapSort {
public static void sort(int[] a) {
// 建堆
heapify(a, a.length);
// 交换,下潜调整
for (int right = a.length - 1; right > 0; right--) {
swap(a, 0, right);
down(a, 0, right);
}
}
// 建堆 O(n)
private static void heapify(int[] array, int size) {
// 如何找到最后这个非叶子节点 size / 2 - 1
for (int i = size / 2 - 1; i >= 0; i--) {
down(array, i, size);
}
}
// 下潜
// leetcode 上数组排序题目用堆排序求解,非递归实现比递归实现大约快 6ms
// 将 parent 索引处的元素下潜: 与两个孩子较大者交换, 直至没孩子或孩子没它大
private static void down(int[] array, int parent, int size) {
while (true) {
int left = parent * 2 + 1;
int right = left + 1;
int max = parent;
if (left < size && array[left] > array[max]) {
max = left;
}
if (right < size && array[right] > array[max]) {
max = right;
}
if (max == parent) { // 没找到更大的孩子
break;
}
swap(array, max, parent);
parent = max;
}
}
// 交换
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {2, 3, 1, 7, 6, 4, 5};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
4. 插入排序
要点:
将数组分为两部分 [0 .. low - 1] [low .. a.length - 1]
- 左边[0 .. low - 1]是已排序部分
- 右边[low .. a.length - 1]是未排序部分
每次从未排序区域取出 low 位置的元素,插入到已排序区域
例如,
递归实现
package com.itheima.algorithms.sort;
import org.checkerframework.checker.units.qual.A;
import java.util.Arrays;
public class InsertionSort {
private static void insertion(int[] a, int low, int high) {
if(low > high) {
return;
}
int i = low - 1;
int t = a[low];
while(i >= 0 && a[i] > t) { // 没有找到插入位置
a[i + 1] = a[i]; // 空出插入位置
i--;
}
if(i + 1 != low) {
a[i + 1] = t;
}
insertion(a, low + 1, high);
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
System.out.println(Arrays.toString(a));
insertion(a, 1, a.length - 1);
System.out.println(Arrays.toString(a));
}
}
非递归实现
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class InsertionSort {
public static void sort(int[] a) {
for(int low = 1; low < a.length; low++) {
// 将low位置的元素插入到[0 .. low - 1]的已排序区域
int t = a[low];
int i = low - 1;
while(i >= 0 && t < a[i]) {
a[i + 1] = a[i];
i--;
}
if(i != low - 1) {
a[i + 1] = t;
}
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
5. 希尔排序
要点:
- 简单的说,就是分组实现插入,每组元素间隙称为gap
- 每轮排序后gap逐渐变小,直至gap为1完成排序
- 对插入排序的优化,让元素更快速地交换到最终位置
- gap初始时通常取数组长度的一半,逐渐减小为之前的一半
下图演示了gap = 4,gap =2,gap = 1的三轮排序前后对比
代码
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class MergeSortTopDown {
/**
* 合并有序数组
* @param a1
*/
public static void sort(int[] a1) {
int[] a2 = new int[a1.length];
split(a1, 0, a1.length - 1, a2);
}
/**
* 划分数据
* @param a1
* @param left
* @param right
*/
private static void split(int[] a1, int left, int right, int[] a2) {
// 2. 治
if(left == right) {
return;
}
// 1. 分
int m = (left + right) >>> 1;
split(a1, left, m, a2);
split(a1, m + 1, right, a2);
// 3. 合
merge(a1, left, m, m + 1, right, a2);
System.arraycopy(a2, left, a1, left, right - left + 1);
}
/**
*
* @param a1 原始数组
* @param i 第一个有序范围
* @param iEnd
* @param j 第二个有序范围
* @param jEnd
* @param a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while(i <= iEnd && j <= jEnd) {
if(a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
// 第二个有序数组的剩余元素
if(i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd -j + 1);
}
// 第一个有序数组的剩余元素
if(j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
6. 归并排序
6.1 递归实现(自上而下)
要点:
- 分 - 每次从中间切一刀,处理的数据少一半
- 治 - 当数据仅剩一个时可以认为有序
- 合 - 两个有序的结果,可以进行合并排序
代码
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class MergeSort {
/**
* 合并有序数组
* @param a1
*/
public static void sort(int[] a1) {
int[] a2 = new int[a1.length];
split(a1, 0, a1.length - 1, a2);
}
/**
* 划分数据
* @param a1
* @param left
* @param right
*/
private static void split(int[] a1, int left, int right, int[] a2) {
// 2. 治
if(left == right) {
return;
}
// 1. 分
int m = (left + right) >>> 1;
split(a1, left, m, a2);
split(a1, m + 1, right, a2);
// 3. 合
merge(a1, left, m, m + 1, right, a2);
System.arraycopy(a2, left, a1, left, right - left + 1);
}
/**
*
* @param a1 原始数组
* @param i 第一个有序范围
* @param iEnd
* @param j 第二个有序范围
* @param jEnd
* @param a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while(i <= iEnd && j <= jEnd) {
if(a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
// 第二个有序数组的剩余元素
if(i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd -j + 1);
}
// 第一个有序数组的剩余元素
if(j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
6.2 时间复杂度
两个长度为m和n的链表合并,时间复杂度是m + n
归并,时间复杂度:f(n) = 2f(n / 2) + n, f(1) = c,等价解 f(n) = nlog_2(n) + cn
6.3 非递归排序
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class MergeSortBottomUp {
/**
*
* @param a1 原始数组
* @param i 第一个有序范围
* @param iEnd
* @param j 第二个有序范围
* @param jEnd
* @param a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while(i <= iEnd && j <= jEnd) {
if(a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
// 第二个有序数组的剩余元素
if(i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd -j + 1);
}
// 第一个有序数组的剩余元素
if(j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void sort(int[] a1) {
int n= a1.length;
int[] a2 = new int[n];
// width代表有序区间的宽度,取值依次为1、2、4、...
for(int width = 1; width < n; width *= 2) {
// [left, right]分别代表待合并区间的左右边界
for (int left = 0; left < n ; left += 2 * width) {
int right = Math.min(left + 2 * width - 1, n - 1);
// System.out.printf("width %d [%d, %d] %n", width, left, right);
int mid = Math.min(left + width - 1, n - 1);
merge(a1, left, mid, mid + 1, right, a2);
}
// 从a2复制到a1
System.arraycopy(a2, 0, a1,0, n);
}
}
public static void main(String[] args) {
int[] a = {8, 3, 6, 2, 5, 7, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
7. 归并 + 插入
基本思路:
1. 选择阈值:设定一个阈值(如,32等),当子数组的大小小于该阈值时,使用插入排序进行排序;否则,使用归并排序。
2. 实现过程:
- 在归并排序的分解阶段,将数组分割为较小的子数组
- 当子数组的大小达到设定的阈值时,停止递归,使用插入排序对该子数组进行排序
- 在较大的子数组上继续使用归并排序
3. 优势:
- 小规模数据使用插入排序速度更快
- 归并排序保证了整体的O(n log n)性能,但通过在小规模时切换到插入排序可以减少常数因子,提升性能。
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class MergeInsertionSort {
/**
* 插入排序
* @param a
* @param left
* @param right
*/
public static void insertionSort(int[] a, int left, int right) {
for(int low = left + 1; low <= right; low++) {
// 将low位置的元素插入到[0 .. low - 1]的已排序区域
int t = a[low];
int i = low - 1;
while(i >= left && t < a[i]) {
a[i + 1] = a[i];
i--;
}
if(i != low - 1) {
a[i + 1] = t;
}
}
}
/**
* 归并排序
* @param a1
*/
public static void sort(int[] a1) {
int[] a2 = new int[a1.length];
split(a1, 0, a1.length - 1, a2);
}
/**
* 划分数据
* @param a1
* @param left
* @param right
*/
private static void split(int[] a1, int left, int right, int[] a2) {
// 2. 治
// 当子数组的大小小于阈值时,使用插入排序,否则使用归并排序
if(right - left <= 32) {
// 插入排序
insertionSort(a1, left, right);
return;
}
// 1. 分
int m = (left + right) >>> 1;
split(a1, left, m, a2);
split(a1, m + 1, right, a2);
// 3. 合
merge(a1, left, m, m + 1, right, a2);
System.arraycopy(a2, left, a1, left, right - left + 1);
}
/**
* 合并有序数组
* @param a1 原始数组
* @param i 第一个有序范围
* @param iEnd
* @param j 第二个有序范围
* @param jEnd
* @param a2 临时数组
*/
public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
int k = i;
while(i <= iEnd && j <= jEnd) {
if(a1[i] < a1[j]) {
a2[k] = a1[i];
i++;
} else {
a2[k] = a1[j];
j++;
}
k++;
}
// 第二个有序数组的剩余元素
if(i > iEnd) {
System.arraycopy(a1, j, a2, k, jEnd -j + 1);
}
// 第一个有序数组的剩余元素
if(j > jEnd) {
System.arraycopy(a1, i, a2, k, iEnd - i + 1);
}
}
public static void main(String[] args) {
int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
8. 快速排序
单边循环(lomuto分区)要点:
-
选择最右侧元素作为基准点
-
j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
-
交换时机:j 找到小的,且与 i 不相等
-
i 找到 >= 基准点元素后,不应自增
-
-
最后基准点与 i 交换,i 即为基准点最终索引
例:
i和j都从左边出发向右查找,i找到比基准点4大的5,j找到比基准点小的2,停下来交换
i找到了比基准点大的5,j找到比基准点小的3,停下来交换
j到达right处结束,right与i交换,一轮分区结束
代码:
package com.itheima.algorithms.sort;
import java.util.Arrays;
/**
* 单边循环快排(lomuto洛穆托分区方案)
* 核心思想:每轮找到一个基准点元素,把比他小的放在它左边,比它大的放在它右边,这称为分区
* 1. 选择最右边的元素作为基准点元素
* 2. j指针负责找到比基准点小的元素,一旦找到则将j位置的元素与i位置的元素交换
* 3. i指针指向大于基准点元素的左边界,也是每次交换的目标索引
* 4. 最后基准点与i位置的元素交换,i即为分区位置
*/
public class QuickSortLomuto {
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if(left < right) {
// 进行分区并获取基准元素的最终位置
int pivotIndex = partition(a, left, right);
// 递归排序基准元素左边的子数组
quick(a, left, pivotIndex - 1);
// 递归排序基准元素右边的子数组
quick(a, pivotIndex + 1, right);
}
}
/**
* 分区
* @param a
* @param left
* @param right
*/
private static int partition(int[] a, int left, int right) {
// 1. 选择最右边的元素作为基准点
int pivot = a[right];
// 2. 初始化i指针
int i = left;
for(int j = left; j < right; j++) {
// 3. j 找到比基准元素小的了,交换i和j位置的元素
if(a[j] < pivot) {
if(i != j) {
swap(a, i, j);
}
i++;
}
}
swap(a, i, right);
return i;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {2, 3, 1, 7, 6, 4, 5};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
双边循环要点:
- 选择最左侧元素作为基准点
- j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
- i从左到右
- j从右到左
- 最后基准点与i交互,i即为基准点最终索引
例:
选择最左侧的4作为基准点,i找到比基准点大的5停下来,j找到比基准点小的1停下来(包含等于),二者交互
i找到8,j找到3,二者交互;i找到7,j找到2,二者交互
i == j,则退出循环,基准点与i位置的元素交互
代码
package com.itheima.algorithms.sort;
import java.util.Arrays;
public class QuickSortHoare {
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (left >= right) {
return;
}
int p = partition(a, left, right);
quick(a, left, p - 1);
quick(a, p + 1, right);
}
/**
*
* @param a
* @param left
* @param right
* @return
*/
private static int partition(int[] a, int left, int right) {
// i找比基准点大的,j找比基准点小的
int i = left, j = right;
int pivot = a[left];
while(i < j) {
// 先右侧扫描
while(i < j && a[j] > pivot) {
j--;
}
// 后左侧扫描(顺序不能调换)
while(i < j && pivot >= a[i]) {
i++;
}
// 一旦找到,二者进行交换
swap(a, i, j);
}
// 最后基准点与i交换,i即为基准点最终索引
swap(a, left, i);
return i;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {2, 3, 1, 7, 6, 4, 5};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
8.1 随机基准点
使用随机数作为基准点,避免万一最大值或最小值作为基准点导致的分区不平衡
例
改进代码
// 随机基准点
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
// 将left与idx位置的元素交换
swap(a, idx, left);
8.2 处理重复值
如果重复值较多,则原来算法中分区效果也不好,如下图中左侧所示,需要想办法改为右侧的分区效果。
改进代码
package com.itheima.algorithms.sort;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class QuickSortHandleDuplicate {
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (left >= right) {
return;
}
int p = partition(a, left, right);
quick(a, left, p - 1);
quick(a, p + 1, right);
}
/**
* 循环内
* i从left + 1开始,从左向右找大的或相等的
* j从right开始,从右向左找小的或相等的
* 交换, i++ j--
* 循环外,j和基准点交换,j即为分区位置
* @param a
* @param left
* @param right
* @return
*/
private static int partition(int[] a, int left, int right) {
// 随机基准点
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
// 将left与idx位置的元素交换
swap(a, idx, left);
// 从left + 1开始
int i = left + 1, j = right;
int pivot = a[left];
while(i <= j) {
// i从左到右找大的或相等的
while(i <= j && a[i] < pivot) {
i++;
}
// j从右到左找小的或者相等的
while(i <= j && a[j] > pivot) {
j--;
}
if(i <= j) {
swap(a, i, j);;
i++;
j--;
}
}
// 最后基准点与i交换,i即为基准点最终索引
swap(a, j, left);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {2, 3, 1, 7, 6, 4, 5};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
8.3 快速排序 + 插入排序
package com.itheima.algorithms.sort;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class QuickSortHandleDuplicate {
/**
* 插入排序
* @param a
* @param left
* @param right
*/
public static void insertionSort(int[] a, int left, int right) {
for(int low = left + 1; low <= right; low++) {
// 将low位置的元素插入到[0 .. low - 1]的已排序区域
int t = a[low];
int i = low - 1;
while(i >= left && t < a[i]) {
a[i + 1] = a[i];
i--;
}
if(i != low - 1) {
a[i + 1] = t;
}
}
}
/**
* 快速排序
* @param a
*/
public static void sort(int[] a) {
quick(a, 0, a.length - 1);
}
private static void quick(int[] a, int left, int right) {
if (right - left <= 32) {
insertionSort(a, left, right);
return;
}
int p = partition(a, left, right);
quick(a, left, p - 1);
quick(a, p + 1, right);
}
/**
* 循环内
* i从left + 1开始,从左向右找大的或相等的
* j从right开始,从右向左找小的或相等的
* 交换, i++ j--
* 循环外,j和基准点交换,j即为分区位置
* @param a
* @param left
* @param right
* @return
*/
private static int partition(int[] a, int left, int right) {
// 随机基准点
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
// 将left与idx位置的元素交换
swap(a, idx, left);
// 从left + 1开始
int i = left + 1, j = right;
int pivot = a[left];
while(i <= j) {
// i从左到右找大的或相等的
while(i <= j && a[i] < pivot) {
i++;
}
// j从右到左找小的或者相等的
while(i <= j && a[j] > pivot) {
j--;
}
if(i <= j) {
swap(a, i, j);;
i++;
j--;
}
}
// 最后基准点与i交换,i即为基准点最终索引
swap(a, j, left);
return j;
}
private static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
public static void main(String[] args) {
int[] a = {2, 3, 1, 7, 6, 4, 5};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
}
性能对比
9. 计数排序
要点
- 1. 找到最大值,创建一个大小为最大值+1的count数组
- 2. count数组的索引对应原始数组的元素,用来统计该元素的出现次数
- 3. 遍历count数组,根据count数组的索引(即原始数组的元素)以及出现次数,生成排序后的内容
- count数组的索引是:已排序好的
- 前提:最大值不能太大
package com.itheima.algorithms.sort;
import java.util.Arrays;
/**
* 计数排序
*/
public class CountingSort {
/*
要点
1. 让原始数组的最小值映射到 count[0] 最大值映射到 count 最右侧
2. 原始数组元素 - 最小值 = count 索引
3. count 索引 + 最小值 = 原始数组元素
*/
public static void main(String[] args) {
int[] a = {5, 1, 1, 3, 0, -1};
System.out.println(Arrays.toString(a));
sort(a);
System.out.println(Arrays.toString(a));
}
// 2N + K n*log(n)
/*
{5, 1, 1, 3, 0, -1} 原始数组 a
[1 1 2 0 1 0 1 ] count
0 1 2 3 4 5 6
-1 0 1 3 5
*/
private static void sort(int[] a) {
int max = a[0];
int min = a[0];
for (int i = 1; i < a.length; i++) {
if(a[i] > max) {
max = a[i];
}
if(a[i] < min) {
min = a[i];
}
}
int[] count = new int[max - min + 1];
// v原始数组元素 - 最小值 = count索引
for (int v : a) {
count[v - min]++;
}
int k = 0;
for (int i = 0; i < count.length; i++) {
// i + min代表原始数组元素,count[i]出现次数
while(count[i] > 0) {
a[k++] = i + min;
count[i]--;
}
}
}
}
性能对比
针对byte[],因为数据范围已知,省去了求最大、最小值的过程,java中对char[]、short[]、byte[]的排序都可能采用counting排序。
public static void sort(byte[] a) {
int[] count = new int[256];
for (int i : a) {
// 记录每个字节值的出现次数
// i & 0xFF是为了将byte转换为int类型,这样可以有效地处理负值
// 因为字节的值范围是-128到127,而count数组的索引应该在0-255之间
count[i & 0xFF]++;
}
// 反向填充排序后的值
int k = a.length - 1;
for(int i = 128 + 256; k >= 0; ) {
// 找到一个计数大于0的字节值
while (count[--i & 0xFF] == 0);
int v = i & 0xFF; // 当前处理的字节值
int c = count[i & 0xFF]; // 获取当前字节值的计数
for(int j = 0; j < c; j++) {
a[k] = (byte) v; // 将字节值填充到结果数组的后面
k--;
}
}
}
稳定计数排序
/**
* 稳定版计数排序
* @param a
*/
public static void sort2(int[] a) {
int min = a[0];
int max = a[0];
for (int i : a) {
if(i > max) {
max = i;
} else if(i < min) {
min = i;
}
}
int[] count = new int[max - min + 1];
for (int i : a) {
count[i - min]++;
}
// 累加计数数组 如果count[i] = 5,则表示有5个元素会放在排序数组中i的位置之前
for (int i = 1; i < count.length; i++) {
count[i] = count[i] + count[i - 1];
}
int[] b = new int[a.length];
for(int i = a.length - 1; i >= 0; i--) {
// 找到当前元素在计数数组中的索引
int j = a[i] - min;
// 减少计数,表示该元素已经放置过了
count[j]--;
// 将元素放入结果数组b的正确位置
b[count[j]] = a[i];
}
System.arraycopy(b, 0, a, 0, a.length);
}
10. 桶排序
初步实现
package com.itheima.algorithms.sort;
import com.itheima.datastructure.Array.DynamicArray;
import java.util.Arrays;
public class BucketSort {
public static void main(String[] args) {
int[] ages = {20, 18, 28, 66, 25, 31, 67, 30}; // 假设年龄范围1~99
System.out.println(Arrays.toString(ages));
sort(ages);
System.out.println(Arrays.toString(ages));
}
/**
* 桶排序
* @param ages
*/
private static void sort(int[] ages) {
// 1. 准备桶
DynamicArray[] buckets = new DynamicArray[10];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new DynamicArray();
}
// 2. 放入年龄数据
for (int age : ages) {
buckets[age / 10].addLast(age);
}
int k = 0;
for (DynamicArray bucket : buckets) {
// 3. 对桶内元素进行排序
int[] array = bucket.stream().toArray();
InsertionSort.sort(array);
// 4. 把每个桶排序好的内容,依次放回原始数组
for (int i : array) {
ages[k++] = i;
}
}
}
}
通用
package com.itheima.algorithms.sort;
import com.itheima.datastructure.Array.DynamicArray;
import java.util.Arrays;
public class BucketSortGeneric {
public static void main(String[] args) {
int[] ages = {9, 0, 5, 1, 4, 7, 2, 8}; // 假设年龄范围1~99
System.out.println(Arrays.toString(ages));
sort(ages, 20);
System.out.println(Arrays.toString(ages));
}
/**
* 桶排序通用版 - 参考计数排序
* @param ages
* @param range
*/
private static void sort(int[] ages, int range) {
// 0. 找最大最小值
int max = ages[0];
int min = ages[0];
for (int i = 1; i < ages.length; i++) {
if (ages[i] > max) {
max = ages[i];
}
if (ages[i] < min) {
min = ages[i];
}
}
// 1. 准备桶 c
// (max - min) / range + 1 减少桶的个数
DynamicArray[] buckets = new DynamicArray[(max - min) / range + 1];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new DynamicArray();
}
// 2. 放入年龄数据
for (int age : ages) {
buckets[(age - min) / range].addLast(age);
}
int k = 0;
for (DynamicArray bucket : buckets) {
// 3. 对桶内元素进行排序
int[] array = bucket.stream().toArray();
InsertionSort.sort(array);
// 4. 把每个桶排序好的内容,依次放回原始数组
for (int i : array) {
ages[k++] = i;
}
}
}
}
11. 基数排序
基数排序(Radix Sort)的核心思路是按位排序,即将数字按位(从最低位到最高位或从最高位到最低位)逐位排序。基数排序通常适用于整数和字符串排序。以下是基数排序的基本步骤和思路:
1. 确定最大位数
- 首先,基数排序需要知道待排序的最大数字的位数,这可以通过遍历整个数组来完成。例如,对于数组中的最大数max,基数排序只需要处理max的位数。
2. 按位排序
- 基数排序通过多次(位数个数)使用一个稳定的排序算法(通常是计数排序)来对数字进行排序,以下是逐步的过程:
- 从最低位开始:首先对每个数字的最低位进行排序,得到的结果是基于该位的排序
- 接着是次低位:然后对于每个数字的次低位进行排序,虽然这个排序会受到前一次排序结果的影响,但依然是基于该新的位进行的
- 重复直到最高位:继续这个过程,直到处理到最高位
例如
代码:
package com.itheima.algorithms.sort;
import java.util.ArrayList;
public class RadixSort {
/**
* 基数排序
*
* @param a 待排序字符串数组
*/
public static void radixSort(String[] a) {
// 1. 找出最长字符串的长度
int maxLength = 0;
for (String s : a) {
if (s.length() > maxLength) {
maxLength = s.length();
}
}
// 2. 准备桶
ArrayList<String>[] buckets = new ArrayList[128];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<>();
}
// 3. 从字符串的低位到高位,进行多轮按位桶排序
for (int i = maxLength - 1; i >= 0; i--) {
// 根据某一个位的值放入到对应桶
for (String s : a) {
int num = Integer.parseInt(s);
if(s.length() < maxLength) {
// 处理字符串长度不等的情况
s = String.format("%0" + maxLength + "d", num);
}
buckets[s.charAt(i) - '0'].add(String.valueOf(num));
}
int k = 0;
for (ArrayList<String> bucket : buckets) {
// 4. 重新取出排好序的字符串,依次放回原始数组
for (String s : bucket) {
a[k++] = s;
}
bucket.clear();
}
}
}
public static void main(String[] args) {
/*String[] phoneNumbers = new String[10];
phoneNumbers[0] = "13812345678";
phoneNumbers[1] = "13912345678";
phoneNumbers[2] = "13612345678";
phoneNumbers[3] = "13712345678";
phoneNumbers[4] = "13512345678";
phoneNumbers[5] = "13412345678";
phoneNumbers[6] = "15012345678";
phoneNumbers[7] = "15112345678";
phoneNumbers[8] = "15212345678";
phoneNumbers[9] = "15712345678";*/
String[] phoneNumbers = new String[10];
phoneNumbers[0] = "138";
phoneNumbers[1] = "139";
phoneNumbers[2] = "1068";
phoneNumbers[3] = "137";
phoneNumbers[4] = "135";
phoneNumbers[5] = "14";
phoneNumbers[6] = "150";
phoneNumbers[7] = "151";
phoneNumbers[8] = "162";
phoneNumbers[9] = "57";
RadixSort.radixSort(phoneNumbers);
for (String phoneNumber : phoneNumbers) {
System.out.print(phoneNumber + " ");
}
}
}
三、习题
1. 数组的相对排序
给你两个数组,arr1
和 arr2
,arr2
中的元素各不相同,arr2
中的每个元素都出现在 arr1
中。
对 arr1
中的元素进行排序,使 arr1
中项的相对顺序和 arr2
中的相对顺序相同。未在 arr2
中出现过的元素需要按照升序放在 arr1
的末尾。
示例 1:
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6] 输出:[2,2,2,1,4,3,3,9,6,7,19]
示例 2:
输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6] 输出:[22,28,8,6,17,44]
提示:
1 <= arr1.length, arr2.length <= 1000
0 <= arr1[i], arr2[i] <= 1000
arr2
中的元素arr2[i]
各不相同arr2
中的每个元素arr2[i]
都出现在arr1
中
解法一:计数排序
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
// 1. 记录每个元素出现的次数
int[] count = new int[1001];
for (int i : arr1) {
count[i]++;
}
int[] result = new int[arr1.length];
int k = 0;
// 2. 如果元素有在arr2出现,添加到result中
for (int i : arr2) {
while (count[i] > 0) {
result[k++] = i;
count[i]--;
}
}
// 3. 处理不在arr2中的元素
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
result[k++] = i;
count[i]--;
}
}
return result;
}
}
2. 按出现频率排序
给你一个整数数组 nums
,请你将数组按照每个值的频率 升序 排序。如果有多个值的频率相同,请你按照数值本身将它们 降序 排序。
请你返回排序后的数组。
示例 1:
输入:nums = [1,1,2,2,2,3] 输出:[3,1,1,2,2,2] 解释:'3' 频率为 1,'1' 频率为 2,'2' 频率为 3 。
示例 2:
输入:nums = [2,3,1,3,2] 输出:[1,3,3,2,2] 解释:'2' 和 '3' 频率都为 2 ,所以它们之间按照数值本身降序排序。
示例 3:
输入:nums = [-1,1,-6,4,5,-6,1,4,1] 输出:[5,-1,4,4,-6,-6,1,1,1]
提示:
1 <= nums.length <= 100
-100 <= nums[i] <= 100
解法一:计数排序。执行耗时8ms
class Solution {
public int[] frequencySort(int[] nums) {
// 1. 记录每个元素出现的次数
int[] count = new int[201];
for (int num : nums) {
// 加100是为了处理num为负数的情况
count[num + 100]++;
}
return Arrays.stream(nums).boxed().sorted((a, b) -> {
int fa = count[a + 100];
int fb = count[b + 100];
if(fa == fb) {
// 如果频率相同,按照数值本身将它们降序排序
return Integer.compare(b, a);
} else {
// 按照每个值的频率升序排序
return fa - fb;
}
}).mapToInt(Integer::intValue).toArray();
}
}
解法二:计数排序 + HashMap。执行耗时6ms
- 使用Map代替固定大小数组
- 减少计算频率的次数
- 使用list和Comparator
import java.util.*;
class Solution {
public int[] frequencySort(int[] nums) {
// 1. 记录每个元素出现的次数
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// 2. 使用 List 存储元素及其频率,方便排序
List<Map.Entry<Integer, Integer>> entryList = new ArrayList<>(count.entrySet());
// 3. 按照频率升序,然后数值降序排序
entryList.sort((a, b) -> {
if (a.getValue().equals(b.getValue())) {
return Integer.compare(b.getKey(), a.getKey()); // 按数值降序
}
return Integer.compare(a.getValue(), b.getValue()); // 按频率升序
});
// 4. 构造结果数组
int index = 0;
for (Map.Entry<Integer, Integer> entry : entryList) {
int frequency = entry.getValue();
for (int i = 0; i < frequency; i++) {
nums[index++] = entry.getKey();
}
}
return nums;
}
}
3. 最大间距
给定一个无序的数组 nums
,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回 0
。
您必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。(桶排序或基数排序)
示例 1:
输入: nums = [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
示例 2:
输入: nums = [10] 输出: 0 解释: 数组元素个数小于 2,因此返回 0。
提示:
1 <= nums.length <= 10^5
0 <= nums[i] <= 10^9
解法一:桶排序。超出内存限制
class Solution {
public int maximumGap(int[] nums) {
int n = nums.length;
if (n < 2) {
return 0;
}
sort(nums, 1);
int ret = 0;
for (int i = 1; i < n; i++) {
ret = Math.max(ret, nums[i] - nums[i - 1]);
}
return ret;
}
public static void sort(int[] nums, int range) {
int max = nums[0];
int min = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
if (nums[i] < min) {
min = nums[i];
}
}
ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList();
}
for (int num : nums) {
buckets[(num - min) / range].addLast(num);
}
int k = 0;
for (ArrayList<Integer> bucket : buckets) {
// 创建一个新的int数组
int[] array = new int[bucket.size()];
// 将ArrayList的元素复制到int数组
for (int i = 0; i < bucket.size(); i++) {
array[i] = bucket.get(i);
}
// 执行插入排序
insertionSort(array, 0, array.length - 1);
// 将排序后的结果写回nums数组
for (int a : array) {
nums[k++] = a;
}
}
}
public static void insertionSort(int[] nums, int left, int right) {
for (int low = left + 1; low <= right; low++) {
int t = nums[low];
int i = low - 1;
while (i >= left && t < nums[i]) {
nums[i + 1] = nums[i];
i--;
}
if (i != low - 1) {
nums[i + 1] = t;
}
}
}
}
解法二:桶排序 - 合理化桶的数量。执行耗时52ms
class Solution {
public int maximumGap(int[] nums) {
int n = nums.length;
if (n < 2) {
return 0;
}
sort(nums);
int ret = 0;
for (int i = 1; i < n; i++) {
ret = Math.max(ret, nums[i] - nums[i - 1]);
}
return ret;
}
public static void sort(int[] nums) {
int max = nums[0];
int min = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
if (nums[i] < min) {
min = nums[i];
}
}
// ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
int range = Math.max((max - min) / (nums.length - 1), 1);
ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList();
}
for (int num : nums) {
buckets[(num - min) / range].addLast(num);
}
int k = 0;
for (ArrayList<Integer> bucket : buckets) {
// 创建一个新的int数组
int[] array = new int[bucket.size()];
// 将ArrayList的元素复制到int数组
for (int i = 0; i < bucket.size(); i++) {
array[i] = bucket.get(i);
}
// 执行插入排序
insertionSort(array, 0, array.length - 1);
// 将排序后的结果写回nums数组
for (int a : array) {
nums[k++] = a;
}
}
}
public static void insertionSort(int[] nums, int left, int right) {
for (int low = left + 1; low <= right; low++) {
int t = nums[low];
int i = low - 1;
while (i >= left && t < nums[i]) {
nums[i + 1] = nums[i];
i--;
}
if (i != low - 1) {
nums[i + 1] = t;
}
}
}
}
解法三:桶排序 - 只保留桶内最大最小值。执行耗时13ms
class Solution {
static class Pair {
int max = 0;
int min = 1000_000_000;
public void add(int v) {
max = Math.max(max, v);
min = Math.min(min, v);
}
@Override
public String toString() {
return "[" + min + "," + max + "]";
}
}
public int maximumGap(int[] nums) {
// 1. 处理特殊情况
if (nums.length < 2) {
return 0;
}
// 2. 桶排序
// 桶个数 (max - min) / range + 1 期望桶个数 nums.length + 1
// range = (max - min) / nums.length
int max = nums[0];
int min = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
if (nums[i] < min) {
min = nums[i];
}
}
if (max == min) {
return 0;
}
int range = Math.max(1, (max - min) / nums.length);
int size = (max - min) / range + 1;
Pair[] buckets = new Pair[size];
// 2. 放入数据
for (int num : nums) {
int idx = (num - min) / range;
if (buckets[idx] == null) {
buckets[idx] = new Pair();
}
buckets[idx].add(num);
}
// 3. 寻找最大差值
int r = 0;
int lastMax = buckets[0].max;
for (int i = 1; i < buckets.length; i++) {
Pair pair = buckets[i];
if (pair != null) {
r = Math.max(r, pair.min - lastMax);
lastMax = pair.max;
}
}
return r;
}
}
解法四:基数排序。执行耗时68ms
public int maximumGap(int[] nums) {
// 0. 数组长度小于2,直接返回0
if(nums.length < 2) {
return 0;
}
// 1. 计算数组的最大值
int max = nums[0];
for (int num : nums) {
max = Math.max(num, max);
}
// 2. 准备10个桶
ArrayList<Integer>[] buckets = new ArrayList[10];
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<>();
}
// 3. 没超过最大值
long exp = 1;
while(max >= exp) {
for (int num : nums) {
buckets[(num / (int) exp % 10)].add(num);
}
int k = 0;
for (ArrayList<Integer> bucket : buckets) {
for (Integer i : bucket) {
nums[k++] = i;
}
bucket.clear();
}
exp *= 10;
}
// 4. 求最大间距
int r = 0;
for (int i = 1; i < nums.length; i++) {
r = Math.max(r, nums[i] - nums[i - 1]);
}
return r;
}
4. 排序数组
给你一个整数数组 nums
,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1] 输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0] 输出:[0,0,1,1,2,5]
提示:
1 <= nums.length <= 5 * 10^4
-5 * 10^4 <= nums[i] <= 5 * 10^4
解法一:单边快排(随机轴值)。执行耗时1918ms
class Solution {
public static int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
private static void quickSort(int[] nums, int left, int right) {
if (left < right) {
int pivot = partition(nums, left, right);
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
}
private static int partition(int[] nums, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(nums, idx, right);
int pivot = nums[right];
int i = left;
for (int j = left; j < right; j++) {
if (nums[j] < pivot) {
if (i != j) {
swap(nums, i, j);
}
i++;
}
}
swap(nums, i, right);
return i;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
解法二:双边快排(随机)。执行耗时1643ms
class Solution {
public static int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
private static void quickSort(int[] nums, int left, int right) {
if (left < right) {
int pivot = partition(nums, left, right);
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
}
private static int partition(int[] nums, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(nums, idx, left);
int pivot = nums[left];
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] > pivot) {
j--;
}
while (i < j && nums[i] <= pivot) {
i++;
}
swap(nums, i, j);
}
swap(nums, left, i);
return i;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
解法三:快排(随机 + 重复)。执行耗时26ms
class Solution {
public static int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
private static void quickSort(int[] nums, int left, int right) {
if (left < right) {
int pivot = partition(nums, left, right);
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
}
private static int partition(int[] nums, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(nums, idx, left);
int pivot = nums[left];
int i = left + 1, j = right;
while (i <= j) {
while (i <= j && nums[i] < pivot) {
i++;
}
while (i <= j && nums[j] > pivot) {
j--;
}
if (i <= j) {
swap(nums, i, j);
i++;
j--;
}
}
swap(nums, j, left);
return j;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
解法四:快排(随机 + 重复 + 插入)。执行耗时18ms
class Solution {
public static int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
public static void insertionSort(int[] nums, int left, int right) {
for (int low = left + 1; low <= right; low++) {
int t = nums[low];
int i = low - 1;
while (i >= left && t < nums[i]) {
nums[i + 1] = nums[i];
i--;
}
if (i != low - 1) {
nums[i + 1] = t;
}
}
}
private static void quickSort(int[] nums, int left, int right) {
if (right - left <= 32) {
insertionSort(nums, left, right);
return;
}
int pivot = partition(nums, left, right);
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
private static int partition(int[] nums, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(nums, idx, left);
int pivot = nums[left];
int i = left + 1, j = right;
while (i <= j) {
while (i <= j && nums[i] < pivot) {
i++;
}
while (i <= j && nums[j] > pivot) {
j--;
}
if (i <= j) {
swap(nums, i, j);
i++;
j--;
}
}
swap(nums, j, left);
return j;
}
public static void swap(int[] a, int i, int j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
解法五:计数排序。执行耗时3ms
class Solution {
public static int[] sortArray(int[] nums) {
int max = nums[0];
int min = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
if (nums[i] < min) {
min = nums[i];
}
}
int[] count = new int[max - min + 1];
for (int num : nums) {
count[num - min]++;
}
int k = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
nums[k++] = i + min;
count[i]--;
}
}
return nums;
}
}
5. 排序链表
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
示例 1:
输入:head = [4,2,1,3] 输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0] 输出:[-1,0,3,4,5]
示例 3:
输入:head = [] 输出:[]
提示:
- 链表中节点的数目在范围
[0, 5 * 10^4]
内 -10^5 <= Node.val <= 10^5
进阶:你可以在 O(n log n)
时间复杂度和常数级空间复杂度下,对链表进行排序吗?
解法一:把链表中的值放入数组当中进行排序,再根据排序后的值创建新的链表返回
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
List<Integer> values = new ArrayList<>();
ListNode current = head;
while (current != null) {
values.add(current.val);
current = current.next;
}
Collections.sort(values);
ListNode newHead = new ListNode(values.get(0));
ListNode temp = newHead;
for (int i = 1; i < values.size(); i++) {
temp.next = new ListNode(values.get(i));
temp = temp.next;
}
return newHead;
}
}
解法二:快慢指针 + 合并有序链表(归并)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode mid = findMiddle(head);
ListNode secondHalf = mid.next;
mid.next = null;
ListNode sortedFirstHalf = sortList(head);
ListNode sortedSecondHalf = sortList(secondHalf);
return merge(sortedFirstHalf, sortedSecondHalf);
}
private ListNode merge(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
current.next = l1 != null ? l1 : l2;
return dummy.next;
}
private ListNode findMiddle(ListNode head) {
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
6. 数组的相对顺序
给你两个数组,arr1
和 arr2
,arr2
中的元素各不相同,arr2
中的每个元素都出现在 arr1
中。
对 arr1
中的元素进行排序,使 arr1
中项的相对顺序和 arr2
中的相对顺序相同。未在 arr2
中出现过的元素需要按照升序放在 arr1
的末尾。
示例 1:
输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6] 输出:[2,2,2,1,4,3,3,9,6,7,19]
示例 2:
输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6] 输出:[22,28,8,6,17,44]
提示:
1 <= arr1.length, arr2.length <= 1000
0 <= arr1[i], arr2[i] <= 1000
arr2
中的元素arr2[i]
各不相同arr2
中的每个元素arr2[i]
都出现在arr1
中
解法一:
class Solution {
public int[] relativeSortArray(int[] arr1, int[] arr2) {
// 1. 记录每个元素出现的次数
int[] count = new int[1001];
for (int i : arr1) {
count[i]++;
}
int[] result = new int[arr1.length];
int k = 0;
// 2. 如果元素有在arr2出现,添加到result中
for (int i : arr2) {
while (count[i] > 0) {
result[k++] = i;
count[i]--;
}
}
// 3. 处理不在arr2中的元素
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
result[k++] = i;
count[i]--;
}
}
return result;
}
}
7. 按照频率将数组升序排序
给你一个整数数组 nums
,请你将数组按照每个值的频率 升序 排序。如果有多个值的频率相同,请你按照数值本身将它们 降序 排序。
请你返回排序后的数组。
示例 1:
输入:nums = [1,1,2,2,2,3] 输出:[3,1,1,2,2,2] 解释:'3' 频率为 1,'1' 频率为 2,'2' 频率为 3 。
示例 2:
输入:nums = [2,3,1,3,2] 输出:[1,3,3,2,2] 解释:'2' 和 '3' 频率都为 2 ,所以它们之间按照数值本身降序排序。
示例 3:
输入:nums = [-1,1,-6,4,5,-6,1,4,1] 输出:[5,-1,4,4,-6,-6,1,1,1]
提示:
1 <= nums.length <= 100
-100 <= nums[i] <= 100
解法一:计数排序
class Solution {
public int[] frequencySort(int[] nums) {
// 1. 记录每个元素出现的次数
int[] count = new int[201];
for (int num : nums) {
// 加100是为了处理num为负数的情况
count[num + 100]++;
}
return Arrays.stream(nums).boxed().sorted((a, b) -> {
int fa = count[a + 100];
int fb = count[b + 100];
if(fa == fb) {
// 如果频率相同,按照数值本身将它们降序排序
return Integer.compare(b, a);
} else {
// 按照每个值的频率升序排序
return fa - fb;
}
}).mapToInt(Integer::intValue).toArray();
}
}
优化:
import java.util.*;
class Solution {
public int[] frequencySort(int[] nums) {
// 1. 记录每个元素出现的次数
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// 2. 使用 List 存储元素及其频率,方便排序
List<Map.Entry<Integer, Integer>> entryList = new ArrayList<>(count.entrySet());
// 3. 按照频率升序,然后数值降序排序
entryList.sort((a, b) -> {
if (a.getValue().equals(b.getValue())) {
return Integer.compare(b.getKey(), a.getKey()); // 按数值降序
}
return Integer.compare(a.getValue(), b.getValue()); // 按频率升序
});
// 4. 构造结果数组
int index = 0;
for (Map.Entry<Integer, Integer> entry : entryList) {
int frequency = entry.getValue();
for (int i = 0; i < frequency; i++) {
nums[index++] = entry.getKey();
}
}
return nums;
}
}
8. 计算右侧小于当前元素的个数
给你一个整数数组 nums
,按要求返回一个新数组 counts
。数组 counts
有该性质: counts[i]
的值是 nums[i]
右侧小于 nums[i]
的元素的数量。
示例 1:
输入:nums = [5,2,6,1] 输出:[2,1,1,0] 解释: 5 的右侧有 2 个更小的元素 (2 和 1) 2 的右侧仅有 1 个更小的元素 (1) 6 的右侧有 1 个更小的元素 (1) 1 的右侧有 0 个更小的元素
示例 2:
输入:nums = [-1] 输出:[0]
示例 3:
输入:nums = [-1,-1] 输出:[0,0]
提示:
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
解法一:暴力解。超出时间限制
class Solution {
public List<Integer> countSmaller(int[] nums) {
List<Integer> counts = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n; i++) {
int count = 0;
for (int j = n - 1; j >= i; j--) {
if (nums[j] < nums[i]) {
count++;
}
}
counts.add(count);
}
return counts;
}
}
解法二:
class Solution {
public List<Integer> countSmaller(int[] nums) {
int n = nums.length;
List<Integer> res = new ArrayList<>(n);
int[] index = new int[n];
int[] helper = new int[n];
int[] count = new int[n];
for (int i = 0; i < n; i++) {
index[i] = i;
}
merge(nums, index, helper, count, 0, nums.length - 1);
for (int i = 0; i < n; i++) {
res.add(i, count[i]);
}
return res;
}
private void merge(int[] nums, int[] index, int[] helper, int[] count, int left, int right) {
if (left == right || left > right)
return;
int mid = (left + right) >> 1;
if (left < mid) {
merge(nums, index, helper, count, left, mid);
}
if (mid + 1 < right) {
merge(nums, index, helper, count, mid + 1, right);
}
int i = left, j = mid + 1;
int hi = left;
while (i <= mid && j <= right) {
if (nums[index[i]] <= nums[index[j]]) {
helper[hi++] = index[j++];
} else {
count[index[i]] += right - j + 1;
helper[hi++] = index[i++];
}
}
while (i <= mid) {
helper[hi++] = index[i++];
}
while (j <= right) {
helper[hi++] = index[j++];
}
for (int k = left; k <= right; k++) {
index[k] = helper[k];
}
}
}
9. 前k个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2 输出: [1,2]
示例 2:
输入: nums = [1], k = 1 输出: [1]
提示:
1 <= nums.length <= 10^5
k
的取值范围是[1, 数组中不相同的元素的个数]
- 题目数据保证答案唯一,换句话说,数组中前
k
个高频元素的集合是唯一的
进阶:你所设计算法的时间复杂度 必须 优于 O(n log n)
,其中 n
是数组大小。
解法一:HashMap + 小根堆
(图片来自. - 力扣(LeetCode))
public class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer> count = new HashMap<>();
// 1. 使用HashMap统计每个元素出现的次数
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// 2. 使用一个小根堆来维护前k个高频元素
PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>(k,
(a, b) -> a.getValue() - b.getValue());
for (Map.Entry<Integer, Integer> entry : count.entrySet()) {
minHeap.offer(entry);
// 如果堆的大小超过k,移除频率最低的元素,最后剩下的k个元素即为所求
if (minHeap.size() > k) {
minHeap.poll();
}
}
// 3. 获取结果
int i = 0;
int[] result = new int[k];
while (!minHeap.isEmpty()) {
result[i++] = minHeap.poll().getKey();
}
return result;
}
}
解法二:HashMap + 桶排序
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer> count = new HashMap<>();
List<Integer> res = new ArrayList<>();
// 1. 使用HashMap统计每个元素出现的次数
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// 2. 将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
List<Integer>[] list = new List[nums.length + 1];
for (Integer key : count.keySet()) {
int i = count.get(key);
if (list[i] == null) {
list[i] = new ArrayList<>();
}
list[i].add(key);
}
// 3. 倒序遍历数组获取出现频率从大到小的排列
for (int i = list.length - 1; i >= 0 && res.size() < k; i--) {
if (list[i] == null) {
continue;
}
res.addAll(list[i]);
}
return res.stream().mapToInt(Integer::intValue).toArray();
}
}
10. 颜色分类
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0] 输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1] 输出:[0,1,2]
提示:
n == nums.length
1 <= n <= 300
nums[i]
为0
、1
或2
进阶:
- 你能想出一个仅使用常数空间的一趟扫描算法吗?
解法一:维护三个指针,分别指向当前分析的元素、红色的末尾边界和蓝色的起始边界。
class Solution {
public void sortColors(int[] nums) {
int zeroIndex = 0;
int twoIndex = nums.length - 1;
int current = 0;
while (current <= twoIndex) {
if (nums[current] == 0) {
// 红色
swap(nums, current, zeroIndex);
zeroIndex++;
current++;
} else if (nums[current] == 2) {
// 蓝色
swap(nums, current, twoIndex);
twoIndex--;
} else {
// 白色
current++;
}
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
11. 数组中的第k个最大元素
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2 输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4 输出: 4
提示:
1 <= k <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
解法一:自定义堆小根堆。执行耗时21ms
class Solution {
static class MinHeap {
int[] array;
int size;
public MinHeap(int capacity) {
array = new int[capacity];
}
public int peek() {
return array[0];
}
public boolean offer(int offered) {
if (size == array.length) {
return false;
}
up(offered);
size++;
return true;
}
public void replace(int replaced) {
array[0] = replaced;
down(0);
}
private void up(int offered) {
int child = size;
while (child > 0) {
int parent = (child - 1) >> 1;
if (offered < array[parent]) {
array[child] = array[parent];
} else {
break;
}
child = parent;
}
array[child] = offered;
}
private void down(int parent) {
int left = (parent << 1) + 1;
int right = left + 1;
int min = parent;
if (left < size && array[left] < array[min]) {
min = left;
}
if (right < size && array[right] < array[min]) {
min = right;
}
if (min != parent) {
swap(min, parent);
down(min);
}
}
// 交换两个索引处的元素
private void swap(int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
}
public int findKthLargest(int[] nums, int k) {
MinHeap heap = new MinHeap(k);
// 先将k个元素入堆
for (int i = 0; i < k; i++) {
heap.offer(nums[i]);
}
// 将剩余的元素与堆顶元素比较,如果比堆顶元素大则替换。比较完成后,堆顶元素即为第k大的元素(小根堆)
for (int i = k; i < nums.length; i++) {
if (nums[i] > heap.peek()) {
heap.replace(nums[i]);
}
}
return heap.peek();
}
}
解法二:堆排序(优先队列)。执行耗时63ms
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int num : nums) {
minHeap.offer(num);
if(minHeap.size() > k) {
minHeap.poll();
}
}
return minHeap.peek();
}
}
解法三:快排(随机 + 处理重复 + 插入)。执行耗时28ms
class Solution {
public int findKthLargest(int[] nums, int k) {
quickSort(nums, 0, nums.length - 1);
return nums[nums.length - k];
}
private static void quickSort(int[] nums, int left, int right) {
if (right - left <= 32) {
insertionSort(nums, left, right);
return;
}
int pivot = partition(nums, left, right);
;
quickSort(nums, left, pivot - 1);
quickSort(nums, pivot + 1, right);
}
private static int partition(int[] nums, int left, int right) {
int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
swap(nums, idx, left);
int i = left + 1, j = right;
int pivot = nums[left];
while (i <= j) {
while (i <= j && nums[i] < pivot) {
i++;
}
while (i <= j && nums[j] > pivot) {
j--;
}
if (i <= j) {
swap(nums, i, j);
i++;
j--;
}
}
swap(nums, j, left);
return j;
}
private static void insertionSort(int[] nums, int left, int right) {
for (int low = left + 1; low <= right; low++) {
int t = nums[low];
int i = low - 1;
while (i >= left && t < nums[i]) {
nums[i + 1] = nums[i];
i--;
}
if (i != low - 1) {
nums[i + 1] = t;
}
}
}
private static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
12. 翻转对
给定一个数组 nums
,如果 i < j
且 nums[i] > 2*nums[j]
我们就将 (i, j)
称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
示例 1:
输入: [1,3,2,3,1] 输出: 2
示例 2:
输入: [2,4,3,5,1] 输出: 3
注意:
- 给定数组的长度不会超过
50000
。 - 输入数组中的所有数字都在32位整数的表示范围内。
解法一:归并排序
class Solution {
public int reversePairs(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
return mergeSort(nums, 0, nums.length - 1);
}
private int mergeSort(int[] nums, int left, int right) {
if (left >= right) {
return 0;
}
int mid = left + (right - left) / 2;
// 统计左半部分和右半部分的翻转对
int count = mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right);
// 统计跨越两部分的翻转对数量
count += countImportantPairs(nums, left, mid, right);
// 合并两个部分
merge(nums, left, mid, right);
return count;
}
private void merge(int[] nums, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
while (i <= mid) {
temp[k++] = nums[i++];
}
while (j <= right) {
temp[k++] = nums[j++];
}
System.arraycopy(temp, 0, nums, left, temp.length);
}
private int countImportantPairs(int[] nums, int left, int mid, int right) {
int count = 0;
int j = mid + 1;
// 使用双指针法,j 代表右边数组的指针
for (int i = left; i <= mid; i++) {
// 转换为long处理超出Integer.MAX_VALUE -> 2147483647的情况
while (j <= right && (long) nums[i] > 2 * (long) nums[j]) {
j++;
}
// 当前 j 的位置表示 (mid + 1) 到 j - 1 都和 nums[i] 满足条件
count += (j - (mid + 1));
}
return count;
}
}
13. 通过删除字母匹配到字典里最长单词
给你一个字符串 s
和一个字符串数组 dictionary
,找出并返回 dictionary
中最长的字符串,该字符串可以通过删除 s
中的某些字符得到。
如果答案不止一个,返回长度最长且字母序最小的字符串。如果答案不存在,则返回空字符串。
示例 1:
输入:s = "abpcplea", dictionary = ["ale","apple","monkey","plea"] 输出:"apple"
示例 2:
输入:s = "abpcplea", dictionary = ["a","b","c"] 输出:"a"
提示:
1 <= s.length <= 1000
1 <= dictionary.length <= 1000
1 <= dictionary[i].length <= 1000
s
和dictionary[i]
仅由小写英文字母组成
解法一:
1. 检查子序列:通过遍历字典中的每个字符串,检查它是否是s中的一个子序列
2. 比较长度和字典序:对于所有符合条件的字符串,选择最长的一个。如果有多个长度相同的字符串,则选择字典序较小的那个
public String findLongestWord(String s, List<String> dictionary) {
String result = "";
for (String word : dictionary) {
if(isSubsequence(word, s)) {
// 比较长度和字典序
if(word.length() > result.length() || (word.length() == result.length() && word.compareTo(result) < 0)) {
result = word;
}
}
}
return result;
}
/**
* 检查字典中的每个字符串是否为s的一个子序列
* @param word
* @param s
* @return
*/
private boolean isSubsequence(String word, String s) {
int wIndex = 0, sIndex = 0;
while(wIndex < word.length() && sIndex < s.length()) {
if(word.charAt(wIndex) == s.charAt(sIndex)) {
wIndex++;
}
sIndex++;
}
return wIndex == word.length();
}
14. 有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11] 输出:[4,9,9,49,121]
提示:
1 <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
nums
已按 非递减顺序 排序
进阶:
- 请你设计时间复杂度为
O(n)
的算法解决本问题
解法一:双指针法。大的元素添加到结果数组末尾
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] result = new int[n];
// 双指针法
int left = 0, right = n - 1;
int position = n - 1;
while (left <= right) {
int leftSquare = nums[left] * nums[left];
int rightSquare = nums[right] * nums[right];
// 大的添加到结果数组
if (leftSquare > rightSquare) {
result[position--] = leftSquare;
left++;
} else {
result[position--] = rightSquare;
right--;
}
}
return result;
}
}