总结:排序算法

一、直接插入排序

基本思想:

     把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含1个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,将它插入到有序表中的适当位置,使之成为新的有序表,重复n-1次可完成排序过程.

图解:

这里写图片描述

代码实现:

void InsertSort(int *a, int size)
{
    assert(a);
    for (int i = 0; i < size - 1; i++)
    {
        int end = i; //有序表最后一个下标
        int key = a[end + 1]; //待插入元素
        while (end >= 0 && key < a[end]) //查找合适位置
        {
            a[end + 1] = a[end];
            end--;
        }
        a[end + 1] = key; //插入
    }
}
分析:

时间复杂度:

  • 最好情况:
    如果待排序的元素本身有序,那么在进行插入排序时,每一个元素直接在前面有序表末尾处进行插入,整个过程下来,时间复杂度为O(N)
  • 最坏情况:
    如果待排序的元素无序,那么在进行插入排序时,每一个元素都需要在前面的有序表中找到其合适的插入位置,整个过程下来,时间复杂度为O(N^2)
  • 平均情况:O(N^2)

空间复杂度:O(1)
稳定性:稳定
说明:设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面,在排序之后,a[i]仍然在a[j]前面,则这个排序算法是稳定的。

二、希尔排序

基本思想:

(1)预排序:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成),分别进行直接插入排序,然后依次缩减增量再进行排序,使整个序列接近有序
(2)当整个序列中的元素基本有序时,再对全体元素进行一次直接插入排序
     因为直接插入排序在元素基本有序的情况下效率是很高的,因此希尔排序在时间效率上相对于直接插入排序有较大提高。

图解:

这里写图片描述

代码实现:

void ShellSort(int *a, int size)
{
    assert(a);
    int gap = size;
    while (gap > 1)
    {
        gap = gap / 3 + 1; //增量(步长)
        for (int i = 0; i < size - gap; i++)
        {
            int end = i; //有序表最后一个下标
            int tmp = a[end + gap]; //待插入元素
            while (end >= 0 && tmp < a[end]) //查找合适位置
            {
                a[end + gap] = a[end];
                end -= gap;
            }
            a[end + gap] = tmp; //插入
        }
    }
}

分析:

时间复杂度:

  • 最好情况:O(N)
  • 最坏情况:O(N^2)
  • 平均情况:O(N^1.3)

空间复杂度:O(1)
稳定性:不稳定
     例如:待排序列3 2 2* 4,当gap为2时进行希尔排序,经过排序后变为2* 2 3 4,此时2和2*之间的相对位置发生变化。

三、直接选择排序

基本思想:

     在元素序列a[i]~a[n-1]中选择关键码最大(最小)的数据元素,将它与这组元素中的最后一个(第一个)元素进行交换,接着在剩余的元素序列a[i]~a[n-2](a[i+1]~-a[n-1])中重复上述步骤,直到剩余1个元素时完成排序。

图解:

这里写图片描述

代码实现:
void SelectSort(int *a, int size)
{
    assert(a);
    int left = 0;
    int right = size - 1;
    while (left < right)
    {
        int min = left;
        int max = left;
        for (int i = left; i <= right; i++)
        {
            if (a[i] < a[min]) //选出最小
            {
                min = i;
            }
            if (a[i] > a[max]) //选出最大
            {
                max = i;
            }
        }
        Swap(&a[left], &a[min]);
        if (max == left) //若max和left相等,则经过上一步交换,导致原max处为最小值,而min处为最大值
        {
            max = min; //更新max,让其位置为最大值
        }
        Swap(&a[right], &a[max]);
        left++;
        right--;
    }
}
分析:

时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
     例如上述图解,排序前25在25*之前,而在排序后25在25*后。

四、堆排序

基本思想:

升序建大堆,降序建小堆
     以升序为例,先将整个序列的元素建造成一个大堆,接着把堆顶元素和当前堆的最后一个元素进行交换,然后堆元素个数减1,接着从根节点通过向下调整使得当前堆恢复到大堆,重复上述过程,直到当前堆的元素个数为1时完成排序。

图解:

这里写图片描述
这里写图片描述

代码实现:
//自顶向下调整
void AdjustDown(int *a, int size, int root)
{
    int parent = root;
    int child = parent * 2 + 1;
    while (child < size)
    {
        int flag = 0;
        if (child + 1 < size)
        {
            if (a[child + 1] > a[child])
            {
                child++;
            }
        }
        if (a[child]>a[parent])
        {
            flag = 1;
            Swap(&a[child], &a[parent]);
        }
        if (flag == 0) //优化
        {
            break;
        }
        parent = child;
        child = parent * 2 + 1;
    }
}

void HeapSort(int *a, int size)
{
    assert(a);
    int i = (size - 2) / 2;
    for (; i >= 0; i--) //先建大堆
    {
        AdjustDown(a, size, i);
    }

    for (i = size - 1; i > 0; i--) //排序
    {
        Swap(&a[0], &a[i]);
        AdjustDown(a, i, 0);
    }   
}
分析:

时间复杂度:O(N*lgN)
空间复杂度:O(1)
稳定性:不稳定

五、冒泡排序

基本思想:

     从元素序列第一个位置开始,进行两两比较,根据大小交换位置,直到最后将最大(最小)的数据元素交换到了当前序列的最后一个位置,成为有序序列的一部分,然后元素个数减1,重复上述过程,直到所有数据元素都排好序。

图解:

这里写图片描述

代码实现:
void BubbleSort(int *a, int size)
{
    assert(a);
    for (int i = 0; i < size - 1; i++)
    {
        int flag = 0;
        for (int j = 0; j < size - i - 1; j++) //一趟排序
        {
            if (a[j]>a[j + 1])
            {
                flag = 1;
                Swap(&a[j], &a[j + 1]);
            }
        }
        if (flag == 0) //如果一趟排序后发现没有一次交换,则说明已经有序
        {
            break;
        }
    }
}
分析:

时间复杂度:

  • 最好情况:O(N)
  • 最坏情况:O(N^2)
  • 平均情况:O(N^2)

空间复杂度:O(1)
稳定性:稳定

六、快速排序

基本思想:

     任取待排列元素序列中的一个元素作为基准值,通过一趟排序将要排序的序列分割成独立的两子序列,其中左子序列的所有元素都比基准值小,右子序列的所有元素都比基准值大,然后左右子序列重复此过程,直到所有元素都排列在相应的位置上为止。

图解:

这里写图片描述

将区间按照基准值划分成左右两部分的方法有:
(1)左右指针法:
     定义两个指针begin和end,将基准值放在最右边,begin从头开始找比基准值大的值(begin++),end从尾开始找比基准值小的值(end–),若都找到且begin小于end,则两者值交换,重复上述过程,直到begin>=end时,将begin所对应的值和最右边的基准值交换,此时整个序列被基准值划分成左右两个子序列。
这里写图片描述

int PartSort1(int *a, int left, int right)
{
    int index = GetMid(a, left, right); //三数取中,选取基准值
    Swap(&a[index], &a[right]); //将基准值和最右边的值交换
    int key = a[right];
    int begin = left;
    int end = right;
    while (begin < end)
    {
        //begin选大
        while (begin < end && a[begin] <= key) //"="可省略
        {
            begin++;
        }
        //end选小
        while (begin < end && a[end] >= key) //"="不可省略
        {
            end--;
        }
        if (begin < end)
        {
            Swap(&a[begin], &a[end]);
        }
    }
    Swap(&a[begin], &a[right]);
    return begin;
}

(2)挖坑法:
     定义两个指针begin和end,将基准值放在最右边并保存该值,此时该位置可视为一个坑。begin从头开始找比基准值大的值,找到后将begin所对应的值填入到刚才的坑中,此时begin这个位置成为新的坑;begin不动,接着end从尾开始找比基准值小的值,找到后将end所对应的值填入到刚才的新坑中,此时end这个位置成为新的坑;end不动,begin从上次的位置接着往后找,重复上述过程,直到begin>=end时,将保存的基准值填入到begin所对应的坑中,此时整个序列被基准值划分成左右两个子序列。
这里写图片描述

int PartSort2(int *a, int left, int right)
{
    int index = GetMid(a, left, right); //三数取中,选取基准值
    Swap(&a[index], &a[right]); //将基准值和最右边的值交换
    int key = a[right]; //保存基准值
    int begin = left;
    int end = right;
    while (begin < end)
    {
        //begin选大
        while (begin < end && a[begin] <= key)
        {
            begin++;
        }
        a[end] = a[begin];
        //end选小
        while (begin < end && a[end] >= key)
        {
            end--;
        }
        a[begin] = a[end];
    }
    a[begin] = key;
    return begin;
}

(3)前后指针法:
     定义两个指针prev和cur,将基准值放在最右边,prev初始位置在left-1处,cur初始位置在left处。cur从头开始找比基准值小的值,找到后若此时++prev的位置和cur的位置不在同一处(说明++prev对应的值一定比基准值大),则交换这两处的值,cur接着刚才的位置往后找,重复上述过程,直到cur>=right时,将最右边的基准值和++prev所对应的值进行交换,此时整个序列被基准值划分成左右两个子序列。
这里写图片描述

int PartSort3(int *a, int left, int right) 
{
    int index = GetMid(a, left, right); //三数取中,选取基准值
    Swap(&a[index], &a[right]); //将基准值和最右边的值交换
    int prev = left - 1;
    int cur = left;
    while (cur < right)
    {
        if (a[cur] < a[right] && ++prev != cur)
        {
            Swap(&a[cur], &a[prev]);
        }
        cur++;
    }
    Swap(&a[++prev], &a[right]);
    return prev;
}

三数取中法:

     在选择基准值时,为了提高排序效率,我们常常利用三数取中法来选择基准值,所谓的三数指的是:序列最左端的值、中间位置的值和最右端的值,计算它们的中位数来作为基准值。

