常见的基于比较的排序算法

Posted 玄鸟轩墨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常见的基于比较的排序算法相关的知识,希望对你有一定的参考价值。

写在前面

我们生活中是离不开排序的,比如一场考试下来,我们需要根据成绩进行排名,有例如去银行取钱,我们需要取号,然后等待.这些都是排序在我们生活中的应用.数据结构的初阶部分快分享完了,今天把排序的分享完后,后面还有一个哈希表。希望大家一直坚持。今天我们说的是基于比较的常见的排序算法。在我们开始正式内容之前我想说一下,排序最重要的是思想,不是代码。我们可以把代码背下来,可是要是不理解原理,照样还是不懂,下次遇到排序的时候,你的脑子可能会欺骗你.

排序

关于什么是排序我们就不用说了吧,简单的理解就是对数字排成升序或者降序。关于排序的实际应用,我们现实生活中有很多例子。例如当我们用手机购物时,可能会选择销量优先或者是价格优先,这些都是排序的实际应用。排序的算法有很多,这里我就谈谈几种常用的。

排序优劣主要从三个方面来分析

  • 时间复杂度
  • 空间复杂度
  • 稳定性

稳定性

稳定性也是衡量排序算法一个很重要的指标,两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。 有人可能会感觉到疑惑,既然值相同了,改变相对位置又会怎么样呢?在大多数场景中, 值相同的元素谁先谁后是无所谓的。 但是在某些场景下, 值相同的元素必须保持原有的顺序 .后面我们会发现,我们排序的可不仅仅是基本类型,大多都是自定义排序.

这里也有个可以简便的判断排序是不是稳定的方法,要是我们跳着数字交换,就一定是不稳定的

排序的分类

排序的算法五花八门,我们只需要见识一下最常见的排序算法就可以了.我个人认为排序分为比较排序和不比较排序,有人可能会疑惑还有不比较就可以排序的吗?开什么玩笑!实际上是存在的,下面的基数排序就是其中的一中。至于我们今天所谈的都是基于比较的排序算法。

  • 基于比较的排序
  • 不基于比较的排序

常见的基于比较的排序

我们要谈的算法有7种,下面我先给出名字和种类,后面会通过代码一一实现。我每一个都会附加一个动图,不用担心自己会模糊。

直接插入排序

前面的几种排序都很简单,所谓的插入排序就是我们选定一个数字,判断这个数字前面的数字是不是比这个数字大,要是大,大的数字往后移动一个单位。把待排序的记录按其关键码值的大小逐个插入到一个已经排好了的有序序列中,当我们完成所有的记录插入完为止,得到一个新的有序序列 。这就是插入排序。

我们要做的就是从有序区间找到一个比待排序数字(没有更好)大的数字,然后对他们依次进行排序,直到无序区间变得没有

代码

我们分为单趟排序和完整代码来写,直到我们可以写出正确的

int j = i - 1;
int ret = arr[i];
while (j >= 0)

    if (arr[j] > ret)
    
        arr[j + 1] = arr[j];
    
    else
    
        break;
    
    j--;


//跳出的两种情况都适合
arr[j + 1] = ret;
//代码  一
void interSort(int* arr,int len)

    assert(arr);
    for (int i = 1; i < len; i++)
    
        int j = i - 1;
        int ret = arr[i];
        while (j >= 0)
        
            if (arr[j] > ret)
            
                arr[j + 1] = arr[j];
            
            else
            
                break;
            
            j--;
        
        arr[j + 1] = ret;
    


//代码 二
void interSort(int* arr, int len)

    assert(arr);
    for (int i = 0; i < len - 1; i++)
    
        int end = i;;
        int ret = arr[end + 1];
        while (end >= 0)
        
            if (arr[end] > ret)
            
                arr[end + 1] = arr[end];
            
            else
            
                break;
            
            end--;
        
        arr[end + 1] = ret;
    

性能分析

