当前位置:   article > 正文

数据结构与算法 - 排序算法

数据结构与算法 - 排序算法

一、概述

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,即为右边界

递归实现

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class BubbleSort {
  4. private static void bubble(int[] a) {
  5. helper(a, a.length - 1);
  6. }
  7. private static void helper(int[] a, int j) {
  8. if(j == 0) {
  9. return;
  10. }
  11. int x = 0; // 记录最后一次交换的位置
  12. for(int i = 0; i < j; i++) {
  13. if(a[i] > a[i + 1]) {
  14. int t = a[i];
  15. a[i] = a[i + 1];
  16. a[i + 1] = t;
  17. x = i;
  18. }
  19. }
  20. helper(a, x);
  21. }
  22. public static void main(String[] args) {
  23. int[] a = {6, 5, 4, 3, 2, 1};
  24. System.out.println(Arrays.toString(a));
  25. bubble(a);
  26. System.out.println(Arrays.toString(a));
  27. }
  28. }

非递归实现

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class BubbleSort {
  4. private static void bubble(int[] a) {
  5. int j = a.length - 1;
  6. while(true) {
  7. int x = 0;
  8. for(int i = 0; i < j; i++) {
  9. if(a[i] > a[i + 1]) {
  10. int t = a[i];
  11. a[i] = a[i + 1];
  12. a[i + 1] = t;
  13. x = i;
  14. }
  15. }
  16. j = x; // 更新右边界
  17. if(j == 0) {
  18. // 没有发生交换,已经有序
  19. break;
  20. }
  21. }
  22. }
  23. public static void main(String[] args) {
  24. int[] a = {6, 5, 4, 3, 2, 1};
  25. System.out.println(Arrays.toString(a));
  26. bubble(a);
  27. System.out.println(Arrays.toString(a));
  28. }
  29. }

2. 选择排序

要点:

  • 每一轮选择,找出最大(最小)的元素,并把它交换到合适的位置

以下面的数组选择最大值为例

非递归实现

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class SelectionSort {
  4. public static void sort(int[] a) {
  5. // 1. 选择轮数 a.length - 1
  6. // 2. 交换的索引位置 初始a.length - 1,每次递减
  7. for(int right = a.length - 1; right >= 0; right--) {
  8. int max = right;
  9. for(int i = 0; i < right; i++) {
  10. if(a[i] > a[max]) {
  11. max = i;
  12. }
  13. }
  14. if(max != right) {
  15. int t = a[max];
  16. a[max] = a[right];
  17. a[right] = t;
  18. }
  19. }
  20. }
  21. public static void main(String[] args) {
  22. int[] a = {6, 5, 4, 3, 2, 1};
  23. System.out.println(Arrays.toString(a));
  24. sort(a);
  25. System.out.println(Arrays.toString(a));
  26. }
  27. }

3. 堆排序

要点:

  • 建立大顶堆
  • 每次将堆顶元素(最大值)交换到末尾,调整堆顶元素,让它重新符合大顶堆特性

建堆

交换,下潜调整

代码实现

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class HeapSort {
  4. public static void sort(int[] a) {
  5. // 建堆
  6. heapify(a, a.length);
  7. // 交换,下潜调整
  8. for (int right = a.length - 1; right > 0; right--) {
  9. swap(a, 0, right);
  10. down(a, 0, right);
  11. }
  12. }
  13. // 建堆 O(n)
  14. private static void heapify(int[] array, int size) {
  15. // 如何找到最后这个非叶子节点 size / 2 - 1
  16. for (int i = size / 2 - 1; i >= 0; i--) {
  17. down(array, i, size);
  18. }
  19. }
  20. // 下潜
  21. // leetcode 上数组排序题目用堆排序求解,非递归实现比递归实现大约快 6ms
  22. // 将 parent 索引处的元素下潜: 与两个孩子较大者交换, 直至没孩子或孩子没它大
  23. private static void down(int[] array, int parent, int size) {
  24. while (true) {
  25. int left = parent * 2 + 1;
  26. int right = left + 1;
  27. int max = parent;
  28. if (left < size && array[left] > array[max]) {
  29. max = left;
  30. }
  31. if (right < size && array[right] > array[max]) {
  32. max = right;
  33. }
  34. if (max == parent) { // 没找到更大的孩子
  35. break;
  36. }
  37. swap(array, max, parent);
  38. parent = max;
  39. }
  40. }
  41. // 交换
  42. private static void swap(int[] a, int i, int j) {
  43. int t = a[i];
  44. a[i] = a[j];
  45. a[j] = t;
  46. }
  47. public static void main(String[] args) {
  48. int[] a = {2, 3, 1, 7, 6, 4, 5};
  49. System.out.println(Arrays.toString(a));
  50. sort(a);
  51. System.out.println(Arrays.toString(a));
  52. }
  53. }

4. 插入排序

要点:

将数组分为两部分 [0 .. low - 1] [low .. a.length - 1]

  • 左边[0 .. low - 1]是已排序部分
  • 右边[low .. a.length - 1]是未排序部分

每次从未排序区域取出 low 位置的元素,插入到已排序区域

例如,

递归实现

  1. package com.itheima.algorithms.sort;
  2. import org.checkerframework.checker.units.qual.A;
  3. import java.util.Arrays;
  4. public class InsertionSort {
  5. private static void insertion(int[] a, int low, int high) {
  6. if(low > high) {
  7. return;
  8. }
  9. int i = low - 1;
  10. int t = a[low];
  11. while(i >= 0 && a[i] > t) { // 没有找到插入位置
  12. a[i + 1] = a[i]; // 空出插入位置
  13. i--;
  14. }
  15. if(i + 1 != low) {
  16. a[i + 1] = t;
  17. }
  18. insertion(a, low + 1, high);
  19. }
  20. public static void main(String[] args) {
  21. int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
  22. System.out.println(Arrays.toString(a));
  23. insertion(a, 1, a.length - 1);
  24. System.out.println(Arrays.toString(a));
  25. }
  26. }

非递归实现

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class InsertionSort {
  4. public static void sort(int[] a) {
  5. for(int low = 1; low < a.length; low++) {
  6. // 将low位置的元素插入到[0 .. low - 1]的已排序区域
  7. int t = a[low];
  8. int i = low - 1;
  9. while(i >= 0 && t < a[i]) {
  10. a[i + 1] = a[i];
  11. i--;
  12. }
  13. if(i != low - 1) {
  14. a[i + 1] = t;
  15. }
  16. }
  17. }
  18. public static void main(String[] args) {
  19. int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
  20. System.out.println(Arrays.toString(a));
  21. sort(a);
  22. System.out.println(Arrays.toString(a));
  23. }
  24. }

5. 希尔排序

要点:

  • 简单的说,就是分组实现插入,每组元素间隙称为gap
  • 每轮排序后gap逐渐变小,直至gap为1完成排序
  • 对插入排序的优化,让元素更快速地交换到最终位置
  • gap初始时通常取数组长度的一半,逐渐减小为之前的一半

下图演示了gap = 4,gap =2,gap = 1的三轮排序前后对比

代码

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class MergeSortTopDown {
  4. /**
  5. * 合并有序数组
  6. * @param a1
  7. */
  8. public static void sort(int[] a1) {
  9. int[] a2 = new int[a1.length];
  10. split(a1, 0, a1.length - 1, a2);
  11. }
  12. /**
  13. * 划分数据
  14. * @param a1
  15. * @param left
  16. * @param right
  17. */
  18. private static void split(int[] a1, int left, int right, int[] a2) {
  19. // 2. 治
  20. if(left == right) {
  21. return;
  22. }
  23. // 1. 分
  24. int m = (left + right) >>> 1;
  25. split(a1, left, m, a2);
  26. split(a1, m + 1, right, a2);
  27. // 3. 合
  28. merge(a1, left, m, m + 1, right, a2);
  29. System.arraycopy(a2, left, a1, left, right - left + 1);
  30. }
  31. /**
  32. *
  33. * @param a1 原始数组
  34. * @param i 第一个有序范围
  35. * @param iEnd
  36. * @param j 第二个有序范围
  37. * @param jEnd
  38. * @param a2 临时数组
  39. */
  40. public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
  41. int k = i;
  42. while(i <= iEnd && j <= jEnd) {
  43. if(a1[i] < a1[j]) {
  44. a2[k] = a1[i];
  45. i++;
  46. } else {
  47. a2[k] = a1[j];
  48. j++;
  49. }
  50. k++;
  51. }
  52. // 第二个有序数组的剩余元素
  53. if(i > iEnd) {
  54. System.arraycopy(a1, j, a2, k, jEnd -j + 1);
  55. }
  56. // 第一个有序数组的剩余元素
  57. if(j > jEnd) {
  58. System.arraycopy(a1, i, a2, k, iEnd - i + 1);
  59. }
  60. }
  61. public static void main(String[] args) {
  62. int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
  63. System.out.println(Arrays.toString(a));
  64. sort(a);
  65. System.out.println(Arrays.toString(a));
  66. }
  67. }

6. 归并排序

6.1 递归实现(自上而下)

要点:

  • 分 - 每次从中间切一刀,处理的数据少一半
  • 治 - 当数据仅剩一个时可以认为有序
  • 合 - 两个有序的结果,可以进行合并排序

