首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

排序算法知识及编程练习总结

  • 25-03-02 11:42
  • 4150
  • 11072
blog.csdn.net

目录

一、背景知识介绍

二、主流排序算法与应用

(一)主流算法介绍

(二)在框架中的应用举例

三、相关排序算法练习

(一)冒泡排序(Bubble Sort)

扩展:基本优化思路展示

扩展:鸡尾酒排序

(二) 插入排序(Insertion Sort)

扩展:优化方案(二分插入排序+希尔排序+对小规模子序列使用插入排序+优化交换操作)

(三)选择排序(Selection Sort)

(四)快速排序(Quick Sort)

扩展:优化方法(基准值的选择和随机化)

扩展:非递归实现方式

(五)归并排序(Merge Sort)

扩展:优化思路分析

(六)堆排序(Heap Sort)

扩展:Top K 问题分析

求解思路罗列

推荐解法分析

最小 Top K代码展示

最大 Top K代码展示

扩展:使用堆排序思想实现优先级队列

(七)计数排序(Counting Sort)

扩展:数组中有大量重复元素,如何高效地进行排序?

扩展:给定一组学生成绩,如何按照成绩范围进行统计?

扩展:如何对字符串数组按照字母顺序进行排序?

扩展:如何对多个关键字进行排序?

相关扩展思路代码验证

(八)桶排序(Bucket Sort)

扩展:按照年龄对一组人员进行排序

扩展:对一组考试成绩进行排序

扩展:对一组具有相同前缀的字符串进行排序

(九)基数排序(Radix Sort)

扩展:对一组手机号码进行排序

扩展:对一组身份证号码进行排序

扩展:对一组IP地址进行排序

四、总结


干货分享,感谢您的阅读!祝你逢考必过!

一、背景知识介绍

排序算法是计算机科学中的一种基本算法,用于将一组元素按照特定的顺序进行排列。排序算法在计算机领域中应用广泛,包括数据库查询、数据分析、搜索引擎、图像处理、科学计算、大数据处理等众多领域。

排序算法的背景可以追溯到很早的计算机科学历史。随着计算机的发展和普及,对于对大量数据进行排序的需求逐渐增加,人们提出了许多不同的排序算法,以满足不同场景下的排序需求。排序算法的研究旨在提高排序的效率、减少排序的时间复杂度、节省排序的空间复杂度,从而更好地应对不同规模和类型的数据。

排序算法可以根据其执行方式和性能特点进行分类,例如比较排序和非比较排序、稳定排序和非稳定排序、内部排序和外部排序等。比较排序是指通过比较元素之间的大小关系来确定元素的排序顺序,而非比较排序是指通过其他方式来确定元素的排序顺序,例如基于元素的键值、计数、桶等。稳定排序是指在排序过程中相等元素的相对顺序保持不变,而非稳定排序则没有这种保证。内部排序是指在内存中直接对数据进行排序,而外部排序是指在外存中对数据进行排序。

不同的排序算法适用于不同的排序场景。例如,快速排序和归并排序在处理大规模数据时效果较好,而插入排序和冒泡排序在处理小规模数据时可能更为高效。选择排序和堆排序在对数据进行实时排序时可能更合适。计数排序和桶排序在处理具有特定分布特性的数据时可能更加高效。因此,选择合适的排序算法对于实际应用的性能和效率非常重要。

二、主流排序算法与应用

(一)主流算法介绍

以下只进行罗列一些基本的算法,展示如下:

  1. 快速排序(Quick Sort):一种分治法的排序算法,通过选择一个基准元素将数组分为两个子数组,然后递归地对子数组进行排序。具有平均情况下较好的性能,时间复杂度为平均O(nlogn),最坏情况下O(n^2)。
  2. 归并排序(Merge Sort):一种基于分治法的排序算法,通过将数组分为两个子数组,递归地对子数组进行排序,然后合并两个已排序的子数组。具有稳定的排序性质,时间复杂度为O(nlogn)。
  3. 插入排序(Insertion Sort):一种简单的排序算法,通过将未排序的元素逐个插入已排序的子数组中,从而将整个数组排序。适合处理小规模数据,时间复杂度为平均O(n^2),但在某些情况下可以达到O(n)。
  4. 选择排序(Selection Sort):一种简单的排序算法,通过选择未排序部分的最小或最大元素,将其与已排序部分的末尾交换,从而将整个数组排序。时间复杂度为平均O(n^2)。
  5. 堆排序(Heap Sort):一种基于二叉堆的排序算法,通过构建最大堆或最小堆,将堆顶元素与末尾元素交换并调整堆,从而将整个数组排序。时间复杂度为O(nlogn)。
  6. 冒泡排序(Bubble Sort):一种简单的排序算法,通过不断交换相邻的元素,将较大或较小的元素逐渐“冒泡”到数组的末尾,从而将整个数组排序。时间复杂度为平均O(n^2)。
  7. 计数排序(Counting Sort):一种非比较排序算法,通过统计每个元素出现的次数,然后根据统计结果将元素放置到正确的位置,从而将整个数组排序。适合处理具有特定分布特性的数据,时间复杂度为O(n+k),其中k为数据范围。
  8. 桶排序(Bucket Sort):一种非比较排序算法,通过将元素分散到不同的桶中,然后对每个桶中的元素进行排序,最后合并桶中的元素,从而将整个数组排序。适合处理具有特定分布特性的数据,时间复杂度为平均O(n+k),其中k为桶的个数。

(二)在框架中的应用举例

实际应用较多,以下展示一些基本的示例:

  1. Java 标准库:Java 提供了丰富的排序算法实现,如 Arrays.sort() 和 Collections.sort() 方法,可以用于对数组和集合进行排序操作。
  2. Python 标准库:Python 也提供了多种排序算法的实现,如 sorted() 和 list.sort() 函数,可以用于对列表进行排序。
  3. JavaScript:JavaScript 语言本身提供了 Array.prototype.sort() 方法,用于对数组进行排序操作,可以根据指定的排序规则进行排序。
  4. .NET Framework:.NET Framework 中的 System.Array 类和 System.Collections.Generic.List 类都提供了排序算法的实现,可用于对数组和集合进行排序操作。
  5. 数据库:数据库系统中通常也内置了排序功能,可以通过 SQL 查询语句中的 ORDER BY 子句进行排序操作,例如在 MySQL、Oracle、SQL Server 等数据库中都可以使用 ORDER BY 子句对查询结果进行排序。
  6. Web 开发框架:在许多 Web 开发框架中,排序算法也被广泛应用于对前端展示的数据进行排序,例如在前端框架如 React、Angular、Vue 等中,可以使用内置的排序函数或自定义排序算法对前端数据进行排序展示。

三、相关排序算法练习

(一)冒泡排序(Bubble Sort)

基本知识介绍

冒泡排序(Bubble Sort)是一种简单的排序算法,它通过不断地交换相邻两个元素的位置,把最大的元素逐渐“冒泡”到数组的最后面,从而实现排序。

bubbleSort.gif

具体来说,冒泡排序的基本思想是:从数组的第一个元素开始,依次比较相邻两个元素的大小,如果它们的顺序不正确,就交换它们的位置,然后继续比较下一组相邻元素,直到数组的末尾。这个过程可以理解为每一轮都将一个最大的元素“冒泡”到了数组的最后面。

冒泡排序的时间复杂度为O(n^2),其中n是数组的长度。虽然它的效率不如其他高级排序算法,但是它简单易懂,实现也很容易,可以用来对小规模的数据进行排序。

具体代码示例

  1. package org.zyf.javabasic.letcode.sort;
  2. /**
  3. * @author yanfengzhang
  4. * @description 冒泡排序(Bubble Sort)
  5. * 通过不断地交换相邻两个元素的位置,把最大的元素逐渐“冒泡”到数组的最后面,从而实现排序.
  6. * @date 2023/4/15 22:15
  7. */
  8. public class BubbleSort {
  9. public static void bubbleSort(int[] array) {
  10. /*获取数组长度*/
  11. int n = array.length;
  12. /*外层循环控制排序轮数*/
  13. for (int i = 0; i < n; i++) {
  14. /*内层循环控制每轮比较的次数*/
  15. for (int j = 0; j < n - i - 1; j++) {
  16. /*如果顺序不正确就交换相邻两个元素的位置*/
  17. if (array[j] > array[j + 1]) {
  18. int temp = array[j];
  19. array[j] = array[j + 1];
  20. array[j + 1] = temp;
  21. }
  22. }
  23. }
  24. }
  25. public static void main(String[] args) {
  26. /*创建一个待排序的数组*/
  27. int[] array = {5, 3, 8, 6, 4};
  28. /*对数组进行排序*/
  29. bubbleSort(array);
  30. /*输出排序后的结果*/
  31. for (int i = 0; i < array.length; i++) {
  32. System.out.print(array[i] + " ");
  33. }
  34. }
  35. }

扩展:基本优化思路展示

对于冒泡排序,有一些常见的优化思路,可以提高它的效率,例如:

  1. 如果在某一轮循环中没有任何元素交换位置,说明数组已经排好序,可以直接退出循环,不必再进行后续的比较。
  2. 对于每一轮循环,如果最后一个发生交换的位置为 pos,说明从 pos+1 到数组末尾的元素已经有序了,可以直接把这部分元素排除在下一轮的比较之外。
  1. public static void bubbleSort(int[] array) {
  2. int n = array.length;
  3. /*记录最后一次交换的位置*/
  4. int lastSwappedIndex = n - 1;
  5. for (int i = 0; i < n; i++) {
  6. /*标记本轮循环是否有交换发生*/
  7. boolean swapped = false;
  8. for (int j = 0; j < lastSwappedIndex; j++) {
  9. if (array[j] > array[j + 1]) {
  10. int temp = array[j];
  11. array[j] = array[j + 1];
  12. array[j + 1] = temp;
  13. swapped = true;
  14. }
  15. }
  16. /*如果本轮循环没有交换发生,说明已经排好序*/
  17. if (!swapped) {
  18. break;
  19. }
  20. /*更新最后一次交换的位置*/
  21. lastSwappedIndex--;
  22. }
  23. }

扩展:鸡尾酒排序

鸡尾酒排序(Cocktail Sort),也称为双向冒泡排序(Bidirectional Bubble Sort)、定向冒泡排序(Cocktail Shaker Sort)等,是一种改进的冒泡排序算法。它与冒泡排序的不同之处在于,鸡尾酒排序的每一轮排序都是双向的,即从头到尾进行一次冒泡排序,然后再从尾到头进行一次冒泡排序,以此类推,直到排序完成。

在排序过程中,鸡尾酒排序可以有效地避免冒泡排序的一些缺点,比如在数组末尾有大量无序的元素时,冒泡排序的排序效率会变得很低。而鸡尾酒排序通过双向排序,可以将大的元素快速地移动到数组末尾,并将小的元素快速地移动到数组开头,从而提高了排序的效率。

