快速排序

快速排序

  • 快速排序思想介绍
  • Hoare法
  • 挖坑法
  • 双指针法
  • 快排的非递归实现(需要 Stack 的知识)
  • 快排的复杂度和稳定性
  • 如何解决快排的一些最坏情况

本篇博客主要介绍以上内容。

快速排序思想介绍

1
首先介绍一下快排的单趟思想。我们拿上面的数组举个栗子。
我们先要找一个key值(或者叫做标准),一般我都是拿最后一个作为key值,这里注意key值的选取是随意的。然后拿两个指针,一个从头往后找值,找什么值呢?
找比key值大的,另外一个指针从后往前找,找比key值小的,然后将这两个值交换,一直重复这个过程,直到两个指针相遇。
有个点注意就是,这里的指针我用的是数组下标,不是通常意义的指针。

先不管有什么用,我们先来实现一遍。
2
begin从前往后找,找到一个6,比key大。停止。begin变成3。
22

end从后往前找,找比key小的,2小于key,然后停止,end变成6。
23
然后交换end和begin的值。
33
重复上述过程,知道begin和end相遇。
24

最后交换key所在的位置和两指针相遇的位置的值。

78
这就是快排的单趟排序,它实现了什么呢?
细心你就会发现,key值前面的数全部小于等于key,key后面的数大于等于key,
这就意味着在排序中key的位置已经固定,排序的时候不需要再动key。
但这并不是排序,那接下来怎么办呢?
我只需要key值前面的有序,key值后面有序不就ok了吗?
57
但显然这两段不有序。那我玩啥?
我再对这两段进行一次快排,取key值,再分割,就又固定几个数,再不断递归,
总会分割成1个数,而1个数显然有序,然后整个数组就有序,这就是快排的思想。
这种思想类似二叉树的后序遍历,而它的时间复杂度也跟二叉树类似。
接下来,我们实现一下。

Hoare法

显然,hoare是一个人的名字,就是快排发明者。

int HoareSort(int* a, int begin, int end)
{
     int key = a[end];
     int keyindex = end; //记录key值的下标
     while(begin < end)
     {
         while(begin < end && a[begin] <= key)
         {
             ++begin;  //找比key大的
         }
         while(begin < end && a[end] >= key)
         {
             --end;  //找比key小的
         }
         Swap(&a[begin],&a[end]); //交换大小
    }
    Swap(&a[begin],&a[keyindex]); //交换key和指针相遇的位置的值
    return begin;  //返回相遇指针,用于递归。
}
void QuickSort(int* a, int begin, int end)
{
   if(begin >= end) return ; //终止条件
   int keyindex = HoareSort(a,begin,end);
    //分成了[begin keyindex-1] keyindex [keyindex+1 end]三段
    QuickSort(a,begin,keyindex-1); //递归前面一段
    QuickSort(a,keyindex+1,end); //递归后面一段
}

我们把我们的例子测试一哈。
result
结果没得问题。

大家会发现,快排实际上就是为了遍历一次固定一个值,逐个固定一个值的位置,知道所有值的位置固定,就是有序。所以为大家介绍下面两种快排的变形。

挖坑法

一眼看过去,大家可能觉得这个名字比较low,(实际上我也觉得),所以我在google翻译上找了英文翻译。
google
接下来为大家介绍一下这个接地气的方法。
同样的,我们先选择一个key值。我选择了最后一个,(大家可以随便选择)。
此时,key的位置就成为了一个坑。
hole
同样的两个指针,begin找大于key的值,填到坑里,那么自己就又成为一个坑。
111
然后end找一个比key小的数,填到坑里,自己再成为坑。
565
不断重复,知道begin和end相遇。这就是挖坑法的一趟。
然后,同样的,再递归左区间&&右区间即可。

DigMethodSort(int* a, int begin, int end)
{
    int key = a[end];  //挖走key
    while(begin < end)
    {
         while(begin < end && a[begin] <= key)
         {
             ++begin;//找大
         }
         a[end] = a[begin];   //填坑 
         while(begin < end && a[end] >= key)
         {
            --end;   //找小
         } 
         a[begin] = a[end];  //填坑
    }
     a[begin] = key;   //填上key
     
    return begin;
}
void QuickSort(int* a, int begin, int end)
{
    if(begin >= end)
    {
       return ;
    }
    int keyindex = DigMethodSort(a,begin,end);
    QuickSort(a,begin,keyindex-1);
    QuickSort(a,keyindex+1,end);
}

双指针法

