尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++快速排序工程化实践:从分治思想到三路分区优化

C++快速排序工程化实践:从分治思想到三路分区优化 1. 项目概述为什么快速排序值得你花时间深挖如果你写过C或者刷过LeetCode排序算法肯定是你绕不开的一道坎。在众多排序算法里快速排序Quick Sort的地位很特殊——它不像冒泡排序那样简单易懂也不像归并排序那样稳定可靠但在实际的生产环境和高性能计算中它往往是默认的首选。为什么因为它在平均情况下拥有O(n log n)的时间复杂度并且是原地排序in-place空间效率极高。但更重要的是快速排序是“分治”Divide and Conquer算法范式的教科书级案例。理解它你不仅是在学一个排序算法更是在掌握一种强大的问题解决思路。这个项目标题“分治范式下的快速排序全解”点出了几个关键维度C实现是落地手段时间复杂度优化是性能追求工程化实践则是从理论到可靠代码的桥梁。市面上很多教程只给一段代码告诉你“这就是快排”但很少深入拆解为什么分区要这么写递归的基准情况base case怎么选最合适面对近乎有序或大量重复元素的“退化”数据如何保证性能不崩盘这些才是工程实践中真正要命的问题。我自己在早期做性能敏感的后台服务时就曾因为一个不经意的快速排序实现导致接口在特定数据分布下响应时间飙升排查了半天才发现是分区策略选错了。所以这篇文章我会结合这些踩坑经验从最基础的Lomuto分区法开始一步步推导到Hoare分区再到应对各种刁钻数据的优化策略如“三数取中”和“三路分区”最后聊聊在真实C项目中如何像使用std::sort一样安全、高效地应用快速排序。无论你是正在准备面试还是希望写出更健壮的工业级代码这里的内容都会给你带来实实在在的收获。2. 分治思想与快速排序的核心骨架在深入代码之前我们必须先吃透“分治”这个思想。分治不是快速排序的专属它是一类算法的通用设计模式核心就三步分解Divide、解决Conquer、合并Combine。对于排序问题分解就是把大数组拆成小数组解决就是递归地排序小数组合并呢对于快速排序巧妙之处在于它在分解的过程中通过“分区”Partition操作已经隐含地完成了合并——分区后基准元素左侧的都小于它右侧的都大于它所以当左右子数组都排好序时整个数组自然就有序了无需额外的合并步骤。这是它比归并排序空间效率高的根本原因。快速排序的算法骨架非常清晰选择基准Pivot从待排序数组中选择一个元素作为“轴心”。分区Partition重新排列数组使得所有小于基准的元素都放在基准前面所有大于基准的元素都放在基准后面。分区完成后基准元素就处于其最终排序后的正确位置。递归Recursively Sort递归地将小于基准的子数组和大于基准的子数组进行排序。这个骨架听起来简单但魔鬼全在细节里。基准怎么选分区怎么实现递归的终止条件是什么每一个选择都直接影响算法在最坏、平均情况下的性能以及代码的健壮性。接下来我们就从最直观但也最容易出问题的实现开始。2.1 经典实现Lomuto分区法剖析Lomuto分区法是很多教科书和入门教程的首选因为它逻辑清晰代码简短。它的核心思路是维护一个“小于基准的边界”索引。我们直接看一个最朴素的C实现// 使用最右元素作为基准的Lomuto分区函数 int partitionLomuto(vectorint arr, int low, int high) { int pivot arr[high]; // 选择最后一个元素作为基准 int i low - 1; // 指向“小于基准区域”的末尾 for (int j low; j high; j) { // 如果当前元素小于等于基准 if (arr[j] pivot) { i; // 扩大“小于基准区域” swap(arr[i], arr[j]); // 把当前元素交换到该区域 } } // 将基准元素放到正确位置即“小于基准区域”的下一个位置 swap(arr[i 1], arr[high]); return i 1; // 返回基准的最终位置 } // 快速排序主函数 void quickSortLomuto(vectorint arr, int low, int high) { if (low high) { // pi 是分区后基准元素的索引 int pi partitionLomuto(arr, low, high); // 递归排序基准左右两部分 quickSortLomuto(arr, low, pi - 1); quickSortLomuto(arr, pi 1, high); } }这段代码在做什么我们以数组[10, 80, 30, 90, 40, 50, 70]选择70为基准为例i初始化为-1j从0开始。arr[0]10 70i变成0arr[0]与自身交换无变化。数组状态[10, 80, 30, 90, 40, 50, 70]i0。arr[1]80 70无事发生j。arr[2]30 70i变成1交换arr[1]和arr[2]。数组状态[10, 30, 80, 90, 40, 50, 70]i1。arr[3]90 70跳过。arr[4]40 70i变成2交换arr[2]和arr[4]。数组状态[10, 30, 40, 90, 80, 50, 70]i2。arr[5]50 70i变成3交换arr[3]和arr[5]。数组状态[10, 30, 40, 50, 80, 90, 70]i3。循环结束。交换arr[i1]即arr[4]和arr[high]arr[6]。最终数组[10, 30, 40, 50, 70, 90, 80]。返回基准位置4。Lomuto的优缺点与坑点优点逻辑直白易于理解和实现。缺点与坑点基准选择固定上面代码总是选最后一个元素arr[high]。如果数组已经有序或逆序这就是最坏情况每次分区只能减少一个元素时间复杂度退化为O(n²)。这是第一个大坑。交换次数可能较多即使元素已经在正确的一侧if (arr[j] pivot)中的等号也可能导致不必要的交换。更关键的是当arr[j]大于pivot时它只是被跳过没有移动这可能不是最效率的。对重复元素处理尚可但不最优由于使用了重复元素会被分到左侧。但如果重复元素非常多依然会导致分区不平衡。实操心得一警惕“有序数组”这个性能杀手在我早期的一个日志处理模块里数据经常是近乎按时间戳有序的。我用了类似上面的朴素快排结果排序耗时成了性能瓶颈。用性能分析工具如perf一看递归深度几乎等于数组长度这就是最坏情况。给你的第一个工程建议永远不要固定选择第一个或最后一个元素作为基准。后面我们会讲如何优化。2.2 更高效的分区Hoare分区法原理解读Hoare分区法是快速排序发明者Tony Hoare最初提出的方案。它使用两个指针分别从数组两端向中间扫描思路更对称通常比Lomuto法执行更少的交换次数。// Hoare分区函数 int partitionHoare(vectorint arr, int low, int high) { int pivot arr[low]; // 选择第一个元素作为基准同样有问题需优化 int i low - 1; int j high 1; while (true) { // 从左向右找到第一个大于等于pivot的元素 do { i; } while (arr[i] pivot); // 从右向左找到第一个小于等于pivot的元素 do { j--; } while (arr[j] pivot); // 如果指针相遇或交叉返回j作为分界 if (i j) { return j; } // 交换这两个错位的元素 swap(arr[i], arr[j]); } } // 使用Hoare分区的快速排序 void quickSortHoare(vectorint arr, int low, int high) { if (low high) { int p partitionHoare(arr, low, high); // 注意递归区间Hoare分区后基准不一定在最终位置 // 但保证arr[low..p] arr[p1..high] quickSortHoare(arr, low, p); quickSortHoare(arr, p 1, high); } }Hoare分区过程图解以[5, 3, 8, 4, 2, 7, 1, 10]为例pivot5i从-1开始右移停在0arr[0]5不小于5。j从8开始左移停在6arr[6]1不大于5。i(0) j(6)交换5和1。数组变为[1, 3, 8, 4, 2, 7, 5, 10]。继续循环i右移停在285j左移停在425。i(2) j(4)交换8和2。数组变为[1, 3, 2, 4, 8, 7, 5, 10]。继续i右移停在485j左移停在345。此时i(4) j(3)循环结束返回j3。最终arr[0..3] [1,3,2,4]都5arr[4..7] [8,7,5,10]都5。注意基准5现在在索引6并非最终位置。Hoare vs Lomuto交换次数Hoare法通常交换次数更少因为它一次交换可以解决两个元素的错位问题。基准位置Lomuto分区后基准元素在最终位置Hoare分区后基准元素不一定在最终位置但分区界限是明确的。这使得Hoare法的递归调用区间稍有不同(low, p)和(p1, high)。代码复杂度Hoare法的边界条件更微妙比如使用do-while防止空数组理解起来稍难。工程选择在追求极致性能的库实现中如某些std::sort的实现可能会看到Hoare分区法的变体。但对于日常使用和理解Lomuto因其简单性更常被用作教学示例。注意事项递归终止条件的细微差别使用Hoare分区时递归调用quickSortHoare(arr, low, p)时p可能等于low-1吗在我们的实现中因为pivotarr[low]且i从low-1开始j从high1开始首次内循环do{i;}while(arr[i]pivot)至少会让i移动到low因为arr[low]pivot不满足条件。所以p即返回的j至少为low。但为了绝对安全确保low high的递归条件依然至关重要。这是一个容易忽略的边界细节。3. 时间复杂度深度优化策略理解了基础分区我们直面核心性能问题如何避免最坏的O(n²)时间复杂度关键在于让每次分区尽可能平衡即子问题规模大致减半。这主要从两方面入手优化基准选择和改进分区策略。3.1 基准选择的艺术随机化与三数取中固定选择端点元素是性能灾难的根源。我们需要引入随机性或启发性方法。1. 随机化基准选择这是最简单有效的优化。在分区前随机在[low, high]范围内选择一个元素并与末尾对Lomuto或开头对Hoare元素交换然后再执行标准分区。int partitionLomutoRandom(vectorint arr, int low, int high) { // 生成low到high之间的随机索引 int randomIndex low rand() % (high - low 1); // 将随机选中的元素交换到末尾作为基准 swap(arr[randomIndex], arr[high]); // 后续与标准Lomuto分区相同 return partitionLomuto(arr, low, high); // 调用之前的标准Lomuto函数 }为什么有效通过随机化算法不依赖于输入数据的特定排列如已排序。在数学上可以证明随机化快速排序的期望时间复杂度是O(n log n)并且对于任何输入其出现最坏情况概率极低。这是从“确定性算法”到“随机化算法”的一个关键思想提升。2. 三数取中法Median-of-Three随机化需要生成随机数有一定开销。三数取中是一种确定性启发式方法通常能很好地避免极端情况。它取数组头、尾、中间三个元素的中位数作为基准。// 辅助函数返回三个值的中位数的索引 int medianOfThree(vectorint arr, int low, int mid, int high) { int a arr[low], b arr[mid], c arr[high]; if ((a b) ^ (a c)) // a是中位数 return low; else if ((b a) ^ (b c)) // b是中位数 return mid; else return high; } int partitionLomutoMedian(vectorint arr, int low, int high) { int mid low (high - low) / 2; int medianIdx medianOfThree(arr, low, mid, high); swap(arr[medianIdx], arr[high]); // 将中位数交换到末尾 return partitionLomuto(arr, low, high); }计算过程示例对于arr[low]10,arr[mid]40,arr[high]70中位数是40。对于5, 1, 9中位数是5。这个方法能有效避免在已排序或逆序数组中选择最值作为基准。3. 更进一步的优化九数取中Introselect在极端要求性能的库如std::nth_element的某些实现中可能会使用类似Introselect算法中的策略当递归深度超过一定阈值如2 * log(n)时怀疑遇到了恶劣分区转而使用更复杂但保证O(n)的算法如BFPRT算法来选择中位数作为基准确保递归深度可控。这在工程上是防御性编程的体现。实操心得二随机种子与性能可复现性使用rand()时别忘了用srand(time(0))初始化随机种子。但在单元测试或基准测试中固定种子如srand(42)非常重要它能保证每次测试的输入序列一致从而使性能测试结果可复现、可比较。这是一个容易被忽略的工程细节。3.2 应对重复元素三路分区算法详解当数组中存在大量重复元素时无论是Lomuto还是Hoare的标准二分分区都会导致严重的不平衡。因为所有等于基准的元素都会被分到同一侧如果重复元素很多另一侧的子数组就会非常小。三路分区Dutch National Flag Problem将数组分为三部分小于、等于、大于基准从而一次性将所有等于基准的元素放到最终位置不再参与后续递归。// 三路分区返回两个边界索引小于区的末尾大于区的开头 pairint, int partitionThreeWay(vectorint arr, int low, int high) { int pivot arr[high]; // 基准可选优化 int lt low; // less than pointer: arr[low..lt-1] pivot int gt high; // greater than pointer: arr[gt1..high] pivot int i low; // current pointer while (i gt) { if (arr[i] pivot) { swap(arr[lt], arr[i]); lt; i; } else if (arr[i] pivot) { swap(arr[i], arr[gt]); gt--; // 注意这里i不递增因为从后面交换过来的元素还未检查 } else { // arr[i] pivot i; } } // 循环结束后 // arr[low..lt-1] pivot // arr[lt..gt] pivot (已在其最终位置) // arr[gt1..high] pivot return {lt - 1, gt 1}; } void quickSortThreeWay(vectorint arr, int low, int high) { if (low high) { // 可选在此处加入随机化或三数取中选择基准的逻辑 // 例如int randomIndex ...; swap(arr[randomIndex], arr[high]); auto [leftEnd, rightStart] partitionThreeWay(arr, low, high); quickSortThreeWay(arr, low, leftEnd); quickSortThreeWay(arr, rightStart, high); } }过程演示数组[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]pivot选末尾的5初始化lt0,gt10,i0。pivot5。arr[0]3 5与arr[lt]自身交换lt1,i1。arr[1]1 5交换自身lt2,i2。arr[2]4 5交换自身lt3,i3。arr[3]1 5交换自身lt4,i4。arr[4]5 5i5。arr[5]9 5与arr[gt]arr[10]5交换gt9。数组变为[3,1,4,1,5,5,2,6,5,3,9]i仍为5检查新来的5。arr[5]5 5i6。arr[6]2 5与arr[lt]arr[4]5交换lt5,i7。数组变为[3,1,4,1,2,5,5,6,5,3,9]。arr[7]6 5与arr[gt]arr[9]3交换gt8。数组变为[3,1,4,1,2,5,5,3,5,6,9]i仍为7。arr[7]3 5与arr[lt]arr[5]5交换lt6,i8。数组变为[3,1,4,1,2,3,5,5,5,6,9]。arr[8]5 5i9。i9gt8循环条件igt不成立结束。返回(lt-15, gt19)。即等于5的区域是索引6到8arr[6..8] [5,5,5]它们已在最终位置。三路分区的巨大优势对于包含大量重复键的数组三路分区能极大提升效率。因为所有等于基准的元素在第一次分区后就固定了不再参与递归。其时间复杂度可以接近O(n)而传统二分分区在这种情况下会退化成O(n²)。3.3 递归深度与栈溢出防护混合排序策略纯粹的快速排序是递归的递归深度在最坏情况下是O(n)可能引发栈溢出。工程上通用的优化是混合排序Hybrid Algorithm小数组切换插入排序当子数组规模小于某个阈值通常为10~20时递归开销可能大于排序本身。此时改用简单的插入排序Insertion Sort因为插入排序对小规模数据、近乎有序数据非常高效。尾递归优化编译器通常能优化尾递归但我们可以手动实现。在递归调用quickSort时先处理较短的那部分子数组然后通过更新参数循环处理长的部分减少递归深度。void quickSortHybrid(vectorint arr, int low, int high) { const int INSERTION_THRESHOLD 16; // 使用循环替代一部分递归减少栈深度 while (high - low INSERTION_THRESHOLD) { int p partitionHoareRandom(arr, low, high); // 使用随机化的Hoare分区 // 总是先递归处理较短的部分减少最坏递归深度 if (p - low high - p) { quickSortHybrid(arr, low, p); low p 1; // 将长的部分转为循环迭代 } else { quickSortHybrid(arr, p 1, high); high p; } } // 小数组使用插入排序 insertionSort(arr, low, high); } void insertionSort(vectorint arr, int low, int high) { for (int i low 1; i high; i) { int key arr[i]; int j i - 1; while (j low arr[j] key) { arr[j 1] arr[j]; j--; } arr[j 1] key; } }这种混合策略被广泛应用于工业级排序库中如std::sort它结合了快速排序在大数据量下的平均高效和插入排序在小数据量下的简单快速同时通过处理短子数组来限制递归深度是一种非常务实的工程化选择。4. 工程化实践从算法到健壮代码理解了优化策略我们最终要落实到可维护、可测试、高性能的C代码上。这涉及到API设计、迭代器抽象、性能测试和异常安全等工程问题。4.1 仿照STL的通用接口设计一个工业级的快速排序实现不应该只针对vectorint。它应该像std::sort一样支持任意随机访问迭代器和自定义比较器。templatetypename RandomIt, typename Compare std::less void quick_sort(RandomIt first, RandomIt last, Compare comp {}) { // 类型别名提高可读性 using value_type typename std::iterator_traitsRandomIt::value_type; const size_t threshold 16; // 插入排序阈值 // 使用栈模拟递归避免深度过大同时支持尾递归优化思想 std::stackstd::pairRandomIt, RandomIt stk; stk.push({first, last}); while (!stk.empty()) { auto [low, high] stk.top(); stk.pop(); size_t dist std::distance(low, high); if (dist 1) continue; // 小范围使用插入排序 if (dist threshold) { insertion_sort(low, high, comp); continue; } // 三数取中选择基准 RandomIt mid low dist / 2; RandomIt pivot_it median_of_three(low, mid, high - 1, comp); std::iter_swap(pivot_it, high - 1); // 将基准交换到末尾 // 进行三路分区 auto [lt, gt] partition_three_way(low, high - 1, comp); // 将较短的区间压栈较长的区间留待下次循环处理减少栈深度 if (std::distance(low, lt) std::distance(gt, high - 1)) { stk.push({gt, high}); // 处理大于区间 stk.push({low, lt}); // 处理小于区间 } else { stk.push({low, lt}); stk.push({gt, high}); } } } // 三数取中辅助函数 templatetypename RandomIt, typename Compare RandomIt median_of_three(RandomIt a, RandomIt b, RandomIt c, Compare comp) { if (comp(*a, *b)) { if (comp(*b, *c)) return b; else if (comp(*a, *c)) return c; else return a; } else { if (comp(*a, *c)) return a; else if (comp(*b, *c)) return c; else return b; } } // 三路分区迭代器版本 templatetypename RandomIt, typename Compare std::pairRandomIt, RandomIt partition_three_way(RandomIt low, RandomIt high, Compare comp) { auto pivot *high; RandomIt lt low; // 小于区的末尾后一位 RandomIt gt high; // 大于区的起始前一位 RandomIt i low; // 当前指针 while (i gt) { if (comp(*i, pivot)) { std::iter_swap(lt, i); lt; i; } else if (comp(pivot, *i)) { std::iter_swap(i, gt); --gt; } else { i; } } return {lt, gt 1}; // 返回小于区的末尾和大于区的起始 }设计要点解析模板化支持任意元素类型和自定义比较规则提升了代码的复用性。迭代器抽象使用RandomIt使其适用于std::vector、std::deque、原生数组等任何支持随机访问的容器。显式栈替代递归虽然代码稍复杂但彻底避免了递归深度过深导致的栈溢出风险这是生产环境代码的常见做法。混合策略集成内部集成了小数组插入排序和三路分区并使用了三数取中选择基准。4.2 性能基准测试与对比理论分析很重要但实际性能需要用数据说话。我们可以编写简单的基准测试对比不同实现的性能。#include chrono #include random #include algorithm #include iostream #include vector void benchmark(const std::string name, void (*sort_func)(std::vectorint), std::vectorint data) { auto start std::chrono::high_resolution_clock::now(); sort_func(data); auto end std::chrono::high_resolution_clock::now(); auto duration std::chrono::duration_caststd::chrono::microseconds(end - start); std::cout name time: duration.count() us std::endl; // 可选验证排序结果是否正确 // assert(std::is_sorted(data.begin(), data.end())); } int main() { const size_t N 100000; std::vectorint testData(N); std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(1, 1000); // 范围较小包含重复元素 // 1. 随机数据 std::generate(testData.begin(), testData.end(), [](){ return dis(gen); }); auto data1 testData; auto data2 testData; auto data3 testData; std::cout Random data (size N ):\n; benchmark(std::sort, [](auto v){std::sort(v.begin(), v.end());}, data1); benchmark(Naive QuickSort (Lomuto), quickSortLomutoWrapper, data2); benchmark(Optimized QuickSort (3-way), quickSortThreeWayWrapper, data3); // 2. 已排序数据朴素快排的噩梦 std::iota(testData.begin(), testData.end(), 0); std::cout \nSorted data:\n; // ... 同上测试观察朴素快排的退化 // 3. 大量重复数据 std::uniform_int_distribution dis2(1, 10); // 范围极小大量重复 std::generate(testData.begin(), testData.end(), [](){ return dis2(gen); }); std::cout \nData with many duplicates:\n; // ... 对比二分分区和三路分区的性能差异 }通过这样的测试你可以直观地看到在随机数据上优化后的快排三路分区随机化性能与std::sort接近。在已排序数据上朴素快排固定基准会极慢而优化后的版本依然稳健。在大量重复数据上三路分区的优势非常明显。4.3 常见陷阱与调试技巧即使有了优化实现快速排序时仍有一些隐蔽的陷阱。陷阱1指针越界与无限循环在Hoare分区中内层的do-while循环必须确保指针不会越界。如果所有元素都小于pivoti会一直增加到超出high。因此更安全的写法是加入边界检查while (i high arr[i] pivot) i; while (j low arr[j] pivot) j--;或者在选择基准时确保pivot值在数组范围内如三数取中。陷阱2递归终止条件递归调用必须保证区间是严格缩小的。在Lomuto分区中如果分区后pi等于low那么递归调用quickSort(arr, low, pi-1)的区间就是(low, low-1)这是一个无效区间。我们的条件if (low high)或if (low pi)和if (pi1 high)确保了这一点。在Hoare分区中递归区间(low, p)和(p1, high)也要确保p可能等于low-1吗在我们的实现中由于基准是arr[low]且i从low-1开始右移至少会停在low因为arr[low]不小于pivot所以j最终返回的值至少是low。但为了健壮性使用if (low high)总没错。陷阱3自定义比较器的严格弱序如果你的快速排序支持自定义比较器如std::less必须确保比较关系满足严格弱序即非自反性comp(a, a)为 false。可传递性如果comp(a, b)和comp(b, c)为真则comp(a, c)为真。反对称性如果comp(a, b)为真则comp(b, a)为假。等价传递性如果!comp(a,b) !comp(b,a)即a和b等价那么它们与其他元素的比较关系一致。 不满足严格弱序的比较器会导致排序结果未定义或程序崩溃如无限循环。例如在分区循环中while (arr[i] pivot)和while (arr[j] pivot)必须使用相同的比较逻辑且不能有等号冲突。调试技巧小数据量测试用大小为0、1、2、3的数组测试这些是边界情况。打印递归树在递归函数入口打印low和high可视化递归过程检查分区是否平衡。单步跟踪分区对于一个小的示例数组如[3,1,4,1,5]在分区函数中每一步后打印数组状态验证指针移动和交换逻辑。使用assert在分区后断言基准左侧元素都小于等于基准右侧都大于等于基准。int pi partition(arr, low, high); for (int k low; k pi; k) assert(arr[k] arr[pi]); for (int k pi 1; k high; k) assert(arr[k] arr[pi]);5. 总结与扩展思考快速排序的魅力在于其简洁的思想和极高的平均效率。通过这个项目我们从最基础的Lomuto分区出发逐步深入到Hoare分区、随机化、三数取中、三路分区、混合排序等优化策略最后探讨了工程化实现的接口设计、性能测试和常见陷阱。这不仅仅是一个排序算法的实现更是一次完整的分治算法工程化实践。在实际项目中除非有极特殊的定制化需求比如需要特定平台的高度优化或者嵌入式环境限制否则直接使用std::sort是最好、最安全的选择。std::sort通常是一种混合排序算法Introsort它结合了快速排序、堆排序和插入排序并且经过了充分的优化和测试在绝大多数场景下都是最优解。那么自己实现快速排序的意义何在我认为有几点深入理解分治思想快速排序是理解递归和分治策略的完美案例。掌握算法优化方法通过它你能学习到随机化、启发式选择、算法混合等通用的性能优化技巧。锻炼工程实现能力从算法伪代码到健壮、通用、高效的C代码中间有大量的细节需要打磨。应对面试与特定场景理解快速排序及其变体如快速选择算法Quickselect用于找第K大元素是技术面试的常客。在某些无法使用标准库的场合如某些内核开发、竞赛环境你也需要能手写一个高效的排序。最后一个延伸的思考快速排序的“不稳定”性具体指什么在分区过程中等值元素的相对位置可能会改变。如果你需要稳定排序并且空间不是问题归并排序通常是更好的选择。但快速排序通过三路分区在一定程度上缓解了等值元素处理的问题虽然它依然不是稳定排序但性能上对重复数据更加友好。理解这些权衡正是从“会用”到“懂行”的关键一步。
返回列表