我们来分析一下直接插入排序的性能.

  • 最好的情况: 数组一开始就是有序(和我们要的有序一样)的,我们就是遍历了一遍数组 时间复杂度O(N)
  • 最坏的情况: 数组是逆序的,我们每个到一个ret,都要进行遍历有序区间 O = 0 + 1+ 2+...+ len-1 ;时间复杂度O(N==^2^==)

这个不用分析,O(1)

我们要是判断arr[end] > ret加上等号 就是不稳定的,不加就是稳定的,所以直接插入排序是一个稳定的排序(稳定的排序可以实现为不稳定,反之则不能).

总结

代码优化

一般了解基础的就可以了,不过有时我们就是害怕面试官让你当场优化这个算法,我这里也谈谈吧.我这里给一个折半插入的优化方案

折半插入

折半插入就是在有序区间内寻找比ret大的数字,和折半查找差不多.只要找到比ret大的有序区间,就要可以进行插入排序了

int halfInsert(int* arr, int keyi)

    // 返回 的是最后一个不比  ret 大的下标
    assert(arr);

    int left = 0;
    int right = keyi-1;

    int ret = arr[keyi];
    while (left <= right)
    
        int mid = (left + right) >> 1;

        if (arr[mid] <= ret)
        
            left = mid + 1;
            if (arr[left] > ret)
            
                return mid;
            
        
        else
        
            right = mid - 1;
        
    
    return right;


void halfInsertSort(int* arr, int len)

    assert(arr);
    for (int i = 1; i < len; i++)
    
        int ret = arr[i];
        int left = halfInsert(arr, i) +1;
        int right = i - 1;
        while (left <= right)
        
            arr[right + 1] = arr[right];
            right--;
        
        arr[left] = ret;
    
#define CAP 100000
int main()

    srand((unsigned)time(NULL));
    int arr1[CAP] =  0 ;
    int arr2[CAP] =  0 ;

    for (int i = 0; i < CAP; i++)
    
        int n = rand() % CAP + 1;
        arr1[i] = n;
        arr2[i] = n;
    
    int begin1 = clock();
    interSort(arr1, CAP);
    int end1 = clock();
    printf("直接插入排序:%d \\n", end1 - begin1);

    int begin2 = clock();
    halfInsertSort(arr2, CAP);
    int end2 = clock();
    printf("优化 :%d \\n", end2 - begin2);

    system("pause");
    return 0;


希尔排序

希尔排序法又称缩小增量法。前面我们说了直接插入排序,好的时候性能可以达到O(N),不好的时候时间复杂度就是O(N==^2^==),这也太不稳定了吧,即使是上面的折半插入也是治标不治本,我们希望有一个更好的排序算法.一个叫做希尔的大佬提出了一种解决方法.又可以进一步优化这个算法.

希尔排序

既然插入排序在接近有序的时候时间复杂的接近O(N),我们是不是可以把这个数组进行分组,每组先进行插入排序,最后在进行整体的插入排序.这不是一个很好的方法吗.是的,我们这个想法就跟希尔大佬是一样的.

如何分组

我知道,我知道,不要拦我,让我说,不就是分组吗,很简单,给我一堆数字,说吧,分成几组,我都给你分出来.

然而希尔大佬的思想不是我们这些普通人可以想到的,这才是真正的厉害,通过这种分法,大的数字可以尽快的跑到后面,小的数字可以跑到前面,

代码

既然我们已经知道如何分组了,下面就可以写出单趟排序的代码了。当gap等于 1时,和我们插入排序是一样的

for (int i = gap; i < len; i += gap)

    int ret = arr[i];
    int j = i - gap;
    while (j >= 0)
    
        if (arr[j] > ret)
        
            arr[j + gap] = arr[j];
        
        else
        
            break;
        
        j -= gap;
    
    arr[j + gap] = ret;
// 代码 一 
void shell(int* arr, int len, int gap)

    assert(arr);
    for (int k = 0; k < gap; k++)
    
        for (int i = gap + k; i < len; i += gap)
        
            int ret = arr[i];
            int j = i - gap;
            while (j >= 0)
            
                if (arr[j] > ret)
                
                    arr[j + gap] = arr[j];
                
                else
                
                    break;
                
                j -= gap;
            
            arr[j + gap] = ret;
        
    

