左哥算法 - 排序 - 选择排序,冒泡排序

202 阅读15分钟

选择排序

🎯 选择排序的基本思想

想象你有一堆扑克牌需要整理,选择排序的方式就像这样:

  1. 首先看整副牌,找到最小的那张
  2. 把它放到最左边第一个位置
  3. 然后在剩下的牌中再找最小的
  4. 把它放到第二个位置
  5. 以此类推...

就像你整理扑克牌一样,每次都从剩下的牌中挑出最小的一张!

📝 具体流程图

flowchart TD
    A[开始] --> B[设置初始位置 i = 0]
    B --> C[在剩余未排序序列中找到最小元素]
    C --> D[将找到的最小元素与第i个位置元素交换]
    D --> E{i < n-1 ?}
    E -->|是| F[i = i + 1]
    F --> C
    E -->|否| G[结束]

💻 Java代码示例

public class SelectionSort {
    public static void selectionSort(int[] arr) {
        // 外层循环:未排序区间的起始位置
        for (int i = 0; i < arr.length - 1; i++) {
            // 记录最小值的索引
            int minIndex = i;
            
            // 内层循环:在未排序区间寻找最小值
            for (int j = i + 1; j < arr.length; j++) {
                // 如果找到更小的值,更新minIndex
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            }
            
            // 将找到的最小值与未排序区间的第一个元素交换
            if (minIndex != i) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
    }
}

🌟 图解过程

假设我们要排序数组:[64, 25, 12, 22, 11]

1️⃣ 第一轮:

[64, 25, 12, 22, 11] → 找到最小值11
[11, 25, 12, 22, 64] → 交换后

2️⃣ 第二轮:

[11, 25, 12, 22, 64] → 找到最小值12
[11, 12, 25, 22, 64] → 交换后

3️⃣ 第三轮:

[11, 12, 25, 22, 64] → 找到最小值22
[11, 12, 22, 25, 64] → 交换后

4️⃣ 第四轮:

[11, 12, 22, 25, 64] → 已经排好序了!

📊 性能特点

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 原地排序:是

🎯 适用场景

  1. 数据量较小的排序
  2. 内存空间要求严格的场景
  3. 对稳定性没有要求的排序需求

算法过程打印

public class SelectionSort {
    public static void selectionSort(int[] arr) {
        System.out.println("开始选择排序...\n");
        
        for (int i = 0; i < arr.length - 1; i++) {
            System.out.printf("第 %d 轮排序开始:\n", i + 1);
            System.out.printf("当前数组: %s\n", arrayToString(arr));
            System.out.printf("i = %d, 已排序区间[0-%d], 寻找区间[%d-%d]的最小值\n", 
                            i, i, i + 1, arr.length - 1);
            
            int minIndex = i;
            System.out.printf("假设位置 %d 的值 %d 是最小值\n", i, arr[i]);
            
            // 在未排序区间寻找最小值
            for (int j = i + 1; j < arr.length; j++) {
                System.out.printf("比较 arr[%d]=%d 和 arr[%d]=%d: ", j, arr[j], minIndex, arr[minIndex]);
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                    System.out.printf("找到新的最小值 %d\n", arr[j]);
                } else {
                    System.out.println("不需要更新最小值");
                }
            }
            
            // 交换
            if (minIndex != i) {
                System.out.printf("\n交换位置 %d(%d) 和位置 %d(%d)\n", 
                                i, arr[i], minIndex, arr[minIndex]);
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
                System.out.printf("交换后数组: %s\n", arrayToString(arr));
            } else {
                System.out.printf("\n位置 %d 已经是最小值,不需要交换\n", i);
            }
            System.out.println("\n" + "=".repeat(50) + "\n");
        }
        
        System.out.println("排序完成!");
        System.out.printf("最终数组: %s\n", arrayToString(arr));
    }
    
    private static String arrayToString(int[] arr) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < arr.length; i++) {
            sb.append(arr[i]);
            if (i < arr.length - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }
    
    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22};
        System.out.println("原始数组: " + arrayToString(arr));
        System.out.println("=".repeat(50));
        selectionSort(arr);
    }
}

运行结果:

原始数组: [64, 34, 25, 12, 22]
==================================================
开始选择排序...

