Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来

Python系列文章目录

PyTorch系列文章目录

机器学习系列文章目录

深度学习系列文章目录

Java系列文章目录

JavaScript系列文章目录

Python系列文章目录

Go语言系列文章目录

Docker系列文章目录

数据结构与算法系列文章目录

01-【数据结构与算法-Day 1】程序世界的基石:到底什么是数据结构与算法?
02-【数据结构与算法-Day 2】衡量代码的标尺:时间复杂度与大O表示法入门
03-【数据结构与算法-Day 3】揭秘算法效率的真相:全面解析O(n^2), O(2^n)及最好/最坏/平均复杂度
04-【数据结构与算法-Day 4】从O(1)到O(n²),全面掌握空间复杂度分析
05-【数据结构与算法-Day 5】实战演练:轻松看懂代码的时间与空间复杂度
06-【数据结构与算法-Day 6】最朴素的容器 - 数组(Array)深度解析
07-【数据结构与算法-Day 7】告别数组束缚,初识灵活的链表 (Linked List)
08-【数据结构与算法-Day 8】手把手带你拿捏单向链表:增、删、改核心操作详解
09-【数据结构与算法-Day 9】图解单向链表:从基础遍历到面试必考的链表反转
10-【数据结构与算法-Day 10】双向奔赴:深入解析双向链表(含图解与代码)
11-【数据结构与算法-Day 11】从循环链表到约瑟夫环,一文搞定链表的终极形态
12-【数据结构与算法-Day 12】深入浅出栈:从“后进先出”原理到数组与链表双实现
13-【数据结构与算法-Day 13】栈的应用:从括号匹配到逆波兰表达式求值,面试高频考点全解析
14-【数据结构与算法-Day 14】先进先出的公平:深入解析队列(Queue)的核心原理与数组实现
15-【数据结构与算法-Day 15】告别“假溢出”:深入解析循环队列与双端队列
16-【数据结构与算法-Day 16】队列的应用:广度优先搜索(BFS)的基石与迷宫寻路实战
17-【数据结构与算法-Day 17】揭秘哈希表:O(1)查找速度背后的魔法
18-【数据结构与算法-Day 18】面试必考!一文彻底搞懂哈希冲突四大解决方案:开放寻址、拉链法、再哈希
19-【数据结构与算法-Day 19】告别线性世界,一文掌握树(Tree)的核心概念与表示法
20-【数据结构与算法-Day 20】从零到一掌握二叉树:定义、性质、特殊形态与存储结构全解析
21-【数据结构与算法-Day 21】精通二叉树遍历(上):前序、中序、后序的递归与迭代实现
22-【数据结构与算法-Day 22】玩转二叉树遍历(下):广度优先搜索(BFS)与层序遍历的奥秘
23-【数据结构与算法-Day 23】为搜索而生:一文彻底搞懂二叉搜索树 (BST) 的奥秘
24-【数据结构与算法-Day 24】平衡的艺术:图解AVL树,彻底告别“瘸腿”二叉搜索树
25-【数据结构与算法-Day 25】工程中的王者:深入解析红黑树 (Red-Black Tree)
26-【数据结构与算法-Day 26】堆:揭秘优先队列背后的“特殊”完全二叉树
27-【数据结构与算法-Day 27】堆的应用:从堆排序到 Top K 问题,一文彻底搞定!
28-【数据结构与算法-Day 28】字符串查找的终极利器:深入解析字典树 (Trie / 前缀树)
29-【数据结构与算法-Day 29】从社交网络到地图导航,一文带你入门终极数据结构:图
30-【数据结构与算法-Day 30】图的存储:邻接矩阵 vs 邻接表,哪种才是最优选?
31-【数据结构与算法-Day 31】图的遍历:深度优先搜索 (DFS) 详解,一条路走到黑的智慧
32-【数据结构与算法-Day 32】掌握广度优先搜索 (BFS),轻松解决无权图最短路径问题
33-【数据结构与算法-Day 33】最小生成树之 Prim 算法:从零构建通信网络
34-【数据结构与算法-Day 34】最小生成树之 Kruskal 算法:从边的视角构建最小网络
35-【数据结构与算法-Day 35】拓扑排序:从依赖关系到关键路径的完整解析
36-【数据结构与算法-Day 36】查找算法入门:从顺序查找的朴素到二分查找的惊艳
37-【数据结构与算法-Day 37】超越二分查找:探索插值、斐波那契与分块查找的奥秘
38-【数据结构与算法-Day 38】排序算法入门:图解冒泡排序与选择排序,从零掌握 O(n²) 经典思想



