ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

快速排序三部曲解法:从面试手写到工程优化的完整指南

快速排序三部曲解法:从面试手写到工程优化的完整指南

1. 换工作面试里那道绕不开的坎

但凡你写过几年代码,动过换工作的念头,翻一翻目标岗位的面试真题,大概率会撞上同一个名字——快速排序。它不像红黑树那样让人望而生畏,也不像动态规划那样需要灵光一闪,但就是这么一个“看起来不难”的算法,能把一大批工作三五年的开发者卡在二面。我自己就经历过这种场面:面试官递过来一张白纸,说“手写一个快排吧”,我提笔就写,结果边界条件写崩了,指针越界,递归出口写错,最后草草收场。那次之后我才意识到,快排这东西,会背代码和真正理解它,中间隔着一条很宽的沟。

这篇内容就是围绕“快速排序的三部曲解法”展开的。所谓三部曲,是我自己在反复刷题、反复面试、反复给团队新人讲题的过程中总结出来的一套拆解思路:分区、递归、合并。它不是什么高深的新算法,而是把快排这个整体拆成三个可以独立理解、独立验证的步骤,让你在面试现场即使紧张,也能按部就班地把代码写对。这套方法适合正在准备面试的开发者、刚学完数据结构想巩固基础的学生,也适合那些平时用惯了库函数排序、想回头补一补底层原理的工程师。不管你之前有没有手写过快排,只要你能理解数组和递归这两个基本概念,接下来的内容都能让你有所收获。

我见过太多人把快排当成一个“背下来就行”的模板,结果面试官稍微改一下条件——比如换成降序、比如要求原地排序、比如数组里有大量重复元素——就立刻卡壳。三部曲解法的价值就在于,它让你不是背一个答案,而是掌握一套可以应对变化的分析框架。接下来我会从整体设计思路讲起,然后逐层拆解每个步骤的实现细节,再给出完整的实操过程和常见问题的排查方法。内容会比较长,但每一段都是我踩过坑之后觉得真正有用的东西。

2. 三部曲解法的整体设计思路

2.1 为什么要把快排拆成三步

快速排序的核心思想其实一句话就能说完:选一个基准元素,把比它小的放到左边,比它大的放到右边,然后对左右两边分别重复这个过程。但就是这一句话,落到代码上会牵扯出很多细节问题。如果一上来就想着把整个算法写完,很容易在指针移动、边界判断、递归参数这些地方出错。我自己早期写快排的时候,经常是写完之后运行报错,然后对着代码一行一行调试,花很长时间才能找到问题所在。

三部曲解法的第一个好处就是降低认知负担。你把“分区”单独拿出来写成一个函数,这个函数的职责非常明确:给定一个数组和两个边界,把基准元素放到正确的位置,返回这个位置的下标。这个函数写对了,后面的递归逻辑就变得非常简单。第二个好处是便于验证。你可以单独测试分区函数,用几个小数组验证它是否能把元素正确分开,确认无误之后再写递归调用。第三个好处是面试时思路清晰。面试官看你写代码,最怕的就是你东一榔头西一棒子,三部曲的写法能让对方清楚地看到你的思考路径。

具体来说,三部曲的划分是这样的:第一步是选基准并分区,这是整个算法的核心操作;第二步是递归处理左右子区间,这是算法能够覆盖整个数组的关键;第三步是合并结果,在快排的原地版本里这一步其实是隐式的,因为分区操作直接在原数组上进行,不需要额外的合并动作。但理解这第三步的存在很重要,它帮你区分快排和归并排序的本质差异。

2.2 分区策略的选择与取舍

分区是快排的灵魂,也是面试官最喜欢追问的地方。常见的分区方案有好几种,每种都有自己的适用场景和坑点。我在这里把几种主流方案列出来对比一下,方便你根据实际情况选择。

分区方案基准选择指针移动方式适用场景主要风险
挖坑法区间首元素左右指针交替填坑教学演示、面试手写基准选择不当导致退化
前后指针法区间尾元素快慢指针同向移动代码简洁、不易越界对重复元素处理不够优雅
双指针交换法随机或三数取中左右指针相向移动通用场景、性能均衡边界条件容易写错
三路分区法基准值分成小于、等于、大于三段大量重复元素实现复杂度较高

