AIGC标识 选择排序(Selection Sort)

选择排序(Selection Sort)

一、算法思想

选择排序的策略比冒泡排序更"果断":每一轮从未排序区域中选出最小的元素,直接放到已排序区域的末尾。

冒泡排序是"边走边换",每对相邻元素都可能触发交换;选择排序是"先看后选",一轮只在最后做一次交换。

[5, 3, 8, 1, 2] 为例:

第1轮: 从 [5, 3, 8, 1, 2] 中找到最小值 1,与第1位 5 交换
       → [1, 3, 8, 5, 2]

第2轮: 从 [3, 8, 5, 2] 中找到最小值 2,与第2位 3 交换
       → [1, 2, 8, 5, 3]

第3轮: 从 [8, 5, 3] 中找到最小值 3,与第3位 8 交换
       → [1, 2, 3, 5, 8]

第4轮: 从 [5, 8] 中找到最小值 5,已在正确位置,无需交换
       → [1, 2, 3, 5, 8]

排序完成!

核心步骤拆解:

  1. 将数组分为已排序区(左侧)和未排序区(右侧)
  2. 每轮遍历未排序区,找到最小值的下标
  3. 将最小值与未排序区的第一个元素交换
  4. 已排序区向右扩展一位

二、Go 实现

2.1 基础版本

package main

import "fmt"

// SelectionSort 选择排序
func SelectionSort(arr []int) {
	n := len(arr)
	for i := 0; i < n-1; i++ {           // 外层:已排序区的右边界
		minIdx := i                         // 默认当前最小值在位置 i
		for j := i + 1; j < n; j++ {        // 内层:在未排序区中寻找更小值
			if arr[j] < arr[minIdx] {
				minIdx = j                   // 更新最小值下标
			}
		}
		if minIdx != i {                     // 如果最小值不在位置 i,交换
			arr[i], arr[minIdx] = arr[minIdx], arr[i]
		}
	}
}

func main() {
	arr := []int{5, 3, 8, 1, 2, 7, 4, 6}
	fmt.Println("排序前:", arr)
	SelectionSort(arr)
	fmt.Println("排序后:", arr)
}

运行结果:

排序前: [5 3 8 1 2 7 4 6]
排序后: [1 2 3 4 5 6 7 8]

2.2 双向选择排序(鸡尾酒选择排序)

每一轮同时选出未排序区的最小值和最大值,分别放到左端和右端,这样可以减少一半的轮数:

// SelectionSortBidirectional 双向选择排序
func SelectionSortBidirectional(arr []int) {
	n := len(arr)
	left, right := 0, n-1

	for left < right {
		// 同时找最小和最大
		minIdx, maxIdx := left, right

		for j := left; j <= right; j++ {
			if arr[j] < arr[minIdx] {
				minIdx = j
			}
			if arr[j] > arr[maxIdx] {
				maxIdx = j
			}
		}

		// 先交换最小值到左端
		arr[left], arr[minIdx] = arr[minIdx], arr[left]

		// 注意:如果 maxIdx == left,刚才的交换已经把最大值移到了 minIdx 位置
		if maxIdx == left {
			maxIdx = minIdx
		}
		// 再交换最大值到右端
		arr[right], arr[maxIdx] = arr[maxIdx], arr[right]

		left++
		right--
	}
}

关键陷阱:当最大值恰好在 left 位置时,第一次交换会把它移走,所以需要修正 maxIdx。这是双向选择排序最容易出 bug 的地方。

三、复杂度分析

情况 时间复杂度 说明
最坏情况 O(n²) 完全逆序,每轮都要全量扫描
最好情况 O(n²) 已排序,每轮仍需扫描确认最小值
平均情况 O(n²) 每轮扫描约 n-i 个元素

| 空间复杂度 | O(1) | 原地排序 |

3.1 为什么最好情况也是 O(n²)

这是选择排序和冒泡排序的关键区别!冒泡排序(优化版)在已排序数组上只需一轮就能检测到没有交换并提前终止——O(n)。但选择排序不管数据是否已排好,每一轮都必须扫描完整个未排序区域来确认最小值位置。

即使 [1, 2, 3, 4, 5] 已经有序,第 1 轮还是要从头到尾扫一遍确认 1 是最小值,第 2 轮还要扫一遍确认 2 是次小值……