void shellSort(int* arr, int len)

    assert(arr);

    int gap = len;
    while (gap > 1)
    
        gap = gap / 3 + 1;
        shell(arr, len, gap);
    


//代码  二
//这个也算是一个优化把,不过只是形式变化了,性能没有提高
void shell(int* arr, int len, int gap)

    assert(arr);
    for (int i = gap ; i < len; i ++)
    
        int ret = arr[i];
        int j = i - gap;
        while (j >= 0)
        
            if (arr[j] > ret)
            
                arr[j + gap] = arr[j];
            
            else
            
                break;
            
            j -= gap;
        
        arr[j + gap] = ret;
    

void shellSort(int* arr, int len)

    assert(arr);

    int gap = len;
    while (gap > 1)
    
        gap = gap / 3 + 1;
        shell(arr, len, gap);
    

性能分析

空间复杂度是O(1)

希尔排序的时间复杂度很难计算,我们也不知道gap等于多少的时候代码的性能最高,gap为多少在数学上也是一个未被解决的问题.当gap很大时,数值较大的数据往后移动的很快,当gap小的时候,数据移动数量有些多,这很矛盾,所以我们可以大概的计算到当gap = len/3的时候,速度可能快点,希尔排序的时间复杂度是O(N==^1.3^==) ~ O(N==^1.5^==).

希尔排序是一个不稳定的排序,数据发生了跨格移动


选择排序

选择排序是我们所有排序当中最挫的,它的思想也很简单,从数组中拿出一个数,和它之后的数据进行比较,要是比他小,就交换,直到比较时遍历整个数组.

  • 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素

代码

for (int j = i + 1; j < len; j++)

    if (arr[j] < arr[i])
    
        int ret = arr[j];
        arr[j] = arr[i];
        arr[i] = ret;
    
void selectSort(int* arr, int len)

    for (int i = 0; i < len-1; i++)
    
        for (int j = i + 1; j < len; j++)
        
            if (arr[j] < arr[i])
            
                int ret = arr[j];
                arr[j] = arr[i];
                arr[i] = ret;
            
        
    

性能分析

  • 时间复杂度 最好最坏都是O(N^2^)
  • 空间复杂度 O(1)
  • 稳定性 不稳定

双向选择排序

我们优化了一下代码,使用双向选择的方法可以帮助提高性能.我们是不是可以从两边选择,最大的给右边,最小的给左边.具体的我就不说了.大家看看代码了解一下就可以了.

void selectSort(int* arr, int len)

    int left = 0;
    int right = len - 1;
    while (left < right)
    
        int mini = left;
        int maxi = left;
        for (int i = left; i <= right; i++)
        
            if (arr[i] < arr[mini])
            
                mini = i;
            
            if (arr[i] > arr[maxi])
            
                maxi = i;
            
        

        swap(&arr[left], &arr[mini]);
        //防止被掉包
        if (left == maxi)
        
            maxi = mini;
        

        swap(&arr[right], &arr[maxi]);

        left++;
        right--;
    
#define CAP 10000
int main()

    srand((unsigned)time(NULL));
    int* arr1 = (int*)malloc(sizeof(4) * CAP);
    int* arr2 = (int*)malloc(sizeof(4) * CAP);
    assert(arr1 && arr2);
    for (int i = 0; i < CAP; i++)
    
        int n = rand()% CAP + 1;
        arr1[i] = n;
        arr2[i] = n;
    

    int begin1 = clock();
    selectSort1(arr1, CAP);
    int end1 = clock();

    int begin2 = clock();
    selectSort(arr2, CAP);
    int end2 = clock();
    printf("没有优化 %d\\n", end1 - begin1);
    printf("优化    %d\\n", end2 - begin2);
    free(arr1);
    free(arr2);
    system("pause");
    return 0;


堆排序

我们说一说堆排序,给定一个数组,我们如何把它排成一个升序。之前我们可以借助冒泡排序,今天谈一个新的排序方式,堆排序。在说这个之前,我们先看看堆的一些性质

  1. 小堆的堆定 是所有元素中最小的
  2. 大堆的堆定 是所有元素中最大的