摘要

欢迎来到排序算法系列!作为算法学习中的核心主题,排序是处理数据的基本操作,也是面试中的高频考点。本篇作为排序系列的开篇,我们将从最基础、最直观的两种算法——冒泡排序(Bubble Sort)和选择排序(Selection Sort)入手。本文将通过图解、核心思想剖析、Java 代码实现、性能优化及全方位对比,带你彻底搞懂这两种时间复杂度为 O ( n 2 ) O(n^2) O(n2) 的经典排序算法。无论你是初学者还是希望巩固基础的开发者,本文都将为你打下坚实的排序算法地基,理解它们的设计哲学、性能瓶颈以及在特定场景下的意义。

一、排序算法:让数据井然有序的艺术

在正式学习具体的排序算法之前,我们首先需要建立一个宏观的认知,了解什么是排序、如何衡量一个排序算法的优劣。

1.1 什么是排序算法?

排序算法(Sorting Algorithm)是一种能将一串数据(如数组、列表)依照特定顺序(升序、降序或按特定规则)进行重新排列的算法。就像我们将书架上的书按高度排列,或者将通讯录里的人名按首字母排序一样,排序是计算机科学中最基础也最核心的操作之一。

核心目标:将一个无序的序列转换为一个有序的序列。

1.2 排序算法的分类

排序算法种类繁多,我们可以从不同维度对其进行分类:

  • 按数据存储位置

    • 内部排序:所有待排序的数据都存放在内存中进行处理。这是我们本系列文章主要讨论的范畴。
    • 外部排序:数据量过大,无法一次性加载到内存中,需要借助外部存储(如硬盘)进行排序。
  • 按比较基础

    • 比较排序:通过比较元素之间的大小关系来决定其顺序,如冒泡排序、选择排序、快速排序等。其时间复杂度的理论下限是 O ( n log ⁡ n ) O(n \log n) O(nlogn)
    • 非比较排序:不通过比较来排序,而是利用元素的自身特性(如数值范围、位数),可以突破 O ( n log ⁡ n ) O(n \log n) O(nlogn) 的限制,达到线性时间复杂度 O ( n ) O(n) O(n)。例如计数排序、桶排序、基数排序。

1.3 衡量排序算法的维度

评判一个排序算法的好坏,我们通常关注以下三个核心指标:

(1)时间复杂度

描述算法执行时间随数据规模 n n n 增长的变化趋势。主要分为三种情况:

  • 最坏时间复杂度:算法在最不利输入下的运行时间。
  • 平均时间复杂度:算法在所有可能输入下,期望的运行时间。这是衡量算法性能最重要的指标。
  • 最好时间复杂度:算法在最理想输入下的运行时间。
(2)空间复杂度

描述算法在运行过程中,额外临时占用的存储空间随数据规模 n n n 增长的变化趋势。如果额外空间是常数级的,则称为原地排序(In-place Sort),空间复杂度为 O ( 1 ) O(1) O(1)

(3)稳定性

稳定性(Stability)是指,如果待排序的序列中有两个或多个相等的元素,排序后这些相等元素的相对位置保持不变,则称该排序算法是稳定的;反之,则为不稳定的。

例如,对于序列 [(5, a), (2, b), (5, c)],按数字大小排序。

  • 稳定排序结果[(2, b), (5, a), (5, c)] (元素 a 仍然在 c 前面)
  • 不稳定排序结果[(2, b), (5, c), (5, a)] (元素 c 跑到了 a 前面)

稳定性在某些需要保持原始次序的业务场景下(如多级排序)至关重要。

二、冒泡排序(Bubble Sort)

冒泡排序是最简单、最广为人知的排序算法之一,它的名字生动地描述了其工作过程:较小(或较大)的元素会像水中的气泡一样,慢慢“浮”到序列的顶端。

2.1 核心思想:邻居间的“沉浮”

冒泡排序的核心思想是重复地遍历待排序的序列,一次比较两个相邻的元素,如果它们的顺序错误就把它们交换过来。这个过程会一直重复,直到没有再需要交换的元素为止,说明序列已经完全有序。

每一轮遍历,都会将当前未排序部分的最大(或最小)元素“冒泡”到该部分的末尾。

2.2 图解执行过程

让我们以数组 [5, 1, 4, 2, 8] 为例,演示升序排序的第一轮冒泡过程。

