常见排序算法总结和比较

常见比较类排序算法总结和实现


这里写图片描述

排序的稳定性和复杂度

类别 排序方式
时间复杂度
空间复杂度 稳定性
平均情况 最好情况 最坏情况
插入排序 直接插入排序 O(N^2) O(N) O(N^2) O(1) 稳定
希尔排序 O(N^1.3) O(N) O(N^2) O(1) 不稳定
选择排序 直接选择排序 O(N^2) O(N^2) O(N^2) O(1) 不稳定
堆排序 O(N*logN) O(N*logN) O(N*logN) O(1) 不稳定
交换排序 冒泡排序 O(N^2) O(N) O(N^2) O(1) 稳定
快速排序 O(N*logN) O(N*logN) O(N^2) O(logN) 不稳定
归并排序 归并排序 O(N*logN) O(N*logN) O(N*logN) O(N) 稳定

每种排序原理和实现

比较类排序可以将其拆分为单步排序

插入排序

单步插入排序是将需要排序的元素,按其排序码的大小,插入到已经有序的序列中
直接插入排序

当插入第 i (i >= 1)个元素时,前面 i - 1个元素已经有序,此时用array[i]的排序码和前面元素array[i - 1], array
[i - 2],…的排序码进行顺序比较,找到arrry[i]要插入的位置将其插入,原来位置上的元素后移。
从小到大排序比较时,如果当前元素排序码大于array[i]时当前元素后移,继续比较直到找到比其小的,将其放到正在比较元素的后边。 可见相等元素比较时,原来靠后的还是排在后边,所以插入排序是稳定的。元素集合越接近有序需要比较的次数越少元素移动相应越少, 排序算法的时间效率越高,相反当元素集合接近逆序时时间效率越低。

void InsretSort(int * array, size_t n)
{
    assert(array);
    for (size_t i = 1; i < n; ++i)  //[1,n)
    {
        int end = i - 1;   //[0,end]为有序区间
        int cur = array[i];
        while (end >= 0 && array[end] > cur) 
        {
            array[end + 1] = array[end--];//元素后移
        }
        array[end + 1] = cur;
    }
}
希尔排序

希尔排序时插入排序的一种改进,基本思想是将元素集合相距某个增量 d 的元素组成一个子序列,通过插入排序使其有序,然后逐渐减少增量 d 直到 d 为 1 进行一次快速排序达到整体有序。
希尔排序步长的选择是最重要的部分,会影响其效率,其设计者最初建议步长选择为n/2并且对步长取半直到步长达到1。
目前已知最好的步长序列是由Sedgwick提出的(1, 5, 19, 41, 109,…)。

void ShallSort(int * array, size_t n)
{
    assert(array);
    for (int gap = n >> 1; gap > 0; gap >>= 1) //步长选择为n/2并对步长取半达到1
    {
        for (size_t i = gap; i < n; i++) 
        {
            int end = i - gap; 
            int cur = array[i];
            while (end >= 0 && array[end] > cur)
            {
                array[end + gap] = array[end];
                end -= gap;
            }
            array[end + gap] = cur;
        }
    }//gap
}

选择排序

直接选择排序

选择排序的思想是首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕

void _Swap(int *left, int *right) //交换两个数
{
    int temp = *left;
    *left = *right;
    *right = temp;
}
//一次选择一个最小值
void SelectionSort(int array[], int len) 
{
    for (int i = 0 ; i < len - 1 ; i++) 
    {
        int min = i;
        for (int j = i + 1; j < len; j++) //遍历未排序的元素 
        {   
            if (array[j] < array[min])//找到最小的元素下标
            {    
                min = j;    
            }
        }
        _Swap(&array[min], &array[i]);    //进行值的交換
    }
}
//每次选择一个最小的和一个最大的
void SelectSort(int * array, size_t n)
{
    assert(array);
    int min = 0, max = 0;

    for (size_t left = 0, right = n - 1; left < right; left++, right--)
    {
        min = left;
        max = right;
        for (size_t i = left; i <= right; ++i) //遍历未排序的元素  
        {
            if (array[i] < array[min])  //找到最小的元素下标
            {
                min = i;
            }
            if (array[i] > array[max])  //找最大的元素下标
            {
                max = i;
            }
        }
        if (min != left)  //最小的元素放在左边
        {
            _Swap(&array[min], &array[left]);
        }
        if (max == left) //如果最大的元素在最小元素位置,将max改为最小元素交换后的位置
        {
            max = min;
        }
        if (max != right) //将最大的元素放在右边
        {
            _Swap(&array[max], &array[right]);
        }
    }
}
堆排序

堆排序是利用堆的数据结构设计的排序算法, 大堆是节点的左右子树均小于自己,小堆相反。堆排其思想是把数组看成堆,排序的第一步是建堆,然后是取堆顶元素调整堆 ,建堆是通过自底向上执行向下调整,向下调整是父亲节点的左右子树满足大(小)堆,选出左右节点最大(小)的和父亲节点进行交换,之后用同样的逻辑调整被交换的子树直到叶子节点。
排序过程是:把堆顶array[0]元素和当前最堆的最后一个元素交换;堆元素个数减1;由于第1步后根节点不再满足最堆定义,向下调整根结点。直到剩一个元素排序结束。因为每次要和堆的最后一个元素交换因此堆排是一种不稳定的排序