排成升序的话我们需要建立大堆,前面我们说了一个一个元素建堆,现在谈谈如何使用数组建堆。我们通过两种方法建堆

使用向上调整对数组进行建堆

我来解释解释下面的代码

i 可以从 0 开始,但是对于一个元素建堆,他本来就可以是一个堆,所以不用在建了,

为何是 i+1,这个看个人对向上调整函数参数的定义,我的是传入的元素的个数,所以要i+1,比如当i = 1时,对前两个元素进行建堆

void HeapSort(int* arr, int len)

    assert(arr);
    for (int i = 1; i < len; i++)
    
        adjustUp(arr, i+1);
    

完整的代码

void HeapSort(int* arr, int len)

    assert(arr);
    //建堆
    for (int i = 1; i < len; i++)
    
        adjustUp(arr, i+1);
    

    //堆排序
    for (int i = len; i > 0;)
    
        //交换
        int ret = arr[0];
        arr[0] = arr[i - 1];
        arr[i - 1] = ret;
        i--;

        adjustDown(arr, i, 0);
    

使用向下调整对数组进行建堆

前面使用的向上调整建堆,这里使用向下调整,

void HeapSort(int* arr,int len)

    assert(arr);
    for (int parent = (len - 1 - 1) / 2; parent >= 0; parent--)
    
        //叶子不需要 调
        adjustDown(arr, len, parent);
    
void HeapSort(int* arr,int len)

    assert(arr);
    for (int parent = (len - 1 - 1) / 2; parent >= 0; parent--)
    
        //叶子不需要 调
        adjustDown(arr, len, parent);
    
    //堆排序
    for (int i = len; i > 0;)
    
        //交换
        int ret = arr[0];
        arr[0] = arr[i - 1];
        arr[i - 1] = ret;
        i--;
        adjustDown(arr, i, 0);
    

性能分析

  • 空间复杂度 O(1)
  • 时间复杂度 O(NlogN)
  • 稳定性 不稳定

冒泡排序

冒泡排序是我们最早接触的排序算法,也是一个很简单的排序算法。我们应该都见识过这种场景,如果水底出现一个水泡,水泡在上升的时候,水泡会逐渐变大,对于许多水泡来说,整个过程仿佛就是一个从小水泡到大水泡的场景.冒泡排序也是如此,我们通过每一次的比较,都使的最大的值跑到数组尾部.冒泡排序很简单,我们这里就不具体展开说了,唯一要注意的就是边界问题.下面的代码我简单的优化了一下.

void bubbleSort(int* arr, int len)

    assert(arr);
    for (int i = 0; i < len - 1; i++)
    
        int flag = 0;
        for (int j = 0; j < len - 1 - i; j++)
        
            if (arr[j] > arr[j + 1])
            
                int ret = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = ret;
                flag = 1;
            

        
        if (0 == flag)
        
            break;
        
    

性能分析

  • 空间复杂度 O(1)
  • 时间复杂度 O(N==^2^==)
  • 稳定性 稳定,这一点是要注意的.

优化

但是这种优化的效果也是不太好的,不过也算是一种优化,书上还有一个更好的优化,鸡尾酒排序,它是基于冒泡的一种升级版,这里篇幅有限,我们就不具体说了。

void bubbleSortIndex(int* arr, int len)

    assert(arr);
    int sortBoundary = len - 1;
    for (int i = 0; i < len - 1; i++)
    
        int flag = 0;
        int lastExchangeIndex = 0;
        for (int j = 0; j < sortBoundary; j++)
        

            if (arr[j] > arr[j + 1])
            
                int ret = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = ret;
                flag = 1;
                lastExchangeIndex = j;
            
        

        sortBoundary = lastExchangeIndex;

        if (0 == flag)
        
            break;
        
    


快速排序

总算谈到快排了,我们在C语言中就是用过qsort,它就是快排.既然敢称快速排序,那么一定有自己额优点.

