前言

         算法的核心问题是排序和搜索。这2个领域应用最广,研究也最透。本文我将讲解排序和搜索领域最高效的两个算法:快速排序算法和二分搜索算法。

         教科书和很多实现库给出的这两个算法的代码非常复杂,很难理解,本文中我给出的代码是最简单的实现代码,易于理解,效率也很高。

 

 

缘起

         刚才有人问我怎样实现快速排序,我在5分钟之内写了一个快速排序的Java代码出来,不知道他们有没有理解。

因此,我想到要写作这篇文章。介绍一下快速排序算法和二分搜索算法的最简实现。

         我的二分搜索算法是在我用Flex开发工作流编辑器时实现的。当时的需求是在2个图形之间画出连接线,要求根据鼠标操作来绘制,并且线段的起点和终点都是在图形的外框上。

         上面的描述可能比较抽象,这么说吧,原来我实现的GUI效果是,2个方框,使用鼠标把它们连接起来,我绘制的线是鼠标点下和释放这2个端点连接起来的线段。但是,这样的线段比较丑,客户要求线段的两头应该在2个方框的边框上。

         怎么解决这个问题呢?我把线段看做是排序后的点的集合,然后就可以使用二分搜索算法搜索到线段和边框的交点,然后把它们绘制出来。

         当时的二分搜索算法是用ActionScript3写的,现在我把它改成Java了。

 

快速排序算法和二分搜索算法

         算法主要分为排序算法、搜索算法、图算法。图算法我用得不多,没有发言权,本文就不说了。

         排序算法中最快的是快速排序算法,搜索算法中最快的是二分搜索算法。我也最喜欢这2个算法。

         因为,它们是使用递归实现的,代码简洁清晰,效率又非常高。

         根据我的理解,算法的本质就是数学。根据输入和设定的目标,采用有限的步骤实现输出。通常,使用计算机实现的算法,都会用到循环,这样才能发挥计算机高速运算的优势。

         循环和递归是等效的,这已经被科学家所证明。数学上没有循环,只有递归的概念,因此使用递归代替循环表示算法有很多好处:

1,  递归的代码要比循环简洁很多,也优雅很多。

2,  递归的代码可以用数学方式建模,可以从数学角度验证其正确性。

很多函数式语言甚至没有循环的概念和关键字,强迫你使用递归来实现循环。如,ErLang

递归也有一些缺点,递归使用栈来保存函数地址和参数、返回值,而栈是有一定大小的,过多的递归调用可能会造成栈溢出。

但是,递归算法会容易转变为循环。我更欣赏递归的简洁,除非真的出现栈溢出的问题,我是不会使用循环的。

 

二分搜索算法

理论:

         二分搜索算法用于针对已排序的集合进行搜索。

它的原理是:

1,  找到排序数组的中间元素,如果它匹配目标值,那么就返回它在数组中的索引。

2,  如果没有找到,那么判断中间值比目标值大还是小,

如果中间值比目标值大,那么就对第一个元素到middle-1的元素递归这个过程。

如果中间值比目标值小,那么就对middle+1到最后一个元素。

3,  如果结束的索引小于开始的索引,返回-1,表示没有找到。

4,  如果子集合有2个元素,那么各自比较。因为Java的整数除法会舍弃小数,如果数组只有2个元素,那么middle值一直都是第一个元素。

经过上述的递归过程,最终将返回匹配元素的索引,或者是-1,表示找不到。

 

         二分搜索算法之所以速度快,是因为它每次可以把数组切分成两半,每次递归调用都能去除一半数据,而不用匹配每一个数据。

 

代码:

         下面是代码,逻辑清楚,代码简单。