代码

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class MergeSort {
  4. /**
  5. * 合并有序数组
  6. * @param a1
  7. */
  8. public static void sort(int[] a1) {
  9. int[] a2 = new int[a1.length];
  10. split(a1, 0, a1.length - 1, a2);
  11. }
  12. /**
  13. * 划分数据
  14. * @param a1
  15. * @param left
  16. * @param right
  17. */
  18. private static void split(int[] a1, int left, int right, int[] a2) {
  19. // 2. 治
  20. if(left == right) {
  21. return;
  22. }
  23. // 1. 分
  24. int m = (left + right) >>> 1;
  25. split(a1, left, m, a2);
  26. split(a1, m + 1, right, a2);
  27. // 3. 合
  28. merge(a1, left, m, m + 1, right, a2);
  29. System.arraycopy(a2, left, a1, left, right - left + 1);
  30. }
  31. /**
  32. *
  33. * @param a1 原始数组
  34. * @param i 第一个有序范围
  35. * @param iEnd
  36. * @param j 第二个有序范围
  37. * @param jEnd
  38. * @param a2 临时数组
  39. */
  40. public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
  41. int k = i;
  42. while(i <= iEnd && j <= jEnd) {
  43. if(a1[i] < a1[j]) {
  44. a2[k] = a1[i];
  45. i++;
  46. } else {
  47. a2[k] = a1[j];
  48. j++;
  49. }
  50. k++;
  51. }
  52. // 第二个有序数组的剩余元素
  53. if(i > iEnd) {
  54. System.arraycopy(a1, j, a2, k, jEnd -j + 1);
  55. }
  56. // 第一个有序数组的剩余元素
  57. if(j > jEnd) {
  58. System.arraycopy(a1, i, a2, k, iEnd - i + 1);
  59. }
  60. }
  61. public static void main(String[] args) {
  62. int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
  63. System.out.println(Arrays.toString(a));
  64. sort(a);
  65. System.out.println(Arrays.toString(a));
  66. }
  67. }

6.2 时间复杂度

两个长度为m和n的链表合并,时间复杂度是m + n

归并,时间复杂度:f(n) = 2f(n / 2) + n, f(1) = c,等价解 f(n) = nlog_2(n) + cn

6.3 非递归排序

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class MergeSortBottomUp {
  4. /**
  5. *
  6. * @param a1 原始数组
  7. * @param i 第一个有序范围
  8. * @param iEnd
  9. * @param j 第二个有序范围
  10. * @param jEnd
  11. * @param a2 临时数组
  12. */
  13. public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
  14. int k = i;
  15. while(i <= iEnd && j <= jEnd) {
  16. if(a1[i] < a1[j]) {
  17. a2[k] = a1[i];
  18. i++;
  19. } else {
  20. a2[k] = a1[j];
  21. j++;
  22. }
  23. k++;
  24. }
  25. // 第二个有序数组的剩余元素
  26. if(i > iEnd) {
  27. System.arraycopy(a1, j, a2, k, jEnd -j + 1);
  28. }
  29. // 第一个有序数组的剩余元素
  30. if(j > jEnd) {
  31. System.arraycopy(a1, i, a2, k, iEnd - i + 1);
  32. }
  33. }
  34. public static void sort(int[] a1) {
  35. int n= a1.length;
  36. int[] a2 = new int[n];
  37. // width代表有序区间的宽度,取值依次为1、2、4、...
  38. for(int width = 1; width < n; width *= 2) {
  39. // [left, right]分别代表待合并区间的左右边界
  40. for (int left = 0; left < n ; left += 2 * width) {
  41. int right = Math.min(left + 2 * width - 1, n - 1);
  42. // System.out.printf("width %d [%d, %d] %n", width, left, right);
  43. int mid = Math.min(left + width - 1, n - 1);
  44. merge(a1, left, mid, mid + 1, right, a2);
  45. }
  46. // 从a2复制到a1
  47. System.arraycopy(a2, 0, a1,0, n);
  48. }
  49. }
  50. public static void main(String[] args) {
  51. int[] a = {8, 3, 6, 2, 5, 7, 1, 4};
  52. System.out.println(Arrays.toString(a));
  53. sort(a);
  54. System.out.println(Arrays.toString(a));
  55. }
  56. }

7. 归并 + 插入

基本思路:

1. 选择阈值:设定一个阈值(如,32等),当子数组的大小小于该阈值时,使用插入排序进行排序;否则,使用归并排序。

2. 实现过程:

  • 在归并排序的分解阶段,将数组分割为较小的子数组
  • 当子数组的大小达到设定的阈值时,停止递归,使用插入排序对该子数组进行排序
  • 在较大的子数组上继续使用归并排序

3. 优势:

  • 小规模数据使用插入排序速度更快
  • 归并排序保证了整体的O(n log n)性能,但通过在小规模时切换到插入排序可以减少常数因子,提升性能。
  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class MergeInsertionSort {
  4. /**
  5. * 插入排序
  6. * @param a
  7. * @param left
  8. * @param right
  9. */
  10. public static void insertionSort(int[] a, int left, int right) {
  11. for(int low = left + 1; low <= right; low++) {
  12. // 将low位置的元素插入到[0 .. low - 1]的已排序区域
  13. int t = a[low];
  14. int i = low - 1;
  15. while(i >= left && t < a[i]) {
  16. a[i + 1] = a[i];
  17. i--;
  18. }
  19. if(i != low - 1) {
  20. a[i + 1] = t;
  21. }
  22. }
  23. }
  24. /**
  25. * 归并排序
  26. * @param a1
  27. */
  28. public static void sort(int[] a1) {
  29. int[] a2 = new int[a1.length];
  30. split(a1, 0, a1.length - 1, a2);
  31. }
  32. /**
  33. * 划分数据
  34. * @param a1
  35. * @param left
  36. * @param right
  37. */
  38. private static void split(int[] a1, int left, int right, int[] a2) {
  39. // 2. 治
  40. // 当子数组的大小小于阈值时,使用插入排序,否则使用归并排序
  41. if(right - left <= 32) {
  42. // 插入排序
  43. insertionSort(a1, left, right);
  44. return;
  45. }
  46. // 1. 分
  47. int m = (left + right) >>> 1;
  48. split(a1, left, m, a2);
  49. split(a1, m + 1, right, a2);
  50. // 3. 合
  51. merge(a1, left, m, m + 1, right, a2);
  52. System.arraycopy(a2, left, a1, left, right - left + 1);
  53. }
  54. /**
  55. * 合并有序数组
  56. * @param a1 原始数组
  57. * @param i 第一个有序范围
  58. * @param iEnd
  59. * @param j 第二个有序范围
  60. * @param jEnd
  61. * @param a2 临时数组
  62. */
  63. public static void merge(int[] a1, int i, int iEnd, int j, int jEnd, int[] a2) {
  64. int k = i;
  65. while(i <= iEnd && j <= jEnd) {
  66. if(a1[i] < a1[j]) {
  67. a2[k] = a1[i];
  68. i++;
  69. } else {
  70. a2[k] = a1[j];
  71. j++;
  72. }
  73. k++;
  74. }
  75. // 第二个有序数组的剩余元素
  76. if(i > iEnd) {
  77. System.arraycopy(a1, j, a2, k, jEnd -j + 1);
  78. }
  79. // 第一个有序数组的剩余元素
  80. if(j > jEnd) {
  81. System.arraycopy(a1, i, a2, k, iEnd - i + 1);
  82. }
  83. }
  84. public static void main(String[] args) {
  85. int[] a = {9, 3, 7, 6, 2, 5, 8, 1, 4};
  86. System.out.println(Arrays.toString(a));
  87. sort(a);
  88. System.out.println(Arrays.toString(a));
  89. }
  90. }

8. 快速排序

单边循环(lomuto分区)要点:

  • 选择最右侧元素作为基准点

  • j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换

    • 交换时机:j 找到小的,且与 i 不相等

    • i 找到 >= 基准点元素后,不应自增

  • 最后基准点与 i 交换,i 即为基准点最终索引

例:

i和j都从左边出发向右查找,i找到比基准点4大的5,j找到比基准点小的2,停下来交换

i找到了比基准点大的5,j找到比基准点小的3,停下来交换

j到达right处结束,right与i交换,一轮分区结束