因此,可以说鸡尾酒排序是冒泡排序的一种优化算法,它可以更快地完成排序任务,并且在某些特定情况下比冒泡排序更加适用。具体步骤如下:

  1. 从左向右进行一次冒泡排序,将最大的元素放到数组的末尾。
  2. 从右向左进行一次冒泡排序,将最小的元素放到数组的开头。
  3. 重复步骤1和2,直到整个数组有序。

这里需要注意的是,每次双向冒泡排序都需要记录当前排序的边界,即已经排好序的部分的位置。在第一次从左向右排序时,最后一个交换元素的位置就是当前的边界。在第一次从右向左排序时,第一个交换元素的位置就是当前的边界。以后每次排序时,边界会根据上一次排序的结果进行更新。

需要注意的是,当数组已经排好序时,鸡尾酒排序仍然会执行完所有的双向冒泡排序,因此最坏情况下的时间复杂度仍然是 O(n^2)。但在一般情况下,鸡尾酒排序的效率要比冒泡排序高。

  1. public static void cocktailSort(int[] arr) {
  2. /*标志位,记录是否进行过交换*/
  3. boolean swapped = true;
  4. /*从左向右排序的起始位置*/
  5. int start = 0;
  6. /*从右向左排序的起始位置*/
  7. int end = arr.length - 1;
  8. /*只要还有元素需要排序就继续循环*/
  9. while (swapped) {
  10. swapped = false;
  11. /*从左向右进行一次冒泡排序,将最大的元素放到数组的末尾*/
  12. for (int i = start; i < end; i++) {
  13. if (arr[i] > arr[i + 1]) {
  14. /*交换两个元素的位置*/
  15. swap(arr, i, i + 1);
  16. /*标记已经进行过交换*/
  17. swapped = true;
  18. }
  19. }
  20. /*如果没有进行过交换,则说明数组已经有序,直接退出循环*/
  21. if (!swapped) {
  22. break;
  23. }
  24. swapped = false;
  25. /*更新从右向左排序的起始位置*/
  26. end--;
  27. /*从右向左进行一次冒泡排序,将最小的元素放到数组的开头*/
  28. for (int i = end - 1; i >= start; i--) {
  29. if (arr[i] > arr[i + 1]) {
  30. swap(arr, i, i + 1);
  31. swapped = true;
  32. }
  33. }
  34. /*更新从左向右排序的起始位置*/
  35. start++;
  36. }
  37. }
  38. /*交换数组中两个元素的位置*/
  39. private static void swap(int[] arr, int i, int j) {
  40. int temp = arr[i];
  41. arr[i] = arr[j];
  42. arr[j] = temp;
  43. }

(二) 插入排序(Insertion Sort)

 基本知识介绍

插入排序是一种简单直观的排序算法,其基本思想是将一个记录插入到已经排好序的有序序列中,直到整个序列有序为止。

其具体步骤如下:

  1. 将数组的第一个元素视为有序序列,第二个元素到最后一个元素视为待排序序列。
  2. 从待排序序列中取出一个元素,将其插入到已排序序列中的适当位置,使得插入后的序列仍然有序。
  3. 重复步骤2,直到待排序序列为空。

在插入排序中,我们通常使用交换或移动元素的方法来实现元素的插入操作。如果使用交换元素的方法,则时间复杂度为O(n^2);如果使用移动元素的方法,则时间复杂度为 O(n)。在实际应用中,我们通常采用后者来实现插入排序。插入排序虽然时间复杂度较高,但由于其代码实现简单、稳定性好等优点,在小规模数据排序时仍然被广泛使用。

具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. /**
  3. * @author yanfengzhang
  4. * @description 插入排序(Insertion Sort)
  5. * 基本思想是将一个记录插入到已经排好序的有序序列中,直到整个序列有序为止。
  6. * @date 2023/4/15 22:40
  7. */
  8. public class InsertionSort {
  9. public static void insertionSort(int[] arr) {
  10. int n = arr.length;
  11. /*i从1开始,因为第一个元素已经是有序的*/
  12. for (int i = 1; i < n; i++) {
  13. /*待插入的元素,当前待排序元素*/
  14. int key = arr[i];
  15. /*从已排序序列中的最后一个元素开始比较*/
  16. int j = i - 1;
  17. /*将已排序序列中大于待插入元素的元素依次后移*/
  18. while (j >= 0 && arr[j] > key) {
  19. arr[j + 1] = arr[j];
  20. j--;
  21. }
  22. /*将待插入元素插入到已排序序列中的合适位置*/
  23. arr[j + 1] = key;
  24. }
  25. }
  26. public static void main(String[] args) {
  27. /*创建一个待排序的数组*/
  28. int[] array = {5, 3, 8, 6, 4};
  29. /*对数组进行排序*/
  30. insertionSort(array);
  31. /*输出排序后的结果*/
  32. for (int i = 0; i < array.length; i++) {
  33. System.out.print(array[i] + " ");
  34. }
  35. }
  36. }

扩展:优化方案(二分插入排序+希尔排序+对小规模子序列使用插入排序+优化交换操作)

插入排序虽然简单易懂,但它的时间复杂度为 O(n^2),对于大规模数据的排序效率较低。在实际应用中,我们可以对插入排序进行一些优化,从而提高其效率。以下是一些常用的插入排序优化方法:

  1. 二分插入排序:在已排序序列中采用二分查找的方式,找到待插入元素的插入位置,从而减少比较次数。时间复杂度为 O(n \log n),略优于插入排序的 O(n^2)。
  2. 希尔排序:希尔排序是插入排序的一种改进版,它通过将待排序序列分为若干子序列,对每个子序列进行插入排序,从而使得整个序列逐渐有序。时间复杂度为 O(n \log n)$或 O(n^{\frac{4}{3}}),具体取决于子序列的划分方法。
  3. 对小规模子序列使用插入排序:对于小规模的子序列,插入排序的效率往往比快速排序等高级排序算法更高。因此,我们可以在快速排序等高级排序算法中,对小规模子序列采用插入排序来进行排序,从而提高整个算法的效率。
  4. 优化交换操作:在插入排序中,交换元素的次数和比较元素的次数都会对效率产生影响。为了减少交换操作的次数,我们可以在每次比较时,将待插入元素的值存储在一个临时变量中,等待所有比它大的元素都后移后,再将它插入到应该插入的位置上。这样一来,交换操作就只需要进行一次,而不是每次比较都进行一次。

以上是常用的几种插入排序的优化方法,通过对这些优化方法的组合使用,我们可以使得插入排序在实际应用中表现更加出色。

  1. public static void insertionSortOpti(int[] arr) {
  2. int n = arr.length;
  3. /*优化1:二分插入排序*/
  4. for (int i = 1; i < n; i++) {
  5. int left = 0, right = i - 1;
  6. int target = arr[i];
  7. while (left <= right) {
  8. int mid = left + (right - left) / 2;
  9. if (arr[mid] > target) {
  10. right = mid - 1;
  11. } else {
  12. left = mid + 1;
  13. }
  14. }
  15. for (int j = i; j > left; j--) {
  16. arr[j] = arr[j - 1];
  17. }
  18. arr[left] = target;
  19. }
  20. /*优化2:希尔排序*/
  21. for (int gap = n / 2; gap > 0; gap /= 2) {
  22. for (int i = gap; i < n; i++) {
  23. int j = i;
  24. int temp = arr[j];
  25. while (j - gap >= 0 && arr[j - gap] > temp) {
  26. arr[j] = arr[j - gap];
  27. j -= gap;
  28. }
  29. arr[j] = temp;
  30. }
  31. }
  32. /*优化3:对小规模子序列使用插入排序*/
  33. for (int i = 1; i < n; i++) {
  34. for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
  35. swap(arr, j, j - 1);
  36. }
  37. }
  38. /*优化4:优化交换操作*/
  39. for (int i = 1; i < n; i++) {
  40. int temp = arr[i];
  41. int j;
  42. for (j = i; j > 0 && arr[j - 1] > temp; j--) {
  43. arr[j] = arr[j - 1];
  44. }
  45. arr[j] = temp;
  46. }
  47. }
  48. private static void swap(int[] arr, int i, int j) {
  49. int temp = arr[i];
  50. arr[i] = arr[j];
  51. arr[j] = temp;
  52. }

(三)选择排序(Selection Sort)

 基本知识介绍

选择排序的基本思路是:每次从待排序的序列中找到最小(或最大)的元素,放到已排序序列的末尾。具体实现时,我们通过两层循环来找到最小元素的下标,然后将该元素与序列的起始位置进行交换,从而将该元素放到已排序序列的末尾。通过不断重复这个过程,直到整个序列都排好序为止。

选择排序的时间复杂度是 O(n^2),比较稳定,适用于对于数据规模较小的排序任务。它的优点在于不需要额外的存储空间,但是它的缺点是效率不够高,尤其是在数据规模较大时。因此,通常在实际应用中,选择排序并不是首选的排序算法。

选择排序的主要特点是交换次数比较少,但是比较次数较多,因此可以通过减少比较次数来优化它的性能。例如,可以通过记录每次查找到的最小元素的下标,而不是直接交换元素来减少交换次数。另外,也可以通过增加一个有序区域来减少比较次数,具体的实现可以参考改进后的选择排序算法。

具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. /**
  3. * @author yanfengzhang
  4. * @description 选择排序算法
  5. * 选择排序的基本思路是:每次从待排序的序列中找到最小(或最大)的元素,放到已排序序列的末尾。
  6. * 具体实现时,我们通过两层循环来找到最小元素的下标,然后将该元素与序列的起始位置进行交换,
  7. * 从而将该元素放到已排序序列的末尾。通过不断重复这个过程,直到整个序列都排好序为止。
  8. * @date 2023/4/15 23:10
  9. */
  10. public class SelectionSort {
  11. /**
  12. * 选择排序算法
  13. *
  14. * @param arr 待排序的数组
  15. */
  16. public static void selectionSort(int[] arr) {
  17. int n = arr.length;
  18. /*依次选择未排序区间的最小值并将其放到已排序区间的末尾*/
  19. for (int i = 0; i < n - 1; i++) {
  20. /*记录未排序区间的最小值下标*/
  21. int minIndex = i;
  22. /*在未排序区间中寻找最小值*/
  23. for (int j = i + 1; j < n; j++) {
  24. if (arr[j] < arr[minIndex]) {
  25. minIndex = j;
  26. }
  27. }
  28. /*将未排序区间的最小值与已排序区间的末尾交换位置*/
  29. if (minIndex != i) {
  30. swap(arr, i, minIndex);
  31. }
  32. }
  33. }
  34. /**
  35. * 交换数组中的两个元素
  36. *
  37. * @param arr 数组
  38. * @param i 第一个元素的下标
  39. * @param j 第二个元素的下标
  40. */
  41. private static void swap(int[] arr, int i, int j) {
  42. int temp = arr[i];
  43. arr[i] = arr[j];
  44. arr[j] = temp;
  45. }
  46. public static void main(String[] args) {
  47. /*创建一个待排序的数组*/
  48. int[] array = {5, 3, 8, 6, 4};
  49. /*对数组进行排序*/
  50. selectionSort(array);
  51. /*输出排序后的结果*/
  52. for (int i = 0; i < array.length; i++) {
  53. System.out.print(array[i] + " ");
  54. }
  55. }
  56. }

