交换排序之冒泡排序&&快速排序

冒泡排序

最简单排序实现
冒泡排序的思想其实很简单,它是一种简单的选择排序,在细节上有很多种优化的方法。
它的基本思想是:两两比较相邻记录的关键字,如果反序则进行交换。
首先先来看一段简单的冒泡排序:

//冒泡排序初级版
void Bubble(int* arr, int sz)
{
    int i, j;
    //从下标为0的第一个元素与后面的每一个元素进行比较
    for (i = 0; i < sz - 1; ++i)    
    {
        for (j = i+1; j < sz - i; ++j)
        {
            if (arr[i]>arr[j])
            {
                swap(arr[i], arr[j]);
            }
        }
     }
}

这个初级版的冒泡排序不算是真正意义上的冒泡排序,它的思路是让每一个数字都和它后面的数字进行比较
这里写图片描述
我们可以看出,当1和0、-1等进行比较了之后,需要进行交换,再与后面的数字进行比较均不需要进行交换,显然这种效率比较低
下面我们来看一个真正的冒泡排序

void Bubble1(int* arr, int sz)
{
    for (int i = 0; i < sz - 1; ++i)
    {
        for (int j = 0; j < sz - i; ++j)
        {
            if (arr[j]>arr[j + 1])
            {
                swap(arr[j], arr[j + 1]);

            }
        }
    }
}

这里写图片描述
我们可以看出,经过一趟冒泡排序之后(假设升序),最大的数一定被排在了最后面,并且不再当前位置的数字也越来越接近正确位置。但是我们试想一下,如果待排序的数字是{0,-1,1,2,3,4,5,6,7,8,9},除了0和-1需要交换之外,其它的数字均不需要交换,尽管不需要交换,但是用上述方法还是需要进行比较,这时我们可以增加标志位来减少不必要的比较。

//添加标志位的冒泡排序 
void Bubble2(int* arr, int sz)
{ 
    int  mark = 1;//用mark来做标记
    for (int i = 0; i < sz - 1&&mark; ++i)//
    {
        mark = 0;
        for (int j = 0; j < sz - i; ++j)
        {
            if (arr[j]>arr[j + 1])
            {
                swap(arr[j], arr[j + 1]);
                mark = 1;
            }
        }
    }
}

代码唯一的改动就是在for循环中增加了对mark的判断经过这样的改进,冒泡排序的性能有了一定的提升,避免了在有序情况下一些无意义的判断。
冒泡排序复杂度的分析
1.最好的情况,即数组有序,可以推断出就是n-1次的比较,时间复杂度为O(n)
2.当最坏的情况,即数组逆序,第一个数需要排n-1次,第二个数需要排n-2次,第3个数需要排n-3次……这样依次类推,最后一个数只需要排一次,加起来共需要排n(n-1)/2次,所以时间复杂度为O(O^2)


快速排序

快速排序的思路:

  • 在数组中随机的选择一个数m,小于m的数放在m的左边,大于m的数放在m的右边,然后再依次调用快速排序
  • 大于或小于m的数值如何放置?在数组的最前面设置一个小于等于区间,初始长度为0
  • 从左向右遍历所有元素,如果当前元素大于划分值,就继续遍历下一个元素,如果当前元素小于等于划分值,就把当前数和小等于区间的下一个数进行交换,然后令小于等于区间向右扩一个位置,在遍历完所有元素,直到最后的数的时候,把最后一个数也就是划分值与小于等于区间的下一个元素进行交换
  • 每一趟排序下来,m左边的值都小于m,m右边的值都大于m,并且m已经被移动到最终排序的数组的位置
  • 对m左右两边的数组分别进行递归的快速排序

快速排序的实现方法:
【递归方法】

1、左右指针法

