快排,任何算法基础教程里必讲的最后一种排序算法,我这个差生直到毕业也没能用java或c默写出来一个快排。我模糊的直到他是一种“分而治之”的思想,可是一写到分而治之的时候就脑子里一片浆糊,搞不清分到哪了。那冗长的代码我想死记硬背也背不下来。重温一下噩梦,贴一个网上搜出来的java实现的快排:
public int getMiddle(Integer[] list, int low, int high) {
int tmp = list[low];
while (low < high) {
while (low < high && list[high] > tmp) {
high--;
}
list[low] = list[high];
while (low < high && list[low] < tmp) {
low++;
}
list[high] = list[low];
}
list[low] = tmp;
return low;
}
public void _quickSort(Integer[] list, int low, int high) {
if (low < high) {
int middle = getMiddle(list, low, high);
_quickSort(list, low, middle - 1);
_quickSort(list, middle + 1, high);
}
}
public void quick(Integer[] str) {
if (str.length > 0) {
_quickSort(str, 0, str.length - 1);
}
}
??
OK,这不是java算法的讨论帖,很显然,我贴这么多是用来黑java的。具体的一会儿接着黑。
继续我悲惨的故事,因为写不出快排,我不知被多少公司在笔试时直接拒掉,直到我从不考算法的小公司积累了经验并且弃暗投明去做前端。那我什么时候才能写出来快排了呢?换一种语言写的时候。
我学haskell的时候又看到了快排这一节,我皱了皱眉头,但是看到代码的时候,我惊呆了,让我吃惊的不是代码多简洁,这我早猜到了,让我吃惊的是快排的思路原来是如此简单。上haskell代码:
qs‘ [] = []
qs‘ (x:xs) =
let lower = qs‘ . filter (< x) $ xs;
higher = qs‘ . filter (>= x) $ xs
in lower ++ [x] ++ higher
五行!当然,我说了让我吃惊的不是五行,而是我看到这段代码只是描述了一下什么是快排的分而治之,至于具体怎么样分而治之似乎根本就不是它的重点。什么叫优雅?优雅就是别人在那苦命工作的时候你点根烟慢悠悠的漫不经心的拿出了成果。
这些符号有些过于简单,我还是稍微解释一下便于没有接触过haskell的同学们理解。qs’是函数名,后面紧跟参数约定,等号后面是对函数的定义,可以理解为这是两个重载的函数。对于空列表就直接返回空列表了;对于有内容的列表,它的形式分解为头加尾(头是第一个元素,剩下的是尾),下面let里是分别对比对头元素小的列表和大的列表的定义,都是把qs函数和过滤函数频道一起,对$后面的参数进行调用,(<x)也是函数,意思很明显,就是判断是否比x小,把符合条件的过滤出来,递归地对它继续执行qs’。最后把小列表,中值(也就是头元素)和大列表拼起来。
对吧,基本就是描述了一下这个算法,没有看到java代码里把一个什么样的条件的元素移到什么地方,那玩意儿是最容易出错的。
然后看看它为什么可以用这么简洁的方式表达。作为对比,看看其它语言的实现。
我用coffeescript来写,也很容易就写出来了
qs = (list) ->
if !list? || !list.slice || !list.length
return list
first = list[0]
rest = list.slice 1
lower = qs rest.filter (x) -> x < first
higher = qs rest.filter (x) -> x >= first
lower.concat(first).concat higher
多了点东西,多了什么呢?coffee里面不但需要判空,还要判断参数类型,因为coffee是动态类型语言;然后头尾需要单独来取一下,因为coffee没有解构。haskell代码里参数上的(x:xs)实际上是解构,也就是顺便就把参数给分解成好用的形式了。后面的行数一样,不过coffee多了个匿名函数,就是(x)->x<first,尽管coffee已经把函数写得很简单了,但还是没有haskell写成(<first)那么优雅,(<x)这个形式叫不全调用,是柯里化的成果(柯里化过两天专门说)。总的来说coffee还是比较优雅的。再跟js比一下:
function qs(list) {
if (!list || !list.slice || !list.length)
return list;
var first = list[0];
var rest = list.slice(1);
var smaller = qs(rest.filter(function(x) {
return x < first;
}));
var bigger = qs(rest.filter(function(x) {
return x >= first;
}));
return smaller.concat(first).concat(bigger);
}
js的代码又长了一点。那些语法糖上的区别就不说了,主要一点是函数需要显示声明返回值,而不像coffee那样默认返回最后一句的结果。看起来只是多个return的问题,实际上这是一个“一切皆为表达式”的思想,这是一个更贴近于函数式编程的思想,因为函数式编程没有副作用,只能返回值,所以一切都得是表达式。
其实到此为止,js的版本其语法元素和整体思路都没有比haskell的版本差太多。但是,如果我开始没有看过haskell的版本,我肯定会照着java的那个思路去写,因为js具有函数式编程的能力,看你怎么用,它只是极度灵活,你用成什么样就是什么样,而且会差很多,这也正是js的魅力所在。不过没有好的引导很容就用成java那样了,记得看过一本写javascript设计模式的书,就是想方设法把js写成java,我没读下去。也正因如此,好好的学一门优秀的函数式编程语言对js的开发功力应该是大有裨益的。
回过头来继续黑java(java程序员请淡定,其实我也是java程序员出身,不久前我的正式职位还是“java开发工程师”)
同样,看看java的版本里多了些什么。我费了很大的定力总算是把这段代码大概看完了。_quickSort这个方法和haskell的整体差不多,都是要表达递归地分而治之。不过java似乎比较在意节约使用内存,整个代码没有返回新的列表,一直是在参数传进去的那个列表上调整,找中值的这个步骤就简直了。。。跟冒泡差不多吧。如果不在传入的列表内部鼓捣,也向前面那几种语言的版本每次过滤出新的小列表呢?是可以吧swap的步骤省了,但是过滤,怎么过滤?前面那几个都是传入一个函数(甚至是不全调用产生的超简单函数)最为过滤器的引擎,java没有内部函数(java8什么样我不知道),悲剧了,要不写个循环一个一个往出挑,要不就写个内部类。。。我想起了当年学写swing的时候满眼内部类的情景。。。
还没黑完,haskell、coffee和js的版本功能是等价的,而java版本的功能是被阉割的。人家排序的对象是任何列表,java这个版本里只能对整数数组排序。可以用泛型和接口吗?我记得好像有个叫Compareable的接口。。。算了,总之我在网上搜了半天无一例外都是对整形数组排序的,这只能说明用java处理通用类型是很痛苦的,为了简化都懒得写出来。
Haskell也是强静态类型语言,为啥代码里都感觉不到类型的存在呢?
优雅就是这样,举重若轻。