(四)快速排序(Quick Sort)

基本知识介绍

快速排序(Quick Sort)是一种基于分治思想的高效排序算法,由C. A. R. Hoare在1960年提出。它的基本思路是选择一个元素作为基准值(Pivot),将待排序序列分割成两个子序列:小于等于基准值的元素序列和大于基准值的元素序列,然后对这两个子序列递归地进行快速排序。

 具体来说,快速排序的实现步骤如下:

  1. 选取基准值:从待排序序列中任选一个元素作为基准值。
  2. 分割操作:通过一趟排序将待排序序列分割成两个部分,其中一部分的所有元素均小于等于基准值,另一部分的所有元素均大于基准值,并把基准值放在它们之间。
  3. 递归地对子序列进行快速排序:递归地对小于等于基准值的子序列和大于基准值的子序列进行快速排序。
  4. 合并操作:已经有序的子序列无需合并。

快速排序算法的时间复杂度为 O(n\log n),在处理大规模数据时效率较高。但在极端情况下,比如待排序序列已经有序或基本有序时,快速排序的时间复杂度会退化到 O(n^2),因此需要针对这种情况进行优化。

具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. /**
  3. * @author yanfengzhang
  4. * @description 快速排序(Quick Sort)
  5. * 基本思路是选择一个元素作为基准值(Pivot),将待排序序列分割成两个子序列:
  6. * 小于等于基准值的元素序列和大于基准值的元素序列,然后对这两个子序列递归地进行快速排序。
  7. * @date 2023/4/15 23:28
  8. */
  9. public class QuickSort {
  10. /**
  11. * 快速排序算法
  12. *
  13. * @param arr 待排序的数组
  14. * @param left 待排序序列的左端点
  15. * @param right 待排序序列的右端点
  16. */
  17. public static void quickSort(int[] arr, int left, int right) {
  18. /*递归结束条件:子序列的长度为1或0*/
  19. if (left >= right) {
  20. return;
  21. }
  22. /*进行分割操作*/
  23. int pivotIndex = partition(arr, left, right);
  24. /*递归地对小于等于基准值的子序列进行快速排序*/
  25. quickSort(arr, left, pivotIndex - 1);
  26. /*递归地对大于基准值的子序列进行快速排序*/
  27. quickSort(arr, pivotIndex + 1, right);
  28. }
  29. /**
  30. * 分割操作
  31. *
  32. * @param arr 待分割的数组
  33. * @param left 待分割序列的左端点
  34. * @param right 待分割序列的右端点
  35. * @return 分割元素的下标
  36. */
  37. private static int partition(int[] arr, int left, int right) {
  38. /*选取基准值*/
  39. int pivot = arr[left];
  40. int i = left, j = right;
  41. /*当 i 和 j 相遇时退出循环*/
  42. while (i < j) {
  43. /*从右往左找到第一个小于基准值的元素*/
  44. while (i < j && arr[j] >= pivot) {
  45. j--;
  46. }
  47. /*将该元素放到基准值的左边*/
  48. arr[i] = arr[j];
  49. /*从左往右找到第一个大于基准值的元素*/
  50. while (i < j && arr[i] <= pivot) {
  51. i++;
  52. }
  53. /*将该元素放到基准值的右边*/
  54. arr[j] = arr[i];
  55. }
  56. /*将基准值放到最终位置*/
  57. arr[i] = pivot;
  58. /*返回基准值的下标*/
  59. return i;
  60. }
  61. public static void main(String[] args) {
  62. /*创建一个待排序的数组*/
  63. int[] array = {5, 3, 8, 6, 4};
  64. /*对数组进行排序*/
  65. quickSort(array, 0, 4);
  66. /*输出排序后的结果*/
  67. for (int i = 0; i < array.length; i++) {
  68. System.out.print(array[i] + " ");
  69. }
  70. }
  71. }

扩展:优化方法(基准值的选择和随机化)

快速排序算法的优化主要集中在两个方面:基准值的选择和随机化。

一般来说,选择合适的基准值可以有效减少快速排序的时间复杂度。如果选择的基准值不够随机或者不够优秀,快速排序的时间复杂度可能会退化到 O(n^2)。以下是一些常见的基准值选择方法:

  • 选择第一个元素或最后一个元素作为基准值。这种方法非常简单,但是在特定情况下容易退化。
  • 选择中间元素作为基准值。这种方法比较合理,但是在序列有序的情况下仍然会退化。
  • 选择随机元素作为基准值。这种方法可以避免序列已经有序的情况,但是需要额外的随机数生成操作。

另一个常见的优化方法是随机化。在快速排序过程中,如果每次选择的基准值都是最小或最大的元素,算法的时间复杂度将达到 O(n^2)。为了避免这种情况,可以随机打乱序列,使得每个元素被选为基准值的概率相等,从而保证算法的期望时间复杂度为 O(n\log n)。

以下是使用随机化和三数取中法的优化版本的Java代码实现:

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.Random;
  3. /**
  4. * @author yanfengzhang
  5. * @description 快速排序算法(优化版)
  6. * @date 2023/4/15 23:34
  7. */
  8. public class QuickSortOpti {
  9. /**
  10. * 快速排序算法(优化版)
  11. *
  12. * @param arr 待排序的数组
  13. * @param left 待排序序列的左端点
  14. * @param right 待排序序列的右端点
  15. */
  16. public static void quickSort(int[] arr, int left, int right) {
  17. /*递归结束条件:子序列的长度为1或0*/
  18. if (left >= right) {
  19. return;
  20. }
  21. /*选择基准值并将其交换到序列的左端点*/
  22. int pivotIndex = left + new Random().nextInt(right - left + 1);
  23. swap(arr, left, pivotIndex);
  24. /*使用三数取中法选择合适的基准值*/
  25. int pivot = medianOfThree(arr, left, right);
  26. /*进行分割操作*/
  27. pivotIndex = partition(arr, left, right, pivot);
  28. /*递归地对小于等于基准值的子序列进行快速排序*/
  29. quickSort(arr, left, pivotIndex - 1);
  30. /*递归地对大于基准值的子序列进行快速排序*/
  31. quickSort(arr, pivotIndex + 1, right);
  32. }
  33. /**
  34. * 交换数组中两个位置的元素
  35. *
  36. * @param arr 待交换元素所在的数组
  37. * @param i 第一个元素的下标
  38. * @param j 第二个元素的下标
  39. */
  40. private static void swap(int[] arr, int i, int j) {
  41. int temp = arr[i];
  42. arr[i] = arr[j];
  43. arr[j] = temp;
  44. }
  45. /**
  46. * 三数取中法选择基准值
  47. *
  48. * @param arr 待选取的数组
  49. * @param left 待选取数组的左端点
  50. * @param right 待选取数组的右端点
  51. * @return 选取的基准值
  52. */
  53. private static int medianOfThree(int[] arr, int left, int right) {
  54. int mid = (left + right) / 2;
  55. if (arr[left] > arr[mid]) {
  56. swap(arr, left, mid);
  57. }
  58. if (arr[mid] > arr[right]) {
  59. swap(arr, mid, right);
  60. }
  61. if (arr[left] > arr[mid]) {
  62. swap(arr, left, mid);
  63. }
  64. return arr[mid];
  65. }
  66. /**
  67. * 将待排序序列分割成两个子序列
  68. *
  69. * @param arr 待分割序列
  70. * @param left 待分割序列的左端点
  71. * @param right 待分割序列的右端点
  72. * @param pivot 基准值
  73. * @return 基准值的最终位置
  74. */
  75. private static int partition(int[] arr, int left, int right, int pivot) {
  76. int i = left + 1, j = right;
  77. while (true) {
  78. /*找到第一个大于基准值的元素*/
  79. while (i < right && arr[i] <= pivot) {
  80. i++;
  81. }
  82. /*找到第一个小于基准值的元素*/
  83. while (j > left && arr[j] >= pivot) {
  84. j--;
  85. }
  86. /*循环结束条件*/
  87. if (i >= j) {
  88. break;
  89. }
  90. /*交换左右两个元素*/
  91. swap(arr, i, j);
  92. i++;
  93. j--;
  94. }
  95. /*将基准值交换到正确的位置*/
  96. swap(arr, left, j);
  97. return j;
  98. }
  99. public static void main(String[] args) {
  100. /*创建一个待排序的数组*/
  101. int[] array = {5, 3, 8, 6, 4};
  102. /*对数组进行排序*/
  103. quickSort(array, 0, 4);
  104. /*输出排序后的结果*/
  105. for (int i = 0; i < array.length; i++) {
  106. System.out.print(array[i] + " ");
  107. }
  108. }
  109. }

扩展:非递归实现方式

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.Random;
  3. import java.util.Stack;
  4. /**
  5. * @author yanfengzhang
  6. * @description 迭代快速排序
  7. * @date 2023/4/15 23:37
  8. */
  9. public class IterativeQuickSort {
  10. /**
  11. * 迭代快速排序
  12. *
  13. * @param arr 待排序的数组
  14. * @param left 待排序序列的左端点
  15. * @param right 待排序序列的右端点
  16. */
  17. public static void iterativeQuickSort(int[] arr, int left, int right) {
  18. Stack stack = new Stack<>();
  19. stack.push(left);
  20. stack.push(right);
  21. while (!stack.isEmpty()) {
  22. int r = stack.pop();
  23. int l = stack.pop();
  24. if (l >= r) {
  25. continue;
  26. }
  27. int pivotIndex = partition(arr, l, r);
  28. stack.push(l);
  29. stack.push(pivotIndex - 1);
  30. stack.push(pivotIndex + 1);
  31. stack.push(r);
  32. }
  33. }
  34. /**
  35. * 分割操作
  36. *
  37. * @param arr 待分割的数组
  38. * @param left 待分割序列的左端点
  39. * @param right 待分割序列的右端点
  40. * @return 基准值的下标
  41. */
  42. private static int partition(int[] arr, int left, int right) {
  43. // 选择基准值并将其交换到序列的左端点*/
  44. int pivotIndex = left + new Random().nextInt(right - left + 1);
  45. swap(arr, left, pivotIndex);
  46. int pivot = arr[left];
  47. int i = left + 1;
  48. int j = right;
  49. while (i <= j) {
  50. while (i <= j && arr[i] <= pivot) {
  51. i++;
  52. }
  53. while (i <= j && arr[j] > pivot) {
  54. j--;
  55. }
  56. if (i < j) {
  57. swap(arr, i, j);
  58. }
  59. }
  60. swap(arr, left, j);
  61. return j;
  62. }
  63. /**
  64. * 交换数组中两个位置的元素
  65. *
  66. * @param arr 待交换元素所在的数组
  67. * @param i 第一个元素的下标
  68. * @param j 第二个元素的下标
  69. */
  70. private static void swap(int[] arr, int i, int j) {
  71. int temp = arr[i];
  72. arr[i] = arr[j];
  73. arr[j] = temp;
  74. }
  75. public static void main(String[] args) {
  76. /*创建一个待排序的数组*/
  77. int[] array = {5, 3, 8, 6, 4};
  78. /*对数组进行排序*/
  79. iterativeQuickSort(array, 0, 4);
  80. /*输出排序后的结果*/
  81. for (int i = 0; i < array.length; i++) {
  82. System.out.print(array[i] + " ");
  83. }
  84. }
  85. }