graph TD
    subgraph 第一轮遍历 (将最大值 8 冒泡到末尾)
        A["[5, 1, 4, 2, 8]"] -- "5 > 1, 交换" --> B["[1, 5, 4, 2, 8]"]
        B -- "5 > 4, 交换" --> C["[1, 4, 5, 2, 8]"]
        C -- "5 > 2, 交换" --> D["[1, 4, 2, 5, 8]"]
        D -- "5 < 8, 不交换" --> E["[1, 4, 2, 5, 8]"]
    end
    subgraph 第一轮结果
        F["[1, 4, 2, 5, 8]"]
    end
    E --> F

经过第一轮,最大的元素 8 已经到达了它的最终位置。第二轮遍历只需对前 n-1 个元素进行同样的操作,依此类推。

2.3 代码实现(Java)

下面是冒泡排序的基础实现。外层循环控制总共需要进行 n-1 轮比较,内层循环则负责在每一轮中进行相邻元素的比较与交换。

public class SortingAlgorithms {
/**
* 基础冒泡排序
* @param arr 待排序数组
*/
public void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int n = arr.length;
// 外层循环控制总轮数,共 n-1 轮
for (int i = 0; i < n - 1; i++) {
// 内层循环负责每轮的比较与交换
// 每轮过后,末尾的元素即为有序,所以比较范围可以缩小
for (int j = 0; j < n - 1 - i; j++) {
// 如果前一个元素大于后一个元素,则交换
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}

2.4 冒泡排序的优化

基础版的冒泡排序无论输入数组的初始状态如何,都会执行完所有的循环。但如果数组在某一轮遍历中没有发生任何交换,说明它已经是有序的了,后续的遍历都是不必要的。我们可以据此进行优化。

(1)提前退出的标志位

我们引入一个布尔型标志位 swapped,用于记录当前轮次是否发生了交换。

/**
* 优化的冒泡排序 (增加标志位)
* @param arr 待排序数组
*/
public void optimizedBubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
boolean swapped = false; // 标志位,默认为 false
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true; // 发生了交换,将标志位置为 true
}
}
// 如果在一轮中一次交换都没有发生,说明数组已经有序,直接退出
if (!swapped) {
break;
}
}
}

这个优化对于近乎有序的数组效果非常显著。

2.5 复杂度与稳定性分析

(1)时间复杂度
  • 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)。当输入数组完全逆序时,需要进行 n-1 轮,每轮的比较次数接近 n n n,总比较次数约为 n 2 / 2 n^2/2 n2/2
  • 平均时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 最好时间复杂度: O ( n ) O(n) O(n)。当输入数组已经有序时,经过优化的冒泡排序只需进行第一轮遍历( n − 1 n-1 n1 次比较),没有发生交换,随即退出。
(2)空间复杂度

冒泡排序只在交换元素时使用了一个临时变量 temp,其空间占用是固定的,与数据规模 n n n 无关。因此,空间复杂度为 O ( 1 ) O(1) O(1),是原地排序。

(3)稳定性

冒泡排序是稳定的。因为只有当 arr[j] > arr[j+1] 时才会交换,对于相等的元素 arr[j] == arr[j+1],它们的位置不会改变,因此相等元素的相对顺序得以保持。

三、选择排序(Selection Sort)

选择排序是另一种简单直观的排序算法,其思路比冒泡排序更加“目标明确”。

3.1 核心思想:每次选出“天选之子”

选择排序的策略是,首先在未排序的序列中找到最小(或最大)的元素,然后将其存放到排序序列的起始位置。接着,再从剩余未排序的元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

可以想象成排队时,每次都从剩下的人里挑出最矮的一个,让他站到队伍的最前面。

3.2 图解执行过程

同样以数组 [5, 1, 4, 2, 8] 为例,演示升序的选择排序过程。

第一轮

  • 未排序区:[5, 1, 4, 2, 8]
  • 找到最小值 1,其索引为 1
  • 1 与未排序区的第一个元素 5 交换。
  • 结果:[1, 5, 4, 2, 8],已排序区:[1]

第二轮

  • 未排序区:[5, 4, 2, 8]
  • 找到最小值 2,其索引为 3
  • 2 与未排序区的第一个元素 5 交换。
  • 结果:[1, 2, 4, 5, 8],已排序区:[1, 2]

…依此类推,直到整个数组有序。

3.3 代码实现(Java)

外层循环 i 标记已排序区域的边界,内层循环 j 负责在 [i..n-1] 范围内寻找最小元素的索引 minIndex

public class SortingAlgorithms {
/**
* 选择排序
* @param arr 待排序数组
*/
public void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int n = arr.length;
// 外层循环控制轮数,也代表已排序部分的边界
for (int i = 0; i < n - 1; i++) {
// 记录当前轮次最小元素的索引
int minIndex = i;
// 内层循环在未排序部分 [i+1, n-1] 中寻找最小元素
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j; // 更新最小元素的索引
}
}
// 如果最小元素不是当前轮次的第一个元素,则交换
if (minIndex != i) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
}