第 1 轮排序开始:
当前数组: [64, 34, 25, 12, 22]
i = 0, 已排序区间[0-0], 寻找区间[1-4]的最小值
假设位置 0 的值 64 是最小值
比较 arr[1]=34arr[0]=64: 找到新的最小值 34
比较 arr[2]=25arr[1]=34: 找到新的最小值 25
比较 arr[3]=12arr[2]=25: 找到新的最小值 12
比较 arr[4]=22arr[3]=12: 不需要更新最小值

交换位置 0(64) 和位置 3(12)
交换后数组: [12, 34, 25, 64, 22]

==================================================

第 2 轮排序开始:
当前数组: [12, 34, 25, 64, 22]
i = 1, 已排序区间[0-1], 寻找区间[2-4]的最小值
假设位置 1 的值 34 是最小值
比较 arr[2]=25arr[1]=34: 找到新的最小值 25
比较 arr[3]=64arr[2]=25: 不需要更新最小值
比较 arr[4]=22arr[2]=25: 找到新的最小值 22

交换位置 1(34) 和位置 4(22)
交换后数组: [12, 22, 25, 64, 34]

==================================================

第 3 轮排序开始:
当前数组: [12, 22, 25, 64, 34]
i = 2, 已排序区间[0-2], 寻找区间[3-4]的最小值
假设位置 2 的值 25 是最小值
比较 arr[3]=64arr[2]=25: 不需要更新最小值
比较 arr[4]=34arr[2]=25: 不需要更新最小值

位置 2 已经是最小值,不需要交换

==================================================

第 4 轮排序开始:
当前数组: [12, 22, 25, 64, 34]
i = 3, 已排序区间[0-3], 寻找区间[4-4]的最小值
假设位置 3 的值 64 是最小值
比较 arr[4]=34arr[3]=64: 找到新的最小值 34

交换位置 3(64) 和位置 4(34)
交换后数组: [12, 22, 25, 34, 64]

==================================================

排序完成!
最终数组: [12, 22, 25, 34, 64]

通过这个详细的打印输出,我们可以看到:

  1. 每一轮排序:

    • 当前数组状态
    • 已排序区间和待排序区间
    • 每一步的比较过程
    • 最小值的更新
    • 交换操作的结果
  2. 关键信息:

    • i 表示已排序区间的边界
    • j 从 i+1 开始遍历未排序区间
    • 每轮找到未排序区间的最小值
    • 将最小值放到已排序区间的末尾
  3. 优化点:

    • 当最小值就在正确位置时,避免不必要的交换
    • 已排序区间的元素不再参与比较

这样的输出可以帮助我们更好地理解选择排序的工作原理和过程。

if (minIndex != i)条件处理

让我用一个简单的例子来解释 if (minIndex != i) 的作用:

public static void selectionSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        int minIndex = i;  // 先假设当前位置i的元素就是最小的
        
        // 在后面的元素中寻找是否有更小的
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;  // 找到更小的,更新minIndex
            }
        }
        
        // 关键判断:如果最小值不在当前位置,才需要交换
        if (minIndex != i) {  
            int temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
}

让我们看两种情况:

  1. 需要交换的情况:
原数组:[5, 2, 8, 1, 9]
i = 0, minIndex = 3 (因为1是最小的)

判断:minIndex(3) != i(0),所以需要交换
交换后:[1, 2, 8, 5, 9]
  1. 不需要交换的情况:
原数组:[1, 5, 8, 2, 9]
i = 0, minIndex = 0 (因为1已经在正确位置)

判断:minIndex(0) == i(0),所以不需要交换
结果:[1, 5, 8, 2, 9]

让我们写一个更详细的示例:

public class SelectionSortExample {
    public static void selectionSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            System.out.printf("\n第 %d 轮排序:\n", i + 1);
            System.out.printf("当前数组:%s\n", Arrays.toString(arr));
            System.out.printf("当前位置 i = %d,值为 %d\n", i, arr[i]);
            
            int minIndex = i;
            System.out.println("开始寻找后续元素中的最小值...");
            
