C++七大经典排序算法:从原理到实战,程序员必备的内功修炼

📅 2026/8/5 2:49:51
C++七大经典排序算法:从原理到实战,程序员必备的内功修炼
1. 排序算法程序员的“基本功”与“内功”在C的世界里无论你是刚入门的新手还是已经工作多年的老手排序算法都是一个绕不开的话题。它不像某些花哨的框架那样引人注目但却是构建高效、稳定程序的基石。面试官喜欢用它来考察候选人的基本功和逻辑思维而在实际开发中一个合适的排序算法选择往往能决定一段代码是“优雅高效”还是“笨拙缓慢”。今天我们不谈那些复杂的理论推导就从最接地气的代码实现和运行逻辑出发把这七大经典排序算法掰开揉碎了讲清楚。我会结合自己这些年写代码、调性能的实际经验告诉你每种算法“为什么”要这么写以及在什么场景下“怎么”选。相信我搞懂这些你的编程内功会扎实一大截。2. 排序算法的“世界观”如何评价一个算法的好坏在深入代码之前我们得先统一“度量衡”。评价一个排序算法通常看四个核心指标理解了这些你才能明白为什么会有这么多不同的算法以及我们该如何取舍。2.1 时间复杂度算法执行时间的“天花板”时间复杂度描述的是算法执行时间随数据规模增长的变化趋势。我们最关心的是平均情况和最坏情况。O(n²)像冒泡、选择、插入排序当数据量n翻倍时最坏情况下所需时间可能变为原来的4倍。这意味着它们处理大规模数据时效率会急剧下降。O(n log n)像快速排序、归并排序、堆排序这是基于比较的排序算法理论上能达到的最好平均时间复杂度。数据量翻倍时间增长远低于平方级适合处理大量数据。O(n)如桶排序、计数排序、基数排序它们能在特定条件下达到线性时间但通常有额外的前提条件如数据范围已知。注意大O表示法忽略常数和低阶项所以O(100n)和O(n)在理论上是同级的。但在实际编码中常数因子和具体实现细节如缓存命中率、分支预测对性能的影响可能非常大这也是我们手动实现时需要优化的地方。2.2 空间复杂度算法对内存的“占用率”空间复杂度指算法运行过程中临时占用的存储空间大小。O(1)原地排序。算法只用到常数级别的额外空间通常就是几个临时变量如冒泡、选择、插入、希尔、堆排序。这对内存受限的环境如嵌入式系统非常友好。O(n)或O(log n)需要借助与原始数据规模相当的额外空间。归并排序是典型的O(n)快速排序在递归实现时递归栈的深度平均为O(log n)最坏情况如数组已有序下会退化为O(n)。2.3 稳定性相等元素的“相对秩序”如果待排序序列中存在两个相等的元素A和B且A原本在B前面排序后A仍然在B前面则称这个排序算法是稳定的否则是不稳定的。为什么重要在多关键字排序时稳定性是关键。例如先按成绩降序排序再按班级升序排序。如果第二次排序不稳定那么同班级内学生的成绩顺序就可能被打乱。稳定排序冒泡排序、插入排序、归并排序、计数排序、桶排序、基数排序。不稳定排序选择排序、快速排序、希尔排序、堆排序。2.4 适用场景没有最好的只有最合适的算法性能还与数据初始状态密切相关。近乎有序的数组插入排序的效率会接近O(n)而快速排序可能退化为O(n²)。数据范围较小且是整数计数排序或桶排序可能比任何基于比较的O(n log n)算法都快。链表结构归并排序是天然适合链表的排序方式而快速排序、堆排序在链表上实现则比较别扭。理解了这些评价维度我们再去看具体的算法就能明白它们各自的设计哲学和适用边界了。3. 基础排序算法理解排序思想的“敲门砖”这类算法思想直观代码简单是理解排序逻辑的绝佳起点但在处理大量数据时效率偏低。3.1 冒泡排序像气泡一样“上浮”核心思想重复遍历序列一次比较两个相邻元素如果顺序错误就交换它们。这样每一轮遍历都会将当前未排序部分的最大或最小元素“冒泡”到正确位置。代码实现与解析void bubbleSort(vectorint arr) { int n arr.size(); // 外层循环控制排序的轮数n个元素最多需要n-1轮 for (int i 0; i n - 1; i) { bool swapped false; // 优化标记本轮是否发生交换 // 内层循环进行相邻比较每轮结束后末尾i个元素已有序 for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { // 如果逆序则交换 swap(arr[j], arr[j 1]); swapped true; } } // 如果本轮没有发生任何交换说明序列已经有序提前结束 if (!swapped) break; } }为什么这样写n-1轮循环n个元素经过n-1轮冒泡后最后一个元素自然有序。内层循环边界n-1-i第i轮结束后数组末尾的i个元素已经是全局最大的i个且已就位无需再参与比较。swapped标志位这是对近乎有序序列的重要优化。如果一轮下来没有发生交换说明序列已经全局有序可以立即终止算法。特点与适用场景时间复杂度平均和最坏均为O(n²)。最好情况已有序为O(n)。空间复杂度O(1)原地排序。稳定性稳定。因为只有相邻元素逆序时才交换相等元素不会交换。适用场景仅适用于教学或数据量极小如n50且对性能不敏感的场景。实际工程中几乎不会被使用。3.2 选择排序每次找到“最小元”核心思想在未排序序列中找到最小或最大元素存放到序列的起始位置然后再从剩余未排序元素中继续寻找最小元素放到已排序序列的末尾。以此类推直到所有元素均排序完毕。代码实现与解析void selectionSort(vectorint arr) { int n arr.size(); for (int i 0; i n - 1; i) { // i代表已排序序列的末尾 int minIndex i; // 假设当前i位置就是最小元素 // 在[i1, n)区间内寻找真正的最小元素下标 for (int j i 1; j n; j) { if (arr[j] arr[minIndex]) { minIndex j; } } // 将找到的最小元素与位置i的元素交换 swap(arr[i], arr[minIndex]); } }为什么这样写minIndex的初始化每一轮都从当前未排序部分的第一个元素开始假设它为最小然后去验证。内层循环找最小值遍历未排序部分更新最小值的索引。这里记录的是索引而非值是为了最后一步能直接交换。交换操作找到最小元素后只进行一次交换将最小元素放到它最终的位置上。这是它与冒泡排序可能进行多次交换的一个区别。特点与适用场景时间复杂度无论数据是否有序都需要进行n(n-1)/2次比较故最好、平均、最坏情况均为O(n²)。它是一个“迟钝”的算法对输入数据不敏感。空间复杂度O(1)原地排序。稳定性不稳定。举例序列[5, 8, 5, 2, 9]。第一轮找到最小元素2与第一个5交换导致两个5的相对顺序改变了。适用场景同样仅适用于教学或极小数据量。它的交换次数比冒泡排序少固定为n-1次但比较次数一样多且不稳定所以实际用处更小。3.3 插入排序像理扑克牌一样“插入”核心思想将待排序序列看作一个有序表和一个无序表。开始时有序表只包含第一个元素然后依次将无序表中的元素插入到有序表的正确位置直到无序表为空。代码实现与解析void insertionSort(vectorint arr) { int n arr.size(); // 从第二个元素开始下标1认为第一个元素自成一个有序序列 for (int i 1; i n; i) { int key arr[i]; // 取出当前待插入的元素 int j i - 1; // 从有序部分的末尾开始向前比较 // 将比key大的元素都向后移动一位为key腾出位置 while (j 0 arr[j] key) { arr[j 1] arr[j]; j--; } // 将key插入到找到的正确位置 arr[j 1] key; } }为什么这样写key的暂存先保存待插入元素arr[i]的值因为后续移动操作会覆盖arr[i]的位置。从后向前比较并移动这是算法的关键。在有序部分从后向前扫描如果当前元素大于key就将其后移一位。这个过程持续到找到key应该插入的位置即某个元素不大于key或者已经到达有序部分头部。插入操作循环结束后j1就是key应该插入的位置。特点与适用场景时间复杂度平均和最坏为O(n²)。最好情况已有序为O(n)因为内层while循环一次都不执行。空间复杂度O(1)原地排序。稳定性稳定。因为是从后向前扫描遇到相等元素时arr[j] key会停止移动所以相等元素的相对顺序不变。适用场景对于小规模数据或近乎有序的数据插入排序非常高效。许多高级排序算法如快速排序、归并排序在递归到小规模子问题时会切换使用插入排序来优化性能。这也是为什么std::sort的实现中常常包含插入排序的优化。4. 进阶排序算法应对大规模数据的“利器”当数据量变大时O(n²)的算法就力不从心了。下面这些O(n log n)的算法是处理大规模数据的核心。4.1 希尔排序插入排序的“威力增强版”核心思想希尔排序是插入排序的改进它通过一个增量序列将整个待排序序列分割成若干个子序列这些子序列的元素是原序列中间隔为增量的元素分别进行插入排序。随着增量逐渐减小子序列越来越长也越来越有序。当增量减至1时整个序列已基本有序此时再做一次插入排序效率就很高。代码实现与解析使用希尔增量序列gap n/2, gap / 2 ...void shellSort(vectorint arr) { int n arr.size(); // 初始增量gap为数组长度的一半并逐步缩小 for (int gap n / 2; gap 0; gap / 2) { // 从第gap个元素开始对其所在子序列进行插入排序 for (int i gap; i n; i) { int temp arr[i]; int j; // 对子序列进行插入排序注意步长是gap for (j i; j gap arr[j - gap] temp; j - gap) { arr[j] arr[j - gap]; } arr[j] temp; } } }为什么这样写三层循环结构最外层控制增量gap的变化。中间层遍历从gap到n-1的所有元素。arr[i]是当前待插入到其所在子序列正确位置的元素。最内层以gap为步长在子序列内进行插入排序移动元素。gap序列的选择这里使用了最简单的希尔增量n/2递减。实际上增量序列的选择直接影响算法效率如Hibbard序列(1, 3, 7, 15,...)、Sedgewick序列等能获得更好的理论性能。特点与适用场景时间复杂度依赖于增量序列。使用希尔增量时最坏为O(n²)但好于直接插入排序。使用某些优化序列如Sedgewick可达到O(n^(4/3))甚至O(n log² n)。它是一个**不稳定的O(n log n)**算法。空间复杂度O(1)原地排序。稳定性不稳定。由于是跳跃式移动元素可能打乱相等元素的相对顺序。适用场景中等规模数据且对稳定性无要求时。它实现简单不需要额外空间且通常比O(n²)的算法快得多。是早期Unix系统qsort函数的实现基础。4.2 归并排序分而治之的“典范”核心思想典型的分治法。将序列递归地分成两半分别对左右两半进行排序然后将两个已排序的子序列合并成一个完整的有序序列。合并过程是算法的核心。代码实现与解析// 合并两个有序子数组 [left, mid] 和 [mid1, right] void merge(vectorint arr, int left, int mid, int right) { vectorint temp(right - left 1); // 临时数组存放合并结果 int i left, j mid 1, k 0; // 比较两个子数组的元素将较小的放入temp while (i mid j right) { if (arr[i] arr[j]) { // 注意这里是 保证了稳定性 temp[k] arr[i]; } else { temp[k] arr[j]; } } // 将剩余元素拷贝到temp while (i mid) temp[k] arr[i]; while (j right) temp[k] arr[j]; // 将temp中的有序数据拷贝回原数组 for (int p 0; p k; p) { arr[left p] temp[p]; } } // 递归排序函数 void mergeSortHelper(vectorint arr, int left, int right) { if (left right) return; // 递归基子数组只有一个元素或为空 int mid left (right - left) / 2; // 防止溢出 mergeSortHelper(arr, left, mid); // 排序左半部分 mergeSortHelper(arr, mid 1, right);// 排序右半部分 merge(arr, left, mid, right); // 合并两个有序部分 } // 对外接口 void mergeSort(vectorint arr) { if (arr.size() 2) return; mergeSortHelper(arr, 0, arr.size() - 1); }为什么这样写分治递归mergeSortHelper函数清晰地体现了“分治”思想先分递归调用自身后治调用merge合并。合并操作merge这是归并排序的灵魂。需要额外的O(n)空间来暂存合并结果。合并时通过双指针i和j分别指向两个子数组的头部比较并选择较小的放入临时数组保证了合并后的有序性。稳定性在merge函数的比较中使用arr[i] arr[j]当元素相等时优先取左边子数组的元素这保证了排序的稳定性。特点与适用场景时间复杂度最好、平均、最坏情况均为O(n log n)。非常稳定不受输入数据影响。空间复杂度O(n)需要与原始数组等大的额外空间。这是它的主要缺点。稳定性稳定。适用场景需要稳定排序且对时间复杂度有严格要求时。链表排序归并排序是链表排序的最佳选择之一因为链表节点可以原地改变指针指向无需额外空间来合并。外部排序数据量太大无法全部加载到内存的基础。4.3 快速排序实践中最快的“通用排序”核心思想同样采用分治法。它选择一个元素作为“基准”pivot通过一趟排序将序列分成独立的两部分其中一部分的所有元素都比基准小另一部分的所有元素都比基准大。然后递归地对这两部分进行快速排序。代码实现与解析经典版本// 分区操作返回基准元素的最终位置 int partition(vectorint arr, int left, int right) { int pivot arr[right]; // 选择最右侧元素作为基准 int i left - 1; // i指向小于pivot区域的最后一个元素 for (int j left; j right; j) { if (arr[j] pivot) { // 当前元素小于等于基准 i; swap(arr[i], arr[j]); // 把它交换到小于区域 } } swap(arr[i 1], arr[right]); // 将基准放到正确位置 return i 1; // 返回基准的索引 } // 递归排序函数 void quickSortHelper(vectorint arr, int left, int right) { if (left right) { int pivotIndex partition(arr, left, right); // 获取基准位置 quickSortHelper(arr, left, pivotIndex - 1); // 递归排序左半部分 quickSortHelper(arr, pivotIndex 1, right); // 递归排序右半部分 } } // 对外接口 void quickSort(vectorint arr) { if (arr.size() 2) return; quickSortHelper(arr, 0, arr.size() - 1); }为什么这样写分区操作partition这是快速排序的核心。我们维护两个区域[left, i]是小于等于pivot的区域[i1, j-1]是大于pivot的区域[j, right-1]是待检查区域。j不断向右扫描将小于等于pivot的元素交换到前面。最后将pivot交换到i1的位置。基准的选择这里简单选择最右侧元素arr[right]作为基准。这是一个潜在的效率陷阱。如果数组已经有序或逆序每次分区都极不平衡会导致递归树退化成链表时间复杂度变为O(n²)。递归分区后基准元素已经在其最终位置然后递归地对左右子数组进行排序。优化与实践经验随机化基准为了避免最坏情况通常随机选择基准元素。在partition开始时随机选取一个下标将其与right交换然后再执行上述逻辑。这能极大降低遇到最坏情况的概率。三数取中法选择左端、中间、右端三个元素的中位数作为基准也能有效避免极端情况。小数组切换插入排序当递归到的子数组规模很小如长度15时快速排序的递归开销可能比排序本身还大。此时切换为插入排序能提升整体性能。std::sort就采用了这种策略。三向切分对于包含大量重复元素的数组标准的快速排序效率也不高。三向切分快速排序Dijkstra提出的可以将数组分为pivot,pivot,pivot三部分能高效处理重复元素。特点与适用场景时间复杂度平均O(n log n)非常高效。最坏O(n²)但通过上述优化可有效避免。空间复杂度主要是递归调用栈的空间平均O(log n)最坏O(n)。稳定性不稳定。分区过程中的交换会打乱相等元素的顺序。适用场景快速排序是实践中最快的通用排序算法。C STL中的std::sort、Java的Arrays.sort()对基本类型底层都使用了快速排序的优化变体。适用于对稳定性没有要求且追求平均性能的场景。4.4 堆排序利用“二叉堆”的智慧核心思想利用二叉堆这种数据结构进行排序。首先将待排序序列构造成一个大顶堆每个节点的值都大于或等于其子节点的值此时整个序列的最大值就是堆顶的根节点。将其与末尾元素交换此时末尾元素为最大值。然后将剩余n-1个元素重新构造成一个大顶堆如此反复执行便能得到一个有序序列。代码实现与解析// 调整以i为根的子树使其满足大顶堆性质n是堆的大小 void heapify(vectorint arr, int n, int i) { int largest i; // 初始化最大元素为根节点 int left 2 * i 1; // 左子节点 int right 2 * i 2; // 右子节点 // 如果左子节点存在且大于根 if (left n arr[left] arr[largest]) largest left; // 如果右子节点存在且大于当前最大 if (right n arr[right] arr[largest]) largest right; // 如果最大值不是根节点 if (largest ! i) { swap(arr[i], arr[largest]); // 交换根和最大值 heapify(arr, n, largest); // 递归调整被交换后的子树 } } // 堆排序主函数 void heapSort(vectorint arr) { int n arr.size(); // 1. 构建初始大顶堆。从最后一个非叶子节点开始向上调整 // 最后一个非叶子节点的索引是 n/2 - 1 for (int i n / 2 - 1; i 0; --i) { heapify(arr, n, i); } // 2. 逐个提取堆顶元素最大值并调整堆 for (int i n - 1; i 0; --i) { swap(arr[0], arr[i]); // 将堆顶元素最大值与当前末尾交换 heapify(arr, i, 0); // 调整剩余i个元素的堆使其满足大顶堆性质 } }为什么这样写heapify函数这是维护堆性质的核心。给定一个节点索引i它假设i的左右子树都已经是堆但i可能小于其子节点。函数通过比较i与其左右孩子找到三者中的最大值。如果最大值不是i则交换并递归地对被交换下去的子树调用heapify。建堆过程从最后一个非叶子节点n/2 - 1开始自底向上地对每个节点调用heapify。为什么从这个位置开始因为叶子节点本身可以看作是一个合法的堆。排序过程建堆后堆顶arr[0]是最大值。我们将其与堆的最后一个元素arr[n-1]交换这样最大值就放到了最终位置。然后堆的大小减1i--并对新的堆顶刚交换上来的元素调用heapify来重新调整堆。重复此过程直到堆的大小为1。特点与适用场景时间复杂度建堆过程为O(n)每次调整堆为O(log n)总共调整n-1次所以总时间复杂度为O(n log n)。且最好、最坏、平均情况都是O(n log n)非常稳定。空间复杂度O(1)原地排序。稳定性不稳定。例如序列[1, 2a, 2b]建堆过程可能交换2a和2b。适用场景需要对超大规模数据排序且对最坏情况时间复杂度有要求时快速排序的最坏O(n²)不可接受时。需要在一个动态数据流中实时获取前k个最大或最小元素时使用“最小堆”或“最大堆”。内存空间受限无法承受归并排序O(n)的额外空间开销时。5. 非比较排序算法突破O(n log n)的理论限制前面所有算法都是基于“比较”的排序它们的时间复杂度下界是O(n log n)。但如果数据有特殊性质我们可以使用非比较排序突破这个限制。5.1 计数排序当数据范围已知且不大时核心思想不是通过比较而是通过统计。假设待排序数组的元素都是整数并且范围在[0, k]之间。我们创建一个长度为k1的计数数组count统计每个元素出现的次数。然后根据计数数组直接计算出每个元素在排序后数组中的最终位置。代码实现与解析void countingSort(vectorint arr) { if (arr.empty()) return; // 1. 找出数组中的最大值和最小值确定数据范围 int maxVal *max_element(arr.begin(), arr.end()); int minVal *min_element(arr.begin(), arr.end()); int range maxVal - minVal 1; // 2. 创建计数数组并统计频率 vectorint count(range, 0); for (int num : arr) { count[num - minVal]; // 偏移将数据映射到[0, range)区间 } // 3. 将计数数组转换为前缀和数组count[i]表示小于等于(iminVal)的元素个数 for (int i 1; i range; i) { count[i] count[i - 1]; } // 4. 从后向前遍历原数组根据前缀和数组放置元素到输出数组为了稳定性 vectorint output(arr.size()); for (int i arr.size() - 1; i 0; --i) { int index arr[i] - minVal; output[count[index] - 1] arr[i]; count[index]--; // 放置一个元素后该值的计数减1 } // 5. 将排序结果拷贝回原数组 arr output; }为什么这样写处理负数与偏移通过找到minVal将所有元素减去minVal映射到从0开始的范围使得计数排序能处理包含负数的整数数组。前缀和转换第二步的count数组存储的是频率。第三步将其转换为前缀和此时count[i]的值就代表了“值等于iminVal及比它小的元素总共有多少个”也就是该元素在输出数组中最后一个出现的位置索引1。从后向前遍历以保证稳定性第四步从原数组末尾开始向前遍历。对于当前元素arr[i]根据count数组找到它应该放入输出数组的位置count[index]-1放入后将该位置的计数减1。这样后出现的相等元素会被放在相对靠后的位置保证了排序的稳定性。特点与适用场景时间复杂度O(n k)其中k是数据范围max-min1。当kO(n)时时间复杂度为O(n)。空间复杂度O(n k)需要输出数组和计数数组。稳定性稳定通过上述实现保证。适用场景数据范围k不大且为整数时效率极高。例如对年龄、考试成绩、像素灰度值等进行排序。5.2 桶排序将数据分到多个“桶”里核心思想假设输入数据服从均匀分布将数据分到有限数量的桶里每个桶再分别排序可以使用其他排序算法。最后按顺序把各个桶里的元素列出来。代码实现与解析简易版void bucketSort(vectorfloat arr) { if (arr.empty()) return; int n arr.size(); // 1. 创建n个空桶 vectorvectorfloat buckets(n); // 2. 将数组元素分配到各个桶中 float maxVal *max_element(arr.begin(), arr.end()); float minVal *min_element(arr.begin(), arr.end()); float bucketRange (maxVal - minVal) / n 1e-9; // 加一个小量避免浮点误差 for (float num : arr) { int bucketIndex (num - minVal) / bucketRange; // 确保索引在有效范围内 bucketIndex min(bucketIndex, n - 1); buckets[bucketIndex].push_back(num); } // 3. 对每个桶内部进行排序这里使用std::sort也可用插入排序 for (auto bucket : buckets) { sort(bucket.begin(), bucket.end()); } // 4. 将排序后的桶依次连接起来 int index 0; for (const auto bucket : buckets) { for (float num : bucket) { arr[index] num; } } }为什么这样写桶的数量通常设置桶的数量等于元素个数n这样平均每个桶只有一个元素排序最快。但桶的数量需要根据数据分布调整。映射函数(num - minVal) / bucketRange将数据映射到桶的索引。这是桶排序的关键决定了数据分布的均匀性。桶内排序由于每个桶内数据量较少可以使用插入排序等简单算法。这里为了简便使用了std::sort。特点与适用场景时间复杂度平均O(n k)最坏O(n²)所有元素落在一个桶里。k为桶的数量。空间复杂度O(n k)。稳定性取决于桶内排序算法的稳定性。如果桶内排序稳定且元素入桶顺序被保持则桶排序稳定。适用场景数据均匀分布在一个范围内时效率很高。常用于浮点数排序也是外部排序如磁盘文件排序的基础思想。5.3 基数排序按“位”来排序核心思想一种非比较的整数排序算法。它将整数按位数切割成不同的数字然后从最低位或最高位开始依次进行稳定排序通常使用计数排序作为子程序。这样从最低位排序到最高位后整个数列就变成了有序序列。代码实现与解析LSD - 最低位优先// 使用计数排序作为子程序对数组arr按照某一位exp进行排序 void countingSortForRadix(vectorint arr, int exp) { int n arr.size(); vectorint output(n); vectorint count(10, 0); // 十进制数字范围0-9 // 统计当前位exp位上每个数字出现的次数 for (int i 0; i n; i) { int digit (arr[i] / exp) % 10; count[digit]; } // 将计数转换为前缀和 for (int i 1; i 10; i) { count[i] count[i - 1]; } // 从后向前遍历根据当前位数字放置元素保证稳定性 for (int i n - 1; i 0; --i) { int digit (arr[i] / exp) % 10; output[count[digit] - 1] arr[i]; count[digit]--; } // 拷贝回原数组 arr output; } // 基数排序主函数 void radixSort(vectorint arr) { if (arr.empty()) return; // 找到最大值确定需要排序的位数 int maxVal *max_element(arr.begin(), arr.end()); // 从最低位开始对每一位进行计数排序 for (int exp 1; maxVal / exp 0; exp * 10) { countingSortForRadix(arr, exp); } }为什么这样写LSDLeast Significant Digit从最低位开始排序。这样操作简单并且对于不等长的数字如[1, 203, 45]可以在高位补0统一处理。子排序必须是稳定的这是基数排序正确性的关键。因为高位排序时需要依赖低位已确定的顺序。计数排序是稳定的所以适合作为子程序。exp参数exp表示当前是哪一位1表示个位10表示十位100表示百位以此类推。(arr[i] / exp) % 10用来提取当前位的数字。特点与适用场景时间复杂度O(d * (n k))其中d是最大数字的位数k是进制数十进制为10。当d为常数且kO(n)时可视为O(n)。空间复杂度O(n k)主要是计数数组和输出数组。稳定性稳定因为使用了稳定的子排序算法。适用场景适用于整数或字符串可以看作字符编码的整数的排序且位数d不是很大。例如对手机号、身份证号、单词字典序进行排序。6. 实战选择与C STL的启示了解了这么多算法在实际项目中到底该怎么选C标准库的std::sort给了我们最好的实践指南。std::sort的实现智慧 现代的std::sort如GCC/Clang的libstdc/libc MSVC的STL通常是一种混合排序算法内省排序Introsort这是主体。它本质上是快速排序但会监控递归深度。如果递归过深超过2 * log2(n)意味着遇到了接近最坏情况它会切换到堆排序来保证O(n log n)的最坏时间复杂度。插入排序当递归到的子数组规模很小比如长度小于16时会切换到插入排序。因为对于小数组插入排序的常数因子小且是原地稳定排序效率更高。给你的选择建议通用场景追求平均性能无脑用std::sort。它是经过千锤百炼的工业级实现在绝大多数情况下都是最佳选择。需要稳定排序使用std::stable_sort。它通常基于归并排序实现。对最坏时间复杂度有严格要求考虑堆排序std::make_heapstd::sort_heap或者直接使用std::sort其内省排序已规避最坏情况。数据量极小或近乎有序插入排序简单有效。数据是整数且范围已知不大计数排序或基数排序可能带来惊喜。链表结构使用std::list::sort通常是归并排序的实现或自己实现归并排序。只需要前k个最大/最小元素使用堆std::priority_queue或快速选择算法std::nth_element。最后一点个人体会理解这些经典算法的意义远不止于通过面试。它们代表了计算机科学中最精妙的思想分治、减治、用空间换时间、利用数据特性。当你面对一个复杂的性能问题时这些思想会像工具箱里的扳手一样让你有章可循。比如当你需要处理一个不断到达的数据流并实时维护Top K时你会立刻想到堆当你需要对一个几乎有序的数组做微调时插入排序会浮现在脑海。这种“算法直觉”才是我们反复练习和理解的真正价值所在。