单趟排序

在谈快排之前,我们必须要理解如何进行一趟排序,这是太重要了。

hoare版本

快排的提出人hoare给了这么一个单趟排序思想

我们在数组的左边或者是右边找出一个key值,遍历整个数组,是的key的左边都不必key大,右边都不必key小.这里需要解决一个问题

它们碰在一起有两种情况

  1. left碰到right right本来就是停到比key小的的地方
  2. right碰到left left虽然是停到bikey大的地方,但是前一轮已经被替换了.所以还是比key小

//选左做 keyi
int PartSort1(int* arr, int left, int right)

    assert(arr);

    int keyi = left;
    while (left < right)
    
        while (arr[right] > arr[keyi])
        
            right--;
        
        while (left < right && arr[left] <= arr[keyi])
        
            left++;
        
        swap(&arr[left], &arr[right]);
    
    swap(&arr[keyi], &arr[left]);
    return left;


//选右做 keyi

int PartSort1(int* arr, int left, int right)

    assert(arr);
    int keyi = right;
    while (left < right)
    
        while (left < right && arr[left] <= arr[keyi])
        
            left++;
        
        while (left < right && arr[right] >= arr[keyi])
        
            right--;
        
        swap(&arr[left], &arr[right]);
    
    swap(&arr[left], &arr[keyi]);
    return left;

挖坑法

前面的方法有点不太好理解,这里有人优化了一下,虽然没有提高效率.但是我们更容易理解.

//选左做 坑
int PartSort2(int* arr, int left, int right)

    assert(arr);
    int ret = arr[left];
    int piti = left;
    while (left < right)
    
        while (arr[right] > ret)
        
            right--;
        
        arr[piti] = arr[right];
        piti = right;
        while (left < right && arr[left] <= ret)
        
            left++;
        
        arr[piti] = arr[left];

        piti = left;
    
    arr[left] = ret;
    return left;


//选右做 坑

int PartSort2(int* arr, int left, int right)

    assert(arr);
    int piti = right;
    int ret = arr[right];
    while (left < right)
    
        while (left < right && arr[left] <= ret)
        
            left++;
        
        arr[piti] = arr[left];
        piti = left;
        while (left < right && arr[right] >= ret)
        
            right--;
        
        arr[piti] = arr[right];
        piti = right;
    
    arr[piti] = ret;
    return piti;

前后指针法

最近又有一种方法,我们来看看吧.使用前后指针的方法可以更加简洁一些.

//选左做 keyi
int PartSort3(int* arr, int left, int right)

    assert(arr);
    int prev = left;
    int keyi = left;
    for (int cur = left + 1; cur <= right; cur++)
    
        if (arr[cur] < arr[keyi] && arr[cur] != arr[++prev])
        
            swap(&arr[prev], &arr[cur]);
        
    
    swap(&arr[keyi], &arr[prev]);
    return prev;


////选右做 keyi

int PartSort3(int* arr, int left, int right)

    assert(arr);
    int keyi = right;
    int prev = left -1;
    for (int cur = left; cur < right; cur++)
    
        if (arr[cur] > arr[keyi] && arr[++prev] != arr[cur])
        
            swap(&arr[prev], &arr[cur]);
        
    
    prev++;
    swap(&arr[keyi], &arr[prev]);
    return prev;

快速排序

上面的三种方法都是单趟排序,得到可以key的左侧和右侧都是很特殊,接着我们通过递归一步步来排成有序,这里我要提一下,使用这种不优化的快排很垃圾,1ow个数,就会导致栈溢出。

void QuickSort(int* arr, int len)

    assert(arr);
    Quick(arr, 0, len-1);


void Quick(int* arr, int left, int right)

    assert(arr);
    //递归结束的条件
    if (left >= right)
    
        return;
    
    int pos = PartSort3(arr, left, right);
    Quick(arr, left, pos - 1);
    Quick(arr, pos + 1, right);

性能分析

最好 NlogN

最坏 N==^2^==

不稳定

优化快速排序

