2.C++-选择排序、冒泡排序、插入排序、希尔排序、归并排序、快速排序

1.常用排序算法介绍

一个排序算法的好坏需要针对它在某个场景下的时间复杂度和空间复杂度来进行判断、并且排序都需要求其稳定性,比如排序之前a在b前面,且a=b,排序之后也要保持a在b前面、

常用排序算法如下所示:

2.选择排序

首先i等于0,每次i++,并从i轮询到末尾,选择一个最小者与第i个位置进行交换.

比如:

16,8,21,10,49

第1次遍历整个数组,找到最小值8,则与arr[0]进行交换:

8,16,21,10,49

第2次遍历下标1~4的数组,找到10,则与arr[1]进行交换:

8,10 ,21,16,49

第3次遍历下标2~4的数组,找到16,则与arr[2]进行交换:

8,10 , 16, 21,49

第4次遍历下标3~4的数组,找到21,由于交换的位置相等,则保持不变

由于第5次,起始小标等于n-1了.所以已经比较完了.则结束了.

所以代码如下所示:

template <typename T>
void mySwap(T& a, T& b)    // 交换函数
{
    T c(a);
    a = b;
    b = c;
}


template <typename T>
void seleteSort(T arr[], int len, bool ascend = true)      // ascend为ture表示升序
{
    for (int i = 0; i < len -1; ++i) {
        int minIdx = i;
        for (int j = i+1; j < len; ++j) {
            if (ascend ? (arr[minIdx] > arr[j]) : (arr[minIdx] < arr[j])) {
                minIdx = j;
            }
        }
        if (minIdx != i) {
            mySwap(arr[i], arr[minIdx]);
        }
    }
}

测试代码如下所示:

    int arr[8] = {25, 22, 12, 8, 9, 11, 30 ,40};

    bubbleSort(arr, 8);
    cout<<"ascend: "<<endl;
    for (int i = 0; i < 8; ++i) {
        cout<<i<<": "<<arr[i]<<endl;
    }

    bubbleSort(arr, 8, false);
    cout<<"descend: "<<endl;
    for (int i = 0; i < 8; ++i) {
        cout<<i<<": "<<arr[i]<<endl;
    }

打印如下所示:

总结:

选择排序是不稳定的排序算法,例如 [3, 4, 3, 1, 5]这个数组,第一次交换,第一个3和1交换位置,此时原来两个3的相对位置发生了变化,所以无法保证值相等的元素的相对位置不变,

3.冒泡排序

重复len次冒泡,每次冒泡都是(int j=len-1; j>i; j--)从数组末尾往前进行冒泡,如果arr[j-1]>arr[j],则将arr[j]与arr[j-1]交换,让值小的冒泡上去.

代码如下所示:

template < typename T >
static void bubbleSort(T arr[], int len, bool ascend = true)
{
    bool isSwap = true;

    for(int i=0; (i<len) && isSwap; i++)    // 每次冒泡时,则判断上次是否发生过交换,如果交换过,那说明还不是个有序的表
    {
        isSwap = false;     // 每次冒泡的时候复位 “交换标志位”

        for(int j=len-1; j>i; j--)
        {
            if( ascend ? ( arr[j-1] > arr[j] ) : (arr[j-1] < arr[j] ) ) // 如果是升序,arr[j-1] > arr[j],则将arr[j]从末尾冒泡冒上去
            {
                mySwap(arr[j], arr[j-1]);
                isSwap = true;     // “交换标志位” 设置为true
            }
        }
    }
}

PS: 由于mySwap()函数和测试代码已经写过了,后面就不再重复写了。

总结:

冒泡排序是稳定的排序算法,因为每次都是冒泡相邻比较,比如升序,如果后者>=前者,是不会进行交换

4.插入排序

将一个数组分为两个部分,左边是排序好的,右边是未排序的.

然后每次提取一个右边的数据出来,然后轮询左边(排序好的),如果找到合适的位置(前者小于它且后者大于它),则插入进去.如果未找到,则插入在左边排序好的末尾位置.

比如16,8,21,10,49,由于左边默认会有一个数值(一个数值无需判断比较),所以无需判断arr[0]:

16  |  8,21,10,49  ( |左边是排序好的,右边是等待排序的 )

第1次,获取arr[1],由于8小于16,插入在16前面,所以插入后:

8,16  |  21,10,49

第2次,获取arr[2],向前轮询,由于21>16,所以停止向前遍历, 并且位置没改变过,所以无需操作:

8,16,21  |  10,49

第3次,获取arr[3], 向前轮询,由于10<21,则21向后挪位,再继续向前轮询,由于10<16,则16向后挪位,最后10>=8,则插入在8后面:

8, 10,16,21  |  49

第4次,获取arr[4],由于49>21,所以停止向前遍历, 并且位置没改变过,所以无需操作最终为:

8, 10,16,21 ,49

代码如下所示:

template < typename T >
static void insertSort(T arr[], int len, bool ascend = true)
{
    for(int i=1; i<len; i++) // 循环取出右边未排序的
    {
        int k = i;
        T temp = arr[i];		// 把值取出来,后面会进行挪位操作,会覆盖掉arr[i]原来的内容

        // 如果ascend为true,则表示升序,那么当后者大于前者时,则停止for轮询,因为左边是有序序列
        for(int j=i-1; (j>=0) && (ascend ? (arr[j]>temp) : (arr[j]<temp)); j--)    
        {
            arr[j+1] = arr[j];   // 进行先后挪位操作
            k = j;				 // 更新要插入的位置
        }

        if( k != i )			// 如果未进行挪位,那么k==i,则不需要重复插入
        {
            arr[k] = temp;
        }
    }
}

总结:

插入排序是稳定的排序算法,例如 [4,3, 3, 1, 5]这个数组,由于左边的3是提前插入的[ 3, 4 3, 1, 5],而后续的3由于>=前面的3,所以插入在后面

5.希尔排序

希尔排序是插入排序的升级版.并且比插入排序快.

希尔排序则是将一个插入排序,比如初始步长间隔gap为3,那么将会分成3个小组进行插入排序,每个小组插入排序好后,再次将步长间隔gap较少一部分再次进行插入排序,直到最后步长偏移为1后,进行整个插入排序.一般gap步长都是以 len/3+1来计算.以后每次以 gap/3+1来缩小。

PS: 也可以设置gap固定增量从 n/2 开始,以后每次缩小到原来的一半。

比如下图所示:

代码如下所示:

template < typename T >
static void shellSort(T arr[], int len, bool ascend = true)
{
    int gap = len;

    while (gap > 1) {

        gap = gap / 3 + 1;      // 加一能够保证gap最后一次为1.


        for(int offset = 0 ; offset < gap; ++offset) {      // 对每组进行插入排序

            for(int i=gap-offset; i<len; i+=gap) // 循环取出右边未排序的
            {
                int k = i;
                T temp = arr[i];		// 把值取出来,后面会进行挪位操作,会覆盖掉arr[i]原来的内容
                // 假如ascend为true,则表示升序,那么当后者大于前者时,则停止for轮询
                for(int j=i-gap; (j>=0) && (ascend ? (arr[j]>temp) : (arr[j]<temp)); j-=gap)     
                {
                    arr[j+gap] = arr[j];   // 进行先后挪位操作
                    k = j;				 // 更新要插入的位置
                }

                if( k != i )			// 如果未进行挪位,那么k==i,则不需要重复插入
                {
                    arr[k] = temp;
                }
            }

        }
    }
}

总结:

希尔排序是不稳定的排序算法,因为希尔排序通过分组方法,可能会将最后的值跳过中间重复的值排序到前面

6.归并排序

归并是将两个或多个有序序列合并成一个新的有序序列。归并方法有多种,一次对两个有序记录序列进行归并,称为二路归并排序,也有三路归并排序多路归并排序。本代码是二路归并排序.

思路如下所示:

通过递归的方式将一个无序序列,对半拆解、直到不能拆解时,则进行两路比较排序(归并)到一个辅助空间数组中,归并完成后则将辅助数组中内容拷贝回去,返回上一级递归再进行两路比较排序(归并)和拷贝.直到最后则整个序列归并完成.

