【数据结构】交换类排序:冒泡与快速排序

考点频率 :★★★★★(数据结构必考,选择题常考冒泡的优化判断与快速排序的一趟划分结果)

难度 :⭐⭐⭐⭐(冒泡简单,快速排序需要重点理解分治与划分过程)

建议:重点掌握冒泡排序的优化(提前结束)及其时间复杂度,重点掌握快速排序的一趟划分过程(选基准、交替填充),熟记两种排序的适用场景

1️⃣ 什么是交换类排序?

交换类排序 的核心思想是:通过比较交换相邻(或不相邻)元素的位置,逐步消除逆序对,使序列有序。

打个比方:交换类排序就像整理一排乱放的书。冒泡排序是"相邻两本比较,高的往后挪",一遍遍扫描直到全部排好。快速排序则是"先挑一本书作为标准(基准),小的放左边,大的放右边",然后分别对左右两部分重复这个过程。

最经典的两种交换排序:

算法 核心策略 一句话概括
冒泡排序 相邻元素两两比较,大的(或小的)逐步"冒泡"到两端 "两两比较,大的冒泡"
快速排序 选一个基准,将序列分成左右两部分(左小右大),递归排序 "选基准,分两边,各自排"

2️⃣ 冒泡排序(Bubble Sort)

2.1 核心思想

冒泡排序通过相邻元素的两两比较和交换,将最大(或最小)的元素逐步"冒泡"到序列的末端。

每一轮排序,都会确定一个元素的最终位置(最大或最小),就像气泡从水底冒到水面一样。

2.2 算法步骤

以升序排列为例:

  1. 从序列的第一个元素开始,依次比较相邻的两个元素
  2. 如果前面的元素大于后面的元素,交换它们的位置
  3. 经过第一轮后,最大的元素"冒泡"到了序列的最后
  4. 对剩余的前 n−1n-1n−1 个元素重复上述过程
  5. 直到整个序列有序
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) 策略:

  1. 划分 :从序列中选取一个基准元素(pivot),将序列分成两部分------左边所有元素 ≤ pivot ≤ 右边所有元素
  2. 递归:对左右两个子序列分别进行快速排序
  3. 合并:不需要额外的合并操作(因为划分已经将元素放到了正确的位置)

打个比方:你面前有一堆随机排列的书,你随手抽出一本(基准),然后:所有书名小于它的放左边,所有书名大于它的放右边。这样,这本"基准书"就找到了它在书架上的最终位置。然后对左边和右边的书分别做同样的操作------这就是递归。

3.2 划分过程(核心考点)

快速排序的划分过程是软考选择题的高频考点------给定一个序列和基准,要求写出划分后的序列。

划分步骤(以升序为例):

  1. 选择基准元素(通常选第一个元素或最后一个元素)
  2. 从序列的两端交替向中间扫描:
    • 从右往左找第一个小于基准的元素,放到左边空位
    • 从左往右找第一个大于基准的元素,放到右边空位
  3. 重复直到左右指针相遇,将基准放入该位置
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(nlog⁡n)O(n \log n)O(nlogn) 递归深度最小
最坏情况(每次基准都是最大/最小) O(n2)O(n^2)O(n2) 退化为冒泡排序
平均情况 O(nlog⁡n)O(n \log n)O(nlogn) 实测表现优异

空间复杂度

  • 最好/平均:O(log⁡n)O(\log n)O(logn)(递归栈深度)
  • 最坏:O(n)O(n)O(n)(递归退化为链表)

稳定性 :❌ 不稳定(划分过程中会跨越式交换元素,相同元素的相对顺序可能改变)

4️⃣ 冒泡 vs 快速排序(完整对比表,重点!)

对比项 冒泡排序 快速排序
核心策略 相邻比较,逐步"冒泡" 分治,选基准,划分
最好时间复杂度 O(n)O(n)O(n)(已有序,优化版) O(nlog⁡n)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(nlog⁡n)O(n \log n)O(nlogn)
空间复杂度 O(1)O(1)O(1) O(log⁡n)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(nlog⁡n)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

🔔 本专栏日更,点击头像 → 专栏《软考中级高频考点》订阅,第一时间接收新内容

#软考中级 #软件设计师 #冒泡排序 #快速排序 #排序算法 #数据结构 #软考备考

相关推荐
chnyi6_ya1 小时前
VideoHallu 论文阅读笔记
论文阅读·笔记
Nil2082 小时前
leetcode 108有序数组转换为二叉搜索树
数据结构·算法·leetcode
高频因子挖掘机2 小时前
QuantDash 成交量单位统一实战:从“手”到“股”的跨市场量化数据清洗全流程
后端·算法·github
Wang's Blog2 小时前
PostgreSQL笔记51: 基于 pgvector 构建企业级智能问答系统
数据库·笔记·postgresql
Escalating_xu2 小时前
【C++ STL简介】从六大组件到容器、迭代器与算法协作
java·c++·算法
自小吃多2 小时前
Capture软件原理图添加差分属性笔记
笔记·嵌入式硬件
纪念 2293 小时前
算法二叉树(一)
算法
日常筹谋记3 小时前
ATS选不对运维两行泪:数据中心市电与柴发切换场景的双电源自动转换开关选型笔记
运维·数据库·笔记
疯狂打码的少年3 小时前
【数据结构】哈希表:构造与冲突处理
数据结构·笔记·哈希算法·散列表