/**

     * by 沈东良/良少     http://blog.csdn.net/shendl/

     * @param array

     * @param start

     * @param end   这是最后一个元素的索引,第一次调用应该是array.length-1

     * @param value

     * @return

     */

    public static int binarySearch(int[] array,int start,int end,int value){

       int middle=(start+end)/2;

       if(end<start){

           return -1;

       }

       if(end==(start+1)){

           if(array[start]==value){

              return start;

           }else if(array[end]==value){

              return end;

           }

          

       }else if(array[middle]==value){

           return middle;

       }else if(value>array[middle]){

           return binarySearch(array,middle+1,end,value);

       }else if(value<array[middle]){

           return binarySearch(array,start,middle-1,value);

       }

       return -1;

   

    }

 

         上述代码稍加改变,就可以排序任意类型。如使用泛型,然后加上对Comparable接口的实现,即可。

 

快速排序算法

         二分搜索算法确实非常快,但是它只能用于已排序数组。如果数组未排序呢,该怎么办呢?简单,先用快速排序算法进行排序,然后再用二分搜索进行搜索。

         先排序再搜索,要比匹配每一个元素快得多。搜索引擎,数据库索引也都使用了对数据集合的排序技术,这样搜索数据才会快速。

 

理论:

         最慢,也是最容易想到的排序算法是插入排序算法:

1,  遍历数组,找出最小的元素,把它放到第一个元素。

2,  循环查找未排序的数组,直到整个数组排序。

这需要2个嵌套的循环,意味着它的效率是O(n^2);

之所以插入排序的效率如此之地,是因为要找出整个数组中最小的数据,需要遍历整个数组的元素。

而插入排序聪明就聪明在它不查找整个数组最小、次小的元素,而是每次仅仅把小于某个元素的值移到一边,通过迭代最终自动实现排序。这就大大节约了排序所需的计算步骤。

 

         快速排序算法的理论:

1,  如果S中的元素个数是0或者1,那么返回。

2,  选取S中的任一元素v,称为中心点。

3,  S集合中的元素分为2个部分:L={小于pivot 的元素+ pivot }R={大于或者等于pivot的元素}

4,  返回LR

我们使用Java使用的是就地排序的方式,因此不需要返回值。

因为Java是一种可以修改变量的语言,用函数式语言的术语来说,就是有“副作用”。我们利用这个副作用直接修改作为参数的Array,不需要返回值。

 

 

代码:

/** by 沈东良/良少     http://blog.csdn.net/shendl/

     * 快速排序,有副作用   从小到大

     * @param array

     * @param start

     * @param end这是最后一个元素的索引,第一次调用应该是array.length-1

     */

    public static void quickSort(int[] array,int start,int end){

       //有可能造成start>end   因为递归调用时j+1,可能引起jend还大1。 另外如果数组是空的,或者输入错误也会出现这种情况

       if(end<=start){

           return;          

       }else {

           //取中间元素为中心点,然后移到最右边

           int sign=(start+end)/2;

           int tmp=array[end];

           array[end]=array[sign];

           array[sign]=tmp;

           int j=start;

           for(int i=start;i<=end-1;i++){ 

              //小于的元素和标记互换,等于的不能互换,否则会形成死循环

              if(array[i]<array[end])  {                

                   tmp=array[i];

                   array[i]=array[j];

                   array[j]=tmp;

                   j=j+1;

              }         

           }

           //把标记元素和第一个>=它的元素位置互换,这样数组就分成2个部分,一个部分比中心值小,一个部分比中心值大。

           tmp=array[j];

           array[j]=array[end];

           array[end]=tmp;

           quickSort(array,start,j);

           quickSort(array,j+1,end);

       }

    }

 

         JavaArrays类也使用快速排序算法进行排序。但它的代码写得像天书一样。

 

         提高快速排序算法执行效率的主要方法是对中心点进行检测,希望选中的中心点有更大的概率是整个数组的中值。

         我的实现中简单的选择数组中间的值作为中心点,一般来说这样的选择效率还是不错的。

 

         注意上面的一项实现技术,我们使用把中心数据元素移到数组最后的方式实现了数组的就地比较。这是比较常用的技术,把数据移到数组的最前面或者最后面,方便比较数据。

 

         另外,我的Java快速排序代码使用了“副作用”,而在纯函数式语言,如Haskell,ErLang中是没有“副作用”的,也就是说变量不可以重新赋值。此时就需要返回值,每次都创建新的子数组。但函数式语言的数组处理功能很强,也会做很多性能优化,因此函数式语言实现快速排序代码更加简单,没有“副作用”,也更加数学化。

 