这里的指针也是数组下标。
类似的,我先找一个key。我有两个指针,prev和cur,prev再cur前面一个位置,cur去找小,一旦找到,prev++,然后与cur所在的位置交换,找到大数prev就不动。最后prev++,然后和key交换。我们直接来看代码实现。

void PrevCurSort(int* a, int begin, int end)
{
    int key = a[end];
    int prev = begin - 1;
    int cur = begin;
    while(cur < end)
    {
        if(a[cur] < key && ++prev != cur)
        {
            Swap(&a[prev],&a[cur]);
        }
        ++cur;
    }
    ++prev;
    Swap(&a[prev],&a[end]);
    return prev;
}
void QuickSort(int* a, int begin, int end)
{
    if(begin >= end)
    {
       return ;
    }
    int keyindex = PrevCurSort(a,begin,end);
    QuickSort(a,begin,keyindex-1);
    QuickSort(a,keyindex+1,end);
}

快排的非递归实现(需要 Stack 的知识)

上面我们对快排的实现用了递归,但是递归是非常消耗内存,因为递归要开栈帧,而栈的空间往往很小,就很可能引发一个错误----栈溢出
(Stack overflow),所以快排也往往被称为以空间换时间。但我们知道,堆上有很多空间,我们能不能想办法把空间转移到堆上呢?
下面我们用数据结构中的Stack来实现。

仔细想想我们对快排的实现,我们实际上只对一个数组进行了操作,而操作一个数组实际上只需要下标即可,我们能不能通过下标的循环来代替递归呢?

下面是实现的思想。由于栈是先进后出,我先将数组下标的begin和end入栈,然后将这两个数出栈,进行一次快排的单趟排序,然后将begin和keyindex-1入栈,
将keyindex+1 和 end入栈,依次循环,直到栈为空。

QuickSortNoR(int* a, int begin, int end)
{
    int key = a[end];
    Stack st;//来一个栈,反正不要钱
    StackInit(&st);//初始化栈
    StackPush(&st,begin);//begin入栈
    StackPush(&st,end); //end入栈
    while(!StackEmpty(&st))  //栈不为空继续
    {
        int right = StackTop(&st); //拿到区间左端点
        StackPop(&st);
        int left = StackTop(&st); //拿到区间右端点
        StackPop(&st);
        int keyindex = HoareSort(a,left,right);//快排来一趟
       if(left < keyindex-1)  //左区间入栈
       {
           StackPush(&st,left);
           StackPush(&st,keyindex-1);
       }
         if(keyindex+1 < right) //右区间入栈
        {
              StackPush(&st,keyindex+1);
              StackPush(&st,right);              
         }
    }
    StackDestroy(&st);//销毁栈
}

快排的复杂度和稳定性

前面我们也说过快排类似二叉树,我们假设每次运气好,都选的key在数组中间左右,然后第一次固定1个,第二次固定2个,第三次固定4个。。。。。。
这不就是二叉树的高度吗 ?
所以我用logN次就完成了快排,而每次虽然遍历的是各个小区间,但合起来还是一个完整的数组,所以是N,

所以快排的时间复杂度是O(N*logN)

因为每次递归都要建立栈帧,而建立栈帧有O(1)的消耗,而递归的logN次,

所以快排的空间复杂度可以认为是O(logN)

而由于数组中可能出现多个相同的数,key值的选取又是随机的,
所以,

快排是不稳定的。

如何解决快排的一些最坏情况

最后,我们来考虑一下快排的最坏情况。
快排什么什么最坏呢?这应该是很容易想到的,我是非洲酋长,每次选的key都是数组中最大的数,那我每次都只能固定一个数!!!
所以时间复杂度变成了O(N*N)。这不扯淡?那我为什么不用冒泡排序?还稳定。
所以,为了解决一些这个问题,我加个三数取中法,选数组的begin mid end
所在位置的数,取第二大的那个作为key。

int GetMid(int*a, int begin, int end)
{
   int mid = (begin + end) >> 1;
   if(a[begin] < a[end])
   {
      if(a[begin] > a[mid])// mid < begin < end (我简写了,直接下标代替数组的数)
      return begin;  
      else    //  //   begin <= end && begin <= mid
      {
         if(a[end] < a[mid])
         return end;
         else return mid;
      }      
   }
   if(a[begin] >= a[end])
   {
      if(a[begin] < a[mid]) // end <= begin < mid 
      return begin;
      else   // begin > mid begin >= end
      {
         if(a[mid] > a[end])
         return end;
         else return end; 
      }
   }
}

这样的话,快排的一些最坏情况可以解决。
(全文完)

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值