• 算法:二元选择排序


    选择排序算法也是可以优化的,既然每轮遍历时找出了最小值,何不把最大值也顺便找出来呢?这就是二元选择排序的思想。

    使用二元选择排序,每轮选择时记录最小值和最大值,可以把数组需要遍历的范围缩小一倍。

    1. public static void selectionSort2(int[] arr) {
    2. int minIndex, maxIndex;
    3. // i 只需要遍历一半
    4. for (int i = 0; i < arr.length / 2; i++) {
    5. minIndex = i;
    6. maxIndex = i;
    7. for (int j = i + 1; j < arr.length - i; j++) {
    8. if (arr[minIndex] > arr[j]) {
    9. // 记录最小值的下标
    10. minIndex = j;
    11. }
    12. if (arr[maxIndex] < arr[j]) {
    13. // 记录最大值的下标
    14. maxIndex = j;
    15. }
    16. }
    17. // 如果 minIndex 和 maxIndex 都相等,那么他们必定都等于 i,且后面的所有数字都与 arr[i] 相等,此时已经排序完成
    18. if (minIndex == maxIndex) break;
    19. // 将最小元素交换至首位
    20. int temp = arr[i];
    21. arr[i] = arr[minIndex];
    22. arr[minIndex] = temp;
    23. // 如果最大值的下标刚好是 i,由于 arr[i] 和 arr[minIndex] 已经交换了,所以这里要更新 maxIndex 的值。
    24. if (maxIndex == i) maxIndex = minIndex;
    25. // 将最大元素交换至末尾
    26. int lastIndex = arr.length - 1 - i;
    27. temp = arr[lastIndex];
    28. arr[lastIndex] = arr[maxIndex];
    29. arr[maxIndex] = temp;
    30. }
    31. }

    我们使用 minIndex 记录最小值的下标,maxIndex 记录最大值的下标。每次遍历后,将最小值交换到首位,最大值交换到末尾,就完成了排序。

    由于每一轮遍历可以排好两个数字,所以最外层的遍历只需遍历一半即可。

    二元选择排序中有一句很重要的代码,它位于交换最小值和交换最大值的代码中间:

    if (maxIndex == i) maxIndex = minIndex;

    这行代码的作用处理了一种特殊情况:如果最大值的下标等于 i,也就是说 arr[i] 就是最大值,由于 arr[i] 是当前遍历轮次的首位,它已经和 arr[minIndex] 交换了,所以最大值的下标需要跟踪到 arr[i] 最新的下标 minIndex。

    二元选择排序的效率

    在二元选择排序算法中,数组需要遍历的范围缩小了一倍。那么这样可以使选择排序的效率提升一倍吗?

    从代码可以看出,虽然二元选择排序最外层的遍历范围缩小了,但 for 循环内做的事情翻了一倍。也就是说二元选择排序无法将选择排序的效率提升一倍。但实测会发现二元选择排序的速度确实比选择排序的速度快一点点,它的速度提升主要是因为两点:

    在选择排序的外层 for 循环中,i 需要加到 arr.length - 1 ,二元选择排序中 i 只需要加到 arr.length / 2
    在选择排序的内层 for 循环中,j 需要加到 arr.length ,二元选择排序中 j 只需要加到 arr.length - i
    我们不妨发扬一下极客精神,一起来做一个统计实验:

    1. public class TestSelectionSort {
    2. public static void selectionSort(int[] arr) {
    3. int countI = 0;
    4. int countJ = 0;
    5. int countArr = 0;
    6. int minIndex;
    7. countI++;
    8. for (int i = 0; i < arr.length - 1; i++, countI++) {
    9. minIndex = i;
    10. countJ++;
    11. for (int j = i + 1; j < arr.length; j++, countJ++) {
    12. if (arr[minIndex] > arr[j]) {
    13. // 记录最小值的下标
    14. minIndex = j;
    15. }
    16. countArr++;
    17. }
    18. // 将最小元素交换至首位
    19. int temp = arr[i];
    20. arr[i] = arr[minIndex];
    21. arr[minIndex] = temp;
    22. }
    23. int count = countI + countJ + countArr;
    24. System.out.println("selectionSort: countI = " + countI + ", countJ = " + countJ + ", countArr = " + countArr + ", count = " + count);
    25. }
    26. public static void selectionSort2(int[] arr) {
    27. int countI = 0;
    28. int countJ = 0;
    29. int countArr = 0;
    30. int minIndex, maxIndex;
    31. countI++;
    32. // i 只需要遍历一半
    33. for (int i = 0; i < arr.length / 2; i++, countI++) {
    34. minIndex = i;
    35. maxIndex = i;
    36. countJ++;
    37. for (int j = i + 1; j < arr.length - i; j++, countJ++) {
    38. if (arr[minIndex] > arr[j]) {
    39. // 记录最小值的下标
    40. minIndex = j;
    41. }
    42. if (arr[maxIndex] < arr[j]) {
    43. // 记录最大值的下标
    44. maxIndex = j;
    45. }
    46. countArr += 2;
    47. }
    48. // 如果 minIndex 和 maxIndex 都相等,那么他们必定都等于 i,且后面的所有数字都与 arr[i] 相等,此时已经排序完成
    49. if (minIndex == maxIndex) break;
    50. // 将最小元素交换至首位
    51. int temp = arr[i];
    52. arr[i] = arr[minIndex];
    53. arr[minIndex] = temp;
    54. // 如果最大值的下标刚好是 i,由于 arr[i] 和 arr[minIndex] 已经交换了,所以这里要更新 maxIndex 的值。
    55. if (maxIndex == i) maxIndex = minIndex;
    56. // 将最大元素交换至末尾
    57. int lastIndex = arr.length - 1 - i;
    58. temp = arr[lastIndex];
    59. arr[lastIndex] = arr[maxIndex];
    60. arr[maxIndex] = temp;
    61. }
    62. int count = countI + countJ + countArr;
    63. System.out.println("selectionSort2: countI = " + countI + ", countJ = " + countJ + ", countArr = " + countArr + ", count = " + count);
    64. }
    65. }

    在这个类中,我们用 countI 记录 i 的比较次数,countJ 记录 j 的比较次数,countArr 记录 arr 的比较次数,count 记录总比较次数。

    测试用例:

    1. import org.junit.Test;
    2. import java.util.ArrayList;
    3. public class UnitTest {
    4. @Test
    5. public void test() {
    6. ArrayList list = new ArrayList<>();
    7. for (int i = 0; i <= 1000; i++) {
    8. // ArrayList 转 int[]
    9. int[] arr = list.stream().mapToInt(Integer::intValue).toArray();
    10. System.out.println("*** arr.length = " + arr.length + " ***");
    11. TestSelectionSort.selectionSort(arr);
    12. TestSelectionSort.selectionSort2(arr);
    13. list.add(i);
    14. }
    15. }
    16. }

    这里列出部分测试结果,感兴趣的读者可以自己运行此测试用例验证:

    数组长度排序算法i 比较次数j 比较次数数组元素比较次数总比较次数
    0选择排序1001
    0二元选择排序1001
    1选择排序1001
    1二元选择排序1001
    2选择排序2215
    2二元选择排序2226
    3选择排序35311
    3二元选择排序2349
    10选择排序105445109
    10二元选择排序6305086
    100选择排序1005049495010099
    100二元选择排序51255050007601
    1000选择排序10005004994995001000999
    1000二元选择排序501250500500000751001

    可以看到,二元选择排序中, arr 数组的比较次数甚至略高于选择排序的比较次数,整体是相差无几的。只是 i 和 j 的比较次数较少,正是在这两个地方提高了效率。

    并且,在二元选择排序中,我们可以做一个剪枝优化,当 minIndex == maxIndex 时,说明后续所有的元素都相等,就好比班上最高的学生和最矮的学生一样高,说明整个班上的人身高都相同了。此时已经排序完成,可以提前跳出循环。通过这个剪枝优化,对于相同元素较多的数组,二元选择排序的效率将远远超过选择排序。

    和选择排序一样,二元选择排序也是不稳定的。

    时间复杂度 & 空间复杂度
    前文已经说到,选择排序使用两层循环,时间复杂度为 O(n^2); 只使用有限个变量,空间复杂度 O(1)。二元选择排序虽然比选择排序要快,但治标不治本,二元选择排序中做的优化无法改变其时间复杂度,二元选择排序的时间复杂度仍然是 O(n^2);只使用有限个变量,空间复杂度 O(1)。

  • 相关阅读:
    C++结构型模式-组合模式
    Java--简介
    [Java]_[中级]_[使用okhttp3和HttpClient代理访问外部网络]
    【Vue】修饰符、表单提交方式、自定义组件的关键步骤
    PyTorch常用代码段合集
    Python基础篇(十二)-- 常用模块
    MyBatis学习
    2023年中国酒店式公寓现状及发展趋势分析[图]
    【无标题】
    DAOS整体设计分析 (二)
  • 原文地址:https://blog.csdn.net/carrie__yang/article/details/133807152