            // 寻找最小值
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                    System.out.printf("找到更小的值:arr[%d]=%d < arr[%d]=%d\n", 
                                    j, arr[j], minIndex, arr[minIndex]);
                    minIndex = j;
                }
            }
            
            // 关键判断
            if (minIndex != i) {
                System.out.printf("最小值 %d 在位置 %d,与当前位置 %d 的值 %d 交换\n", 
                                arr[minIndex], minIndex, i, arr[i]);
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
                System.out.printf("交换后数组:%s\n", Arrays.toString(arr));
            } else {
                System.out.printf("当前位置 %d 的值 %d 已经是最小值,无需交换\n", 
                                i, arr[i]);
            }
        }
    }

    public static void main(String[] args) {
        int[] arr1 = {5, 2, 8, 1, 9};  // 需要交换的例子
        System.out.println("示例1 - 需要交换:");
        selectionSort(arr1);
        
        System.out.println("\n" + "=".repeat(50) + "\n");
        
        int[] arr2 = {1, 5, 8, 2, 9};  // 第一个元素不需要交换的例子
        System.out.println("示例2 - 不需要交换:");
        selectionSort(arr2);
    }
}

运行结果:

示例1 - 需要交换:

第 1 轮排序:
当前数组:[5, 2, 8, 1, 9]
当前位置 i = 0,值为 5
开始寻找后续元素中的最小值...
找到更小的值:arr[1]=2 < arr[0]=5
找到更小的值:arr[3]=1 < arr[1]=2
最小值 1 在位置 3,与当前位置 0 的值 5 交换
交换后数组:[1, 2, 8, 5, 9]

==================================================

示例2 - 不需要交换:

第 1 轮排序:
当前数组:[1, 5, 8, 2, 9]
当前位置 i = 0,值为 1
开始寻找后续元素中的最小值...
当前位置 0 的值 1 已经是最小值,无需交换

这个判断的作用是:

  1. 避免不必要的交换操作(当最小值已经在正确位置时)
  2. 提高算法效率
  3. 减少数组元素的移动次数

简单来说:

  • 如果 minIndex == i:说明当前位置的元素就是最小的,不需要交换
  • 如果 minIndex != i:说明找到了更小的元素,需要与当前位置交换

冒泡排序

🫧 冒泡排序的基本思想

想象一下水中的气泡上升,大的气泡会更快地浮到上面。冒泡排序就是这样:

  1. 比较相邻的两个数
  2. 如果前面的数比后面的大,就交换它们
  3. 一轮下来,最大的数就"浮"到了最后
  4. 重复这个过程