int GetMid(int *a, int left, int right)
{
    int mid = (left + right) >> 1;
    if (a[left] < a[right])
    {
        if (a[mid] < a[left])
        {
            return left;
        }
        else if (a[mid] > a[right])
        {
            return right;
        }
        else
        {
            return mid;
        }
    }
    else
    {
        if (a[mid] > a[left])
        {
            return left;
        }
        else if (a[mid] < a[right])
        {
            return right;
        }
        else
        {
            return mid;
        }
    }
}

代码实现:

(1)递归法:

void QuickSortR(int *a, int left, int right)
{
    assert(a);
    if (left >= right)
    {
        return;
    }

    if (right - left < 10) //优化:在区间较小时,插入排序的效率高
    {
        InsertSort(a, right - left + 1);
    }
    else
    {
        int div = PartSort1(a, left, right);
        QuickSortR(a, left, div - 1);
        QuickSortR(a, div + 1, right);
    }
}

(2)非递归法:
     借用栈的结构来模仿递归(相关栈的函数定义请查看顺序栈

void QuickSort(int *a, int left, int right)
{
    assert(a);
    Stack s;
    StackInit(&s);
    StackPush(&s, left);
    StackPush(&s, right);
    while (!StackEmpty(&s))
    {
        int end = StackTop(&s);
        StackPop(&s);
        int begin = StackTop(&s);
        StackPop(&s);
        int div = PartSort1(a, begin, end);
        if (begin < div - 1)
        {
            StackPush(&s, begin);
            StackPush(&s, div - 1);
        }
        if (div + 1 < end)
        {
            StackPush(&s, div + 1);
            StackPush(&s, end);
        }
    }
}

分析:

时间复杂度:

  • 最好情况:O(N*lgN)
  • 最坏情况:O(N^2)
  • 平均情况:O(N*lgN)

空间复杂度:O(lgN)——递归深度
稳定性:不稳定

七、归并排序

基本思想:

     将待排序的元素序列分成两个长度相等的子序列,对每一个子序列排序,然后再将它们合并成一个有序序列。

图解:

这里写图片描述

合并图解:

这里写图片描述

代码实现:

//归并局部递归
void _MergeSort(int *a, int left, int right, int *tmp)
{
    if (left >= right)
    {
        return;
    }

    if (right - left < 10) //优化:在区间较小时,插入排序的效率高
    {
        InsertSort(a, right - left + 1);
        return;
    }

    int mid = left + ((right - left) >> 1);
    _MergeSort(a, left, mid, tmp);
    _MergeSort(a, mid + 1, right, tmp);

    int p = left;
    int q = mid + 1;
    int index = 0;
    while (p <= mid&&q <= right)
    {
        if (a[p] <= a[q]) //加上"="可保持稳定性
        {
            tmp[index++] = a[p++];
        }
        else
        {
            tmp[index++] = a[q++];
        }
    }
    while (p <= mid)
    {
        tmp[index++] = a[p++];
    }
    while (q <= mid)
    {
        tmp[index++] = a[q++];
    }

    int j = 0;
    for (int i = left; i <= right; i++)
    {
        a[i] = tmp[j++];
    }
}

void MergeSort(int *a, int left, int right)
{
    assert(a);
    int *tmp = (int*)malloc((right - left + 1)*sizeof(int));
    memset(tmp, 0, (right - left + 1)*sizeof(int));
    //或者:int *tmp = (int *)calloc(right - left + 1,sizeof(int));
    _MergeSort(a, left, right, tmp);
    free(tmp);
}

分析:

时间复杂度:O(N*lgN)
空间复杂度:O(N)——临时数组
稳定性:稳定

八、计数排序

基本思想:

     统计待排序序列中每个元素出现的次数,再根据统计的结果重新对元素进行回收。

图解:

这里写图片描述

代码实现:

void CountSort(int *a, int size)
{
    assert(a);
    //(1)找出最值,确定范围,以便开辟空间
    int max = a[0];
    int min = a[0];
    int index = 0;
    for (index = 1; index < size; index++)
    {
        if (a[index]>max)
        {
            max = a[index];
        }
        if (a[index] < min)
        {
            min = a[index];
        }
    }
    int range= max - min + 1;
    //(2)统计出现次数
    int *tmp = (int*)calloc(range, sizeof(int));
    for (index = 0; index < size; index++)
    {
        tmp[a[index] - min]++;
    }
    //(3)回收
    int i = 0;
    for (index = 0; index < range; index++)
    {
        while (tmp[index])
        {
            a[i++] = index + min;
            tmp[index]--;
        }
    }
    free(tmp);
    tmp = NULL;
}

分析:

时间复杂度:O(N+range)
空间复杂度:O(range)
稳定性:稳定

九、总结:

这里写图片描述

猜你喜欢

转载自blog.csdn.net/X_Perseverance/article/details/80489804