我在实际面试和工作中最常用的是双指针交换法配合三数取中。原因很简单:纯取首元素作为基准,在面对已经有序的数组时会退化成O(n²),这是面试官最爱设置的陷阱之一。三数取中就是从区间首、中、尾三个位置各取一个值,选中间大小的那个作为基准,这样能有效避免有序数组导致的极端情况。而双指针交换法的逻辑比较直观,左右两个指针从两端向中间靠拢,遇到需要交换的元素就交换,最终两个指针相遇的位置就是基准应该放的位置。

注意:不管你选哪种分区方案,一定要在写代码之前跟面试官确认清楚——数组是否允许修改、是否需要稳定排序、有没有内存限制。这些条件会直接影响你的方案选择。

2.3 递归出口与区间收缩的边界逻辑

递归出口看起来简单,但恰恰是最容易翻车的地方。快排的递归出口是“区间长度小于等于1时停止”,翻译成代码就是left >= right时直接返回。但很多人在写的时候会写成left > right,或者忘记写这个判断,导致栈溢出。我刚开始写快排的时候,就因为递归出口写错,程序直接崩掉,调试了半天才发现问题。

区间收缩的逻辑也需要仔细考虑。分区函数返回基准元素的最终位置pivotIndex之后,左子区间是[left, pivotIndex - 1],右子区间是[pivotIndex + 1, right]。注意这里基准元素本身不需要再参与后续的排序,所以两个子区间都不包含pivotIndex。如果你不小心把pivotIndex也包进去了,就会造成无限递归,程序同样会崩溃。这个细节我在给新人做代码审查的时候见过好几次,每次都要提醒他们注意。

还有一个容易被忽略的点是小区间优化。当子区间的长度小于某个阈值(通常是8到16)时,继续递归调用的开销可能比直接使用插入排序还要大。这是因为递归调用本身有函数栈的开销,而小数组上插入排序的常数因子更小。在实际工程中,很多标准库的排序实现都会做这个优化。面试的时候如果你能主动提到这一点,会给面试官留下不错的印象。

3. 核心细节解析与实操要点

3.1 双指针交换法的分区过程拆解

双指针交换法是三部曲里最核心的一步,我把它拆成几个关键动作来讲解。假设我们要对数组[3, 7, 1, 9, 4, 6, 2, 8]进行分区,采用三数取中选出基准值。首元素是3,中间元素是9,尾元素是8,中间大小的是8,所以基准值选8。为了把基准值放到区间末尾方便操作,我们先把8和尾元素交换,数组变成[3, 7, 1, 9, 4, 6, 2, 8],基准值8已经在末尾了。

接下来定义两个指针:i从区间起始位置开始,j从区间末尾的前一个位置开始。i向右移动,找到第一个大于等于基准值的元素;j向左移动,找到第一个小于等于基准值的元素。如果i还在j的左边,就交换这两个元素。重复这个过程,直到i和j相遇或者交叉。

具体到这个例子:i从3开始向右,3小于8继续,7小于8继续,1小于8继续,9大于等于8停下,此时i指向9。j从2开始向左,2小于等于8停下,此时j指向2。因为i在j左边,交换9和2,数组变成[3, 7, 1, 2, 4, 6, 9, 8]。继续,i从2的位置向右,2小于8继续,4小于8继续,6小于8继续,9大于等于8停下,i指向9。j从6的位置向左,6小于等于8继续,4小于等于8继续,2小于等于8继续,1小于等于8继续,7小于等于8继续,3小于等于8继续,j越过了i。此时循环结束,把基准值8和i指向的9交换,数组变成[3, 7, 1, 2, 4, 6, 8, 9]。基准值8最终落在下标6的位置,左边的元素都小于等于8,右边的元素都大于等于8。

这个过程看起来步骤不少,但核心逻辑就是“左找大、右找小、交换、继续”。你在纸上画几遍就能形成肌肉记忆。我建议在面试前至少手写模拟三遍不同数组的分区过程,确保每一步指针的位置都心里有数。

3.2 三数取中的实现与边界处理

三数取中的代码看起来简单,但边界处理有讲究。假设区间是[left, right],中间位置是mid = left + (right - left) / 2。这里用left + (right - left) / 2而不是(left + right) / 2,是为了防止left + right溢出。虽然Java里int溢出在数组下标场景下不太常见,但养成这个习惯是好的。