💻 超简单的Java代码

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        // 外层循环:表示需要进行多少轮比较
        for (int i = 0; i < arr.length - 1; i++) {
            
            // 内层循环:每轮比较的次数
            for (int j = 0; j < arr.length - 1 - i; j++) {
                
                // 比较相邻元素
                if (arr[j] > arr[j + 1]) {
                    // 交换元素位置
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}
📚 详细说明

1️⃣ 外层循环 for (int i = 0; i < arr.length - 1; i++)

  • i 代表已经排序完成的轮数

  • 为什么是 length-1?因为当只剩最后一个数时,不需要再排序

  • 每完成一轮,就会确定一个最大数的位置

2️⃣ 内层循环 for (int j = 0; j < arr.length - 1 - i; j++)

  • j 是当前比较的位置

  • arr.length - 1 - i 表示本轮需要比较的次数

  • 每轮比较次数会减少,因为后面的i个数已经排好序了

3️⃣ 比较和交换 if (arr[j] > arr[j + 1])

  • 比较相邻的两个数

  • 如果前面的数大于后面的数,就交换它们的位置

  • 使用临时变量 temp 来完成交换

为什么要 arr.length - 1 - i

让我解释一下 arr.length - 1 - i 这个表达式的含义:

🎯 详细分解

假设我们有一个数组 [5, 3, 8, 4],长度为4:

  1. arr.length = 4
  2. arr.length - 1 = 3 (因为数组索引从0开始)
  3. -i 是关键,它表示每轮循环后,最后的i个数已经排好序了
📝 为什么要减i?

让我们看看排序过程:

1️⃣ 第一轮 (i=0):

  • j的范围是 0 到 3-0=3
  • [5, 3, 8, 4][3, 5, 4, 8]
  • 最大的数8已经到最后了!

2️⃣ 第二轮 (i=1):

  • j的范围是 0 到 3-1=2
  • [3, 5, 4, 8][3, 4, 5, 8]
  • 倒数第二大的数5也排好了!

3️⃣ 第三轮 (i=2):

  • j的范围是 0 到 3-2=1
  • [3, 4, 5, 8]
  • 已经排好序了
💡 为什么要这样做?
  1. 效率优化:每轮结束后,最后的i个数已经是最大的且已排序
  2. 避免无谓比较:不需要再比较已经排好序的部分
  3. 减少循环次数:随着外层循环的进行,内层循环的次数会越来越少

如果不减i,就会:

// 低效的写法
for (int j = 0; j < arr.length - 1; j++) {
    // 这样会重复比较已经排好序的部分,浪费计算资源
}

所以 arr.length - 1 - i 是一个优化,让算法更高效!

🌟 举个简单例子

假设要排序:[5, 3, 8, 4]

1️⃣ 第一轮:

[5, 3, 8, 4]  比较53[3, 5, 8, 4]
[3, 5, 8, 4]  比较58[3, 5, 8, 4]
[3, 5, 8, 4]  比较84[3, 5, 4, 8]

2️⃣ 第二轮:

[3, 5, 4, 8]  比较35[3, 5, 4, 8]
[3, 5, 4, 8]  比较54[3, 4, 5, 8]

3️⃣ 第三轮:

[3, 4, 5, 8]  已经排好序了!

💡 为什么这个更简单?

  1. 代码更容易理解和记忆
  2. 实现逻辑更直观
  3. 只需要会比较和交换两个操作
  4. 不需要找最小值,只要比较相邻的就行

⚠️ 注意事项

虽然冒泡排序代码简单,但是:

  • 速度不是很快(时间复杂度也是O(n²))
  • 适合小数据量的排序
  • 主要用于教学目的

如果你觉得这个更容易理解,就用这个方法吧!实际工作中,我们通常会使用Java内置的Arrays.sort()这样的方法,它使用了更高效的算法。

算法过程打印

public class BubbleSort {
    public static void bubbleSort(int[] arr) {
        System.out.println("开始冒泡排序...\n");
        
        // 外层循环:表示需要进行多少轮比较
        for (int i = 0; i < arr.length - 1; i++) {
            System.out.printf("第 %d 轮冒泡:\n", i + 1);
            System.out.printf("当前数组:%s\n", arrayToString(arr));
            boolean swapped = false;  // 标记本轮是否发生交换
            
            // 内层循环:每轮比较的次数
            for (int j = 0; j < arr.length - 1 - i; j++) {
                System.out.printf("比较 arr[%d]=%d 和 arr[%d]=%d: ", 
                                j, arr[j], j + 1, arr[j + 1]);
                
                // 比较相邻元素
                if (arr[j] > arr[j + 1]) {
                    System.out.printf("需要交换\n");
                    // 交换元素位置
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true;
                    System.out.printf("交换后数组:%s\n", arrayToString(arr));
                } else {
                    System.out.printf("不需要交换\n");
                }
            }
            
            if (!swapped) {
                System.out.println("\n本轮未发生交换,数组已经有序,提前结束排序!");
                break;
            }
            
            System.out.printf("\n第 %d 轮结束,最大的 %d 个数已经冒泡到右侧\n", 
                            i + 1, i + 1);
            System.out.println("=" .repeat(50) + "\n");
        }
        
        System.out.println("排序完成!");
        System.out.printf("最终数组:%s\n", arrayToString(arr));
    }
    
    private static String arrayToString(int[] arr) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < arr.length; i++) {
            sb.append(arr[i]);
            if (i < arr.length - 1) {
                sb.append(", ");
            }
        }
        sb.append("]");
        return sb.toString();
    }
    
    public static void main(String[] args) {
        int[] arr = {64, 34, 25, 12, 22};
        System.out.println("原始数组:" + arrayToString(arr));
        System.out.println("=" .repeat(50));
        bubbleSort(arr);
    }
}

运行结果:

原始数组:[64, 34, 25, 12, 22]
==================================================
开始冒泡排序...