代码:

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. /**
  4. * 单边循环快排(lomuto洛穆托分区方案)
  5. * 核心思想:每轮找到一个基准点元素,把比他小的放在它左边,比它大的放在它右边,这称为分区
  6. * 1. 选择最右边的元素作为基准点元素
  7. * 2. j指针负责找到比基准点小的元素,一旦找到则将j位置的元素与i位置的元素交换
  8. * 3. i指针指向大于基准点元素的左边界,也是每次交换的目标索引
  9. * 4. 最后基准点与i位置的元素交换,i即为分区位置
  10. */
  11. public class QuickSortLomuto {
  12. public static void sort(int[] a) {
  13. quick(a, 0, a.length - 1);
  14. }
  15. private static void quick(int[] a, int left, int right) {
  16. if(left < right) {
  17. // 进行分区并获取基准元素的最终位置
  18. int pivotIndex = partition(a, left, right);
  19. // 递归排序基准元素左边的子数组
  20. quick(a, left, pivotIndex - 1);
  21. // 递归排序基准元素右边的子数组
  22. quick(a, pivotIndex + 1, right);
  23. }
  24. }
  25. /**
  26. * 分区
  27. * @param a
  28. * @param left
  29. * @param right
  30. */
  31. private static int partition(int[] a, int left, int right) {
  32. // 1. 选择最右边的元素作为基准点
  33. int pivot = a[right];
  34. // 2. 初始化i指针
  35. int i = left;
  36. for(int j = left; j < right; j++) {
  37. // 3. j 找到比基准元素小的了,交换i和j位置的元素
  38. if(a[j] < pivot) {
  39. if(i != j) {
  40. swap(a, i, j);
  41. }
  42. i++;
  43. }
  44. }
  45. swap(a, i, right);
  46. return i;
  47. }
  48. private static void swap(int[] a, int i, int j) {
  49. int t = a[i];
  50. a[i] = a[j];
  51. a[j] = t;
  52. }
  53. public static void main(String[] args) {
  54. int[] a = {2, 3, 1, 7, 6, 4, 5};
  55. System.out.println(Arrays.toString(a));
  56. sort(a);
  57. System.out.println(Arrays.toString(a));
  58. }
  59. }

双边循环要点:

  • 选择最左侧元素作为基准点
  • j 找比基准点小的,i 找比基准点大的,一旦找到,二者进行交换
  • i从左到右
  • j从右到左
  • 最后基准点与i交互,i即为基准点最终索引

例:

选择最左侧的4作为基准点,i找到比基准点大的5停下来,j找到比基准点小的1停下来(包含等于),二者交互

i找到8,j找到3,二者交互;i找到7,j找到2,二者交互

i == j,则退出循环,基准点与i位置的元素交互

代码

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. public class QuickSortHoare {
  4. public static void sort(int[] a) {
  5. quick(a, 0, a.length - 1);
  6. }
  7. private static void quick(int[] a, int left, int right) {
  8. if (left >= right) {
  9. return;
  10. }
  11. int p = partition(a, left, right);
  12. quick(a, left, p - 1);
  13. quick(a, p + 1, right);
  14. }
  15. /**
  16. *
  17. * @param a
  18. * @param left
  19. * @param right
  20. * @return
  21. */
  22. private static int partition(int[] a, int left, int right) {
  23. // i找比基准点大的,j找比基准点小的
  24. int i = left, j = right;
  25. int pivot = a[left];
  26. while(i < j) {
  27. // 先右侧扫描
  28. while(i < j && a[j] > pivot) {
  29. j--;
  30. }
  31. // 后左侧扫描(顺序不能调换)
  32. while(i < j && pivot >= a[i]) {
  33. i++;
  34. }
  35. // 一旦找到,二者进行交换
  36. swap(a, i, j);
  37. }
  38. // 最后基准点与i交换,i即为基准点最终索引
  39. swap(a, left, i);
  40. return i;
  41. }
  42. private static void swap(int[] a, int i, int j) {
  43. int t = a[i];
  44. a[i] = a[j];
  45. a[j] = t;
  46. }
  47. public static void main(String[] args) {
  48. int[] a = {2, 3, 1, 7, 6, 4, 5};
  49. System.out.println(Arrays.toString(a));
  50. sort(a);
  51. System.out.println(Arrays.toString(a));
  52. }
  53. }

8.1 随机基准点

使用随机数作为基准点,避免万一最大值或最小值作为基准点导致的分区不平衡

改进代码

  1. // 随机基准点
  2. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  3. // 将left与idx位置的元素交换
  4. swap(a, idx, left);

8.2 处理重复值

如果重复值较多,则原来算法中分区效果也不好,如下图中左侧所示,需要想办法改为右侧的分区效果。

改进代码

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. import java.util.concurrent.ThreadLocalRandom;
  4. public class QuickSortHandleDuplicate {
  5. public static void sort(int[] a) {
  6. quick(a, 0, a.length - 1);
  7. }
  8. private static void quick(int[] a, int left, int right) {
  9. if (left >= right) {
  10. return;
  11. }
  12. int p = partition(a, left, right);
  13. quick(a, left, p - 1);
  14. quick(a, p + 1, right);
  15. }
  16. /**
  17. * 循环内
  18. * i从left + 1开始,从左向右找大的或相等的
  19. * j从right开始,从右向左找小的或相等的
  20. * 交换, i++ j--
  21. * 循环外,j和基准点交换,j即为分区位置
  22. * @param a
  23. * @param left
  24. * @param right
  25. * @return
  26. */
  27. private static int partition(int[] a, int left, int right) {
  28. // 随机基准点
  29. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  30. // 将left与idx位置的元素交换
  31. swap(a, idx, left);
  32. // 从left + 1开始
  33. int i = left + 1, j = right;
  34. int pivot = a[left];
  35. while(i <= j) {
  36. // i从左到右找大的或相等的
  37. while(i <= j && a[i] < pivot) {
  38. i++;
  39. }
  40. // j从右到左找小的或者相等的
  41. while(i <= j && a[j] > pivot) {
  42. j--;
  43. }
  44. if(i <= j) {
  45. swap(a, i, j);;
  46. i++;
  47. j--;
  48. }
  49. }
  50. // 最后基准点与i交换,i即为基准点最终索引
  51. swap(a, j, left);
  52. return j;
  53. }
  54. private static void swap(int[] a, int i, int j) {
  55. int t = a[i];
  56. a[i] = a[j];
  57. a[j] = t;
  58. }
  59. public static void main(String[] args) {
  60. int[] a = {2, 3, 1, 7, 6, 4, 5};
  61. System.out.println(Arrays.toString(a));
  62. sort(a);
  63. System.out.println(Arrays.toString(a));
  64. }
  65. }

8.3 快速排序 + 插入排序

  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. import java.util.concurrent.ThreadLocalRandom;
  4. public class QuickSortHandleDuplicate {
  5. /**
  6. * 插入排序
  7. * @param a
  8. * @param left
  9. * @param right
  10. */
  11. public static void insertionSort(int[] a, int left, int right) {
  12. for(int low = left + 1; low <= right; low++) {
  13. // 将low位置的元素插入到[0 .. low - 1]的已排序区域
  14. int t = a[low];
  15. int i = low - 1;
  16. while(i >= left && t < a[i]) {
  17. a[i + 1] = a[i];
  18. i--;
  19. }
  20. if(i != low - 1) {
  21. a[i + 1] = t;
  22. }
  23. }
  24. }
  25. /**
  26. * 快速排序
  27. * @param a
  28. */
  29. public static void sort(int[] a) {
  30. quick(a, 0, a.length - 1);
  31. }
  32. private static void quick(int[] a, int left, int right) {
  33. if (right - left <= 32) {
  34. insertionSort(a, left, right);
  35. return;
  36. }
  37. int p = partition(a, left, right);
  38. quick(a, left, p - 1);
  39. quick(a, p + 1, right);
  40. }
  41. /**
  42. * 循环内
  43. * i从left + 1开始,从左向右找大的或相等的
  44. * j从right开始,从右向左找小的或相等的
  45. * 交换, i++ j--
  46. * 循环外,j和基准点交换,j即为分区位置
  47. * @param a
  48. * @param left
  49. * @param right
  50. * @return
  51. */
  52. private static int partition(int[] a, int left, int right) {
  53. // 随机基准点
  54. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  55. // 将left与idx位置的元素交换
  56. swap(a, idx, left);
  57. // 从left + 1开始
  58. int i = left + 1, j = right;
  59. int pivot = a[left];
  60. while(i <= j) {
  61. // i从左到右找大的或相等的
  62. while(i <= j && a[i] < pivot) {
  63. i++;
  64. }
  65. // j从右到左找小的或者相等的
  66. while(i <= j && a[j] > pivot) {
  67. j--;
  68. }
  69. if(i <= j) {
  70. swap(a, i, j);;
  71. i++;
  72. j--;
  73. }
  74. }
  75. // 最后基准点与i交换,i即为基准点最终索引
  76. swap(a, j, left);
  77. return j;
  78. }
  79. private static void swap(int[] a, int i, int j) {
  80. int t = a[i];
  81. a[i] = a[j];
  82. a[j] = t;
  83. }
  84. public static void main(String[] args) {
  85. int[] a = {2, 3, 1, 7, 6, 4, 5};
  86. System.out.println(Arrays.toString(a));
  87. sort(a);
  88. System.out.println(Arrays.toString(a));
  89. }
  90. }

性能对比

9. 计数排序