3.2 比较次数

  • 第 1 轮比较 n-1 次
  • 第 2 轮比较 n-2 次
  • ...
  • 第 n-1 轮比较 1 次

总比较次数 = n(n-1)/2 → O(n²)(无论什么输入)

3.3 交换次数

选择排序每轮最多交换 1 次,总交换次数最多 n-1 次 → O(n)

这是选择排序相对于冒泡排序的优势——交换操作远少于冒泡。在交换代价高的场景(如交换大对象、交换涉及磁盘IO)中,这个优势有意义。

四、稳定性分析

选择排序是不稳定的。

原因在于交换可能跨越相等元素。看这个例子:

原始: [5a, 5b, 3, 2]  (5a 和 5b 值相同,a 在 b 前面)

第1轮: 最小值是 2(位置3),与 5a(位置0)交换
       → [2, 5b, 3, 5a]  ← 5a 和 5b 的相对顺序改变了!不稳定 ✗

交换把位置 0 的 5a 直接搬到了位置 3,跳过了中间的 5b——两者的相对顺序从 5a在前 变成了 5b在前

如何让选择排序变稳定?用插入代替交换——找到最小值后,不直接与首位交换,而是把最小值取出,首位及中间元素依次右移一格,再把最小值插入首位。这本质上就是插入排序了。

五、选择排序 vs 冒泡排序

对比维度 选择排序 冒泡排序
比较次数 固定 n(n-1)/2 固定 n(n-1)/2(最坏)
交换次数 最多 n-1 次 最坏 n(n-1)/2 次
最好时间 O(n²) O(n)(优化版)
稳定性 ❌ 不稳定 ✅ 稳定
适应性 不适应(对有序数据无加速) 适应(优化版能提前终止)

核心区别:

  • 冒泡排序适合近乎有序的数据——优化版可以提前终止
  • 选择排序适合交换代价高的场景——交换次数远少于冒泡
  • 冒泡排序稳定,选择排序不稳定——需要稳定性时只能选冒泡

六、适用场景

选择排序在实际开发中同样很少使用,但以下场景可以考虑:

  1. 交换代价极高:当元素是大型结构体或涉及磁盘IO时,减少交换次数有实际意义
  2. 小数据量:n < 20 时,O(n²) 排序的开销差异不大
  3. 内存极其受限:选择排序只需要常数额外空间和最少的交换次数
  4. 辅助理解排序:与冒泡排序对比,理解"扫描式选择"和"逐步式冒泡"两种策略的差异

七、选择排序的变体应用

7.1 每轮选出 Top-K

不需要完全排序,只跑 k 轮选择排序,就能把前 k 个最小值放到数组前部:

// SelectTopK 将前 k 个最小元素放到数组前 k 位
func SelectTopK(arr []int, k int) {
	n := len(arr)
	for i := 0; i < k && i < n-1; i++ {
		minIdx := i
		for j := i + 1; j < n; j++ {
			if arr[j] < arr[minIdx] {
				minIdx = j
			}
		}
		if minIdx != i {
			arr[i], arr[minIdx] = arr[minIdx], arr[i]
		}
	}
}

func main() {
	arr := []int{8, 3, 5, 1, 7, 2, 6, 4}
	SelectTopK(arr, 3)
	fmt.Println("前3小:", arr[:3]) // [1, 2, 3]
}

时间复杂度 O(n × k),比完全排序 O(n²) 快。

八、小结

选择排序的核心思想是每轮选最小,一次交换到位。与冒泡排序的"边比边换"相比,它更"先看后做"——先扫描整个区间找到目标,再一次性交换。

关键记忆点:

  • 选择排序每轮只交换一次,这是它相对于冒泡排序的核心优势
  • 最好情况也是 O(n²)——因为它无法利用数据的有序性加速
  • 不稳定——交换可能跨越相等元素,破坏相对顺序
  • 与冒泡排序形成对照:冒泡稳定但交换多,选择交换少但不稳定

冒泡和选择是排序入门的两块基石——理解它们各自的优缺点,才能在后续学习插入排序、快排、归并时做出更好的选择。

posted @ 2026-07-28 11:14  FfHUCisI  阅读(12)  评论(0)    收藏  举报