第 1 轮冒泡:
当前数组:[64, 34, 25, 12, 22]
比较 arr[0]=64 和 arr[1]=34: 需要交换
交换后数组:[34, 64, 25, 12, 22]
比较 arr[1]=64 和 arr[2]=25: 需要交换
交换后数组:[34, 25, 64, 12, 22]
比较 arr[2]=64 和 arr[3]=12: 需要交换
交换后数组:[34, 25, 12, 64, 22]
比较 arr[3]=64 和 arr[4]=22: 需要交换
交换后数组:[34, 25, 12, 22, 64]

第 1 轮结束,最大的 1 个数已经冒泡到右侧
==================================================

第 2 轮冒泡:
当前数组:[34, 25, 12, 22, 64]
比较 arr[0]=34 和 arr[1]=25: 需要交换
交换后数组:[25, 34, 12, 22, 64]
比较 arr[1]=34 和 arr[2]=12: 需要交换
交换后数组:[25, 12, 34, 22, 64]
比较 arr[2]=34 和 arr[3]=22: 需要交换
交换后数组:[25, 12, 22, 34, 64]

第 2 轮结束,最大的 2 个数已经冒泡到右侧
==================================================

第 3 轮冒泡:
当前数组:[25, 12, 22, 34, 64]
比较 arr[0]=25 和 arr[1]=12: 需要交换
交换后数组:[12, 25, 22, 34, 64]
比较 arr[1]=25 和 arr[2]=22: 需要交换
交换后数组:[12, 22, 25, 34, 64]

第 3 轮结束,最大的 3 个数已经冒泡到右侧
==================================================

第 4 轮冒泡:
当前数组:[12, 22, 25, 34, 64]
比较 arr[0]=12 和 arr[1]=22: 不需要交换
比较 arr[1]=22 和 arr[2]=25: 不需要交换
比较 arr[2]=25 和 arr[3]=34: 不需要交换

本轮未发生交换,数组已经有序,提前结束排序!

排序完成!
最终数组:[12, 22, 25, 34, 64]

关键点解释:

  1. 每一轮冒泡:
1轮:[64, 34, 25, 12, 22][34, 25, 12, 22, 64]
       最大值64冒泡到最右边

第2轮:[34, 25, 12, 22, 64][25, 12, 22, 34, 64]
       第二大值34冒泡到倒数第二位

第3轮:[25, 12, 22, 34, 64][12, 22, 25, 34, 64]
       第三大值25冒泡到倒数第三位
  1. 优化点:
boolean swapped = false;  // 标记是否发生交换
if (!swapped) {          // 如果一轮中没有发生交换
    break;               // 说明数组已经有序,可以提前结束
}
  1. 为什么是 arr.length - 1 - i
- 每轮冒泡后,最大的i个数已经在正确位置
- 不需要再比较已经排好序的部分
- 减少不必要的比较次数

那也就是说 因为i代表每轮 而每轮都已经排了一个最大值到右边 所以 减去i就是等于减去已经拍好的右边最大值的个数

原数组:[5, 3, 8, 4, 2]

第1轮冒泡:
[5, 3, 8, 4, 2]  原始状态
[3, 5, 8, 4, 2]  53比较,交换
[3, 5, 8, 4, 2]  58比较,不交换
[3, 5, 4, 8, 2]  84比较,交换
[3, 5, 4, 2, 8]  82比较,交换
                  ↑
                  最大的数8已经到达最终位置

第2轮冒泡:
[3, 5, 4, 2 | 8]  注意:8已经在正确位置,不需要再比较
[3, 5, 4, 2 | 8]  35比较,不交换
[3, 4, 5, 2 | 8]  54比较,交换
[3, 4, 2, 5 | 8]  52比较,交换
                   ↑
                   第二大的数5到达最终位置

第3轮冒泡:
[3, 4, 2 | 5, 8]  注意:5,8已经在正确位置
[3, 4, 2 | 5, 8]  34比较,不交换
[3, 2, 4 | 5, 8]  42比较,交换
                   ↑
                   第三大的数4到达最终位置

第4轮冒泡:
[3, 2 | 4, 5, 8]  注意:4,5,8已经在正确位置
[2, 3 | 4, 5, 8]  32比较,交换
                   ↑
                   完成排序

这样的详细输出可以帮助我们更好地理解冒泡排序的工作原理和过程。