(1)实现方法
- 选取基准值m,一般可以选择数组最后一个元素的值
- 定义要排序区间两端的位置,分别用left和right来表示
- left从左边第一个数向右找大于key的数,找到后停止
- right从右边第一个数向左找小于key的数,找到后停止
- 将left位置处的数据和right处的数据进行交换
- left继续向右找,right继续向左找,当left和right相遇时,跳出循环将相遇位置的数据和m位置的数据进行交换,一趟快速排序完成,此时m所在的位置就是最终排序完m所在的位置,m所在位置左边数据的值全部小于m的值,m所在位置右边数据的值全部大于m的值
- 递归的对m左右两边的数组进行排序
(2)用图表示如下
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

(3)代码实现:

#include<iostream>
#include<assert.h>
using namespace std;
void QuickSort1(int *arr, int left, int right)
{
    int key = right;//记录选取的随机值,一般选取数组的最后一个元素
    int begin = left;//记录数组刚开始的位置
    int end = right;//记录数组结束位置
    while (left >= right)//递归结束条件
    {
        return;
    }
    while (left < right)
    {
        while (left < right&&arr[left] <= arr[key])
        {
            left++;
        }
        while (left < right&&arr[right] >=arr[key])
        {
            right--;
        }
        if (left < right&&arr[left] != arr[right])
        {
            swap(arr[left], arr[right]);
        }
    }
//一次快速排序完成,交换相交位置的值与key的值
    swap(arr[left], arr[key]);
    QuickSort1(arr, begin, left - 1);
    QuickSort1(arr, left+1, end);
}
void Printf(int arr[], int sz)
{
    assert(arr);
    for (int i = 0; i < sz; i++)
    {
        cout << arr[i]<<"  ";
    }

}
int main()
{
    int arr[] = { 4,5,3,0,1,7,2,6 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    QuickSort1(arr,0,sz-1);
    Printf(arr, sz);
    system("pause");
}

2、挖坑法

(1)实现思路
- 用两个位置来表示排序区间的范围,key值表示初始坑的位置上的数值(最右边的数),hollow记录坑的位置
- 两个位置left和right,从左边向右边找比key大的数,找到后,将left所在的位置的数据填入key坑中,然后将left变为坑
- 从左边向右边找比key小的数,找到后,直接将此值填入left坑中,然后又将right变为坑
- 重复上两个操作步骤,直到left和right相遇,将坑的值填充为key的值,一趟快排完成
- 此时比key大的数全部在key的右边,比key小的数全部在key的左边
- 利用递归的方法将基准值两边的序列进行排序
(2)挖坑法用图表示入下:
这里写图片描述
(3)代码实现如下:

#include<iostream>
#include<assert.h>
using namespace std;
void QuickSort2(int *arr, int left, int right)
{
    assert(arr);
    while (left >= right)
    {
        return;
    }
    int hellow = right;//hollow用来表示坑的位置,坑的初始位置为序列的最后一个位置
    int key = arr[right];//记录key的值
    int begin = left;
    int end = right;
    while (left < right)
    {
        while (left < right&&arr[left] <=key)
        {
            left++;//从左边开始找,找到大于key的值停止
        }
        arr[hellow] = arr[left];
        hellow = left;
        while (left < right&&arr[right] >= key)
        {
            right--;//从右边开始找,找到大于key的值停止
        }
        arr[hellow] = arr[right];
        hellow = right;
    }
    //跳出循环后,左右left与right相遇,将key的值赋给left或者right任意一个即可
    if (left == right)
    {
        arr[left] = key;
    }
    QuickSort2(arr, begin, left - 1);
    QuickSort2(arr, left + 1, end);
}
void Printf(int arr[], int sz)
{
    assert(arr);
    for (int i = 0; i < sz; i++)
    {
        cout << arr[i]<<"  ";
    }

}
int main()
{
    int arr[] = { 4,5,3,0,1,7,2,6};
    int sz = sizeof(arr) / sizeof(arr[0]);
    QuickSort2(arr, 0, sz - 1);
    Printf(arr, sz);
    system("pause");
}

挖坑法的代码优化:

  • 用swap代替赋值;少去最后一步给相遇点赋值key值,提高程序性能
    代码如下:
#include<iostream>
#include<assert.h>
using  namespace std;
void QuickSort3(int *arr, int left, int right)
{
    assert(arr);
    while (left >= right)
    {
        return;
    }
    int hellow = right;//hollow用来表示坑的位置,坑的初始位置为序列的最后一个位置
    int key = arr[right];//记录key的值
    int begin = left;
    int end = right;
    while (left < right)
    {
        while (left < right&&arr[left] <=key)
        {
            left++;//从左边开始找,找到大于key的值停止
        }
        //arr[hellow] = arr[left];
        swap(arr[hellow], arr[left]);
        hellow = left;
        while (left < right&&arr[right] >= key)
        {
            right--;//从右边开始找,找到大于key的值停止
        }
        //arr[hellow] = arr[right];
        swap(arr[hellow], arr[right]);
        hellow = right;
    }
    //跳出循环后,左右left与right相遇,将key的值赋给left或者right任意一个即可
    /*if (left == right)
    {
        arr[left] = key;
    }*/
    QuickSort2(arr, begin, left - 1);
    QuickSort2(arr, left + 1, end);
}
void Printf(int arr[], int sz)
{
    assert(arr);
    for (int i = 0; i < sz; i++)
    {
        cout << arr[i]<<"  ";
    }

}
int main()
{
    int arr[] = {  4,5,3,0,1,7,2,6 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    QuickSort3(arr, 0, sz - 1);
    Printf(arr, sz);
    system("pause");
}

分析赋值挖坑法和交换挖坑法:

  • 其实赋值挖坑法只是每次将找到的大于或者小于的数赋值给了当前的坑,这就势必导致在数组中,第一个被赋值的数,也就是key的值,被前一个数覆盖,在数组中找到不key;也就是说每次赋值完以后,其实就是当前被赋值的坑保存了下一个坑的值,那么最后一个坑的值会被他的前一个坑保存,所以,在left和right相遇的坑即最后一个坑的值会在数组中出现两个,所以要把这个数赋值为key;
  • 那么交换赋值法,是每次把找到的数和坑值交换,这就保证数组 中不会多数也不会少数,每次更新的坑都保存key,最后不用那key赋值给最后的坑;

3、前后指针法

(1)前后指针法的思路

  • 在left到right的闭合区间里,取cur的值为left,prev为cur的前一个值,key的值为right位置对应的值
  • cur从左往右找比key小的值,找到以后停下来,此时prev++,然后将perv处的值与cur处的值进行交换
  • 重复步骤2,直到cur走到最后一个位置,此时,将prev++,然后将perv处的值与cur处的值进行交换,至此一趟快排完成
  • 利用递归将key左右的序列分进行排序

(2)用图表示如下:
这里写图片描述
(3)代码实现如下:

#include<iostream>
#include<assert.h>
using namespace std;
void QuickSort3(int *arr, int left, int right)
{
    assert(arr);
    int begin = left;
    int end = right;
    if (left >= right)
    {
        return;
    }
    int cur = left;
    int prev = left - 1;
    int key = arr[right];
    while (cur != right)
    {
        if (arr[cur] < key&&++prev != cur)
        {
            swap(arr[prev], arr[cur]);//当找到比key值小的数时,先将prev自增,再进行交换
        }
        cur++;
    }
    //出了循环后,cur走到了最后一个位置,将cur于perv进行交换
    swap(arr[++prev], arr[right]);//先将prev的值自增,然后再交换
    QuickSort3(arr, begin, prev - 1);
    QuickSort3(arr, prev+1, end);
}
void Printf(int arr[], int sz)
{
    assert(arr);
    for (int i = 0; i < sz; i++)
    {
        cout << arr[i]<<"  ";
    }

}
int main()
{
    int arr[] = { 6,0,5,1,3,4 };
    int sz = sizeof(arr) / sizeof(arr[0]);
    QuickSort3(arr, 0, sz - 1);
    Printf(arr, sz);
    system("pause");
}

前后指针法分析:

prev和cur两个指针之间有两种状态:

  • 状态一:prev始终紧跟着cur,这种状态的保持条件是cur向右走时没有遇到比key值答的数据;每次遇到一个小的数据就停下来让prev++;和自己处在同一位置;
  • 状态二:prev和cur分离,prev在一个小数据位置原地等待,cur勇敢的朝前取寻找小数据,当在寻找的过程中碰见了比key大的数据,此时cur不会停止,继续向右寻找,而prev始终在当前第一个比key值大的数据的紧前面,直到cur找到比key小的值,此时将prev++;来到区间从左到右第一个大于key值得数据的位置,然后prev位置的值和cur位置的小于key的值交换;让小的值房左边,大的值放右边,交换完毕之后,prev所处位置的值小于key;当cur到达区间的尾端时,将prev++使其来到第一个大于key的值得位置和key值交换;这时一趟前后指针的快排完成;小数据全在prev的右面,大数据全在prev的左边

    【非递归方法】
    利用容器适配器实现快速排序的非递归

快速排序递归方法的优化
【三数取中法】
当选取的m值为最大值或者最小值时,快速排序的时间复杂度为最坏情况O(n^2),三数取中法,可以避免取到的值为最大值或者最小值
每次取待排序区间的左端,右端,中间,这三个数中的中间大小数作为key值;
当我们取得基准值key为要排序区间的最大值或者最小值的时候,这时就是快排的最坏情况,时间复杂度最大位O(N^2);这种情况最明显的例子就是要排序的区间本身就是一个有序区间时,此时该区间的左右两端的值就位最大值和最小值;
那么即便不是有序区间,我们在取key值时,也有可能取到最大或者最小值;
那么如何避免取到的key值为最大值或者最小值呢?方法就是三数区中法,
三数区中法保证了所取得key值不是最大也不是最小,避免了快排最坏情况的出现,当数据有序是,利用三数取中法,可以将原来的这种情况为快排的最坏情况变为快排的最好情况,因为,这时,利用三数取中,每次取到的key值都是该待排序有序区间的中间的数,没趟都不用交换,且递归的次数会少一点;这样时间复杂度将达到最低;

int GetMidIndex(int* arr,int left,int right)
{
     assert(arr);
     int mid=(left+right)/2;
     if (arr[left]>arr[mid])
     {
         if (arr[mid]>arr[right])//arr[left]>arr[mid]>arr[right]
         {
             return mid;
         }
         //arr[mid]<=arr[right]
         else if (arr[left]>arr[right])//arr[left]>arr[right]=>arr[mid]
         {
             return right;
         }
         else//arr[right]>=arr[left]>=arr[mid]
         {
             return left;
         }
     }
     else//arr[left]<=arr[mid]
     {
         if (arr[left]>arr[right])//arr[mid]>=arr[left]>arr[right]
         {
             return left;
         }
         //arr[left]<=arr[right]
         else if(arr[mid]>arr[right])//arr[mid]>arr[right]>arr[left]
         {
             return right;
         }
         //arr[mid]<=arr[right]
         else//arr[right]>=arr[mid]=>arr[left]
         {
             return mid;
         }
     }
}

【小区间优化法】
快排的递归实现,在递归时会进行函数的压栈开销很大,如果对于一堆很多的数据,完全采用递归的快排进行排序,那么递归的越深,出现的小区间越多,函数的压栈开销越大,从而让快排的时间复杂度更高,而且,对于小区间的较少数据的排序,利用快排实际和利用插入排序的时间效率差不多,可能插入排序更高效,所以可以对快排的算法递归到一定的程度的小区间时,不在利用快排的原理进行排序,而是利用插入排序进行排序,这样就会没有大的函数压栈的时间空间开销,让快排整体来看效率更高;

QuickSort(int* arr,int left ,int right)
{
    //由于递归太深会导致栈溢出,效率低,所以,区间比较小时(小于13)采用插入排序。
   if (right-left>13)
   {
       //三种快排的有效函数体
   }
   else
       InsertSort(a+begin,end-begin + 1);//要排的区间小于13,则使用选择排序进行排序;
}

使用两个优化以后的前后指针法快排程序:


#include<iostream>
#include<assert.h>
using namespace std;
//三数取中
int GetMidIndex(int* arr,int left,int right)
{
     assert(arr);
     int mid=(left+right)/2;
     if (arr[left]>arr[mid])
     {
         if (arr[mid]>arr[right])//arr[left]>arr[mid]>arr[right]
         {
             return mid;
         }
         //arr[mid]<=arr[right]
         else if (arr[left]>arr[right])//arr[left]>arr[right]=>arr[mid]
         {
             return right;
         }
         else//arr[right]>=arr[left]>=arr[mid]
         {
             return left;
         }
     }
     else//arr[left]<=arr[mid]
     {
         if (arr[left]>arr[right])//arr[mid]>=arr[left]>arr[right]
         {
             return left;
         }
         //arr[left]<=arr[right]
         else if(arr[mid]>arr[right])//arr[mid]>arr[right]>arr[left]
         {
             return right;
         }
         //arr[mid]<=arr[right]
         else//arr[right]>=arr[mid]=>arr[left]
         {
             return mid;
         }
     }
}
//左右指针法
void QuickSort1(int* arr,int left,int right)
{
   assert(arr);
   if (right-left<13)//小区间优化法
   {    
       int key=GetMidIndex(arr,left,right);//三数取中法,优化key的最坏情况的取值
       int begin=left;
       int end=right;
       while (left>=right)//递归返回条件
       {
           return ;
       }
       while(left<right)
       {
           while (left<right&&arr[left]<=arr[key])//从左边开始找到比基准值大的数停下来
           {
                ++left;
           }
           while (right>left&&arr[right]>=arr[key])//从右边开始找到比基准值小的数停下来
           {
               --right;
           }
           if (left<right&&arr[left]!=arr[right])//如果左右指针都停下来了,并且不是同一个值,自身和自身交换,没意义,降低效率
           {
               swap(arr[left],arr[right]);
           }
       }
       //一趟排序完成,此时left=right,交换相遇点位置的数据和key位置的数据
       swap(arr[left],arr[key]);
       //递归地把小于基准值元素的子数列和大于基准值元素的子数列排序
       QuickSort1(arr,begin,left-1);
       QuickSort1(arr,left+1,end);
   }
   else
         InsertSort(a+begin,end-begin + 1);
}

【STL中sort()的底层实现就是使用了三数取中法和小区间优化法来实现的】
快速排序的非递归实现


#include<iostream>
#include<stack>
#include<assert.h>
using namespace  std;
int QuickSort(int* arr,int left,int right)
{
    int key=right;
    while(left<right)
    {
        while (left<right&& arr[left]<=arr[key])
        {
            ++left;
        }
        while(right>left&&arr[right]>=arr[key])
        {
            --right;
        }
        if (left<right&&arr[left]!=arr[right])
        {
            swap(arr[left],arr[right]);
        }
    }
    if (left==right&&arr[left]!=arr[key])
    {
        swap(arr[left],arr[key]);
    }
    return left;
}
//**************************核心代码*********************************
void NoRecursiveQuickSort(int* arr,int left,int right)
{
    assert(arr);
    stack<int>  s;
    s.push(left);//先压入区间的左边
    s.push(right);//再压入区间的右边
    while(!s.empty())
    {
       int end=s.top();
       s.pop();
       int begin=s.top();
       s.pop();
       int div=QuickSort(arr,begin,end);
       if (begin<div-1)
       {
           s.push(begin);
           s.push(div-1);
       }
       if (div+1<end)
       {
           s.push(div+1);
           s.push(end);
       }
    }
}
//**************************核心代码*********************************
void Rrintf(int* arr,int left,int right)
{
    assert(arr);
    while (left<=right)
    {
        cout<<arr[left++]<<" ";
    }
}
int main()
{
    int a[]={5,2,7,3,4,1,6,9,8};
    int sz=sizeof(a)/sizeof(a[0]);
    NoRecursiveQuickSort(a,0,sz-1);
    Rrintf(a,0,sz-1);
    return 0;
}

快速排序的时间复杂度分析

  • 一般情况(包含最好情况):n(logn)
  • 最坏情况:O(n^2)
  • 7
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值