快速排序的非递归实现

来源:互联网 发布:局域网未识别的网络 编辑:程序博客网 时间:2024/05/21 21:01

   快速排序的思想是先从一数列中找出一个枢轴点,这个枢轴点左边的数都比它小,右边的数都比它大,也就是说枢轴点的位置已经是有序的了。例如 5 1 7 9  2   3 8   使用一次快速排序后会变成这样· · · 5 · · ·。

1,2,3会在5的左边,7,8,9在5的右边,5在第四个位置,5是枢轴点,且不会再变动了。

 如果最终排序完毕,数列会是   1 2 3  5  7 8 9    的确5也在第四i个位置。

然后枢轴点确定好位置后,数列被分成了2个部分。即枢轴点左半段和枢轴点有半段。这两段子数列也是无序的。因此我们在对这两个子数列使用快速排序,分别从左边和右边 各找到一个枢轴点。 这时新找的这两个枢轴点加上最开始找的枢轴点已经有3个点了,3个点把数列分成了5段!!

然后在对这5个新的子数列使用快速排序.就又会·多出一些枢轴点。

这样下去枢轴点的数量会越来越接近原始数列的长度。每个子数列的长度会越分越趋向1。

到最后不能再分的时候,枢轴点的个数就等于了原始数列的数字的个数。我们又知道枢轴点就是按有序数列的位置放的,所以这个时候整个数列也有序了。

 

 使用递归对这种划分来说很好想,只要递归下去,加上递归出口,轻而易举的就能写出快速排序。可是如果不使用递归呢。递归我们知道,递归出来的函数会占用栈空间,且存在尾递归重复操作导致效率底下等问题。

 

因此使用非递归也是一种对于快速排序的优化。

不能使用递归了,我们先分析下首先面临的问题是什么?

由快速排序的核心代码块 Quick(arr,int s,int  e)

来分析。我们知道。每次快排的数列段。是要给它发送2个重要的参数的,即开始下标,和结尾下标。

对于快排的普通递归算法来说。使用 mid=Quick(arr,int s,int e);

用mid记录了枢轴点。

然后再使用递归QuickSort(arr,s,mid-1);和QuickSort(arr,mid+1,e);

就可以不必管每次给Quick()函数的s和e到底是什么。递归会一层层的更新s和e。

但是如果要使用循环,我们必须手动来存储每个枢轴点。

 

因此我们首要任务就是创建一个数据结构来存放每次遍历后产生的枢轴点!

我们先看下这张图



 

图中我们可以清楚的发现,每次产生的枢轴点 都是 在上一次快排产生的两个枢轴点之间的!

第一次只有开头s和结尾e这两个枢轴点。产生了一个枢轴点mid0;

第二次就有了两个分段,s至mid0-1 和mid0+1至e。这两个分段每个又个产生一个枢轴点,前者产生mid1,后者产生mid2.至此就用了三个枢轴点了!

。。。。

。。。。

。。。。

以此类推

到了最后,每个分段只有一个数了,返回的枢轴点就是它本身的位置了,数列就有序了。

 

因此我们一开始要创建一个数据结构存放所有枢轴点,我们知道每次更新的枢轴点都在上次的相邻枢轴点之间出生的,因此插入的操作肯定是非常频繁的。因此使用双向链表应该非常适用。但是本着偷懒和照顾没有学到链表的读者。我使用了数组。

创建一个数组tmp存放所有的枢轴点,并定义它的有效长度tmp_len;至于tmp的大小,最少要把它设置为待排数列的长度。因为到了最后枢轴点的个数就是数列的长度。

初始有效长度设置为2。因为最开始tmp数组要存入待排数列的开始下标和结尾下标。

因此这样定义

int tmp[N+1]={s-1,e+1};//N为待排数列长度

int tmp_len=2;          

那为什么放入的是s-1和e+1呢?我来分析。既然定义了tmp来存放枢轴点,用的时候该怎么用呢?、

我们由递归算法也知道 每次取     s到 mid-1   mid+1到e。mid枢轴点的值是直接跳过的。

到时候tmp存放的全是枢轴点,用的时候也是这样使用的Quick(arr,tmp[i]+1,tmp[i+1]-1);

意思就是只把两个枢轴点之间的数列进行Quick排序,枢轴点本身是不用参与进去的。

那么如果我们定义成  int tmp[N]={s,e};  第一次循环使用Quick(arr,s+1,e-1);那岂不是开头就错了,把开头和结尾的两个值忽略了,谁又能保证它俩的大小呢?

 

至于具体的循环算法。 我们先想想什么情况下,数列还未有序,我们要继续排序呢。

那就是tmp保存的枢轴点不够待排数列的长度。代表还有枢轴点没被找出来,我们就不能结束。

因此最外面的循环while(tmp_len小于len)     【题外话:不知怎么回事,博客中只要有'<'和‘len’连一起的时候,它们及它们后面的内容就会消失。这也是为啥我给代码总是截图。难道是触发什么敏感词组被屏蔽了?<和len难道是什么黄暴信息?搞不懂!】

那么while里面该如何实现呢。

再回想递归算法,是不是该一半一半的往下分。而while已经替我们做了。while里是不是该做一件事。

那就是每次如何分。分的结果又是什么!

我们拿第一次划分来举例。第一次tmp里只有s-1和e+1。传入参数后Quick(arr,s,e).把数列分成了2段。并返回了一个枢轴点mid0;这个mid0是关键,如何处理它?是不是该把它插入到tmp中!

插入到哪个位置?是不是该插入到Quick参数的s和e之间!

     for(int k=0;k
   {
        if((tmp[k]+1)<=(tmp[k+1]-1))      //这个if条件意思是2个枢轴点不相邻,即中间还有空出的子数列
       {                                                     //比如3和4都在tmp中相邻。3+1>4-1  它们之间没空位了,不进入if

                                                              //再比如7和9都在tmp中相邻,7+1=9-1 他们之间还有空位,进入if
            ttmp=Quick(arr,tmp[k]+1,tmp[k+1]-1);  //把tmp[k]+1 和tmp[k+1]-1之间的数列进行快排,返                                                      // 回的枢轴点赋值给ttmp;ttmp将用在后面插入tmp数组中
            tmp_len++;                         //让tmp数组有效长度+1,因为即将插入新的枢轴点
           Insert(tmp,k+1,tmp_len,ttmp);  //把ttmp插入在tmp数组中k+1的位置。原本k+1以后的数据整体后移
            k++;            //由于k+1的位置插入了一个新枢轴点,此次循环用不到它。因此k++
      }
   }

然后第一次就划分完了!

while会持续监测到n次,第n次tmp数组的有效长度tmp_len为数列长度时,那就跳出,数列排序完毕。


  以下为代码:

 




 

运行结果:



当然如果使用数组这种数据结构来当作枢轴点存放的话 ,由于每次插入新的枢轴点都需要将后面的所有点向后移位,所以时间复杂度很高。正确的方式应该使用双向链表。每次插入的复杂度都是0(1),时间复杂度会有很大很大的改善。用数组只是方便写代码表达非递归的思想。如果写非递归的快排目的是为了优化效率,那千万不可用数组,否则复杂度会比原版的递归还要高很多。