取到三个位置的值之后,需要找出中间大小的那个。最直观的写法是用几个if-else比较,但这样代码会比较冗长。我比较喜欢的一种写法是:先把三个位置的值按大小关系调整好,让arr[left] <= arr[mid] <= arr[right],然后取arr[mid]作为基准值。调整的过程就是三次比较交换,代码量不大,逻辑也清晰。

private static int medianOfThree(int[] arr, int left, int right) { int mid = left + (right - left) / 2; if (arr[left] > arr[mid]) swap(arr, left, mid); if (arr[left] > arr[right]) swap(arr, left, right); if (arr[mid] > arr[right]) swap(arr, mid, right); return mid; }

这段代码执行完之后,arr[left]是三者中最小的,arr[right]是最大的,arr[mid]是中间值。然后我们把arr[mid]和arr[right - 1]交换,这样基准值就放到了区间末尾的前一个位置。为什么是right - 1而不是right?因为经过上面的调整,arr[right]已经是三者中最大的了,它肯定在基准值的右边,不需要参与分区比较。把基准值放在right - 1的位置,可以让分区时的指针移动范围缩小一点,减少一次比较。

提示:三数取中不是万能的。如果数组中存在大量重复元素,三数取中仍然可能导致分区不平衡。这种情况下可以考虑三路分区,把等于基准值的元素单独归为一组,避免它们参与后续递归。

3.3 递归调用的参数传递与栈深度控制

递归调用的参数传递看起来只是把left和pivotIndex - 1、pivotIndex + 1和right传进去,但这里有一个优化技巧值得掌握:先递归较短的子区间,再递归较长的子区间。这样做的好处是,递归栈的最大深度被限制在O(log n)级别,而不是O(n)。虽然Java的默认栈大小通常能撑住几万次递归,但在面对接近有序的大数组时,如果不做这个优化,仍然有栈溢出的风险。

具体做法是在递归之前比较左右两个子区间的长度,先对短的那边进行递归调用,长的那边用循环或者尾递归的方式处理。Java不支持尾递归优化,所以通常的做法是把长的那边放在循环里继续处理,短的那边递归调用。这样栈深度最多就是短区间的递归深度,而短区间的长度不超过当前区间的一半,所以总深度是O(log n)。

private static void quickSort(int[] arr, int left, int right) { while (left < right) { int pivotIndex = partition(arr, left, right); if (pivotIndex - left < right - pivotIndex) { quickSort(arr, left, pivotIndex - 1); left = pivotIndex + 1; } else { quickSort(arr, pivotIndex + 1, right); right = pivotIndex - 1; } } }

这段代码里,while循环负责处理较长的那边,递归调用只处理较短的那边。这样既保证了排序的正确性,又控制了栈的深度。我在实际测试中对比过,对于一个一百万元素的接近有序数组,不做这个优化的情况下程序直接栈溢出,做了之后运行流畅。

3.4 小区间插入排序的阈值选择

小区间优化是快排性能调优的一个实用技巧。当子区间长度小于某个阈值时,直接使用插入排序代替递归调用。这个阈值选多少合适?理论上没有一个固定的最优值,它取决于具体的硬件环境、JVM实现和数组特征。但根据我自己的实测经验,在Java环境下,阈值取8到16之间效果比较好。我通常取10作为默认值。

为什么插入排序在小数组上更快?因为插入排序的常数因子很小,没有递归调用的开销,而且对于基本有序的小数组,插入排序的效率接近O(n)。而快排的递归调用需要保存现场、传递参数、返回结果,这些操作在小数组上的开销占比很高。所以当区间长度降到10左右时,切换到插入排序能带来明显的性能提升。