(五)归并排序(Merge Sort)

基本知识介绍

归并排序是一种经典的基于比较的排序算法,它采用分治的思想来进行排序,其基本思路可以概括为以下三个步骤:

  1. 分解:将待排序的n个元素分成各含n/2个元素的子序列,递归地将子序列进行归并排序,直到子序列中只剩下一个元素。
  2. 合并:将两个已排好序的子序列合并成一个有序序列,即将两个有序序列不断地按照大小顺序取出元素,直到其中一个序列的元素全部取完,然后将剩余的序列中的元素直接添加到有序序列的末尾。
  3. 输出:当子序列的长度达到n时,整个序列也被排序完成。

归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),是一种稳定的排序算法。

具体实现中,可以采用递归或迭代两种方式实现归并排序,其中迭代实现的归并排序常被称为“自底向上的归并排序”。

具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. /**
  3. * @author yanfengzhang
  4. * @description 归并排序算法
  5. * 采用分治的思想来进行排序,其基本思路可以概括为以下三个步骤:
  6. * 分解:将待排序的n个元素分成各含n/2个元素的子序列,递归地将子序列进行归并排序,直到子序列中只剩下一个元素。
  7. * 合并:将两个已排好序的子序列合并成一个有序序列,即将两个有序序列不断地按照大小顺序取出元素,直到其中一个序列的元素全部取完,然后将剩余的序列中的元素直接添加到有序序列的末尾。
  8. * 输出:当子序列的长度达到n时,整个序列也被排序完成。
  9. * @date 2023/4/15 23:50
  10. */
  11. public class MergeSort {
  12. /**
  13. * 归并排序算法
  14. *
  15. * @param arr 待排序的数组
  16. * @param left 待排序序列的左端点
  17. * @param right 待排序序列的右端点
  18. */
  19. public static void mergeSort(int[] arr, int left, int right) {
  20. /*递归结束条件:子序列的长度为1或0*/
  21. if (left >= right) {
  22. return;
  23. }
  24. /*计算中间位置*/
  25. int mid = left + (right - left) / 2;
  26. /*对左半部分进行归并排序*/
  27. mergeSort(arr, left, mid);
  28. /*对右半部分进行归并排序*/
  29. mergeSort(arr, mid + 1, right);
  30. /*合并左右两个有序序列*/
  31. merge(arr, left, mid, right);
  32. }
  33. /**
  34. * 将两个有序数组合并成一个有序数组
  35. *
  36. * @param arr 待合并的数组
  37. * @param left 左边有序数组的起始下标
  38. * @param mid 中间位置下标
  39. * @param right 右边有序数组的结束下标
  40. */
  41. private static void merge(int[] arr, int left, int mid, int right) {
  42. /*用于存放合并后的有序序列*/
  43. int[] temp = new int[right - left + 1];
  44. /*i和j分别指向左右两个有序数组的起始下标,k指向临时数组的起始下标*/
  45. int i = left, j = mid + 1, k = 0;
  46. while (i <= mid && j <= right) {
  47. if (arr[i] <= arr[j]) {
  48. temp[k++] = arr[i++];
  49. } else {
  50. temp[k++] = arr[j++];
  51. }
  52. }
  53. while (i <= mid) {
  54. temp[k++] = arr[i++];
  55. }
  56. while (j <= right) {
  57. temp[k++] = arr[j++];
  58. }
  59. /*将临时数组中的元素复制回原数组*/
  60. System.arraycopy(temp, 0, arr, left, temp.length);
  61. }
  62. public static void main(String[] args) {
  63. /*创建一个待排序的数组*/
  64. int[] array = {5, 3, 8, 6, 4};
  65. /*对数组进行排序*/
  66. mergeSort(array, 0, 4);
  67. /*输出排序后的结果*/
  68. for (int i = 0; i < array.length; i++) {
  69. System.out.print(array[i] + " ");
  70. }
  71. }
  72. }

扩展:优化思路分析

归并排序已经是一个时间复杂度为 O(nlogn) 的高效排序算法了,不过还是有一些优化思路可以提高其效率,主要包括以下几点:

  1. 对小规模子数组使用插入排序:当待排序的子数组长度比较小的时候,使用插入排序会比归并排序更加高效。一般来说,当子数组长度小于 15 左右时,使用插入排序效果最好。
  2. 对已经有序的子数组进行合并:当左右两个子数组都已经有序时,可以直接将它们合并而不需要再进行排序操作。这样可以节省一定的时间复杂度。
  3. 避免重复地创建辅助数组:在递归调用归并排序的过程中,会多次创建新的辅助数组。为了避免这种情况,可以在程序初始化的时候就创建一个足够大的辅助数组,然后在每次递归调用时都传入该数组的引用即可。
  4. 自底向上地进行归并排序:在对一个大数组进行排序时,归并排序需要递归调用很多次,这会带来较大的函数调用和栈空间的消耗。为了避免这种情况,可以使用自底向上的归并排序,从小的子数组开始逐渐合并到大的数组。

需要注意的是,虽然归并排序的时间复杂度已经非常高效,但是其空间复杂度仍然比较高,需要使用辅助数组来进行排序,因此在排序大规模数据时需要注意空间限制。

建议:当数据量较小时,插入排序的性能优于归并排序,因此可以在递归过程中判断子序列的长度,当其小于一定值时采用插入排序来优化归并排序的性能。

代码展示如下(只展示了mergeSort方法的修改部分):

  1. /**
  2. * 归并排序算法(优化版)
  3. *
  4. * @param arr 待排序的数组
  5. * @param left 待排序序列的左端点
  6. * @param right 待排序序列的右端点
  7. */
  8. public static void mergeSort(int[] arr, int left, int right) {
  9. /*递归结束条件:子序列的长度为1或0*/
  10. if (left >= right) {
  11. return;
  12. }
  13. int mid = left + (right - left) / 2;
  14. /*当子序列长度小于等于阈值时采用插入排序进行优化*/
  15. if (right - left + 1 <= INSERTION_SORT_THRESHOLD) {
  16. insertionSort(arr, left, right);
  17. } else {
  18. /*递归地对左半部分进行归并排序*/
  19. mergeSort(arr, left, mid);
  20. /*递归地对右半部分进行归并排序*/
  21. mergeSort(arr, mid + 1, right);
  22. /*对于已经有序的两个子序列不进行合并操作*/
  23. if (arr[mid] > arr[mid + 1]) {
  24. /*合并两个有序子序列*/
  25. merge(arr, left, mid, right);
  26. }
  27. }
  28. }
  29. /**
  30. * 插入排序算法(用于优化归并排序)
  31. *
  32. * @param arr 待排序的数组
  33. * @param left 待排序序列的左端点
  34. * @param right 待排序序列的右端点
  35. */
  36. private static void insertionSort(int[] arr, int left, int right) {
  37. for (int i = left + 1; i <= right; i++) {
  38. int temp = arr[i];
  39. int j = i - 1;
  40. while (j >= left && arr[j] > temp) {
  41. arr[j + 1] = arr[j];
  42. j--;
  43. }
  44. arr[j + 1] = temp;
  45. }
  46. }

(六)堆排序(Heap Sort)

基本知识介绍

堆排序是一种利用堆数据结构进行排序的算法,可以看作是一种选择排序。堆是一种特殊的树形数据结构,它满足堆的性质:对于每个非叶节点的值都大于等于(或小于等于)其左右子节点的值。在堆中,最大元素总是位于根节点,因此堆排序也称为选择排序的一种。

堆排序的基本思想是将待排序的序列构建成一个大根堆(或小根堆),然后将根节点(最大元素)与序列最后一个元素交换,然后对前面的序列重新构建堆,重复这个过程直到序列排序完成。

堆排序的时间复杂度为 O(n\log n),其中 n$是待排序序列的长度,空间复杂度为 O(1),是一种原地排序算法。

堆排序的具体步骤如下:

  1. 构建初始堆:将待排序序列构造成一个大根堆(或小根堆)。
  2. 将根节点与最后一个节点交换位置,最大值就移动到了序列末尾。
  3. 重新调整堆:将剩余 n-1 个节点重新构造成一个堆,重复步骤2直到排序完成。

由于堆排序对原序列的要求不高,适用于一些数据规模比较大的场景,因此在实际应用中被广泛使用。

具体代码展示

这里使用的是最大堆进行排序。堆排序的时间复杂度为 O(nlogn),空间复杂度为 O(1)。

  1. package org.zyf.javabasic.letcode.sort;
  2. /**
  3. * @author yanfengzhang
  4. * @description 堆排序的时间复杂度为 O(nlogn),空间复杂度为 O(1)。
  5. * 使用的是最大堆进行排序
  6. * @date 2023/4/16 00:13
  7. */
  8. public class HeapSort {
  9. /**
  10. * 堆排序算法
  11. *
  12. * @param arr 待排序的数组
  13. */
  14. public static void heapSort(int[] arr) {
  15. if (arr == null || arr.length == 0) {
  16. return;
  17. }
  18. int n = arr.length;
  19. /*构建初始堆*/
  20. for (int i = n / 2 - 1; i >= 0; i--) {
  21. heapify(arr, i, n);
  22. }
  23. /*排序*/
  24. for (int i = n - 1; i >= 0; i--) {
  25. swap(arr, 0, i);
  26. heapify(arr, 0, i);
  27. }
  28. }
  29. /**
  30. * 对指定节点进行堆调整
  31. *
  32. * @param arr 待调整的数组
  33. * @param i 待调整的节点
  34. * @param size 堆的大小
  35. */
  36. private static void heapify(int[] arr, int i, int size) {
  37. int largest = i;
  38. /*左孩子节点*/
  39. int l = 2 * i + 1;
  40. /*右孩子节点*/
  41. int r = 2 * i + 2;
  42. if (l < size && arr[l] > arr[largest]) {
  43. largest = l;
  44. }
  45. if (r < size && arr[r] > arr[largest]) {
  46. largest = r;
  47. }
  48. if (largest != i) {
  49. swap(arr, i, largest);
  50. heapify(arr, largest, size);
  51. }
  52. }
  53. /**
  54. * 交换数组中两个位置的元素
  55. *
  56. * @param arr 待交换元素所在的数组
  57. * @param i 第一个元素的下标
  58. * @param j 第二个元素的下标
  59. */
  60. private static void swap(int[] arr, int i, int j) {
  61. int temp = arr[i];
  62. arr[i] = arr[j];
  63. arr[j] = temp;
  64. }
  65. public static void main(String[] args) {
  66. /*创建一个待排序的数组*/
  67. int[] array = {5, 3, 8, 6, 4};
  68. /*对数组进行排序*/
  69. heapSort(array);
  70. /*输出排序后的结果*/
  71. for (int i = 0; i < array.length; i++) {
  72. System.out.print(array[i] + " ");
  73. }
  74. }
  75. }