要点

  • 1. 找到最大值,创建一个大小为最大值+1的count数组
  • 2. count数组的索引对应原始数组的元素,用来统计该元素的出现次数
  • 3. 遍历count数组,根据count数组的索引(即原始数组的元素)以及出现次数,生成排序后的内容
  • count数组的索引是:已排序好的
  • 前提:最大值不能太大
  1. package com.itheima.algorithms.sort;
  2. import java.util.Arrays;
  3. /**
  4. * 计数排序
  5. */
  6. public class CountingSort {
  7. /*
  8. 要点
  9. 1. 让原始数组的最小值映射到 count[0] 最大值映射到 count 最右侧
  10. 2. 原始数组元素 - 最小值 = count 索引
  11. 3. count 索引 + 最小值 = 原始数组元素
  12. */
  13. public static void main(String[] args) {
  14. int[] a = {5, 1, 1, 3, 0, -1};
  15. System.out.println(Arrays.toString(a));
  16. sort(a);
  17. System.out.println(Arrays.toString(a));
  18. }
  19. // 2N + K n*log(n)
  20. /*
  21. {5, 1, 1, 3, 0, -1} 原始数组 a
  22. [1 1 2 0 1 0 1 ] count
  23. 0 1 2 3 4 5 6
  24. -1 0 1 3 5
  25. */
  26. private static void sort(int[] a) {
  27. int max = a[0];
  28. int min = a[0];
  29. for (int i = 1; i < a.length; i++) {
  30. if(a[i] > max) {
  31. max = a[i];
  32. }
  33. if(a[i] < min) {
  34. min = a[i];
  35. }
  36. }
  37. int[] count = new int[max - min + 1];
  38. // v原始数组元素 - 最小值 = count索引
  39. for (int v : a) {
  40. count[v - min]++;
  41. }
  42. int k = 0;
  43. for (int i = 0; i < count.length; i++) {
  44. // i + min代表原始数组元素,count[i]出现次数
  45. while(count[i] > 0) {
  46. a[k++] = i + min;
  47. count[i]--;
  48. }
  49. }
  50. }
  51. }

性能对比

针对byte[],因为数据范围已知,省去了求最大、最小值的过程,java中对char[]、short[]、byte[]的排序都可能采用counting排序。

  1. public static void sort(byte[] a) {
  2. int[] count = new int[256];
  3. for (int i : a) {
  4. // 记录每个字节值的出现次数
  5. // i & 0xFF是为了将byte转换为int类型,这样可以有效地处理负值
  6. // 因为字节的值范围是-128到127,而count数组的索引应该在0-255之间
  7. count[i & 0xFF]++;
  8. }
  9. // 反向填充排序后的值
  10. int k = a.length - 1;
  11. for(int i = 128 + 256; k >= 0; ) {
  12. // 找到一个计数大于0的字节值
  13. while (count[--i & 0xFF] == 0);
  14. int v = i & 0xFF; // 当前处理的字节值
  15. int c = count[i & 0xFF]; // 获取当前字节值的计数
  16. for(int j = 0; j < c; j++) {
  17. a[k] = (byte) v; // 将字节值填充到结果数组的后面
  18. k--;
  19. }
  20. }
  21. }

稳定计数排序

  1. /**
  2. * 稳定版计数排序
  3. * @param a
  4. */
  5. public static void sort2(int[] a) {
  6. int min = a[0];
  7. int max = a[0];
  8. for (int i : a) {
  9. if(i > max) {
  10. max = i;
  11. } else if(i < min) {
  12. min = i;
  13. }
  14. }
  15. int[] count = new int[max - min + 1];
  16. for (int i : a) {
  17. count[i - min]++;
  18. }
  19. // 累加计数数组 如果count[i] = 5,则表示有5个元素会放在排序数组中i的位置之前
  20. for (int i = 1; i < count.length; i++) {
  21. count[i] = count[i] + count[i - 1];
  22. }
  23. int[] b = new int[a.length];
  24. for(int i = a.length - 1; i >= 0; i--) {
  25. // 找到当前元素在计数数组中的索引
  26. int j = a[i] - min;
  27. // 减少计数,表示该元素已经放置过了
  28. count[j]--;
  29. // 将元素放入结果数组b的正确位置
  30. b[count[j]] = a[i];
  31. }
  32. System.arraycopy(b, 0, a, 0, a.length);
  33. }

10. 桶排序

初步实现

  1. package com.itheima.algorithms.sort;
  2. import com.itheima.datastructure.Array.DynamicArray;
  3. import java.util.Arrays;
  4. public class BucketSort {
  5. public static void main(String[] args) {
  6. int[] ages = {20, 18, 28, 66, 25, 31, 67, 30}; // 假设年龄范围1~99
  7. System.out.println(Arrays.toString(ages));
  8. sort(ages);
  9. System.out.println(Arrays.toString(ages));
  10. }
  11. /**
  12. * 桶排序
  13. * @param ages
  14. */
  15. private static void sort(int[] ages) {
  16. // 1. 准备桶
  17. DynamicArray[] buckets = new DynamicArray[10];
  18. for (int i = 0; i < buckets.length; i++) {
  19. buckets[i] = new DynamicArray();
  20. }
  21. // 2. 放入年龄数据
  22. for (int age : ages) {
  23. buckets[age / 10].addLast(age);
  24. }
  25. int k = 0;
  26. for (DynamicArray bucket : buckets) {
  27. // 3. 对桶内元素进行排序
  28. int[] array = bucket.stream().toArray();
  29. InsertionSort.sort(array);
  30. // 4. 把每个桶排序好的内容,依次放回原始数组
  31. for (int i : array) {
  32. ages[k++] = i;
  33. }
  34. }
  35. }
  36. }

通用

  1. package com.itheima.algorithms.sort;
  2. import com.itheima.datastructure.Array.DynamicArray;
  3. import java.util.Arrays;
  4. public class BucketSortGeneric {
  5. public static void main(String[] args) {
  6. int[] ages = {9, 0, 5, 1, 4, 7, 2, 8}; // 假设年龄范围1~99
  7. System.out.println(Arrays.toString(ages));
  8. sort(ages, 20);
  9. System.out.println(Arrays.toString(ages));
  10. }
  11. /**
  12. * 桶排序通用版 - 参考计数排序
  13. * @param ages
  14. * @param range
  15. */
  16. private static void sort(int[] ages, int range) {
  17. // 0. 找最大最小值
  18. int max = ages[0];
  19. int min = ages[0];
  20. for (int i = 1; i < ages.length; i++) {
  21. if (ages[i] > max) {
  22. max = ages[i];
  23. }
  24. if (ages[i] < min) {
  25. min = ages[i];
  26. }
  27. }
  28. // 1. 准备桶 c
  29. // (max - min) / range + 1 减少桶的个数
  30. DynamicArray[] buckets = new DynamicArray[(max - min) / range + 1];
  31. for (int i = 0; i < buckets.length; i++) {
  32. buckets[i] = new DynamicArray();
  33. }
  34. // 2. 放入年龄数据
  35. for (int age : ages) {
  36. buckets[(age - min) / range].addLast(age);
  37. }
  38. int k = 0;
  39. for (DynamicArray bucket : buckets) {
  40. // 3. 对桶内元素进行排序
  41. int[] array = bucket.stream().toArray();
  42. InsertionSort.sort(array);
  43. // 4. 把每个桶排序好的内容,依次放回原始数组
  44. for (int i : array) {
  45. ages[k++] = i;
  46. }
  47. }
  48. }
  49. }

11. 基数排序

基数排序(Radix Sort)的核心思路是按位排序,即将数字按位(从最低位到最高位或从最高位到最低位)逐位排序。基数排序通常适用于整数和字符串排序。以下是基数排序的基本步骤和思路:

1. 确定最大位数

  • 首先,基数排序需要知道待排序的最大数字的位数,这可以通过遍历整个数组来完成。例如,对于数组中的最大数max,基数排序只需要处理max的位数。

2. 按位排序

  • 基数排序通过多次(位数个数)使用一个稳定的排序算法(通常是计数排序)来对数字进行排序,以下是逐步的过程:
  • 从最低位开始:首先对每个数字的最低位进行排序,得到的结果是基于该位的排序
  • 接着是次低位:然后对于每个数字的次低位进行排序,虽然这个排序会受到前一次排序结果的影响,但依然是基于该新的位进行的
  • 重复直到最高位:继续这个过程,直到处理到最高位

例如

代码:

  1. package com.itheima.algorithms.sort;
  2. import java.util.ArrayList;
  3. public class RadixSort {
  4. /**
  5. * 基数排序
  6. *
  7. * @param a 待排序字符串数组
  8. */
  9. public static void radixSort(String[] a) {
  10. // 1. 找出最长字符串的长度
  11. int maxLength = 0;
  12. for (String s : a) {
  13. if (s.length() > maxLength) {
  14. maxLength = s.length();
  15. }
  16. }
  17. // 2. 准备桶
  18. ArrayList<String>[] buckets = new ArrayList[128];
  19. for (int i = 0; i < buckets.length; i++) {
  20. buckets[i] = new ArrayList<>();
  21. }
  22. // 3. 从字符串的低位到高位,进行多轮按位桶排序
  23. for (int i = maxLength - 1; i >= 0; i--) {
  24. // 根据某一个位的值放入到对应桶
  25. for (String s : a) {
  26. int num = Integer.parseInt(s);
  27. if(s.length() < maxLength) {
  28. // 处理字符串长度不等的情况
  29. s = String.format("%0" + maxLength + "d", num);
  30. }
  31. buckets[s.charAt(i) - '0'].add(String.valueOf(num));
  32. }
  33. int k = 0;
  34. for (ArrayList<String> bucket : buckets) {
  35. // 4. 重新取出排好序的字符串,依次放回原始数组
  36. for (String s : bucket) {
  37. a[k++] = s;
  38. }
  39. bucket.clear();
  40. }
  41. }
  42. }
  43. public static void main(String[] args) {
  44. /*String[] phoneNumbers = new String[10];
  45. phoneNumbers[0] = "13812345678";
  46. phoneNumbers[1] = "13912345678";
  47. phoneNumbers[2] = "13612345678";
  48. phoneNumbers[3] = "13712345678";
  49. phoneNumbers[4] = "13512345678";
  50. phoneNumbers[5] = "13412345678";
  51. phoneNumbers[6] = "15012345678";
  52. phoneNumbers[7] = "15112345678";
  53. phoneNumbers[8] = "15212345678";
  54. phoneNumbers[9] = "15712345678";*/
  55. String[] phoneNumbers = new String[10];
  56. phoneNumbers[0] = "138";
  57. phoneNumbers[1] = "139";
  58. phoneNumbers[2] = "1068";
  59. phoneNumbers[3] = "137";
  60. phoneNumbers[4] = "135";
  61. phoneNumbers[5] = "14";
  62. phoneNumbers[6] = "150";
  63. phoneNumbers[7] = "151";
  64. phoneNumbers[8] = "162";
  65. phoneNumbers[9] = "57";
  66. RadixSort.radixSort(phoneNumbers);
  67. for (String phoneNumber : phoneNumbers) {
  68. System.out.print(phoneNumber + " ");
  69. }
  70. }
  71. }