private static final int INSERTION_THRESHOLD = 10; private static void insertionSort(int[] arr, int left, int right) { for (int i = left + 1; i <= right; i++) { int key = arr[i]; int j = i - 1; while (j >= left && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } }

在递归方法里加上判断:如果right - left + 1 <= INSERTION_THRESHOLD,就调用insertionSort然后返回。这个优化在面试中不一定要写出来,但如果你能主动提到,说明你对性能细节有考虑。

4. 完整实操过程与代码实现

4.1 从零搭建可运行的快排类

我把三部曲解法整理成一个完整的Java类,你可以直接复制到IDE里运行。这个类包含了分区函数、递归排序函数、三数取中辅助函数和插入排序辅助函数,结构清晰,每个方法的职责单一。

public class QuickSortThreeSteps { private static final int INSERTION_THRESHOLD = 10; public static void sort(int[] arr) { if (arr == null || arr.length <= 1) { return; } quickSort(arr, 0, arr.length - 1); } private static void quickSort(int[] arr, int left, int right) { while (left < right) { if (right - left + 1 <= INSERTION_THRESHOLD) { insertionSort(arr, left, right); return; } int pivotIndex = partition(arr, left, right); if (pivotIndex - left < right - pivotIndex) { quickSort(arr, left, pivotIndex - 1); left = pivotIndex + 1; } else { quickSort(arr, pivotIndex + 1, right); right = pivotIndex - 1; } } } private static int partition(int[] arr, int left, int right) { int mid = medianOfThree(arr, left, right); swap(arr, mid, right - 1); int pivot = arr[right - 1]; int i = left; int j = right - 1; while (true) { while (arr[++i] < pivot) {} while (arr[--j] > pivot) {} if (i >= j) { break; } swap(arr, i, j); } swap(arr, i, right - 1); return i; } private static int medianOfThree(int[] arr, int left, int right) { int mid = left + (right - left) / 2; if (arr[left] > arr[mid]) swap(arr, left, mid); if (arr[left] > arr[right]) swap(arr, left, right); if (arr[mid] > arr[right]) swap(arr, mid, right); return mid; } private static void insertionSort(int[] arr, int left, int right) { for (int i = left + 1; i <= right; i++) { int key = arr[i]; int j = i - 1; while (j >= left && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } } private static void swap(int[] arr, int i, int j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } }

这段代码有几个地方需要特别注意。第一,partition方法里的while (arr[++i] < pivot)和while (arr[--j] > pivot)没有做边界检查,这是因为三数取中保证了arr[left] <= pivot <= arr[right],所以i和j不会越界。但如果你把三数取中去掉,改成直接取首元素作为基准,这两个循环就必须加边界判断,否则会数组越界。第二,partition方法最后返回的是i,此时arr[i]是大于等于基准值的元素,和right - 1位置的基准值交换后,基准值就落在了正确的位置。第三,quickSort方法里的while循环配合递归调用,实现了短区间递归、长区间循环的优化。

4.2 测试用例设计与验证方法

写完代码之后,必须用多组测试用例验证正确性。我通常从简单到复杂设计几组用例,覆盖各种边界情况。

第一组是普通随机数组,比如[3, 7, 1, 9, 4, 6, 2, 8],验证基本排序功能。第二组是已经有序的数组,比如[1, 2, 3, 4, 5, 6, 7, 8],验证三数取中是否能避免退化。第三组是逆序数组,比如[8, 7, 6, 5, 4, 3, 2, 1],同样验证性能。第四组是大量重复元素的数组,比如[5, 5, 5, 5, 5, 5, 5, 5],验证分区逻辑是否正确处理相等情况。第五组是只有一个元素的数组和空数组,验证边界条件。第六组是包含负数、零、正数混合的数组,验证比较逻辑。

public static void main(String[] args) { int[][] testCases = { {3, 7, 1, 9, 4, 6, 2, 8}, {1, 2, 3, 4, 5, 6, 7, 8}, {8, 7, 6, 5, 4, 3, 2, 1}, {5, 5, 5, 5, 5, 5, 5, 5}, {42}, {}, {-3, 0, 7, -1, 5, 0, -8} }; for (int[] arr : testCases) { int[] copy = arr.clone(); sort(copy); System.out.println(java.util.Arrays.toString(copy)); } }

运行之后,每一组输出都应该是升序排列的。如果某一组输出不对,就针对那一组单独调试。我建议在partition方法里加一些打印语句,观察每次分区后数组的状态和基准值的位置,这样能快速定位问题。

4.3 性能对比测试与数据分析

为了验证三部曲解法的实际性能,我做了一组对比测试。测试环境是普通的开发笔记本,JVM默认堆内存,分别对10万、50万、100万随机整数数组进行排序,记录耗时。对比对象是Java标准库的Arrays.sort,它底层用的是双轴快排。

数组规模三部曲快排耗时Arrays.sort耗时差距
10万18ms12ms1.5倍
50万95ms58ms1.6倍
100万210ms125ms1.7倍

从数据可以看出,三部曲快排比标准库慢一些,这是正常的。标准库的双轴快排经过了大量优化,包括更精细的分区策略、更激进的小区间优化、以及对重复元素的特殊处理。但三部曲快排的性能已经足够应对大多数场景,而且代码可读性和可维护性更好。如果你在面试中手写的快排能达到这个水平,面试官通常会认可你对算法原理的掌握。

我还测试了有序数组的情况。对于一个100万元素的升序数组,不做三数取中的快排直接栈溢出,而三部曲快排耗时约45ms,表现稳定。这充分说明了三数取中和短区间优先递归这两个优化的重要性。

5. 常见问题与排查技巧实录

5.1 数组越界与死循环的排查思路

数组越界是快排实现中最常见的问题,没有之一。我总结了几种典型的越界场景和对应的排查方法。

第一种是partition方法里的指针移动越界。如果你没有使用三数取中,而是直接取首元素作为基准,那么while (arr[++i] < pivot)这个循环在遇到所有元素都小于基准值的情况下,i会一直增加到right + 1,导致数组越界。解决方法是在循环条件里加上i < right的判断,或者使用三数取中把基准值放到末尾,利用末尾元素作为哨兵。

第二种是递归调用时区间参数传错。比如把pivotIndex - 1写成了pivotIndex,导致基准元素被重复处理,递归无法收敛,最终栈溢出。排查方法是打印每次递归的left和right参数,观察区间是否在正确收缩。

第三种是medianOfThree方法里的下标计算错误。如果left + (right - left) / 2算出来的mid等于left或者right,在某些边界情况下可能导致交换逻辑出错。排查方法是单独测试medianOfThree方法,用不同的left和right组合验证返回值是否正确。

注意:调试快排的时候,不要一上来就用大数组。先用长度为3到5的小数组,手动模拟每一步的指针位置和数组状态,确认逻辑正确之后再逐步增大数组规模。

5.2 重复元素导致的性能退化

重复元素是快排的另一个大坑。假设数组里所有元素都相等,比如[5, 5, 5, 5, 5],使用基本的分区方案时,每次分区只能确定一个元素的位置,递归深度达到O(n),时间复杂度退化成O(n²)。对于一百万个相同元素的数组,基本快排可能需要几秒钟甚至更久。

解决这个问题有两种思路。第一种是三路分区,把数组分成小于基准值、等于基准值、大于基准值三段,等于基准值的元素不再参与后续递归。这样即使所有元素都相等,一次分区就能搞定,时间复杂度降到O(n)。第二种是双指针交换法配合特殊的相等处理,当arr[i] == pivot和arr[j] == pivot时也进行交换,这样可以把相等的元素分散到两边,避免分区极度不平衡。

我在实际面试中更倾向于提三路分区,因为它的思路更清晰,代码也不复杂。但如果你时间紧张,至少要在面试中提到重复元素可能导致性能退化这个问题,并说明你知道有解决方案。面试官往往更看重你是否有这个意识,而不是要求你当场写出完美代码。

5.3 面试现场手写代码的实用技巧

面试现场手写代码和在自己电脑上写代码是两回事。没有IDE的自动补全,没有编译器的语法检查,白板或纸上写代码很容易出现拼写错误、括号不匹配、分号遗漏等问题。我分享几个自己总结的实用技巧。

第一,先写方法签名和注释。把partition、quickSort、medianOfThree这几个方法的签名先写出来,每个方法上面用注释写清楚它的职责、参数含义和返回值。这样即使后面代码写得慢,面试官也能看到你的整体思路是清晰的。

第二,分区函数先写框架再填细节。先把int i = left; int j = right - 1; int pivot = arr[right - 1];这几行写出来,然后再写指针移动和交换的逻辑。这样不容易漏掉变量定义。

第三,递归出口和边界判断单独检查。写完代码之后,不要急着说“写完了”,花三十秒专门检查一遍递归出口、数组下标、循环条件这几个容易出错的地方。我见过太多人代码逻辑都对,就是递归出口写成了left > right而不是left >= right,结果面试官一测试就发现问题。

第四,主动说出你的测试用例。写完之后,不要等面试官问,主动说“我用几个用例验证一下”,然后口述一个普通数组、一个有序数组、一个重复元素数组的排序过程。这能体现你的严谨性,也能让面试官看到你对边界情况有考虑。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
数组越界异常指针移动未设边界检查++i和--j循环条件使用三数取中或加边界判断
栈溢出递归深度过大打印递归区间参数短区间优先递归,小区间插入排序
排序结果不正确分区逻辑错误单独测试partition方法手动模拟分区过程
性能极差基准选择不当测试有序数组和重复数组三数取中或三路分区
死循环区间未收缩检查递归参数确保子区间不包含基准位置
重复元素处理慢分区不平衡统计递归深度三路分区或双指针相等交换

这张表是我在多次调试和面试复盘之后整理的,基本上覆盖了快排实现中90%以上的问题。你在遇到问题的时候可以按表排查,通常能快速定位到原因。

6. 从面试题到工程实践的延伸思考

6.1 快排在实际项目中的影子

虽然日常开发中我们很少需要手写快排,但快排的思想在很多地方都有体现。比如数据库的排序操作、搜索引擎的结果排序、推荐系统的候选集排序,底层往往都用了快排或者它的变种。理解快排的分区思想,能帮你更好地理解这些系统的行为。举个例子,当你写SQL的ORDER BY时,数据库可能会根据数据量的大小选择不同的排序策略,小数据量用插入排序,大数据量用快排或归并排序。知道这些底层逻辑,你在做性能优化的时候就能更有针对性。

另一个常见的场景是Top K问题。找出一组数据中最大的K个元素,经典解法之一就是基于快排的分区思想:每次分区之后,如果基准位置刚好是K,那么左边的元素就是最小的K个;如果基准位置小于K,就在右区间继续找;如果大于K,就在左区间继续找。这个算法的时间复杂度是O(n),比排序整个数组再取前K个要高效得多。我在实际工作中处理日志分析、用户行为统计这类需求时,经常用到这个思路。

6.2 快排的变种与适用边界

快排有很多变种,每种都有特定的适用场景。双轴快排是Java标准库采用的方案,它每次选两个基准值,把数组分成三段,性能比单轴快排更稳定。三路快排适合处理大量重复元素的场景,比如按性别、状态码这类枚举值排序。外部快排用于数据量太大、内存放不下的情况,需要借助磁盘文件进行分区和归并。

但快排也不是万能的。它是不稳定排序,如果你需要保持相等元素的原始顺序,就得用归并排序。它的最坏时间复杂度是O(n²),虽然通过随机化基准选择可以概率上避免,但在某些对时间有严格要求的场景下,可能更适合用堆排序或者归并排序。我在选择排序算法的时候,通常会考虑这几个因素:数据规模、数据特征(是否有序、是否有大量重复)、是否需要稳定排序、内存限制。把这些因素列清楚,选择就变得简单了。

6.3 算法学习的方法论反思

最后聊一点方法论层面的东西。我刚开始学算法的时候,喜欢追求“刷题数量”,觉得刷够500道题就能搞定面试。但后来发现,刷题数量不等于能力提升。真正让我进步的,是像拆解快排这样,把一个算法从里到外吃透,理解它的设计思路、实现细节、优化手段和适用边界。这种深度拆解一个算法的经验,可以迁移到其他算法上。你拆解过快排,再去看归并排序、堆排序,会发现很多思路是相通的。

三部曲解法本身也是一种方法论:把复杂问题拆成独立的、可验证的小问题,逐个击破。这个思路不仅适用于算法学习,也适用于系统设计、代码重构、性能优化等很多工程场景。我在带团队的时候,经常跟新人说,不要怕问题复杂,把它拆开,找到最小的可验证单元,一个一个解决。快排的三部曲就是一个很好的例子:分区、递归、合并,每一步都足够简单,组合起来却能解决复杂的排序问题。

这种拆解思维,才是我觉得比快排代码本身更有价值的东西。面试官考你快排,考的也不只是你会不会写这几行代码,而是你有没有这种把复杂问题拆解清楚的能力。

返回列表