我们会疑惑,上面最好的时间复杂度最好才是NlogN,还好意思称快速排序,好意思吗?但是我们库里面给的是快排,不是希尔排序,这又是什么原因呢?事实上,我们可以优化快速排序,让他的性能大大提高,标准库里卖弄也是优化过的.

选key值优化

我们前面选择key的都是左边的.这就给有序的数据快排有一定的困难.我们主要就选l来优化

  1. 随机选key 适合运气呱呱的来
  2. 三数取中

为了避免排序的数据有序,也就是最坏的情况 N==^2^==,我们选择最左边和最右边以及中间的数据则中间值,把中间值和最左边交换再进行递归

int getMid(int* arr, int left, int right)

    assert(arr);
    int mid = (left + right) >> 1;
    int i = left;
    if (arr[left] < arr[mid])
    
        if (arr[mid] < arr[right])
        
            return mid;
        
        else
        
            if (arr[left] < arr[right])
            
                return left;
            
            else
            
                return right;
            
        
    
    //3 1 2
    else
    
        if (arr[mid] > arr[right])
        
            return mid;
        
        else
        
            if (arr[left] < arr[right])
            
                return left;
            
            else
            
                return right;

            
        

    

小区间优化

在较小的的区间内,我们可以直接选择插入排序,避免往下再次递归了.

if (right - left + 1 < 13)

    InterSort(arr + left, right - left + 1);
    return;

非递归实现快速排序

我们前面使用递归的方法实现了快速排序,这里通过非递归来实现。这样面试的时候用哪个都可以,这里我就不写关于堆的源码了,之前和大家分享过。

void QuickSort(int* arr, int len)

    QuickSortNonR(arr, 0, len - 1);


void QuickSortNonR(int* arr, int left, int right)

    assert(arr);
    Stack stack;
    InitStack(&stack);
    int pivot = PartSort2(arr, left, right);
    if (pivot > left + 1)
    
        push(&stack, left);
        push(&stack, pivot - 1);
    
    if(pivot < right-1)
    
        push(&stack, pivot + 1);
        push(&stack, right);
    

    while (!IsEmpty(&stack))
    
        right = pop(&stack);
        left = pop(&stack);
        pivot = PartSort2(arr, left, right);
        if (pivot > left + 1)
        
            push(&stack, left);
            push(&stack, pivot - 1);
        
        if (pivot < right - 1)
        
            push(&stack, pivot + 1);
            push(&stack, right);
        
    

归并排序

归并排序也是一个很好的排序算法.归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并.

排序两个有序数组

在说这个之前,我们需要对两个有序数组进行排序,这位后面打下基础.

int* sortTwoArray(int* arr1, int len1, int* arr2, int len2, int* returnSize)

    assert(arr1 && arr2);
    *returnSize = len1 + len2;
    int* array = (int*)malloc(sizeof(int) * (len1 + len2));
    assert(array);
    int s1 = 0;
    int e1 = len1-1;

    int s2 = 0;
    int e2 = len2-1;
    int i = 0;
    while (s1 <= e1 && s2 <= e2)
    
        if (arr1[s1] <= arr2[s2])
        
            array[i++] = arr1[s1++];
        
        else
        
            array[i++] = arr2[s2++];
        
    

    while (s1 <= e1)
    
        array[i++] = arr1[s1++];
    
    while (s2 <= e2)
    
        array[i++] = arr2[s2++];
    
    return array;


int main()

    int arr1[] =  1,3;
    int sz1 = sizeof(arr1) / sizeof(arr1[0]);
    int arr2[] =  2,4,6,8,10;
    int sz2 = sizeof(arr2) / sizeof(arr2[0]);
    int count = 0;
    int* array = sortTwoArray(arr1, sz1, arr2, sz2,&count);

    for (int i = 0; i < count; i++)
    
        printf("%d ", array[i]);
    
    return 0;

递归实现归并排序

我们先用递归的方法实现一下,递归的思想就是分治,它额和我们的二叉树的后续遍历很相似,我们再递归的末尾进行两个有序数组的排序,借助一个额外的数组记录排序的结果.最后每次把排序的结果再次给到原数组.