static void AdjustDown(int * array, size_t n, int root) //向下调整
{
    assert(array);
    //在阵列起始位置为0的情形中:

    //父节点i的左子节点在位置(2i+1);
    //父节点i的右子节点在位置(2i+2);
    //子节点i的父节点在位置 (i-1)/2);
    int parents = root;
    int child = parents * 2 + 1;

    while (child < (int)n)
    {
        if (child + 1 < (int)n && array[child] < array[child + 1])  //找到左右孩子大的一个
        {
            child++;
        }
        if (array[child] > array[parents]) //和父亲节点进行比较如果父亲节点小进行交换继续调整其字树
        {
            _Swap(&array[parents], &array[child]);
            parents = child;
            child = parents * 2 + 1;
        }
        else //如果其满足大堆直接跳出
        {
            break;
        }
    }
}

void HeapSort(int * array, size_t n)
{
    assert(array);

    //建堆
    for (int i = ((n - 2) >> 1); i >= 0; --i)
    {
        AdjustDown(array, n, i);
    }

    //进行排序
    int end = n - 1;
    while (end > 0)
    {
        _Swap(&array[end], &array[0]); //和最后一个进行交换
        AdjustDown(array, end, 0); //重新调整成大堆
        end--;
    }
}

交换排序

冒泡排序

冒泡排序的步骤如下:
1、比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3、针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较

void BubbleSort(int * a, int n)
{
    assert(a);
    int end = n - 1;
    while (end > 0)        //控制冒泡结束的位置
    {
        int flag = 0;  //设标志
        for (int i = 0; i < end; ++i) //在区间 [0, end)
        {
            //比较相邻的元素。如果第一个比第二个大,就交换他们两个。
            if (a[i] > a[i + 1])
            {
                _Swap(&a[i], &a[i+1]);
                flag = 1;  //如果交换标志就设为1
            }
        }
        if (flag == 0) //如果没有交换就说明有序直接结束
        {
            break;
        }
        end--;
    }
}
快速排序

快速排序使用分治法策略来把一个序列分为两个子序列。
步骤为:
从数列中挑出一个元素,称为”基准”,pivot
重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面在这个分割结束之后,该基准就处于数列的中间位置。
把数组分为 [left piviot - 1] [pivot + 1, right]两个区间
到最底部时,数列的大小是零或一,也就是已经排序好了

插入排序在小数组的排序上是非常高效的,在快速排序递归的子序列,如果序列规模足够小,可以使用插入排序替代快速排序,因此可以在快排之前判断数组大小,如果小于一个阀值就使用插入排序可以对快排进行优化

//三数取中
int GetMin(int x[], int a, int b, int c)
{
    return x[a] < x[b] ? (x[b] < x[c] ? b : x[a] < x[c] ? c : a)
        : x[b] > x[c] ? b : x[a] > x[c] ? c : a;
}

//左右指针法
int PartSort1(int * array, int left, int right)
{
    assert(array);
    //使用三数取中法规避数据一边倒
    int mid = GetMin(array, left, left + ((right - left) >> 1), right);
    _Swap(&array[mid], &array[right]);   

    int pivot = right;//选数组最右边值为枢纽值

    while (left < right) //当左右相遇时结束交换枢纽值
    {
        //左边遇到比枢纽值小的和相等值往后走, 遇到大于枢纽值的的值停止
        while (left < right && array[left] <= array[pivot]) 
        {
            left++;
        }
        //右边遇到比枢纽值大的和相等值往后走, 遇到小于枢纽值的的值停止   
        while (left < right && array[right] >= array[pivot])
        {
            right--;
        }
        //将左右值交换 使小于枢纽值的放在左边, 大于枢纽值的放在右边
        _Swap(&array[left], &array[right]);
    }
    //找到枢纽值的位置并和枢纽值交换
    _Swap(&array[left], &array[pivot]);
    return left;
}

//挖坑法
int PartSort2(int * array, int left, int right)
{
    assert(array);
    //选数组最右边值为枢纽值,使用三数取中法规避数据一边倒
    int mid = GetMin(array, left, left + ((right - left) >> 1), right);
    _Swap(&array[mid], &array[right]);

    //第一次最右面为坑
    int pivot = array[right];
    while (left < right)
    {
        while (left < right && array[left] <= pivot)
        {
            left++;
        }
        array[right] = array[left]; //将右边坑填上后左边为坑
        while (left < right && array[right] >= pivot)
        {
            right--;
        }
        array[left] = array[right]; //将左边坑填上之后右边为坑
    }
    array[left] = pivot;//填上左右相遇点的坑
    return left;
}

//前后指针法
int PartSort3(int * array, int left, int right)
{
    assert(array);
    //选数组最右边值为枢纽值,使用三数取中法规避数据一边倒
    int mid = GetMin(array, left, left + ((right - left) >> 1), right);
    _Swap(&array[mid], &array[right]);

    int prev = left - 1, cur = left;
    while (cur < right)
    {
        if (array[cur] <= array[right] && ++prev != cur )
        {
            _Swap(&array[prev], &array[cur]);
        }
        cur++;
    }
    _Swap(&array[right], &array[++prev]);
    return prev;
}