扩展:Top K 问题分析

Top K 问题是指从一个集合中找出前 K 个最大或最小的元素的问题。通常,这个集合可以是一个数组、列表、堆、树等数据结构。

求解思路罗列

求最大的 K 个元素可以使用以下几种方法:

  1. 排序法:将集合进行排序,然后取前 K 个元素,时间复杂度为 O(NlogN)。
  2. 堆(优先队列)法:使用堆(通常是最大堆)来维护 K 个最大的元素,时间复杂度为 O(NlogK)。
  3. 快速选择法:基于快速排序的思想,通过每次选择一个基准元素进行分区,直到找到第 K 大的元素,时间复杂度为平均情况下 O(N),最坏情况下 O(N^2)。
  4. 部分排序法:类似于快速选择,但不需要完全排序,只需要部分排序直到找到第 K 大的元素,时间复杂度为平均情况下 O(N),最坏情况下 O(N^2)。

求最小的 K 个元素可以使用以下几种方法:

  1. 排序法:将集合进行排序,然后取前 K 个元素,时间复杂度为 O(NlogN)。
  2. 堆(优先队列)法:使用堆(通常是最小堆)来维护 K 个最小的元素,时间复杂度为 O(NlogK)。
  3. 快速选择法:类似于求最大 K 个元素的快速选择法,但是需要找第 K 小的元素,时间复杂度为平均情况下 O(N),最坏情况下 O(N^2)。
  4. 部分排序法:类似于求最大 K 个元素的部分排序法,但是需要找第 K 小的元素,时间复杂度为平均情况下 O(N),最坏情况下 O(N^2)。

这些方法在不同的情况下有不同的适用性和性能表现,具体选择哪种方法取决于问题的规模、要求和性能需求。

推荐解法分析

推荐使用堆(优先队列)法来解决 Top K 问题,因为它在时间复杂度和空间复杂度上都有较好的性能。堆是一种特殊的二叉树,通常使用数组来实现,可以高效地维护一个部分有序的集合。在解决 Top K 问题时,可以使用最大堆或最小堆来维护 K 个最大或最小的元素,具体选择最大堆还是最小堆取决于问题的要求。

使用堆的优势在于:

  1. 时间复杂度较低:堆的插入、删除、获取最值等操作的时间复杂度都是 O(logN),其中 N 是集合的大小,对于大规模的数据集,性能较好。
  2. 空间复杂度较低:堆只需要额外的 O(K) 的空间来维护 K 个最大或最小的元素,相比于其他方法如排序法需要额外的 O(N) 空间来排序,节省了空间。
  3. 实现简单:使用优先队列(堆)的接口和方法较为简单,易于实现和理解。

需要注意的是,堆法的适用性和性能也受到具体问题的影响,例如对于较小规模的数据集或对时间复杂度要求不高的场合,排序法、快速选择法或部分排序法等方法也可能是合适的选择。因此,在选择解决 Top K 问题的算法时,应根据具体情况综合考虑问题规模、要求和性能需求。

最小 Top K代码展示

以下是使用堆排序(最小堆)求最小 Top K 的 Java 代码示例:

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.PriorityQueue;
  3. /**
  4. * @author yanfengzhang
  5. * @description 使用堆排序(最小堆)求最小 Top K
  6. * @date 2023/4/16 12:13
  7. */
  8. public class MinTopK {
  9. public static int[] getMinTopK(int[] arr, int k) {
  10. if (arr == null || arr.length == 0 || k <= 0 || k > arr.length) {
  11. return new int[0];
  12. }
  13. /*创建一个最小堆,使用默认的初始容量和比较器*/
  14. PriorityQueue minHeap = new PriorityQueue<>();
  15. /*遍历数组,将前 k 个元素加入最小堆*/
  16. for (int i = 0; i < k; i++) {
  17. minHeap.offer(arr[i]);
  18. }
  19. /*遍历数组中剩余的元素,与最小堆的堆顶元素比较*/
  20. /*若当前元素比堆顶元素小,则将堆顶元素出堆,当前元素入堆*/
  21. for (int i = k; i < arr.length; i++) {
  22. if (arr[i] < minHeap.peek()) {
  23. minHeap.poll();
  24. minHeap.offer(arr[i]);
  25. }
  26. }
  27. /*将最小堆中的元素取出放入结果数组*/
  28. int[] result = new int[k];
  29. for (int i = 0; i < k; i++) {
  30. result[i] = minHeap.poll();
  31. }
  32. return result;
  33. }
  34. public static void main(String[] args) {
  35. int[] arr = {10, 5, 8, 1, 7, 6};
  36. int k = 3;
  37. int[] result = getMinTopK(arr, k);
  38. System.out.println("最小的 " + k + " 个元素是:");
  39. for (int num : result) {
  40. System.out.print(num + " ");
  41. }
  42. }
  43. }

最大 Top K代码展示

求最大 Top K 元素可以使用类似的思路,只是需要使用最大堆(大顶堆)来实现。以下是使用最大堆来求最大 Top K 元素的 Java 代码示例

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.PriorityQueue;
  3. /**
  4. * @author yanfengzhang
  5. * @description 使用堆排序(最大堆)求最大 Top K
  6. * @date 2023/4/16 12:14
  7. */
  8. public class MaxTopK {
  9. public static int[] getMaxTopK(int[] arr, int k) {
  10. if (arr == null || arr.length == 0 || k <= 0 || k > arr.length) {
  11. return new int[0];
  12. }
  13. /*创建一个最大堆,使用自定义的比较器*/
  14. PriorityQueue maxHeap = new PriorityQueue<>((a, b) -> b - a);
  15. /*遍历数组,将前 k 个元素加入最大堆*/
  16. for (int i = 0; i < k; i++) {
  17. maxHeap.offer(arr[i]);
  18. }
  19. /*遍历数组中剩余的元素,与最大堆的堆顶元素比较*/
  20. /*若当前元素比堆顶元素大,则将堆顶元素出堆,当前元素入堆*/
  21. for (int i = k; i < arr.length; i++) {
  22. if (arr[i] > maxHeap.peek()) {
  23. maxHeap.poll();
  24. maxHeap.offer(arr[i]);
  25. }
  26. }
  27. /*将最大堆中的元素取出放入结果数组*/
  28. int[] result = new int[k];
  29. for (int i = 0; i < k; i++) {
  30. result[i] = maxHeap.poll();
  31. }
  32. return result;
  33. }
  34. public static void main(String[] args) {
  35. int[] arr = {10, 5, 8, 1, 7, 6};
  36. int k = 3;
  37. int[] result = getMaxTopK(arr, k);
  38. System.out.println("最大的 " + k + " 个元素是:");
  39. for (int num : result) {
  40. System.out.print(num + " ");
  41. }
  42. }
  43. }

扩展:使用堆排序思想实现优先级队列

对于一个优先级队列,每次插入一个元素后需要重新排序,可以使用堆排序的思想,将插入的元素加入堆中,然后进行堆排序,即可维护优先级队列的有序性。

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.ArrayList;
  3. import java.util.Collections;
  4. import java.util.List;
  5. /**
  6. * @author yanfengzhang
  7. * @description 使用堆排序思想实现优先级队列
  8. * 对于一个优先级队列,每次插入一个元素后需要重新排序,可以使用堆排序的思想,
  9. * 将插入的元素加入堆中,然后进行堆排序,即可维护优先级队列的有序性。
  10. * @date 2023/4/16 12:21
  11. */
  12. public class PriorityQueueUseHeapextends Comparable> {
  13. private List heap;
  14. public PriorityQueueUseHeap() {
  15. heap = new ArrayList<>();
  16. }
  17. public void insert(T item) {
  18. heap.add(item);
  19. int i = heap.size() - 1;
  20. while (i > 0) {
  21. int j = (i - 1) / 2;
  22. if (heap.get(i).compareTo(heap.get(j)) <= 0) {
  23. break;
  24. }
  25. Collections.swap(heap, i, j);
  26. i = j;
  27. }
  28. }
  29. public T peek() {
  30. if (heap.size() == 0) {
  31. return null;
  32. }
  33. return heap.get(0);
  34. }
  35. public T remove() {
  36. if (heap.size() == 0) {
  37. return null;
  38. }
  39. T root = heap.get(0);
  40. T last = heap.remove(heap.size() - 1);
  41. if (heap.size() > 0) {
  42. heap.set(0, last);
  43. int i = 0;
  44. while (i < heap.size()) {
  45. int left = i * 2 + 1;
  46. int right = i * 2 + 2;
  47. if (left >= heap.size()) {
  48. break;
  49. }
  50. int j = left;
  51. if (right < heap.size() && heap.get(right).compareTo(heap.get(left)) > 0) {
  52. j = right;
  53. }
  54. if (heap.get(i).compareTo(heap.get(j)) >= 0) {
  55. break;
  56. }
  57. Collections.swap(heap, i, j);
  58. i = j;
  59. }
  60. }
  61. return root;
  62. }
  63. public boolean isEmpty() {
  64. return heap.isEmpty();
  65. }
  66. public int size() {
  67. return heap.size();
  68. }
  69. public static void main(String[] args) {
  70. /*创建一个自定义的最大堆*/
  71. PriorityQueueUseHeap maxHeap = new PriorityQueueUseHeap<>();
  72. /*插入一些测试数据*/
  73. maxHeap.insert(10);
  74. maxHeap.insert(5);
  75. maxHeap.insert(8);
  76. maxHeap.insert(1);
  77. maxHeap.insert(7);
  78. maxHeap.insert(6);
  79. /*求最大的 k 个元素*/
  80. int k = 3;
  81. System.out.println("最大的 " + k + " 个元素是:");
  82. for (int i = 0; i < k; i++) {
  83. Integer num = maxHeap.remove();
  84. System.out.print(num + " ");
  85. }
  86. }
  87. }

(七)计数排序(Counting Sort)

基本知识介绍

计数排序(Counting Sort)是一种用于对一组具有确定范围的非负整数进行排序的线性时间复杂度的排序算法。它不基于比较的方式,而是通过计算每个元素在输入数组中的出现次数,然后按照计数的结果将元素放置到有序的输出数组中。

计数排序的基本思想是将输入数组中的元素作为计数数组的索引,然后通过遍历输入数组,统计每个元素的出现次数,将统计结果保存在计数数组中。接着,根据计数数组中的统计结果,将输入数组中的元素放置到正确的位置上,从而得到排序后的输出数组。

