看懂工业级sort快速排序,打动面试官

本文探讨了在面试中如何准备排序算法,尤其是工业级别的排序。以Go语言的sort包为例,介绍了从算法知识到实际工作场景的学习路径。通过对Go语言Sort包的源码分析,详细解析了快速排序的实现,包括入口函数、最大递归深度限制、取中策略、分区平衡性和处理大量重复值的方法,以帮助读者深入理解工程级排序算法的工作原理。
摘要由CSDN通过智能技术生成

面试中,我们遇到很多排序算法的考核。

快速排序,堆排,希尔排序,插入排序,归并排序等等…

由于现在竞争激烈,面试官除了问这些经典的排序算法,偶尔也会考核一下候选人对很多工业排序的认知,比如MySQL的order by,PHP的array系列排序函数,Go的sort包,Java的Collections.sort()。

对排序这块,确实让人头疼,存在这么几个痛处:

  • 找不到比较规范的算法
  • 很难完全理解规范的算法
  • 好不容易学会了又会忘记
  • 和工作中的内容不搭边
  • 由于上面几点,面试的排序怎么都准备不踏实

这里我介绍一个比较实用的排序学习方向,也是今天要探讨的一部分:

算法知识 -> 普通练手 -> 工业源码 -> 工作场景排序

  • 算法知识:特定排序对应的知识,比如插入排序的规范写法(教材),原理等
  • 普通练手:理解了算法后,在编辑器中敲出来并运行,配合LeetCode之类
  • 工业源码:去了解你熟悉的语言包如何实现排序,因为它们经受住了工业级别的应用,显然非常有价值
  • 工作场景:现实场景中的排序,比如1000万用户的多维度排序,限定内存的多文件大文件排序等

只要踏踏实实掌握上面4点,排序算法这块就可以安心了,即使你会忘记一些细节,你在跟别人讲述排序的时候,别人都可以感受到你「由点及面」的能力。

今天我们就第三点「工业源码」的排序展开学习,由于我最近使用Go比较多,也比较生疏,正好就拿它的排序包sort来学习了。

读源码是一项脑力+体力活,难度确实不小。

作为工程师,「拆解问题」是我们的本能,对于今天这个特殊的主题:「Go语言Sort包中的工程级排序算法」,我们将它拆解成不同粒度的子问题,一直到可以理解。

我会从入口开始,一层一层分析,直接最后理解了整个排序,希望也能帮助到你。

开始:

从使用层面的粒度:

// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
   
	n := data.Len()
	quickSort(data, 0, n, maxDepth(n))
}

注释里写到:Sort不是一个稳定排序算法,也就是说排序后有可能会破坏数据的稳定性,比如 [5, 1, 2, 3, 5],经过不稳定排序后,有可能最后一个元素5会跑到第一个5的前面,变成[1, 2, 3, 5(原来的最后一个), 5(原来的第一个)]

本篇关注的重点是工程级的排序算法如何实现,所以假设各位都清楚了排序相关的一些前置知识,包括:时间复杂度分析,插入排序,希尔排序,堆排序,快速排序和Go语言的基本语法,如果还不是很清楚这些知识点的话,建议先逐个击破,再回到该篇一起探讨这个算法,会更高效。

说明一点,源码的注释我都保留了,以免因为我的片面理解歪曲了作者的本意。

进入正题,先看一下Interface的结构:

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
   
	// Len is the number of elements in the collection.
	Len() int
	// Less reports whether the element with
	// index i should sort before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

经典三大配件,无需多言。

熟悉排序的我们知道,排序过程中,比较和交换是必要环节,比较可以判断是否需要交换,而交换会减少逆序度(人话:数据集合会变得有序一些)。

了解了data要求的结构之后,我们就可以进入Sort这个入口函数了,但在这提醒一点,Less()的实现完全是可以由我们来自定义的,所以对于复杂对象的比较,比如 注册时间+用户年龄的多维度排序,我们可以自定义Less()。

往下走:

入口函数告诉我们,会调用一次data.Len来获取数据的长度,并且以O(n*log(n))的规模来调用Less和Swap,像这种说明,先看一眼,不去细究。

重点来了:

quickSort(data, 0, n, maxDepth(n))

这个 maxDepth(n)是干什么用的? 我们先大概看一眼它的功能,不要太深究背后的数学原理。

根据数据规模,不断二分,累计深度。

// maxDepth returns a threshold at which quicksort should switch
// to heapsort. It returns 2*ceil(lg(n+1)).
func maxDepth(n int) int {
   
	var depth int
	for i := n; i > 0; i >>= 1 {
   
		depth++
	}
	//下面这行不要纠结,后面会解释
	return depth * 2
}

注释告诉我们:maxDepth返回一个适当的阈值,用于判断当前的排序算法应不应该由快排切换到堆排,这里maxDepth的作用就是限制递归的深度。递归的空间成本很高,面对巨大规模的数据,递归还有内存溢出风险,maxDepth的存在规避了风险。
这里我们可能会好奇最后return的时候为什么乘于2,没事,后面进入快排的时候,我会解释一下。

看完maxDepth,我们回到quickSort。

func quickSort(data Interface, a, b, maxDepth int) {
   
	for b-a > 12 {
    // Use ShellSort for slices <= 12 elements
		if maxDepth == 0 {
   
			heapSort(data, a, b)
			return
		}
		maxDepth--
		mlo, mhi := doPivot(data, a, b)
		// Avoiding recursion on the larger subproblem guarantees
		// a stack depth of at most lg(b-a).
		if mlo-a < b-mhi {
   
			quickSort(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值