考点频率 :★★★★★(数据结构必考,选择题常考冒泡的优化判断与快速排序的一趟划分结果)
难度 :⭐⭐⭐⭐(冒泡简单,快速排序需要重点理解分治与划分过程)
建议:重点掌握冒泡排序的优化(提前结束)及其时间复杂度,重点掌握快速排序的一趟划分过程(选基准、交替填充),熟记两种排序的适用场景
1️⃣ 什么是交换类排序?
交换类排序 的核心思想是:通过比较 和交换相邻(或不相邻)元素的位置,逐步消除逆序对,使序列有序。
打个比方:交换类排序就像整理一排乱放的书。冒泡排序是"相邻两本比较,高的往后挪",一遍遍扫描直到全部排好。快速排序则是"先挑一本书作为标准(基准),小的放左边,大的放右边",然后分别对左右两部分重复这个过程。
最经典的两种交换排序:
| 算法 | 核心策略 | 一句话概括 |
|---|---|---|
| 冒泡排序 | 相邻元素两两比较,大的(或小的)逐步"冒泡"到两端 | "两两比较,大的冒泡" |
| 快速排序 | 选一个基准,将序列分成左右两部分(左小右大),递归排序 | "选基准,分两边,各自排" |
2️⃣ 冒泡排序(Bubble Sort)
2.1 核心思想
冒泡排序通过相邻元素的两两比较和交换,将最大(或最小)的元素逐步"冒泡"到序列的末端。
每一轮排序,都会确定一个元素的最终位置(最大或最小),就像气泡从水底冒到水面一样。
2.2 算法步骤
以升序排列为例:
- 从序列的第一个元素开始,依次比较相邻的两个元素
- 如果前面的元素大于后面的元素,交换它们的位置
- 经过第一轮后,最大的元素"冒泡"到了序列的最后
- 对剩余的前 n−1n-1n−1 个元素重复上述过程
- 直到整个序列有序
c
// 基础版本
void BubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
Swap(&arr[j], &arr[j + 1]);
}
}
}
}
2.3 执行过程示例
对数组 [5, 1, 4, 2, 8] 进行冒泡排序(升序):
第1轮(i=0):
- 5和1比较 → 交换 →
[1, 5, 4, 2, 8] - 5和4比较 → 交换 →
[1, 4, 5, 2, 8] - 5和2比较 → 交换 →
[1, 4, 2, 5, 8] - 5和8比较 → 不交换 →
[1, 4, 2, 5, 8]← 最大值8已就位
第2轮(i=1):
- 1和4比较 → 不交换
- 4和2比较 → 交换 →
[1, 2, 4, 5, 8] - 4和5比较 → 不交换 ← 5已就位
第3轮(i=2):
- 1和2比较 → 不交换
- 2和4比较 → 不交换 ← 4已就位
第4轮(i=3):
- 1和2比较 → 不交换
结果 :[1, 2, 4, 5, 8]
2.4 冒泡排序的优化(软考常考)
如果某一轮排序中没有发生任何交换 ,说明序列已经有序,可以提前结束排序。
c
// 优化版本
void BubbleSortOptimized(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int swapped = 0; // 标记本轮是否发生交换
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
Swap(&arr[j], &arr[j + 1]);
swapped = 1;
}
}
if (swapped == 0) break; // 没有交换,提前结束
}
}
考点:当题目说"某排序算法在某轮后没有发生元素交换"时,往往是在考冒泡排序的提前终止条件。
2.5 时间复杂度与空间复杂度
| 情况 | 比较次数 | 交换次数 | 时间复杂度 |
|---|---|---|---|
| 最好情况(已有序) | n−1n-1n−1 次 | 0 次 | O(n)O(n)O(n) |
| 最坏情况(逆序) | n(n−1)/2n(n-1)/2n(n−1)/2 次 | n(n−1)/2n(n-1)/2n(n−1)/2 次 | O(n2)O(n^2)O(n2) |
| 平均情况 | 约 n2/2n^2/2n2/2 次 | 约 n2/2n^2/2n2/2 次 | O(n2)O(n^2)O(n2) |
空间复杂度 :O(1)O(1)O(1)(原地排序,仅需几个临时变量)
稳定性 :✅ 稳定(相等元素不交换位置)
3️⃣ 快速排序(Quick Sort)
3.1 核心思想
快速排序采用分治(Divide and Conquer) 策略:
- 划分 :从序列中选取一个基准元素(
pivot),将序列分成两部分------左边所有元素 ≤ pivot ≤ 右边所有元素 - 递归:对左右两个子序列分别进行快速排序
- 合并:不需要额外的合并操作(因为划分已经将元素放到了正确的位置)
打个比方:你面前有一堆随机排列的书,你随手抽出一本(基准),然后:所有书名小于它的放左边,所有书名大于它的放右边。这样,这本"基准书"就找到了它在书架上的最终位置。然后对左边和右边的书分别做同样的操作------这就是递归。
3.2 划分过程(核心考点)
快速排序的划分过程是软考选择题的高频考点------给定一个序列和基准,要求写出划分后的序列。
划分步骤(以升序为例):
- 选择基准元素(通常选第一个元素或最后一个元素)
- 从序列的两端交替向中间扫描:
- 从右往左找第一个小于基准的元素,放到左边空位
- 从左往右找第一个大于基准的元素,放到右边空位
- 重复直到左右指针相遇,将基准放入该位置
c
int Partition(int arr[], int low, int high) {
int pivot = arr[low]; // 选择第一个元素作为基准
while (low < high) {
// 从右往左找第一个小于基准的元素
while (low < high && arr[high] >= pivot) high--;
arr[low] = arr[high]; // 移到左边空位
// 从左往右找第一个大于基准的元素
while (low < high && arr[low] <= pivot) low++;
arr[high] = arr[low]; // 移到右边空位
}
arr[low] = pivot; // 基准归位
return low; // 返回基准的最终位置
}
3.3 执行过程示例
对数组 [5, 3, 8, 1, 4, 7, 2, 6] 进行一趟划分(基准取第一个元素 5):
初始: [5, 3, 8, 1, 4, 7, 2, 6]
↑ ↑
low high
第1步:从右往左找 < 5 的元素 → 6≥5不动,2<5停
arr[low] = arr[high] → 5的位置被2覆盖
[2, 3, 8, 1, 4, 7, 2, 6]
↑
high
第2步:从左往右找 > 5 的元素 → 2≤5不动,3≤5不动,8>5停
arr[high] = arr[low] → high位置被8覆盖
[2, 3, 8, 1, 4, 7, 8, 6]
↑
low
第3步:从右往左找 < 5 的元素 → 8≥5不动,7≥5不动,4<5停
arr[low] = arr[high] → low位置被4覆盖
[2, 3, 4, 1, 4, 7, 8, 6]
↑
high
第4步:从左往右找 > 5 的元素 → 4≤5不动,1≤5不动,low与high相遇
此时 low == high 在索引3(原值1的位置)
将基准5放入 arr[low]
[2, 3, 4, 1, 5, 7, 8, 6]
一趟划分后结果 :[2, 3, 4, 1, 5, 7, 8, 6]
基准 5 左边的元素都小于等于5(2,3,4,1),右边的元素都大于等于5(7,8,6)。
3.4 时间复杂度与空间复杂度
| 情况 | 时间复杂度 | 说明 |
|---|---|---|
| 最好情况(每次基准都在中间) | O(nlogn)O(n \log n)O(nlogn) | 递归深度最小 |
| 最坏情况(每次基准都是最大/最小) | O(n2)O(n^2)O(n2) | 退化为冒泡排序 |
| 平均情况 | O(nlogn)O(n \log n)O(nlogn) | 实测表现优异 |
空间复杂度:
- 最好/平均:O(logn)O(\log n)O(logn)(递归栈深度)
- 最坏:O(n)O(n)O(n)(递归退化为链表)
稳定性 :❌ 不稳定(划分过程中会跨越式交换元素,相同元素的相对顺序可能改变)
4️⃣ 冒泡 vs 快速排序(完整对比表,重点!)
| 对比项 | 冒泡排序 | 快速排序 |
|---|---|---|
| 核心策略 | 相邻比较,逐步"冒泡" | 分治,选基准,划分 |
| 最好时间复杂度 | O(n)O(n)O(n)(已有序,优化版) | O(nlogn)O(n \log n)O(nlogn)(基准在中间) |
| 最坏时间复杂度 | O(n2)O(n^2)O(n2) | O(n2)O(n^2)O(n2)(基准极值) |
| 平均时间复杂度 | O(n2)O(n^2)O(n2) | O(nlogn)O(n \log n)O(nlogn) |
| 空间复杂度 | O(1)O(1)O(1) | O(logn)O(\log n)O(logn)(递归栈) |
| 稳定性 | ✅ 稳定 | ❌ 不稳定 |
| 适用场景 | 数据量小、基本有序 | 数据量大、随机排列 |
| 是否原地排序 | ✅ 是 | ✅ 是 |
5️⃣ 经典例题
例题1(冒泡排序优化):某序列经过一轮冒泡排序后没有发生任何元素交换,则下列说法正确的是( )。
A. 序列已经完全有序
B. 序列可能还未完全有序,但最大元素已在末尾
C. 需要继续排序
D. 序列一定是逆序的
解析 :冒泡排序中,如果某一轮没有发生任何交换,说明相邻元素之间已经满足有序关系,整个序列已经有序,可以提前结束。选 A。
例题2 (快速排序划分):对序列 [6, 1, 3, 9, 2, 7, 4, 8, 5] 以第一个元素 6 为基准进行一趟划分,划分后的序列为( )。
A. [5, 1, 3, 4, 2, 6, 7, 8, 9]
B. [5, 1, 3, 4, 2, 6, 9, 7, 8]
C. [5, 1, 3, 4, 2, 6, 7, 8, 9]
D. [4, 1, 3, 2, 5, 6, 7, 8, 9]
解析 :序列中大于6的元素为9,7,8(顺序需按划分过程调整);小于6的元素为1,3,2,4,5。一趟划分后,6左边的元素都应该小于它,右边的都大于它,且小于部分的顺序是交替填充的。按划分过程模拟,正确答案为 A。
例题3(判断):快速排序在所有情况下都比冒泡排序快。( )
解析 :错误。快速排序在最坏情况下(如序列已经有序且基准每次都取第一个元素)时间复杂度为 O(n2)O(n^2)O(n2),与冒泡排序相同。快速排序的优势体现在平均情况下。
6️⃣ 记忆口诀
冒泡相邻两两比,大的往后小的移。
没有交换就可停,最好情况 O(n)O(n)O(n) 行。
快排选基准分两边,左小右大递归连。
平均 O(nlogn)O(n \log n)O(nlogn),最坏 O(n2)O(n^2)O(n2) 当有序。
冒泡稳定快排不稳,数据量大用快排准。
7️⃣ 小测验(评论区对答案)
对序列
[7, 2, 9, 1, 5, 8, 3]以第一个元素 7 为基准进行快速排序的一趟划分,划分后基准 7 所在的位置是( )。A. 下标 3
B. 下标 4
C. 下标 5
D. 下标 6
🔔 本专栏日更,点击头像 → 专栏《软考中级高频考点》订阅,第一时间接收新内容
#软考中级 #软件设计师 #冒泡排序 #快速排序 #排序算法 #数据结构 #软考备考