计数排序的步骤如下:

  1. 找出输入数组中的最大值和最小值,确定计数数组的范围。
  2. 创建一个计数数组,长度为最大值和最小值之间的范围。
  3. 遍历输入数组,统计每个元素的出现次数,将统计结果保存在计数数组中。
  4. 根据计数数组中的统计结果,将输入数组中的元素放置到正确的位置上,从而得到排序后的输出数组。

计数排序的优点是排序速度快,时间复杂度为 O(n+k),其中 n 是输入数组的大小,k 是输入数组中的最大值和最小值之间的范围。计数排序对于输入数据的范围没有限制,且适用于重复元素较多的情况。然而,计数排序需要额外的空间来保存计数数组,因此在输入数据范围较大时,会占用较多的空间。同时,计数排序只适用于非负整数的排序,对于其他类型的数据需要进行转换才能使用。

具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.Arrays;
  3. /**
  4. * @author yanfengzhang
  5. * @description 计数排序(Counting Sort)
  6. * 基本思想是将输入数组中的元素作为计数数组的索引,
  7. * 然后通过遍历输入数组,统计每个元素的出现次数,将统计结果保存在计数数组中。
  8. * 接着,根据计数数组中的统计结果,将输入数组中的元素放置到正确的位置上,从而得到排序后的输出数组。
  9. * @date 2023/4/16 12:49
  10. */
  11. public class CountingSort {
  12. public static void countingSort(int[] arr) {
  13. if (arr == null || arr.length <= 1) {
  14. return;
  15. }
  16. /*找出输入数组中的最大值和最小值*/
  17. int min = arr[0];
  18. int max = arr[0];
  19. for (int i = 1; i < arr.length; i++) {
  20. if (arr[i] < min) {
  21. min = arr[i];
  22. }
  23. if (arr[i] > max) {
  24. max = arr[i];
  25. }
  26. }
  27. /*创建计数数组并统计每个元素的出现次数*/
  28. int[] count = new int[max - min + 1];
  29. for (int i = 0; i < arr.length; i++) {
  30. count[arr[i] - min]++;
  31. }
  32. /*根据计数数组中的统计结果,将元素放置到正确的位置上*/
  33. int index = 0;
  34. for (int i = 0; i < count.length; i++) {
  35. while (count[i] > 0) {
  36. arr[index++] = i + min;
  37. count[i]--;
  38. }
  39. }
  40. }
  41. public static void main(String[] args) {
  42. int[] arr = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
  43. System.out.println("Before sorting: " + Arrays.toString(arr));
  44. countingSort(arr);
  45. System.out.println("After sorting: " + Arrays.toString(arr));
  46. }
  47. }

扩展:数组中有大量重复元素,如何高效地进行排序?

计数排序可以在线性时间复杂度内对包含大量重复元素的数组进行排序,因为它通过统计每个元素的出现次数并根据统计结果将元素放置到正确的位置上,避免了多次比较和交换操作。

  1. /**
  2. * 数组中有大量重复元素,如何高效地进行排序?
  3. */
  4. public static void countSort(int[] arr, int maxVal) {
  5. int[] count = new int[maxVal + 1];
  6. /*统计每个元素的出现次数*/
  7. for (int num : arr) {
  8. count[num]++;
  9. }
  10. /*将统计结果放置到正确的位置上*/
  11. int idx = 0;
  12. for (int i = 0; i <= maxVal; i++) {
  13. while (count[i] > 0) {
  14. arr[idx++] = i;
  15. count[i]--;
  16. }
  17. }
  18. }

扩展:给定一组学生成绩,如何按照成绩范围进行统计?

计数排序可以用于统计一组学生成绩的分布情况。可以将学生成绩作为输入数组,然后根据成绩的范围创建计数数组,并统计每个成绩范围内的学生人数。最后可以根据计数数组中的统计结果生成相应的统计报表。

  1. /**
  2. * 给定一组学生成绩,如何按照成绩范围进行统计?
  3. */
  4. public static int[] countSortGrades(int[] grades, int maxGrade) {
  5. int[] count = new int[maxGrade + 1];
  6. /*统计每个成绩范围内的学生人数*/
  7. for (int grade : grades) {
  8. count[grade]++;
  9. }
  10. return count;
  11. }

扩展:如何对字符串数组按照字母顺序进行排序?

计数排序可以用于对字符串数组按照字母顺序进行排序。可以将字符串数组中的每个字符串转换为对应的字符数组,然后将字符数组的每个字符看作是一个整数,使用计数排序对字符数组进行排序,从而实现字符串数组的排序。

  1. /**
  2. * 如何对字符串数组按照字母顺序进行排序?
  3. */
  4. public static void countSortStrings(String[] arr) {
  5. /*假设字符串只包含 ASCII 字符*/
  6. int[] count = new int[256];
  7. /*统计每个字符的出现次数*/
  8. for (String str : arr) {
  9. for (char c : str.toCharArray()) {
  10. count[c]++;
  11. }
  12. }
  13. /*将统计结果生成排序后的字符串数组*/
  14. int idx = 0;
  15. for (int i = 0; i < 256; i++) {
  16. while (count[i] > 0) {
  17. arr[idx++] = String.valueOf((char) i);
  18. count[i]--;
  19. }
  20. }
  21. }

扩展:如何对多个关键字进行排序?

计数排序可以用于对多个关键字进行排序。可以将多个关键字组合成一个整数或者字符串作为排序的依据,然后使用计数排序对输入数组进行排序。

  1. /**
  2. * 如何对多个关键字进行排序?
  3. */
  4. public static void countSortMultiKeys(int[][] arr, int keyIdx, int maxVal) {
  5. /*统计每个关键字的出现次数*/
  6. int[] count = new int[maxVal + 1];
  7. for (int[] row : arr) {
  8. count[row[keyIdx]]++;
  9. }
  10. /*计算每个关键字在输出数组中的起始索引*/
  11. for (int i = 1; i <= maxVal; i++) {
  12. count[i] += count[i - 1];
  13. }
  14. /*创建临时数组存储排序结果*/
  15. int[][] output = new int[arr.length][];
  16. for (int i = arr.length - 1; i >= 0; i--) {
  17. output[--count[arr[i][keyIdx]]] = arr[i];
  18. }
  19. /*将排序结果复制到原数组*/
  20. for (int i = 0; i < arr.length; i++) {
  21. arr[i] = output[i];
  22. }
  23. }

相关扩展思路代码验证

  1. public static void main(String[] args) {
  2. System.out.println("数组中有大量重复元素,如何高效地进行排序? 验证测试");
  3. int[] arr = {5, 2, 9, 1, 5, 6};
  4. int maxVal = 9;
  5. System.out.println("排序前的数组:" + Arrays.toString(arr));
  6. countSort(arr, maxVal);
  7. System.out.println("排序后的数组:" + Arrays.toString(arr));
  8. System.out.println("给定一组学生成绩,如何按照成绩范围进行统计? 验证测试");
  9. int[] grades = {90, 87, 92, 78, 95, 89, 88, 92, 91, 88};
  10. int maxGrade = 100;
  11. int[] count = countSortGrades(grades, maxGrade);
  12. System.out.println("成绩统计结果:");
  13. for (int i = 0; i < count.length; i++) {
  14. if (count[i] > 0) {
  15. System.out.println(i + ": " + count[i] + "人");
  16. }
  17. }
  18. System.out.println("如何对字符串数组按照字母顺序进行排序? 验证测试");
  19. String[] zimuArr = {"apple", "banana", "cherry", "date", "fig", "grape"};
  20. System.out.println("排序前的字符串数组:");
  21. for (String str : zimuArr) {
  22. System.out.print(str + " ");
  23. }
  24. countSortStrings(zimuArr);
  25. System.out.println("\n排序后的字符串数组:");
  26. for (String str : zimuArr) {
  27. System.out.print(str + " ");
  28. }
  29. System.out.println("如何对多个关键字进行排序? 验证测试");
  30. /*输入数据*/
  31. int[][] keyWordArr = {{3, 5}, {1, 2}, {2, 3}, {2, 1}, {1, 4}, {3, 6}};
  32. /*原始数组*/
  33. System.out.println("原始数组:");
  34. for (int[] row : keyWordArr) {
  35. System.out.println(Arrays.toString(row));
  36. }
  37. /*调用 countSortMultiKeys 进行排序*/
  38. countSortMultiKeys(keyWordArr, 0, 3);
  39. /*排序后的数组*/
  40. System.out.println("排序后的数组:");
  41. for (int[] row : keyWordArr) {
  42. System.out.println(Arrays.toString(row));
  43. }
  44. }

(八)桶排序(Bucket Sort)

基本知识介绍

桶排序(Bucket Sort)是一种排序算法,它将输入数据分割成若干个桶(Bucket),然后对每个桶中的数据进行排序,最后将所有桶中的数据合并得到最终的排序结果。

桶排序的基本思想是将待排序的数据按照一定的范围划分成若干个桶,每个桶内的数据单独进行排序,可以使用任何一种排序算法,例如插入排序、快速排序等。然后,将所有的桶按照顺序依次合并,得到最终的排序结果。

桶排序的时间复杂度取决于两个因素:桶的个数和每个桶内数据的排序算法。如果每个桶内的数据是均匀分布的,那么桶排序的时间复杂度可以接近O(n),其中n为待排序数据的个数。但是,如果桶的个数过于少或者过于多,都可能导致桶排序的性能下降。

桶排序适合用于处理大量数据且数据分布较为均匀的情况,例如对一段范围较大的浮点数进行排序。桶排序在处理大数据量时可以充分利用多核处理器的优势,同时也可以通过调整桶的个数和每个桶内数据的排序算法来进行性能优化。然而,需要注意的是,桶排序的空间复杂度较高,因为需要额外的空间来存储桶。

具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.ArrayList;
  3. import java.util.Collections;
  4. /**
  5. * @author yanfengzhang
  6. * @description 桶排序(Bucket Sort)
  7. * 将待排序的数据按照一定的范围划分成若干个桶,
  8. * 每个桶内的数据单独进行排序,可以使用任何一种排序算法,例如插入排序、快速排序等。
  9. * 然后,将所有的桶按照顺序依次合并,得到最终的排序结果。
  10. * @date 2023/4/16 14:09
  11. */
  12. public class BucketSort {
  13. public static void bucketSort(double[] arr) {
  14. int n = arr.length;
  15. /*创建桶*/
  16. ArrayList[] buckets = new ArrayList[n];
  17. for (int i = 0; i < n; i++) {
  18. buckets[i] = new ArrayList<>();
  19. }
  20. /*将元素分配到桶中*/
  21. for (int i = 0; i < n; i++) {
  22. int index = (int) (n * arr[i]);
  23. buckets[index].add(arr[i]);
  24. }
  25. /*对每个桶内的元素进行排序*/
  26. for (int i = 0; i < n; i++) {
  27. Collections.sort(buckets[i]);
  28. }
  29. /*将桶中的元素合并到原数组中*/
  30. int index = 0;
  31. for (int i = 0; i < n; i++) {
  32. for (int j = 0; j < buckets[i].size(); j++) {
  33. arr[index++] = buckets[i].get(j);
  34. }
  35. }
  36. }
  37. public static void main(String[] args) {
  38. double[] arr = {0.8, 0.2, 0.6, 0.4, 0.1, 0.9, 0.3, 0.5, 0.7};
  39. System.out.println("原始数组:");
  40. for (double num : arr) {
  41. System.out.print(num + " ");
  42. }
  43. System.out.println();
  44. bucketSort(arr);
  45. System.out.println("排序后数组:");
  46. for (double num : arr) {
  47. System.out.print(num + " ");
  48. }
  49. System.out.println();
  50. }
  51. }