三、习题

1. 数组的相对排序

给你两个数组,arr1 和 arr2arr2 中的元素各不相同,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 中

解法一:计数排序

  1. class Solution {
  2. public int[] relativeSortArray(int[] arr1, int[] arr2) {
  3. // 1. 记录每个元素出现的次数
  4. int[] count = new int[1001];
  5. for (int i : arr1) {
  6. count[i]++;
  7. }
  8. int[] result = new int[arr1.length];
  9. int k = 0;
  10. // 2. 如果元素有在arr2出现,添加到result中
  11. for (int i : arr2) {
  12. while (count[i] > 0) {
  13. result[k++] = i;
  14. count[i]--;
  15. }
  16. }
  17. // 3. 处理不在arr2中的元素
  18. for (int i = 0; i < count.length; i++) {
  19. while (count[i] > 0) {
  20. result[k++] = i;
  21. count[i]--;
  22. }
  23. }
  24. return result;
  25. }
  26. }

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

  1. class Solution {
  2. public int[] frequencySort(int[] nums) {
  3. // 1. 记录每个元素出现的次数
  4. int[] count = new int[201];
  5. for (int num : nums) {
  6. // 加100是为了处理num为负数的情况
  7. count[num + 100]++;
  8. }
  9. return Arrays.stream(nums).boxed().sorted((a, b) -> {
  10. int fa = count[a + 100];
  11. int fb = count[b + 100];
  12. if(fa == fb) {
  13. // 如果频率相同,按照数值本身将它们降序排序
  14. return Integer.compare(b, a);
  15. } else {
  16. // 按照每个值的频率升序排序
  17. return fa - fb;
  18. }
  19. }).mapToInt(Integer::intValue).toArray();
  20. }
  21. }

解法二:计数排序 + HashMap。执行耗时6ms

  • 使用Map代替固定大小数组
  • 减少计算频率的次数
  • 使用list和Comparator
  1. import java.util.*;
  2. class Solution {
  3. public int[] frequencySort(int[] nums) {
  4. // 1. 记录每个元素出现的次数
  5. Map<Integer, Integer> count = new HashMap<>();
  6. for (int num : nums) {
  7. count.put(num, count.getOrDefault(num, 0) + 1);
  8. }
  9. // 2. 使用 List 存储元素及其频率,方便排序
  10. List<Map.Entry<Integer, Integer>> entryList = new ArrayList<>(count.entrySet());
  11. // 3. 按照频率升序,然后数值降序排序
  12. entryList.sort((a, b) -> {
  13. if (a.getValue().equals(b.getValue())) {
  14. return Integer.compare(b.getKey(), a.getKey()); // 按数值降序
  15. }
  16. return Integer.compare(a.getValue(), b.getValue()); // 按频率升序
  17. });
  18. // 4. 构造结果数组
  19. int index = 0;
  20. for (Map.Entry<Integer, Integer> entry : entryList) {
  21. int frequency = entry.getValue();
  22. for (int i = 0; i < frequency; i++) {
  23. nums[index++] = entry.getKey();
  24. }
  25. }
  26. return nums;
  27. }
  28. }

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

解法一:桶排序。超出内存限制

  1. class Solution {
  2. public int maximumGap(int[] nums) {
  3. int n = nums.length;
  4. if (n < 2) {
  5. return 0;
  6. }
  7. sort(nums, 1);
  8. int ret = 0;
  9. for (int i = 1; i < n; i++) {
  10. ret = Math.max(ret, nums[i] - nums[i - 1]);
  11. }
  12. return ret;
  13. }
  14. public static void sort(int[] nums, int range) {
  15. int max = nums[0];
  16. int min = nums[0];
  17. for (int i = 1; i < nums.length; i++) {
  18. if (nums[i] > max) {
  19. max = nums[i];
  20. }
  21. if (nums[i] < min) {
  22. min = nums[i];
  23. }
  24. }
  25. ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
  26. for (int i = 0; i < buckets.length; i++) {
  27. buckets[i] = new ArrayList();
  28. }
  29. for (int num : nums) {
  30. buckets[(num - min) / range].addLast(num);
  31. }
  32. int k = 0;
  33. for (ArrayList<Integer> bucket : buckets) {
  34. // 创建一个新的int数组
  35. int[] array = new int[bucket.size()];
  36. // 将ArrayList的元素复制到int数组
  37. for (int i = 0; i < bucket.size(); i++) {
  38. array[i] = bucket.get(i);
  39. }
  40. // 执行插入排序
  41. insertionSort(array, 0, array.length - 1);
  42. // 将排序后的结果写回nums数组
  43. for (int a : array) {
  44. nums[k++] = a;
  45. }
  46. }
  47. }
  48. public static void insertionSort(int[] nums, int left, int right) {
  49. for (int low = left + 1; low <= right; low++) {
  50. int t = nums[low];
  51. int i = low - 1;
  52. while (i >= left && t < nums[i]) {
  53. nums[i + 1] = nums[i];
  54. i--;
  55. }
  56. if (i != low - 1) {
  57. nums[i + 1] = t;
  58. }
  59. }
  60. }
  61. }

解法二:桶排序 - 合理化桶的数量。执行耗时52ms

  1. class Solution {
  2. public int maximumGap(int[] nums) {
  3. int n = nums.length;
  4. if (n < 2) {
  5. return 0;
  6. }
  7. sort(nums);
  8. int ret = 0;
  9. for (int i = 1; i < n; i++) {
  10. ret = Math.max(ret, nums[i] - nums[i - 1]);
  11. }
  12. return ret;
  13. }
  14. public static void sort(int[] nums) {
  15. int max = nums[0];
  16. int min = nums[0];
  17. for (int i = 1; i < nums.length; i++) {
  18. if (nums[i] > max) {
  19. max = nums[i];
  20. }
  21. if (nums[i] < min) {
  22. min = nums[i];
  23. }
  24. }
  25. // ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
  26. int range = Math.max((max - min) / (nums.length - 1), 1);
  27. ArrayList[] buckets = new ArrayList[(max - min) / range + 1];
  28. for (int i = 0; i < buckets.length; i++) {
  29. buckets[i] = new ArrayList();
  30. }
  31. for (int num : nums) {
  32. buckets[(num - min) / range].addLast(num);
  33. }
  34. int k = 0;
  35. for (ArrayList<Integer> bucket : buckets) {
  36. // 创建一个新的int数组
  37. int[] array = new int[bucket.size()];
  38. // 将ArrayList的元素复制到int数组
  39. for (int i = 0; i < bucket.size(); i++) {
  40. array[i] = bucket.get(i);
  41. }
  42. // 执行插入排序
  43. insertionSort(array, 0, array.length - 1);
  44. // 将排序后的结果写回nums数组
  45. for (int a : array) {
  46. nums[k++] = a;
  47. }
  48. }
  49. }
  50. public static void insertionSort(int[] nums, int left, int right) {
  51. for (int low = left + 1; low <= right; low++) {
  52. int t = nums[low];
  53. int i = low - 1;
  54. while (i >= left && t < nums[i]) {
  55. nums[i + 1] = nums[i];
  56. i--;
  57. }
  58. if (i != low - 1) {
  59. nums[i + 1] = t;
  60. }
  61. }
  62. }
  63. }