3.4 复杂度与稳定性分析

(1)时间复杂度
  • 最坏、平均、最好时间复杂度均为: O ( n 2 ) O(n^2) O(n2)
    无论输入数组的初始顺序如何,选择排序都需要完整的双重循环来找到并放置每个元素。外层循环执行 n-1 次,内层循环的执行次数从 n-1 递减到 1,总比较次数是固定的,约为 n 2 / 2 n^2/2 n2/2
(2)空间复杂度

与冒泡排序一样,选择排序也仅使用了常数个额外变量,空间复杂度为 O ( 1 ) O(1) O(1),是原地排序。

(3)稳定性

选择排序是不稳定的。这是它与冒泡排序的一个关键区别。在交换过程中,找到的最小元素可能会越过序列中其他与它相等的元素,从而打乱它们的原始相对顺序。

举例说明不稳定性
假设排序序列 [5a, 8, 5b, 2] (用 ab 区分两个5)。

  • 第一轮:在 [5a, 8, 5b, 2] 中找到最小值 2。将 2 与第一个元素 5a 交换。
  • 结果[2, 8, 5b, 5a]
    此时,5b 跑到了 5a 的前面,两个相等元素的原始相对顺序被改变了。因此,选择排序是不稳定的。

四、冒泡排序 vs. 选择排序:全方位对比

现在,我们将这两种入门级排序算法放在一起进行比较,以加深理解。

特性冒泡排序 (Bubble Sort)选择排序 (Selection Sort)
核心思想相邻元素比较交换,每轮将最大/小值“冒泡”到一端。每次从无序区选出最小/大值,放到有序区的末尾。
时间复杂度最好: O ( n ) O(n) O(n)
平均: O ( n 2 ) O(n^2) O(n2)
最坏: O ( n 2 ) O(n^2) O(n2)
最好/平均/最坏: 均为 O ( n 2 ) O(n^2) O(n2)
空间复杂度 O ( 1 ) O(1) O(1) (原地排序) O ( 1 ) O(1) O(1) (原地排序)
稳定性稳定不稳定
交换次数最多可达 O ( n 2 ) O(n^2) O(n2)确定为 O ( n ) O(n) O(n) 次 (最多 n-1 次)

对比洞察

  • 从时间复杂度看,两者平均和最坏情况都是 O ( n 2 ) O(n^2) O(n2),性能不佳,不适合处理大规模数据。
  • 选择排序的优势在于其交换次数远少于冒泡排序。在元素交换成本远高于比较成本的场景下(例如,存储记录非常大),选择排序会比冒泡排序更有优势。
  • 冒泡排序的主要优势在于其稳定性以及在数据近乎有序时,优化后能达到 O ( n ) O(n) O(n) 的时间复杂度。

五、总结

本文作为排序算法系列的开篇,我们详细探讨了两种最基础的 O ( n 2 ) O(n^2) O(n2) 排序算法。通过今天的学习,我们应掌握以下核心要点:

  1. 排序算法基础:理解了排序的定义、分类(比较/非比较)以及衡量算法优劣的三大指标:时间复杂度、空间复杂度和稳定性。
  2. 冒泡排序:掌握了其“邻里比较,逐步浮动”的核心原理。它的时间复杂度通常为 O ( n 2 ) O(n^2) O(n2),但优化后对近乎有序的序列可达 O ( n ) O(n) O(n)。关键特性是它是一种稳定的排序算法。
  3. 选择排序:掌握了其“全局扫描,择优上岗”的核心原理。它的时间复杂度始终为 O ( n 2 ) O(n^2) O(n2),与输入数据顺序无关。其关键特性是不稳定,但元素交换次数少,仅为 O ( n ) O(n) O(n)
  4. 算法对比:明确了冒泡排序和选择排序在性能、稳定性、交换次数上的关键差异,为在特定场景下选择合适的算法提供了理论依据。
  5. 实践意义:虽然这两种排序算法在实际工程中因性能问题很少被直接使用,但它们是理解更复杂排序算法(如插入排序、快速排序)思想的基石,是每个程序员必须掌握的基础内功。

在接下来的文章中,我们将继续探索其他排序算法,逐步深入到更高效的 O ( n log ⁡ n ) O(n \log n) O(nlogn) 排序世界。敬请期待!


posted on 2025-10-03 14:16  lxjshuju  阅读(8)  评论(0)    收藏  举报