JDK使用二分搜索和快速排序算法实现搜索和排序,足见上述两个算法的性能优势。

 

 

 

 

 

发表于 @ 2009年04月07日 12:38:00|评论(25 )|举报|收藏

 | 旧一篇: ASAble开源项目诞生了

why0603_2000 发表于2009年4月9日 11:34:04  IP:举报
HASH。。。
fenix124 发表于2009年4月10日 17:11:47  IP:举报
二分查找不用递归�?d=0.9097523746027392
ranxiutao 发表于2009年4月11日 20:16:40  IP:举报
好东西,顶博主!
daiweb163 发表于2009年4月11日 22:13:43  IP:举报
学习了,相当的感谢哦···
jia611 发表于2009年4月13日 12:48:29  IP:举报
嗯,好!�?d=0.2589336680875436
kinsan 发表于2009年4月13日 15:22:11  IP:举报
非常好 讲解清晰 易懂 顶
xuting 发表于2009年4月13日 18:35:56  IP:举报
查找和排序代码均有错误,没考虑 (start end)/2 的溢出
shendl 发表于2009年4月14日 13:46:45  IP:举报
xuting 发表于Monday, April 13, 2009 18:35:56 举报查找和排序代码均有错误,没考虑 (start end)/2 的溢出====================这是给大家看的简单代码,要完善的话,包括使用泛型、IComparable等,至于溢出的情况,不必考虑这么大数量的集合。如果你遇到了,可以使用BigInteger。相信你也用不到。
shendl 发表于2009年4月14日 20:40:21  IP:举报
查找和排序代码均有错误,没考虑 (start end)/2 的溢出===========不会溢出的,(start end)会被转为long类型,怎么会溢出呢。
wwjLonely 发表于2009年4月15日 9:50:09  IP:举报
代码明了易懂,顶了。。。
likenk26 发表于2009年4月15日 10:40:06  IP:举报
int j=start; for(int i=start;i<=end-1;i ){ //小于的元素和标记互换,等于的不能互换,否则会形成死循�? if(array[i]<array[end]) { tmp=array[i]; array[i]=array[j]; array[j]=tmp; j=j 1; } }这里看的不明白,for循环是不是应该改�?for(int i=end;i>j;i--)
lenglengdeyu 发表于2009年4月15日 11:07:07  IP:举报
学习中。。谢谢。�?d=0.7830751328921277
freerll 发表于2009年4月15日 13:58:06  IP:举报
数量非常大的数组的话,也许基排也是一个不错的选择
shendl 发表于2009年4月15日 16:39:36  IP:举报
to likenk26 : 变量j的作用是用来把小于标记值的值存储到该元素里。j最初==0;这样,如果找到一个元素小于标记值,那么就把它和【0】元素互换。然后j=1;这样再找到一个小于标记值的值,再和【1】互换位置。一直继续。 结束之后,再把【j】和【end】标记值互换。 现在,数组就分为2个部分,一个部分是0-j-1,都比【j】小,另一个部分是j 1到end,都比[j]大或者相等。
gigger_123 发表于2009年4月17日 15:35:04  IP:举报
收藏了
renhit 发表于2009年4月17日 16:13:52  IP:举报
代码好懂!但是前面说了有Bug,另外我要说的是快速排序递归实现的话碰上几百万、千万的数据就挂了 :)所以最好用非递归的快排。数据少的话怎么排都可以
andy850107 发表于2009年4月17日 16:40:41  IP:举报
for(int i=start;i<=end-1;i ){ //小于的元素和标记互换,等于的不能互换,否则会形成死循�? if(array[i]<array[end]) { tmp=array[i]; array[i]=array[j]; array[j]=tmp; j=j 1; } }j是从start来标识当前要交换的位置,一直到end-1,那已经比较过的,在交换之后还是要比较的,为什么不再设置一个标识jend呢,放到后面为end-1,将已经比较过的,比arry[end]大的放在jend上,然后jend--,这样当j>=jend的时候就可以停止比较了。不知道说明白没有,我想这样,应该比楼主的再快一些吧�?d=0.7861569813380348
shendl 发表于2009年4月17日 17:11:25  IP:举报
to renhit: 函数式语言的话,递归是没有限制的。 而Java,C
shendl 发表于2009年4月17日 17:19:54  IP:举报
to renhit: 函数式语言的话,递归是没有限制的。我个人比较喜欢递归,因为代码比循环更加简洁。 实际上,递归和循环是等价的,任何递归都可以改写成循环。上面的 二分查找法 和 快速排序代码都可以 改成 循环,不用任何递归。 这个留给读者自己实现吧,很简单。 循环 BigInteger 应该可以适用于绝大部分大数据的情况,数据量再大的话,可以使用MapReduce分给多个计算机进行处理。
shendl 发表于2009年4月17日 17:23:19  IP:举报
to andy850107 :你应该没有看明白这个算法。 快速排序只是把数组分成3个部分,小于某个元素,某个元素,大于或者等于某个元素。 本身并不排序,而是通过递归,最后子数组的元素个数为0或者1就返回。 实际上 “快速排序 本身并不排序”! 这个是这个算法的关键点。
andy850107 发表于2009年4月17日 22:08:37  IP:举报
to shendl:我觉得我看懂楼主的意思了,挺巧妙的,就是我想能不能做如下的改动,效率更高一些,可能我表述的不清楚吧,上代码,那这段“int j=start; for(int i=start;i<=end-1;i ){ //小于的元素和标记互换,等于的不能互换,否则会形成死循环 if(array[i]<array[end]) { tmp=array[i]; array[i]=array[j]; array[j]=tmp; j=j 1; } }”改成“int i = start;int j=start;int jend = end-1; while(j<jend){ //小于的元素和标记互换,等于的不能互换,否则会形成死循环 if(array[i]<array[end]) { tmp=array[i]; array[i]=array[j]; array[j]=tmp; j=j 1;i ; } else { tmp=array[i]; array[i]=array[jend]; array[jend]=tmp; jend--;} }”
andy850107 发表于2009年4月17日 22:10:21  IP:举报
to shendl:我觉得我看懂楼主的意思了,挺巧妙的,就是我想能不能做如下的改动,效率更高一些,可能我表述的不清楚吧,上代码,那这段“int j=start; for(int i=start;i<=end-1;i ){ //小于的元素和标记互换,等于的不能互换,否则会形成死循环 if(array[i]<array[end]) { tmp=array[i]; array[i]=array[j]; array[j]=tmp; j=j 1; } }”改成“int i = start;int j=start;int jend = end-1; while(j<jend){ //小于的元素和标记互换,等于的不能互换,否则会形成死循环 if(array[i]<array[end]) { tmp=array[i]; array[i]=array[j]; array[j]=tmp; j= ; i ; } else { tmp=array[i]; array[i]=array[jend]; array[jend]=tmp; jend--;} }”
andy850107 发表于2009年4月17日 22:23:41  IP:举报
回复怎么总是少东西啊,要不就是乱码,呵呵。应该是j ;i ;在获取L和R集合的时候,要和元素V比较吧,上面的代码是改良这个过程的。
andy850107 发表于2009年4月17日 22:25:53  IP:举报
MLGBD,j加加,i加加
shendl 发表于2009年4月18日 16:56:35  IP:举报
不需要把大的元素往后移,只需要把小的元素移动到前面就可以了。最后会把标记元素移到小的元素后面。 所以jend是废的。