解法三:桶排序 - 只保留桶内最大最小值。执行耗时13ms

  1. class Solution {
  2. static class Pair {
  3. int max = 0;
  4. int min = 1000_000_000;
  5. public void add(int v) {
  6. max = Math.max(max, v);
  7. min = Math.min(min, v);
  8. }
  9. @Override
  10. public String toString() {
  11. return "[" + min + "," + max + "]";
  12. }
  13. }
  14. public int maximumGap(int[] nums) {
  15. // 1. 处理特殊情况
  16. if (nums.length < 2) {
  17. return 0;
  18. }
  19. // 2. 桶排序
  20. // 桶个数 (max - min) / range + 1 期望桶个数 nums.length + 1
  21. // range = (max - min) / nums.length
  22. int max = nums[0];
  23. int min = nums[0];
  24. for (int i = 1; i < nums.length; i++) {
  25. if (nums[i] > max) {
  26. max = nums[i];
  27. }
  28. if (nums[i] < min) {
  29. min = nums[i];
  30. }
  31. }
  32. if (max == min) {
  33. return 0;
  34. }
  35. int range = Math.max(1, (max - min) / nums.length);
  36. int size = (max - min) / range + 1;
  37. Pair[] buckets = new Pair[size];
  38. // 2. 放入数据
  39. for (int num : nums) {
  40. int idx = (num - min) / range;
  41. if (buckets[idx] == null) {
  42. buckets[idx] = new Pair();
  43. }
  44. buckets[idx].add(num);
  45. }
  46. // 3. 寻找最大差值
  47. int r = 0;
  48. int lastMax = buckets[0].max;
  49. for (int i = 1; i < buckets.length; i++) {
  50. Pair pair = buckets[i];
  51. if (pair != null) {
  52. r = Math.max(r, pair.min - lastMax);
  53. lastMax = pair.max;
  54. }
  55. }
  56. return r;
  57. }
  58. }

解法四:基数排序。执行耗时68ms

  1. public int maximumGap(int[] nums) {
  2. // 0. 数组长度小于2,直接返回0
  3. if(nums.length < 2) {
  4. return 0;
  5. }
  6. // 1. 计算数组的最大值
  7. int max = nums[0];
  8. for (int num : nums) {
  9. max = Math.max(num, max);
  10. }
  11. // 2. 准备10个桶
  12. ArrayList<Integer>[] buckets = new ArrayList[10];
  13. for (int i = 0; i < buckets.length; i++) {
  14. buckets[i] = new ArrayList<>();
  15. }
  16. // 3. 没超过最大值
  17. long exp = 1;
  18. while(max >= exp) {
  19. for (int num : nums) {
  20. buckets[(num / (int) exp % 10)].add(num);
  21. }
  22. int k = 0;
  23. for (ArrayList<Integer> bucket : buckets) {
  24. for (Integer i : bucket) {
  25. nums[k++] = i;
  26. }
  27. bucket.clear();
  28. }
  29. exp *= 10;
  30. }
  31. // 4. 求最大间距
  32. int r = 0;
  33. for (int i = 1; i < nums.length; i++) {
  34. r = Math.max(r, nums[i] - nums[i - 1]);
  35. }
  36. return r;
  37. }

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

  1. class Solution {
  2. public static int[] sortArray(int[] nums) {
  3. quickSort(nums, 0, nums.length - 1);
  4. return nums;
  5. }
  6. private static void quickSort(int[] nums, int left, int right) {
  7. if (left < right) {
  8. int pivot = partition(nums, left, right);
  9. quickSort(nums, left, pivot - 1);
  10. quickSort(nums, pivot + 1, right);
  11. }
  12. }
  13. private static int partition(int[] nums, int left, int right) {
  14. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  15. swap(nums, idx, right);
  16. int pivot = nums[right];
  17. int i = left;
  18. for (int j = left; j < right; j++) {
  19. if (nums[j] < pivot) {
  20. if (i != j) {
  21. swap(nums, i, j);
  22. }
  23. i++;
  24. }
  25. }
  26. swap(nums, i, right);
  27. return i;
  28. }
  29. public static void swap(int[] a, int i, int j) {
  30. int t = a[i];
  31. a[i] = a[j];
  32. a[j] = t;
  33. }
  34. }

解法二:双边快排(随机)。执行耗时1643ms

  1. class Solution {
  2. public static int[] sortArray(int[] nums) {
  3. quickSort(nums, 0, nums.length - 1);
  4. return nums;
  5. }
  6. private static void quickSort(int[] nums, int left, int right) {
  7. if (left < right) {
  8. int pivot = partition(nums, left, right);
  9. quickSort(nums, left, pivot - 1);
  10. quickSort(nums, pivot + 1, right);
  11. }
  12. }
  13. private static int partition(int[] nums, int left, int right) {
  14. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  15. swap(nums, idx, left);
  16. int pivot = nums[left];
  17. int i = left, j = right;
  18. while (i < j) {
  19. while (i < j && nums[j] > pivot) {
  20. j--;
  21. }
  22. while (i < j && nums[i] <= pivot) {
  23. i++;
  24. }
  25. swap(nums, i, j);
  26. }
  27. swap(nums, left, i);
  28. return i;
  29. }
  30. public static void swap(int[] a, int i, int j) {
  31. int t = a[i];
  32. a[i] = a[j];
  33. a[j] = t;
  34. }
  35. }

解法三:快排(随机 + 重复)。执行耗时26ms

  1. class Solution {
  2. public static int[] sortArray(int[] nums) {
  3. quickSort(nums, 0, nums.length - 1);
  4. return nums;
  5. }
  6. private static void quickSort(int[] nums, int left, int right) {
  7. if (left < right) {
  8. int pivot = partition(nums, left, right);
  9. quickSort(nums, left, pivot - 1);
  10. quickSort(nums, pivot + 1, right);
  11. }
  12. }
  13. private static int partition(int[] nums, int left, int right) {
  14. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  15. swap(nums, idx, left);
  16. int pivot = nums[left];
  17. int i = left + 1, j = right;
  18. while (i <= j) {
  19. while (i <= j && nums[i] < pivot) {
  20. i++;
  21. }
  22. while (i <= j && nums[j] > pivot) {
  23. j--;
  24. }
  25. if (i <= j) {
  26. swap(nums, i, j);
  27. i++;
  28. j--;
  29. }
  30. }
  31. swap(nums, j, left);
  32. return j;
  33. }
  34. public static void swap(int[] a, int i, int j) {
  35. int t = a[i];
  36. a[i] = a[j];
  37. a[j] = t;
  38. }
  39. }

解法四:快排(随机 + 重复 + 插入)。执行耗时18ms

  1. class Solution {
  2. public static int[] sortArray(int[] nums) {
  3. quickSort(nums, 0, nums.length - 1);
  4. return nums;
  5. }
  6. public static void insertionSort(int[] nums, int left, int right) {
  7. for (int low = left + 1; low <= right; low++) {
  8. int t = nums[low];
  9. int i = low - 1;
  10. while (i >= left && t < nums[i]) {
  11. nums[i + 1] = nums[i];
  12. i--;
  13. }
  14. if (i != low - 1) {
  15. nums[i + 1] = t;
  16. }
  17. }
  18. }
  19. private static void quickSort(int[] nums, int left, int right) {
  20. if (right - left <= 32) {
  21. insertionSort(nums, left, right);
  22. return;
  23. }
  24. int pivot = partition(nums, left, right);
  25. quickSort(nums, left, pivot - 1);
  26. quickSort(nums, pivot + 1, right);
  27. }
  28. private static int partition(int[] nums, int left, int right) {
  29. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  30. swap(nums, idx, left);
  31. int pivot = nums[left];
  32. int i = left + 1, j = right;
  33. while (i <= j) {
  34. while (i <= j && nums[i] < pivot) {
  35. i++;
  36. }
  37. while (i <= j && nums[j] > pivot) {
  38. j--;
  39. }
  40. if (i <= j) {
  41. swap(nums, i, j);
  42. i++;
  43. j--;
  44. }
  45. }
  46. swap(nums, j, left);
  47. return j;
  48. }
  49. public static void swap(int[] a, int i, int j) {
  50. int t = a[i];
  51. a[i] = a[j];
  52. a[j] = t;
  53. }
  54. }

解法五:计数排序。执行耗时3ms

  1. class Solution {
  2. public static int[] sortArray(int[] nums) {
  3. int max = nums[0];
  4. int min = nums[0];
  5. for (int i = 1; i < nums.length; i++) {
  6. if (nums[i] > max) {
  7. max = nums[i];
  8. }
  9. if (nums[i] < min) {
  10. min = nums[i];
  11. }
  12. }
  13. int[] count = new int[max - min + 1];
  14. for (int num : nums) {
  15. count[num - min]++;
  16. }
  17. int k = 0;
  18. for (int i = 0; i < count.length; i++) {
  19. while (count[i] > 0) {
  20. nums[k++] = i + min;
  21. count[i]--;
  22. }
  23. }
  24. return nums;
  25. }
  26. }

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) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

解法一:把链表中的值放入数组当中进行排序,再根据排序后的值创建新的链表返回

  1. /**
  2. * Definition for singly-linked list.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode() {}
  7. * ListNode(int val) { this.val = val; }
  8. * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
  9. * }
  10. */
  11. class Solution {
  12. public ListNode sortList(ListNode head) {
  13. if (head == null || head.next == null) {
  14. return head;
  15. }
  16. List<Integer> values = new ArrayList<>();
  17. ListNode current = head;
  18. while (current != null) {
  19. values.add(current.val);
  20. current = current.next;
  21. }
  22. Collections.sort(values);
  23. ListNode newHead = new ListNode(values.get(0));
  24. ListNode temp = newHead;
  25. for (int i = 1; i < values.size(); i++) {
  26. temp.next = new ListNode(values.get(i));
  27. temp = temp.next;
  28. }
  29. return newHead;
  30. }
  31. }