扩展:按照年龄对一组人员进行排序

假设有一组人员信息,包括姓名、年龄等属性,需要按照年龄对这组人员进行排序。可以使用桶排序,以年龄作为排序的关键字,将人员信息分配到不同的年龄桶中,再对每个年龄桶内的人员信息进行排序,最后合并所有桶内的人员信息即可。具体代码展示:

  1. package org.zyf.javabasic.letcode.sort;
  2. import lombok.Data;
  3. import java.util.ArrayList;
  4. import java.util.Collections;
  5. import java.util.Comparator;
  6. /**
  7. * @author yanfengzhang
  8. * @description 按照年龄对一组人员进行排序:
  9. * 假设有一组人员信息,包括姓名、年龄等属性,需要按照年龄对这组人员进行排序。
  10. * 可以使用桶排序,以年龄作为排序的关键字,将人员信息分配到不同的年龄桶中,
  11. * 再对每个年龄桶内的人员信息进行排序,最后合并所有桶内的人员信息即可。
  12. * @date 2023/4/16 14:12
  13. */
  14. public class BucketSortByAge {
  15. public static void bucketSortByAge(Person[] arr, int maxAge) {
  16. if (arr == null || arr.length <= 1 || maxAge <= 0) {
  17. return;
  18. }
  19. /*创建年龄桶*/
  20. ArrayList[] ageBuckets = new ArrayList[maxAge + 1];
  21. for (int i = 0; i <= maxAge; i++) {
  22. ageBuckets[i] = new ArrayList<>();
  23. }
  24. /*将人员信息分配到年龄桶中*/
  25. for (Person person : arr) {
  26. int age = person.getAge();
  27. ageBuckets[age].add(person);
  28. }
  29. /*对每个年龄桶内的人员信息进行排序*/
  30. int idx = 0;
  31. for (ArrayList bucket : ageBuckets) {
  32. if (bucket != null && !bucket.isEmpty()) {
  33. /*对每个年龄桶内的人员信息进行排序,这里可以使用任何合适的排序算法*/
  34. Collections.sort(bucket, new Comparator() {
  35. @Override
  36. public int compare(Person o1, Person o2) {
  37. /*按照年龄升序排序*/
  38. return o1.getAge() - o2.getAge();
  39. }
  40. });
  41. /*将排序后的人员信息合并到原数组中*/
  42. for (Person person : bucket) {
  43. arr[idx++] = person;
  44. }
  45. }
  46. }
  47. }
  48. @Data
  49. public static class Person {
  50. private String name;
  51. private int age;
  52. public Person(String name, int age) {
  53. this.name = name;
  54. this.age = age;
  55. }
  56. }
  57. public static void main(String[] args) {
  58. /*示例数据*/
  59. Person[] persons = new Person[6];
  60. persons[0] = new Person("Alice", 25);
  61. persons[1] = new Person("Bob", 30);
  62. persons[2] = new Person("Charlie", 22);
  63. persons[3] = new Person("Dave", 25);
  64. persons[4] = new Person("Eve", 22);
  65. persons[5] = new Person("Frank", 30);
  66. System.out.println("排序前:");
  67. for (Person person : persons) {
  68. System.out.println(person.getName() + " - " + person.getAge() + "岁");
  69. }
  70. /*调用桶排序算法进行排序*/
  71. bucketSortByAge(persons, 30);
  72. System.out.println("\n排序后:");
  73. for (Person person : persons) {
  74. System.out.println(person.getName() + " - " + person.getAge() + "岁");
  75. }
  76. }
  77. }

扩展:对一组考试成绩进行排序

假设有一组学生的考试成绩,需要按照成绩进行排序。可以使用桶排序,以考试成绩作为排序的关键字,将学生信息分配到不同的成绩桶中,再对每个成绩桶内的学生信息进行排序,最后合并所有桶内的学生信息即可。具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. import lombok.Data;
  3. import java.util.ArrayList;
  4. import java.util.Collections;
  5. import java.util.Comparator;
  6. /**
  7. * @author yanfengzhang
  8. * @description 对一组考试成绩进行排序
  9. * 假设有一组学生的考试成绩,需要按照成绩进行排序。
  10. * 可以使用桶排序,以考试成绩作为排序的关键字,将学生信息分配到不同的成绩桶中,
  11. * 再对每个成绩桶内的学生信息进行排序,最后合并所有桶内的学生信息即可。
  12. * @date 2023/4/16 14:18
  13. */
  14. public class BucketSortByScore {
  15. public static void bucketSortByScore(Student[] arr, int maxScore) {
  16. if (arr == null || arr.length <= 1 || maxScore <= 0) {
  17. return;
  18. }
  19. /*创建成绩桶*/
  20. ArrayList[] scoreBuckets = new ArrayList[maxScore + 1];
  21. for (int i = 0; i <= maxScore; i++) {
  22. scoreBuckets[i] = new ArrayList<>();
  23. }
  24. /*将学生信息分配到成绩桶中*/
  25. for (Student student : arr) {
  26. int score = student.getScore();
  27. scoreBuckets[score].add(student);
  28. }
  29. /*对每个成绩桶内的学生信息进行排序*/
  30. int idx = 0;
  31. for (ArrayList bucket : scoreBuckets) {
  32. if (bucket != null && !bucket.isEmpty()) {
  33. /*对每个成绩桶内的学生信息进行排序,这里可以使用任何合适的排序算法*/
  34. Collections.sort(bucket, new Comparator() {
  35. @Override
  36. public int compare(Student o1, Student o2) {
  37. /*按照成绩降序排序*/
  38. return o2.getScore() - o1.getScore();
  39. }
  40. });
  41. /*将排序后的学生信息合并到原数组中*/
  42. for (Student student : bucket) {
  43. arr[idx++] = student;
  44. }
  45. }
  46. }
  47. }
  48. @Data
  49. public static class Student {
  50. private String name;
  51. private int score;
  52. /*构造函数*/
  53. public Student(String name, int score) {
  54. this.name = name;
  55. this.score = score;
  56. }
  57. }
  58. public static void main(String[] args) {
  59. /*示例数据*/
  60. Student[] students = new Student[6];
  61. students[0] = new Student("Alice", 90);
  62. students[1] = new Student("Bob", 78);
  63. students[2] = new Student("Charlie", 88);
  64. students[3] = new Student("Dave", 95);
  65. students[4] = new Student("Eve", 82);
  66. students[5] = new Student("Frank", 88);
  67. System.out.println("排序前:");
  68. for (Student student : students) {
  69. System.out.println(student.getName() + " - 成绩:" + student.getScore());
  70. }
  71. /*调用桶排序算法进行排序*/
  72. bucketSortByScore(students, 100);
  73. System.out.println("\n排序后:");
  74. for (Student student : students) {
  75. System.out.println(student.getName() + " - 成绩:" + student.getScore());
  76. }
  77. }
  78. }

扩展:对一组具有相同前缀的字符串进行排序

假设有一组字符串,这些字符串都具有相同的前缀,需要按照字符串的字典序进行排序。可以使用桶排序,以字符串的第一个字符作为排序的关键字,将字符串分配到不同的字符桶中,再对每个字符桶内的字符串进行排序,最后合并所有桶内的字符串即可。具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.ArrayList;
  3. import java.util.Collections;
  4. /**
  5. * @author yanfengzhang
  6. * @description 对一组具有相同前缀的字符串进行排序
  7. * 假设有一组字符串,这些字符串都具有相同的前缀,需要按照字符串的字典序进行排序。
  8. * 可以使用桶排序,以字符串的第一个字符作为排序的关键字,将字符串分配到不同的字符桶中,
  9. * 再对每个字符桶内的字符串进行排序,最后合并所有桶内的字符串即可。
  10. * @date 2023/4/16 14:20
  11. */
  12. public class BucketSortByPrefix {
  13. public static void bucketSortByPrefix(String[] arr, int prefixLength) {
  14. if (arr == null || arr.length <= 1 || prefixLength <= 0) {
  15. return;
  16. }
  17. /*创建字符桶*/
  18. /*假设字符串只包含小写字母,共26个桶*/
  19. ArrayList[] charBuckets = new ArrayList[26];
  20. for (int i = 0; i < 26; i++) {
  21. charBuckets[i] = new ArrayList<>();
  22. }
  23. /*将字符串分配到字符桶中*/
  24. for (String str : arr) {
  25. charBuckets[str.charAt(prefixLength - 1) - 'a'].add(str);
  26. }
  27. /*对每个字符桶内的字符串进行排序*/
  28. int idx = 0;
  29. for (ArrayList bucket : charBuckets) {
  30. if (bucket != null && !bucket.isEmpty()) {
  31. /*对每个字符桶内的字符串进行排序,这里可以使用任何合适的排序算法*/
  32. Collections.sort(bucket);
  33. /*将排序后的字符串合并到原数组中*/
  34. for (String str : bucket) {
  35. arr[idx++] = str;
  36. }
  37. }
  38. }
  39. }
  40. public static void main(String[] args) {
  41. /*示例数据*/
  42. String[] strings = new String[]{"apple", "banana", "bear", "bell", "cat", "dog", "duck"};
  43. System.out.println("排序前:");
  44. for (String str : strings) {
  45. System.out.println(str);
  46. }
  47. /*调用桶排序算法进行排序*/
  48. bucketSortByPrefix(strings, 1);
  49. System.out.println("\n排序后:");
  50. for (String str : strings) {
  51. System.out.println(str);
  52. }
  53. }
  54. }

(九)基数排序(Radix Sort)

基本知识介绍

基数排序(Radix Sort)是一种非比较性的排序算法,它根据元素的位数或字符的位置来进行排序。基数排序的基本思想是将待排序的元素按照各位或字符的值,从低位到高位依次进行排序,直到最高位排序完成后,整个序列就变成了有序的。

基数排序通常用于对整数、字符串等具有固定位数的数据进行排序,例如对手机号码、身份证号码等进行排序。