void merge(int* arr, int left, int right,int* ret)

    if (left >= right)
    
        return;
    
    int mid = (left + right) >> 1;
    merge(arr, left, mid,ret);
    merge(arr, mid+1, right,ret);
    int i = left;
    int s1 = left;
    int e1 = mid;
    int s2 = mid + 1;
    int e2 = right;
    while (s1 <= e1 && s2 <= e2)
    
        if (arr[s1] <= arr[s2])
        
            ret[i++] = arr[s1++];
        
        else
        
            ret[i++] = arr[s2++];
        
    

    while (s1 <= e1)
    
        ret[i++] = arr[s1++];
    
    while (s2 <= e2)
    
        ret[i++] = arr[s2++];
    
    memcpy(arr+left, ret+left, sizeof(int) * (right - left + 1));


void mergeSort(int* arr, int len)

    assert(arr);
    int* ret = (int*)malloc(sizeof(int) * len);
    assert(ret);
    merge(arr, 0, len - 1,ret);
    free(ret);

性能分析

  • 时间复杂的 NlogN
  • 空间复杂度 O(N)
  • 稳定性 稳定

非递归实现归并排序

现在我们要用非递归实现归并排序.一般情况下,我们都是用栈来模拟递归,不过归并排序用栈模拟有些困难,不过使用循环就可以模拟实现了,困难的地方在于我们优点不太好控制边界.

void mergeSortNoR(int* arr, int len)

    assert(arr);
    int* ret = (int*)malloc(sizeof(int)*len);
    assert(ret);
    int gap = 1;
    while (gap < len)
    
        for (int i = 0; i < len; i+=2*gap)
        
            int left = i;
            int mid = left + gap - 1;
            //防止数组越界
            if (mid >= len)
            
                mid = len - 1;
            
            int right = mid + gap;
             //防止数组越界
            if (right >= len)
            
                right = len - 1;
            

            int j = left;

            int s1 = left;
            int e1 = mid;

            int s2 = mid+1;
            int e2 = right;

            while (s1 <= e1 && s2 <= e2)
            
                if (arr[s1] <= arr[s2])
                
                    ret[j++] = arr[s1++];
                
                else
                
                    ret[j++] = arr[s2++];
                
            

            while (s1 <= e1)
            
                ret[j++] = arr[s1++];
            
            while (s2 <= e2)
            
                ret[j++] = arr[s2++];
            
        
        //最后再把元素拷贝会原数组
        memcpy(arr, ret , sizeof(int) * len);
        gap *= 2;
    
    free(ret);


int main()

    int arr[] =  3,3,4,5,6,7,3,3,3 ;
    int sz = sizeof(arr) / sizeof(arr[0]);
    mergeSortNoR(arr, sz);
    for (int i = 0; i < sz; i++)
    
        printf("%d ", arr[i]);
    
    return 0;

海量数据的排序问题

在工作中,我们经常会遇到这种问题,我们要排序100G的数据,但是我们的内存只有1G,这怎么办.因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序 .

  1. 先把文件切分成 200 份,每个 512 M
  2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
  3. 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了

排序总结

我们已经看了这么多排序,现在要总结一下

排序 时间复杂度 空间复杂度 稳定性
插入排序 最好 O(N) 最坏 O(N^2^) O(1) 稳定
希尔排序 O(N^1.3^~O(N^1.5^)) O(1) 不稳定
选择排序 O(N^2^) O(1) 不稳定
堆排序 O(NlogN) O(N) 不稳定
冒泡排序 O(N^2^) O(1) 稳定
快速排序 最好 O(NlogN) 最坏 O(N^2^) 最好 O(logN) 最坏 O(N) 不稳定
归并排序 O(n * log(n)) O(N) 稳定

以上是关于常见的基于比较的排序算法的主要内容,如果未能解决你的问题,请参考以下文章

基于比较的七种常见排序算法

基于比较的七种常见排序算法

基于比较的七种常见排序算法

排序算法——冒泡排序

JS常见排序算法

Python|冒泡算法快速入门