//递归法
void QuickSort(int * a, int left, int right)
{
    assert(a);
    if (left >= right)
    {
        return;
    }

    //如果小于阀值7就使用插入排序
    if (right - left + 1 == 7)
    {
        InsretSort(array, right - left + 1);
        return;
    }
    //从数列中挑出一个元素,称为"基准",pivot
    //重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面在这个分割结束之后,该基准就处于数列的中间位置。
    int div = PartSort3(a, left, right);

    QuickSort(a, left, div - 1);
    QuickSort(a, div + 1, right);
}

//非递归
void QuickSortNR(int * array, int left, int right)
{
    assert(array);
    Stack S;
    StackInit(&S, 10);

    //先入左然后入右即取出时相反
    StackPush(&S, left);
    StackPush(&S, right);

    while (!StackEmpty(&S)) 
    {
        //出栈时先出右再出左
        int _right = StackTop(&S);
        StackPop(&S);
        int _left = StackTop(&S);
        StackPop(&S);

        //如果小于阀值7就使用插入排序
        if (right - left + 1 == 7)
        {
            InsretSort(array, right - left + 1);
            continue;
        }
        int div = PartSort1(array, _left, _right);

        if (_left < div - 1)
        {
            StackPush(&S, _left);
            StackPush(&S, div - 1);
        }
        if (div + 1 < _right)
        {
            StackPush(&S, div + 1);
            StackPush(&S, _right);
        }
    }
}
归并排序

归并操作,也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。
递归法:
1、申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2、设定两个指针,最初位置分别为两个已经排序序列的起始位置
3、比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针到达序列尾
将另一序列剩下的所有元素直接复制到合并序列尾

迭代法:
原理如下假设序列共有 n 个元素:
将序列每相邻两个数字进行归并操作,形成 ceil(n/2) 个序列,排序后每个序列包含两个/一个元素若此时序列数不是1个则将上述序列再次归并,形成 ceil(n/4)}个序列,每个序列包含四个/三个元素,重复操作,直到所有元素排序完毕,即序列数为1。

void _MergeSort(int * array, int left, int right, int * tmp)
{
    assert(array);
    if (left >= right)
    {
        return;
    }
    int mid = left + ((right - left) >> 1);

    //分为[left mid] [mid + 1 right]两个区间
    _MergeSort(array, left, mid, tmp);
    _MergeSort(array, mid + 1, right, tmp);

    //至此左右区间均有序
    int left_a = left, right_a = mid;
    int left_b = mid + 1, right_b = right;
    int index = left;
    while (left_a <= right_a && left_b <= right_b)
    {
        //从小到大排序
        if (array[left_a] > array[left_b])
        {
            tmp[index++] = array[left_b++];
        }
        else
        {
            tmp[index++] = array[left_a++];
        }
    }

    while (left_a <= right_a)
    {
        tmp[index++] = array[left_a++];
    }
    while (left_b <= right_b)
    {
        tmp[index++] = array[left_b++];
    }
    memcpy(&array[left], &tmp[left], sizeof(int)* (right - left + 1));
}

void MergeSort(int * array, int left, int right)
{
    assert(array);
    int * tmp = (int *)malloc(sizeof(int) * (right - left + 1));
    _MergeSort(array, left, right, tmp);
    free(tmp);
}

//迭代法
void MergeSortNR(int * array, int left, int right)
{
    assert(array);
    int * tmp = (int *)malloc(sizeof(int) * (right - left + 1));

    for (int gap = 1; gap <= (right - left + 1); gap <<= 1)
    {
        int cur = left;
        while (cur <= right)
        {
            //计算区间中点分为两个区间 [cur mid - 1] [mid mid + gap - 1]
            int mid = cur + gap;

            int left_a = cur, right_a = mid - 1;
            if (right_a >= right)
            {
                break;
            }

            int left_b = mid, right_b = left_b + gap - 1;
            if (right_b > right)
            {
                right_b = right;
            }

            int index = cur;
            while (left_a <= right_a && left_b <= right_b)
            {
                //从小到大排序
                if (array[left_a] > array[left_b])
                {
                    tmp[index++] = array[left_b++];
                }
                else
                {
                    tmp[index++] = array[left_a++];
                }
            }

            while (left_a <= right_a)
            {
                tmp[index++] = array[left_a++];
            }
            while (left_b <= right_b)
            {
                tmp[index++] = array[left_b++];
            }
            ////计算需要拷贝的空间字节大小
            int count_byte = (right_b - cur + 1) < (gap << 1) ? (right_b - cur + 1) : (gap << 1);
            memcpy(&array[cur], &tmp[cur], sizeof(int)* count_byte);

            cur += (gap << 1);
        }//while(cur <= right)
    }
    free(tmp);
}

源码

猜你喜欢

转载自blog.csdn.net/Jocker_D/article/details/80511974
今日推荐