解法二:快慢指针 + 合并有序链表(归并)

  1. /**
  2. * Definition for singly-linked list.
  3. * public class ListNode {
  4. * int val;
  5. * ListNode next;
  6. * ListNode() {}
  7. * ListNode(int val) { this.val = val; }
  8. * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
  9. * }
  10. */
  11. class Solution {
  12. public ListNode sortList(ListNode head) {
  13. if (head == null || head.next == null) {
  14. return head;
  15. }
  16. ListNode mid = findMiddle(head);
  17. ListNode secondHalf = mid.next;
  18. mid.next = null;
  19. ListNode sortedFirstHalf = sortList(head);
  20. ListNode sortedSecondHalf = sortList(secondHalf);
  21. return merge(sortedFirstHalf, sortedSecondHalf);
  22. }
  23. private ListNode merge(ListNode l1, ListNode l2) {
  24. ListNode dummy = new ListNode(0);
  25. ListNode current = dummy;
  26. while (l1 != null && l2 != null) {
  27. if (l1.val < l2.val) {
  28. current.next = l1;
  29. l1 = l1.next;
  30. } else {
  31. current.next = l2;
  32. l2 = l2.next;
  33. }
  34. current = current.next;
  35. }
  36. current.next = l1 != null ? l1 : l2;
  37. return dummy.next;
  38. }
  39. private ListNode findMiddle(ListNode head) {
  40. ListNode slow = head;
  41. ListNode fast = head.next;
  42. while (fast != null && fast.next != null) {
  43. slow = slow.next;
  44. fast = fast.next.next;
  45. }
  46. return slow;
  47. }
  48. }

6. 数组的相对顺序

给你两个数组,arr1 和 arr2arr2 中的元素各不相同,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 中

解法一:

  1. class Solution {
  2. public int[] relativeSortArray(int[] arr1, int[] arr2) {
  3. // 1. 记录每个元素出现的次数
  4. int[] count = new int[1001];
  5. for (int i : arr1) {
  6. count[i]++;
  7. }
  8. int[] result = new int[arr1.length];
  9. int k = 0;
  10. // 2. 如果元素有在arr2出现,添加到result中
  11. for (int i : arr2) {
  12. while (count[i] > 0) {
  13. result[k++] = i;
  14. count[i]--;
  15. }
  16. }
  17. // 3. 处理不在arr2中的元素
  18. for (int i = 0; i < count.length; i++) {
  19. while (count[i] > 0) {
  20. result[k++] = i;
  21. count[i]--;
  22. }
  23. }
  24. return result;
  25. }
  26. }

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

解法一:计数排序

  1. class Solution {
  2. public int[] frequencySort(int[] nums) {
  3. // 1. 记录每个元素出现的次数
  4. int[] count = new int[201];
  5. for (int num : nums) {
  6. // 加100是为了处理num为负数的情况
  7. count[num + 100]++;
  8. }
  9. return Arrays.stream(nums).boxed().sorted((a, b) -> {
  10. int fa = count[a + 100];
  11. int fb = count[b + 100];
  12. if(fa == fb) {
  13. // 如果频率相同,按照数值本身将它们降序排序
  14. return Integer.compare(b, a);
  15. } else {
  16. // 按照每个值的频率升序排序
  17. return fa - fb;
  18. }
  19. }).mapToInt(Integer::intValue).toArray();
  20. }
  21. }

优化:

  1. import java.util.*;
  2. class Solution {
  3. public int[] frequencySort(int[] nums) {
  4. // 1. 记录每个元素出现的次数
  5. Map<Integer, Integer> count = new HashMap<>();
  6. for (int num : nums) {
  7. count.put(num, count.getOrDefault(num, 0) + 1);
  8. }
  9. // 2. 使用 List 存储元素及其频率,方便排序
  10. List<Map.Entry<Integer, Integer>> entryList = new ArrayList<>(count.entrySet());
  11. // 3. 按照频率升序,然后数值降序排序
  12. entryList.sort((a, b) -> {
  13. if (a.getValue().equals(b.getValue())) {
  14. return Integer.compare(b.getKey(), a.getKey()); // 按数值降序
  15. }
  16. return Integer.compare(a.getValue(), b.getValue()); // 按频率升序
  17. });
  18. // 4. 构造结果数组
  19. int index = 0;
  20. for (Map.Entry<Integer, Integer> entry : entryList) {
  21. int frequency = entry.getValue();
  22. for (int i = 0; i < frequency; i++) {
  23. nums[index++] = entry.getKey();
  24. }
  25. }
  26. return nums;
  27. }
  28. }

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

解法一:暴力解。超出时间限制

  1. class Solution {
  2. public List<Integer> countSmaller(int[] nums) {
  3. List<Integer> counts = new ArrayList<>();
  4. int n = nums.length;
  5. for (int i = 0; i < n; i++) {
  6. int count = 0;
  7. for (int j = n - 1; j >= i; j--) {
  8. if (nums[j] < nums[i]) {
  9. count++;
  10. }
  11. }
  12. counts.add(count);
  13. }
  14. return counts;
  15. }
  16. }

解法二:

  1. class Solution {
  2. public List<Integer> countSmaller(int[] nums) {
  3. int n = nums.length;
  4. List<Integer> res = new ArrayList<>(n);
  5. int[] index = new int[n];
  6. int[] helper = new int[n];
  7. int[] count = new int[n];
  8. for (int i = 0; i < n; i++) {
  9. index[i] = i;
  10. }
  11. merge(nums, index, helper, count, 0, nums.length - 1);
  12. for (int i = 0; i < n; i++) {
  13. res.add(i, count[i]);
  14. }
  15. return res;
  16. }
  17. private void merge(int[] nums, int[] index, int[] helper, int[] count, int left, int right) {
  18. if (left == right || left > right)
  19. return;
  20. int mid = (left + right) >> 1;
  21. if (left < mid) {
  22. merge(nums, index, helper, count, left, mid);
  23. }
  24. if (mid + 1 < right) {
  25. merge(nums, index, helper, count, mid + 1, right);
  26. }
  27. int i = left, j = mid + 1;
  28. int hi = left;
  29. while (i <= mid && j <= right) {
  30. if (nums[index[i]] <= nums[index[j]]) {
  31. helper[hi++] = index[j++];
  32. } else {
  33. count[index[i]] += right - j + 1;
  34. helper[hi++] = index[i++];
  35. }
  36. }
  37. while (i <= mid) {
  38. helper[hi++] = index[i++];
  39. }
  40. while (j <= right) {
  41. helper[hi++] = index[j++];
  42. }
  43. for (int k = left; k <= right; k++) {
  44. index[k] = helper[k];
  45. }
  46. }
  47. }

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)

  1. public class Solution {
  2. public int[] topKFrequent(int[] nums, int k) {
  3. HashMap<Integer, Integer> count = new HashMap<>();
  4. // 1. 使用HashMap统计每个元素出现的次数
  5. for (int num : nums) {
  6. count.put(num, count.getOrDefault(num, 0) + 1);
  7. }
  8. // 2. 使用一个小根堆来维护前k个高频元素
  9. PriorityQueue<Map.Entry<Integer, Integer>> minHeap = new PriorityQueue<>(k,
  10. (a, b) -> a.getValue() - b.getValue());
  11. for (Map.Entry<Integer, Integer> entry : count.entrySet()) {
  12. minHeap.offer(entry);
  13. // 如果堆的大小超过k,移除频率最低的元素,最后剩下的k个元素即为所求
  14. if (minHeap.size() > k) {
  15. minHeap.poll();
  16. }
  17. }
  18. // 3. 获取结果
  19. int i = 0;
  20. int[] result = new int[k];
  21. while (!minHeap.isEmpty()) {
  22. result[i++] = minHeap.poll().getKey();
  23. }
  24. return result;
  25. }
  26. }

解法二:HashMap + 桶排序

  1. class Solution {
  2. public int[] topKFrequent(int[] nums, int k) {
  3. HashMap<Integer, Integer> count = new HashMap<>();
  4. List<Integer> res = new ArrayList<>();
  5. // 1. 使用HashMap统计每个元素出现的次数
  6. for (int num : nums) {
  7. count.put(num, count.getOrDefault(num, 0) + 1);
  8. }
  9. // 2. 将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
  10. List<Integer>[] list = new List[nums.length + 1];
  11. for (Integer key : count.keySet()) {
  12. int i = count.get(key);
  13. if (list[i] == null) {
  14. list[i] = new ArrayList<>();
  15. }
  16. list[i].add(key);
  17. }
  18. // 3. 倒序遍历数组获取出现频率从大到小的排列
  19. for (int i = list.length - 1; i >= 0 && res.size() < k; i--) {
  20. if (list[i] == null) {
  21. continue;
  22. }
  23. res.addAll(list[i]);
  24. }
  25. return res.stream().mapToInt(Integer::intValue).toArray();
  26. }
  27. }

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] 为 01 或 2

进阶:

  • 你能想出一个仅使用常数空间的一趟扫描算法吗?