以下是基数排序的完整步骤:

  1. 确定排序的位数或字符的位置,从最低位(个位)到最高位(最高位)。
  2. 对每一位或字符进行计数排序或桶排序,根据位数或字符的值将元素分配到不同的桶或计数数组中。
  3. 将所有桶或计数数组中的元素按照顺序合并到一起,形成新的排序序列。
  4. 重复步骤2和步骤3,直到最高位排序完成后,整个序列就变成了有序的。

具体代码展示

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.Arrays;
  3. /**
  4. * @author yanfengzhang
  5. * @description 基数排序(Radix Sort)
  6. * 根据元素的位数或字符的位置来进行排序。
  7. * 基数排序的基本思想是将待排序的元素按照各位或字符的值,从低位到高位依次进行排序,
  8. * 直到最高位排序完成后,整个序列就变成了有序的。
  9. * @date 2023/4/16 14:39
  10. */
  11. public class RadixSort {
  12. /**
  13. * 基数排序函数
  14. */
  15. public static void radixSort(int[] arr) {
  16. /*获取数组中的最大值,确定排序的位数*/
  17. int max = getMax(arr);
  18. /*对每一位进行计数排序*/
  19. for (int exp = 1; max / exp > 0; exp *= 10) {
  20. countSort(arr, exp);
  21. }
  22. }
  23. /**
  24. * 获取数组中的最大值
  25. */
  26. public static int getMax(int[] arr) {
  27. int max = arr[0];
  28. for (int i = 1; i < arr.length; i++) {
  29. if (arr[i] > max) {
  30. max = arr[i];
  31. }
  32. }
  33. return max;
  34. }
  35. /**
  36. * 对数组按照指定位数进行计数排序
  37. */
  38. public static void countSort(int[] arr, int exp) {
  39. int n = arr.length;
  40. int[] output = new int[n];
  41. int[] count = new int[10];
  42. /*统计当前位的数字出现次数*/
  43. for (int i = 0; i < n; i++) {
  44. count[(arr[i] / exp) % 10]++;
  45. }
  46. /*计算累计次数*/
  47. for (int i = 1; i < 10; i++) {
  48. count[i] += count[i - 1];
  49. }
  50. /*从后往前遍历原数组,按照当前位的数字放置到输出数组中*/
  51. for (int i = n - 1; i >= 0; i--) {
  52. output[count[(arr[i] / exp) % 10] - 1] = arr[i];
  53. count[(arr[i] / exp) % 10]--;
  54. }
  55. /*将输出数组复制回原数组*/
  56. System.arraycopy(output, 0, arr, 0, n);
  57. }
  58. public static void main(String[] args) {
  59. int[] arr = {170, 45, 75, 90, 802, 24, 2, 66};
  60. System.out.println("原始数组: " + Arrays.toString(arr));
  61. radixSort(arr);
  62. System.out.println("排序后数组: " + Arrays.toString(arr));
  63. }
  64. }

扩展:对一组手机号码进行排序

假设有一组手机号码,需要按照手机号码进行排序。可以使用基数排序,以手机号码的每一位数字作为排序的关键字,从左到右进行排序。代码展示:

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.Arrays;
  3. /**
  4. * @author yanfengzhang
  5. * @description 对一组手机号码进行排序
  6. * 假设有一组手机号码,需要按照手机号码进行排序。
  7. * 可以使用基数排序,以手机号码的每一位数字作为排序的关键字,从左到右进行排序。
  8. * @date 2023/4/16 14:43
  9. */
  10. public class PhoneNumRadixSort {
  11. /**
  12. * 获取手机号码的指定位数字
  13. */
  14. public static int getDigit(long phoneNum, int d) {
  15. if (d >= 1 && d <= 11) {
  16. String numStr = String.valueOf(phoneNum);
  17. if (numStr.length() >= d) {
  18. return Integer.parseInt(String.valueOf(numStr.charAt(numStr.length() - d)));
  19. }
  20. /*若手机号码不足 d 位,则在高位补0*/
  21. return 0;
  22. }
  23. return -1;
  24. }
  25. /**
  26. * 使用基数排序对手机号码进行排序
  27. */
  28. public static void radixSort(long[] arr) {
  29. /*进行基数排序*/
  30. for (int d = 11; d >= 1; d--) {
  31. /*计数数组,用于存储每个位数字出现的次数*/
  32. int[] count = new int[10];
  33. /*临时数组,用于存储排序结果*/
  34. long[] output = new long[arr.length];
  35. /*统计每个位数字出现的次数*/
  36. for (long phoneNum : arr) {
  37. int digit = getDigit(phoneNum, d);
  38. count[digit]++;
  39. }
  40. /*计算每个位数字的累计次数*/
  41. for (int i = 1; i < 10; i++) {
  42. count[i] += count[i - 1];
  43. }
  44. /*将手机号码按当前位数字放入临时数组中*/
  45. for (int i = arr.length - 1; i >= 0; i--) {
  46. int digit = getDigit(arr[i], d);
  47. output[--count[digit]] = arr[i];
  48. }
  49. /*将临时数组中的结果复制回原数组*/
  50. System.arraycopy(output, 0, arr, 0, arr.length);
  51. }
  52. }
  53. public static void main(String[] args) {
  54. long[] arr = {18888888888L, 13333333333L, 17777777777L, 16666666666L, 19999999999L};
  55. System.out.println("排序前:" + Arrays.toString(arr));
  56. radixSort(arr);
  57. System.out.println("排序后:" + Arrays.toString(arr));
  58. }
  59. }

扩展:对一组身份证号码进行排序

假设有一组身份证号码,需要按照身份证号码进行排序。可以使用基数排序,以身份证号码的每一位数字作为排序的关键字,从左到右进行排序。代码展示:

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.Arrays;
  3. /**
  4. * @author yanfengzhang
  5. * @description 对一组身份证号码进行排序
  6. * 假设有一组身份证号码,需要按照身份证号码进行排序。
  7. * 可以使用基数排序,以身份证号码的每一位数字作为排序的关键字,从左到右进行排序。
  8. * @date 2023/4/16 14:52
  9. */
  10. public class IDNumberRadixSort {
  11. /**
  12. * 获取身份证号码的指定位数字
  13. */
  14. public static int getDigit(char ch, int d) {
  15. return Character.getNumericValue(ch) / (int) Math.pow(10, d - 1) % 10;
  16. }
  17. /**
  18. * 使用基数排序对身份证号码进行排序
  19. */
  20. public static void radixSort(String[] arr) {
  21. /*获取身份证号码的最大位数*/
  22. int maxDigits = 0;
  23. for (String id : arr) {
  24. int digits = id.length();
  25. if (digits > maxDigits) {
  26. maxDigits = digits;
  27. }
  28. }
  29. /*进行基数排序*/
  30. for (int d = maxDigits; d >= 1; d--) {
  31. /*计数数组,用于存储每个位数字出现的次数*/
  32. int[] count = new int[10];
  33. /*临时数组,用于存储排序结果*/
  34. String[] output = new String[arr.length];
  35. /*统计每个位数字出现的次数*/
  36. for (String id : arr) {
  37. if (id.length() >= d) {
  38. int digit = getDigit(id.charAt(id.length() - d), 1);
  39. count[digit]++;
  40. }
  41. }
  42. /*计算每个位数字的累计次数*/
  43. for (int i = 1; i < 10; i++) {
  44. count[i] += count[i - 1];
  45. }
  46. /*将身份证号码按当前位数字放入临时数组中*/
  47. for (int i = arr.length - 1; i >= 0; i--) {
  48. if (arr[i].length() >= d) {
  49. int digit = getDigit(arr[i].charAt(arr[i].length() - d), 1);
  50. output[--count[digit]] = arr[i];
  51. } else {
  52. output[--count[0]] = arr[i];
  53. }
  54. }
  55. /*将临时数组中的结果复制回原数组*/
  56. System.arraycopy(output, 0, arr, 0, arr.length);
  57. }
  58. }
  59. public static void main(String[] args) {
  60. String[] arr = {"320583199901010001", "320583199901010002", "320583199901010003", "320583199901010004",
  61. "320583199901010005"};
  62. System.out.println("排序前:" + Arrays.toString(arr));
  63. radixSort(arr);
  64. System.out.println("排序后:" + Arrays.toString(arr));
  65. }
  66. }

扩展:对一组IP地址进行排序

假设有一组IP地址,需要按照IP地址进行排序。可以使用基数排序,以IP地址的每一位数字作为排序的关键字,从左到右进行排序。代码展示:

  1. package org.zyf.javabasic.letcode.sort;
  2. import java.util.Arrays;
  3. /**
  4. * @author yanfengzhang
  5. * @description 对一组IP地址进行排序
  6. * 假设有一组IP地址,需要按照IP地址进行排序。
  7. * 可以使用基数排序,以IP地址的每一位数字作为排序的关键字,从左到右进行排序。
  8. * @date 2023/4/16 14:56
  9. */
  10. public class IPAddressesRadixSort {
  11. /**
  12. * 获取IP地址的指定位数字
  13. */
  14. public static int getDigit(String ip, int d) {
  15. String[] segments = ip.split("\\.");
  16. if (segments.length == 4 && d >= 1 && d <= 4) {
  17. return Integer.parseInt(segments[d - 1]);
  18. }
  19. return -1;
  20. }
  21. /**
  22. * 使用基数排序对IP地址进行排序
  23. */
  24. public static void radixSort(String[] arr) {
  25. /*进行基数排序*/
  26. for (int d = 4; d >= 1; d--) {
  27. /*计数数组,用于存储每个位数字出现的次数*/
  28. int[] count = new int[256];
  29. /*临时数组,用于存储排序结果*/
  30. String[] output = new String[arr.length];
  31. /*统计每个位数字出现的次数*/
  32. for (String ip : arr) {
  33. int digit = getDigit(ip, d);
  34. count[digit]++;
  35. }
  36. /*计算每个位数字的累计次数*/
  37. for (int i = 1; i < 256; i++) {
  38. count[i] += count[i - 1];
  39. }
  40. /*将IP地址按当前位数字放入临时数组中*/
  41. for (int i = arr.length - 1; i >= 0; i--) {
  42. int digit = getDigit(arr[i], d);
  43. output[--count[digit]] = arr[i];
  44. }
  45. /*将临时数组中的结果复制回原数组*/
  46. System.arraycopy(output, 0, arr, 0, arr.length);
  47. }
  48. }
  49. public static void main(String[] args) {
  50. String[] arr = {"192.168.1.2", "192.168.1.1", "10.0.0.1", "10.0.0.2", "172.16.0.1", "172.16.0.2"};
  51. System.out.println("排序前:" + Arrays.toString(arr));
  52. radixSort(arr);
  53. System.out.println("排序后:" + Arrays.toString(arr));
  54. }
  55. }

四、总结

还是找一张图来总结吧

文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览62532 人正在系统学习中
注:本文转载自blog.csdn.net的张彦峰ZYF的文章"https://blog.csdn.net/xiaofeng10330111/article/details/127557182"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top