如下图所示(红色数字表示递归拆解和归并的先后顺序):

 代码如下所示:

template < typename T >
static void merge(T arr[], int l, int mid, int r, bool ascend = true)    // 进行比较归并
{
    int n = r - l + 1;
    T* helper = new T[n];          // 创建一个辅助空间,用来临时存储

    int i = 0;
    int left = l;
    int right = mid+1;
    while (left <= mid && right <= r) {
        // 如果ascend = true, 则判断左边如果大于右边的,则存放右边的
        if (ascend ? arr[left] > arr[right] : arr[left] < arr[right])
            helper[i++] = arr[right++];
        else
            helper[i++] = arr[left++];
    }

    // 将某一侧剩余的存放进去
    while (left <= mid)
         helper[i++] = arr[left++];

    while (right <= r)
         helper[i++] = arr[right++];


    // 最后将排序好的替换到原数组
    for (i = 0; i < n; ++i) {
        arr[l + i] = helper[i];
    }

    delete [] helper;

}



template < typename T >
static void mergeSort(T arr[], int l, int r, bool ascend = true)    // 递归拆解
{
    if (l >= r)      // 不能再进行拆解了,直接退出
        return;

    int mid = (l+r)/2;

    mergeSort(arr, l, mid, ascend);
    mergeSort(arr, mid+1, r, ascend);

    merge(arr, l, mid, r, ascend);


}


template < typename T >
static void mergeSort(T arr[], int len, bool ascend = true)
{
    mergeSort(arr, 0, len-1, ascend);
}

总结:

由于归并排序需要使用一个辅助空间,所以空间复杂度为O(n),由于都是附近的两两比较,所以是个稳定的算法.

7.快速排序

快速排序是冒泡排序的一种改进,主要的算法思想是在待排序的 n 个数据中取第一个数据作为Pivot基准值,然后采用二分的思想,把大于Pivot的数据放在右边,把小于Pivot的数据放在左边最后确定Pivot基准值的中间位置.然后再次递归进行左右分区确定基准值,直到左右两边都是一个数值时,则完成排序.

如下图所示:

步骤如下:

1)  设置两个变量l、r,排序开始的时候:l=0,r=n-1;
2)第一个数组值作为Pivot基准值,pivot=arr[0];
3)然后r-- ,向左搜索,找到小于pivot后,因为arr[l]的值保存在pivot中,所以直接赋值,s[l]=s[r]
4)然后l++,向右搜索,找到大于pivot后,因为arr[r]的值保存在第2步的s[l]中,所以直接赋值,s[r]=s[l],然后r--,避免死循环
5)重复第3、4步,直到l=r,最后将pivot值放在arr[l]中(确定了位置)
6)  然后采用“二分”的思想,以i为分界线,拆分成两个数组 s[0,l-1]、s[l+1,n-1]又开始排序

代码如下所示:

template < typename T >
static void quickSort(T arr[], int left, int right,  bool ascend = true)
{
    if (left>=right)
        return;

    int l =left, r = right;

    T pivot = arr[l];           // 默认将最左边的设置为基值
    while(l<=r) {

        while(r > l && (ascend ? arr[r] >= pivot : arr[r] <= pivot)) r--;

        arr[l] = arr[r];       // 由于arr[l]已经存放在pivot了,所以直接替换

        while(l < r && (ascend ? arr[l] <= pivot: arr[r] >= pivot)) l++;

        arr[r--] = arr[l];     // 由于arr[r]之前的值已经存在了左边,所以直接替换

    }
    arr[l]=pivot;             // 最后找到基值的位置
    quickSort(arr, left, l-1, ascend);
    quickSort(arr, l+1, right, ascend);

}

template < typename T >
static void quickSort(T arr[], int len, bool ascend = true)
{
    quickSort(arr, 0, len-1, ascend);

}

总结:

由于快速排序是判断Pivot基准值的排序,可能会将后者的某个相等的数放在最前面, 是个不稳定的算法.

快速排序比归并排序总结

由于归并排序涉及内存读写、所以快速排序比归并排序快、但是快速排序是个不稳定的算法、但是如果在局部大部分有序的情况下,还是使用归并排序快

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>