解法一:维护三个指针,分别指向当前分析的元素、红色的末尾边界和蓝色的起始边界。

  1. class Solution {
  2. public void sortColors(int[] nums) {
  3. int zeroIndex = 0;
  4. int twoIndex = nums.length - 1;
  5. int current = 0;
  6. while (current <= twoIndex) {
  7. if (nums[current] == 0) {
  8. // 红色
  9. swap(nums, current, zeroIndex);
  10. zeroIndex++;
  11. current++;
  12. } else if (nums[current] == 2) {
  13. // 蓝色
  14. swap(nums, current, twoIndex);
  15. twoIndex--;
  16. } else {
  17. // 白色
  18. current++;
  19. }
  20. }
  21. }
  22. private void swap(int[] nums, int i, int j) {
  23. int temp = nums[i];
  24. nums[i] = nums[j];
  25. nums[j] = temp;
  26. }
  27. }

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

  1. class Solution {
  2. static class MinHeap {
  3. int[] array;
  4. int size;
  5. public MinHeap(int capacity) {
  6. array = new int[capacity];
  7. }
  8. public int peek() {
  9. return array[0];
  10. }
  11. public boolean offer(int offered) {
  12. if (size == array.length) {
  13. return false;
  14. }
  15. up(offered);
  16. size++;
  17. return true;
  18. }
  19. public void replace(int replaced) {
  20. array[0] = replaced;
  21. down(0);
  22. }
  23. private void up(int offered) {
  24. int child = size;
  25. while (child > 0) {
  26. int parent = (child - 1) >> 1;
  27. if (offered < array[parent]) {
  28. array[child] = array[parent];
  29. } else {
  30. break;
  31. }
  32. child = parent;
  33. }
  34. array[child] = offered;
  35. }
  36. private void down(int parent) {
  37. int left = (parent << 1) + 1;
  38. int right = left + 1;
  39. int min = parent;
  40. if (left < size && array[left] < array[min]) {
  41. min = left;
  42. }
  43. if (right < size && array[right] < array[min]) {
  44. min = right;
  45. }
  46. if (min != parent) {
  47. swap(min, parent);
  48. down(min);
  49. }
  50. }
  51. // 交换两个索引处的元素
  52. private void swap(int i, int j) {
  53. int t = array[i];
  54. array[i] = array[j];
  55. array[j] = t;
  56. }
  57. }
  58. public int findKthLargest(int[] nums, int k) {
  59. MinHeap heap = new MinHeap(k);
  60. // 先将k个元素入堆
  61. for (int i = 0; i < k; i++) {
  62. heap.offer(nums[i]);
  63. }
  64. // 将剩余的元素与堆顶元素比较,如果比堆顶元素大则替换。比较完成后,堆顶元素即为第k大的元素(小根堆)
  65. for (int i = k; i < nums.length; i++) {
  66. if (nums[i] > heap.peek()) {
  67. heap.replace(nums[i]);
  68. }
  69. }
  70. return heap.peek();
  71. }
  72. }

解法二:堆排序(优先队列)。执行耗时63ms

  1. class Solution {
  2. public int findKthLargest(int[] nums, int k) {
  3. PriorityQueue<Integer> minHeap = new PriorityQueue<>();
  4. for (int num : nums) {
  5. minHeap.offer(num);
  6. if(minHeap.size() > k) {
  7. minHeap.poll();
  8. }
  9. }
  10. return minHeap.peek();
  11. }
  12. }

解法三:快排(随机 + 处理重复 + 插入)。执行耗时28ms

  1. class Solution {
  2. public int findKthLargest(int[] nums, int k) {
  3. quickSort(nums, 0, nums.length - 1);
  4. return nums[nums.length - k];
  5. }
  6. private static void quickSort(int[] nums, int left, int right) {
  7. if (right - left <= 32) {
  8. insertionSort(nums, left, right);
  9. return;
  10. }
  11. int pivot = partition(nums, left, right);
  12. ;
  13. quickSort(nums, left, pivot - 1);
  14. quickSort(nums, pivot + 1, right);
  15. }
  16. private static int partition(int[] nums, int left, int right) {
  17. int idx = ThreadLocalRandom.current().nextInt(right - left + 1) + left;
  18. swap(nums, idx, left);
  19. int i = left + 1, j = right;
  20. int pivot = nums[left];
  21. while (i <= j) {
  22. while (i <= j && nums[i] < pivot) {
  23. i++;
  24. }
  25. while (i <= j && nums[j] > pivot) {
  26. j--;
  27. }
  28. if (i <= j) {
  29. swap(nums, i, j);
  30. i++;
  31. j--;
  32. }
  33. }
  34. swap(nums, j, left);
  35. return j;
  36. }
  37. private static void insertionSort(int[] nums, int left, int right) {
  38. for (int low = left + 1; low <= right; low++) {
  39. int t = nums[low];
  40. int i = low - 1;
  41. while (i >= left && t < nums[i]) {
  42. nums[i + 1] = nums[i];
  43. i--;
  44. }
  45. if (i != low - 1) {
  46. nums[i + 1] = t;
  47. }
  48. }
  49. }
  50. private static void swap(int[] nums, int i, int j) {
  51. int temp = nums[i];
  52. nums[i] = nums[j];
  53. nums[j] = temp;
  54. }
  55. }

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

注意:

  1. 给定数组的长度不会超过50000
  2. 输入数组中的所有数字都在32位整数的表示范围内。

解法一:归并排序

  1. class Solution {
  2. public int reversePairs(int[] nums) {
  3. if (nums == null || nums.length < 2) {
  4. return 0;
  5. }
  6. return mergeSort(nums, 0, nums.length - 1);
  7. }
  8. private int mergeSort(int[] nums, int left, int right) {
  9. if (left >= right) {
  10. return 0;
  11. }
  12. int mid = left + (right - left) / 2;
  13. // 统计左半部分和右半部分的翻转对
  14. int count = mergeSort(nums, left, mid) + mergeSort(nums, mid + 1, right);
  15. // 统计跨越两部分的翻转对数量
  16. count += countImportantPairs(nums, left, mid, right);
  17. // 合并两个部分
  18. merge(nums, left, mid, right);
  19. return count;
  20. }
  21. private void merge(int[] nums, int left, int mid, int right) {
  22. int[] temp = new int[right - left + 1];
  23. int i = left, j = mid + 1, k = 0;
  24. while (i <= mid && j <= right) {
  25. if (nums[i] <= nums[j]) {
  26. temp[k++] = nums[i++];
  27. } else {
  28. temp[k++] = nums[j++];
  29. }
  30. }
  31. while (i <= mid) {
  32. temp[k++] = nums[i++];
  33. }
  34. while (j <= right) {
  35. temp[k++] = nums[j++];
  36. }
  37. System.arraycopy(temp, 0, nums, left, temp.length);
  38. }
  39. private int countImportantPairs(int[] nums, int left, int mid, int right) {
  40. int count = 0;
  41. int j = mid + 1;
  42. // 使用双指针法,j 代表右边数组的指针
  43. for (int i = left; i <= mid; i++) {
  44. // 转换为long处理超出Integer.MAX_VALUE -> 2147483647的情况
  45. while (j <= right && (long) nums[i] > 2 * (long) nums[j]) {
  46. j++;
  47. }
  48. // 当前 j 的位置表示 (mid + 1) 到 j - 1 都和 nums[i] 满足条件
  49. count += (j - (mid + 1));
  50. }
  51. return count;
  52. }
  53. }

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. 比较长度和字典序:对于所有符合条件的字符串,选择最长的一个。如果有多个长度相同的字符串,则选择字典序较小的那个

  1. public String findLongestWord(String s, List<String> dictionary) {
  2. String result = "";
  3. for (String word : dictionary) {
  4. if(isSubsequence(word, s)) {
  5. // 比较长度和字典序
  6. if(word.length() > result.length() || (word.length() == result.length() && word.compareTo(result) < 0)) {
  7. result = word;
  8. }
  9. }
  10. }
  11. return result;
  12. }
  13. /**
  14. * 检查字典中的每个字符串是否为s的一个子序列
  15. * @param word
  16. * @param s
  17. * @return
  18. */
  19. private boolean isSubsequence(String word, String s) {
  20. int wIndex = 0, sIndex = 0;
  21. while(wIndex < word.length() && sIndex < s.length()) {
  22. if(word.charAt(wIndex) == s.charAt(sIndex)) {
  23. wIndex++;
  24. }
  25. sIndex++;
  26. }
  27. return wIndex == word.length();
  28. }

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) 的算法解决本问题

解法一:双指针法。大的元素添加到结果数组末尾

  1. class Solution {
  2. public int[] sortedSquares(int[] nums) {
  3. int n = nums.length;
  4. int[] result = new int[n];
  5. // 双指针法
  6. int left = 0, right = n - 1;
  7. int position = n - 1;
  8. while (left <= right) {
  9. int leftSquare = nums[left] * nums[left];
  10. int rightSquare = nums[right] * nums[right];
  11. // 大的添加到结果数组
  12. if (leftSquare > rightSquare) {
  13. result[position--] = leftSquare;
  14. left++;
  15. } else {
  16. result[position--] = rightSquare;
  17. right--;
  18. }
  19. }
  20. return result;
  21. }
  22. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/990052
推荐阅读
相关标签
  

闽ICP备14008679号