十种常用排序算法总结

前言

常用的有十种排序算法,包含了插入、选择、交换、分治、线性五种类别,本篇博客将对这十种排序算法做一个总结,并附带C++代码

总体表格

在这里插入图片描述

分别来看

插入排序

在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。

void InserttSort(int array[], int length)
{
    for(int i = 0; i < length - 1; ++ i)
    {
        for(int j = i + 1; j > 0; --j)
        {
            if(array[j] < array[j - 1])
                MySwap(array, j, j - 1);
            else
                break;
        }
    }
}

希尔排序

插入排序一种高效率的实现,也叫缩小增量排序
简单插入排序中,如果序列是基本有序的,使用直接插入排序效率就非常高
希尔排序利用了这个特点:先将整个序列分割成若干个子序列进行直接插入排序,待整个序列中的记录基本有序时再对全体记录进行一次直接插入排序
注意:分割子序列的时候不是逐段分割,而是将某个相隔增量的元素组成一个子序列
较小的元素跳跃式往前挪动,比直接插入排序效率高
时间复杂度需要复杂的数学推算

void ShellSort(int array[], int length)
{
    int incre = length;
    while (true)
    {
        incre = incre / 2;
        cout << "incre: " << incre << endl;
        for(int k = 0; k < incre; ++k)//根据增量分为若干子列
        {
            for(int i = k + incre; i < length ; i += incre)
            {
                for(int j = i; j > k; j -= incre)
                {
                    if(array[j] < array[j - incre])
                        MySwap(array, j, j - incre);
                    else
                        break;
                }
                
            }
        }
        if(incre == 1)
            break;
    } 
}

选择排序

依次选择最小、第二小。。。的数放在第一位、第二位。。。
第一次遍历n-1个数,第二次n-2个数

void SelectSort(int array[], int length)
{
    for(int i = 0; i < length -1; ++i)
    {
        int minIndex = i;
        for(int j = i + 1; j < length; ++j)
        {
            if(array[j] < array[minIndex])
                minIndex = j;
        }
        if(minIndex != i)
            MySwap(array, minIndex, i);
    }
    
}

堆排序

利用堆这种数据结构而设计的一种排序算法,是一种选择排序
最坏、最好、平均时间复杂度均为O(nlogn)

是具有以下性质的完全二叉树:
每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
在这里插入图片描述
映射到数组中
在这里插入图片描述
所以大顶堆:arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]
小顶堆:arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]
堆排序思路:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
步骤

  • 构建初始堆(一般升序采用大顶堆,降序采用小顶堆)
  • 从最后一个非叶子节点开始,从左到右,从下至上进行调整
  • 继续调整构成一个大顶堆
  • 将堆顶元素与末尾元素进行交换,使末尾元素最大,然后继续调整堆,再将栈顶元素与末尾元素进行交换,得到第二大的元素,如此反复的重建、交换、重建、交换

总结基本思路:

  • 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  • 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  • 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
void AdjustHeapNode(int array[], int i, int length)//调整大顶堆
{
    int k = i * 2 + 1;
    while(k < length)
    {
        if(k + 1 < length && array[k] < array[k + 1])//如果左子节点小于右子节点,k指向右子节点
            ++k;
        if(array[k] > array[i])//如果子节点大于父节点,将子节点赋值给父节点(不用进行交换)
        {
            MySwap(array, i, k);
        }
        else   
            break;
        i = k;      //检查更换的节点是否满足最大堆的特性
        k = 2 * i + 1;
    }
}
void HeapSort(int array[], int length)
{
    //构建大顶堆
    for(int i = (length - 1) / 2; i >= 0; i--) //最后一个非叶子节点开始
    {
        cout << i << " "; 
        AdjustHeapNode(array, i, length);
    }
    cout << endl;
    //调整堆结构+交换堆顶元素与末尾元素
    for(int j = length - 1; j > 0; j--)
    {
        MySwap(array, 0, j);
        AdjustHeapNode(array, 0, j);
    }
}

冒泡排序

两个数比较大小,较大的下沉,较小的数冒起来
从最后开始冒,总共走length-1趟,每次排好一个数

void BubbleSort(int array[], int length)
{
    if(array == nullptr || length <= 0)
        return;
    for(int i = 0; i < length - 1; ++i)
    {
        for(int j = length - 1; j > i; --j)
            if(array[j] < array[j - 1])
                MySwap(array, j, j-1);
    }
}

优化
因为冒泡排序可能出现已经排好序但是依然要走满length-1趟的情况
添加一个标志,每趟开始前置零,当发生交换置1
每趟跑完检测是否为0,如果为0,说明本次没有发生交换,已经排好序了

void BubbleSort_Better(int array[], int length)
{
    if(array == nullptr || length <= 0)
        return;
    for(int i = 0; i < length - 1; ++i)
    {
        bool flag = false;
        for(int j = length - 1; j > i; --j)
            if(array[j] < array[j - 1])
            {
                MySwap(array, j, j-1);
                flag = true;
            }  
        if(!flag) break;  
    }
}

快速排序

基本思路(分治)

  • 先从数组中取出一个数作为key
  • 将比这个数小的数全部放在它的左边,比这个数大的全放在它右边
  • 对左右两个小数列重复执行第二步,直至各区间只有一个数

key值的选取有多种形式,例如中间数或者随机数
快排的时间性能取决于快排递归的深度
最优情况就是每次都恰好把数组分成两半,最优时间复杂度为O(nlogn)
最坏情况就是正序或者逆序(恰好与所要求的的排序相反),最差时间负责度是O(n^2)

时间复杂度和空间复杂度推导:可以看看这篇文章 文章地址

void QuickSort(int array[], int left, int right)
{
    if(left >= right)
        return;
    int i = left, j = right, key = array[left];
    while(i < j)
    {
        while(i < j && array[j] >= key)//从右开始找第一个小于等于key的值
            j--;
        if(i < j)
        {
            array[i] = array[j];
            i++;
        }
        while(i < j && array[i] < key)
            i++;
        if(i < j)
        {
            array[j] = array[i];
            j--;
        }
    }
    array[i] = key;
    QuickSort(array, left, right - 1);
    QuickSort(array, left + 1, right);
}

归并排序

建立在归并操作上的一种有效的排序算法
分治法的典型应用
首先要考虑如何将两个有序数列合并(有序数列合并问题,三个指针)
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。
空间复杂度为O(n)

void mergeArray(int array[], int first, int middle, int last)
{
    int temp[last - first +1];
    int i = first;
    int m = middle;
    int j = middle + 1;
    int n = last;
    int k = 0;

    while(i <= m && j <= n)
    {
        if(array[i] <= array[j])
        {
            temp[k] = array[i];
            k++;
            i++;
        }
        else
        {
            temp[k] = array[j];
            k++;
            j++;
        }
    }
    while(i <= m)
    {
        temp[k] = array[i];
        k++;
        i++;
    }
    while(j <= n)
    {
        temp[k] = array[j];
        k++;
        j++;
    }
    for(int ii = 0; ii < k; ii++)
        array[first+ii] = temp[ii];
}
void MergeSort(int array[], int first, int last)
{
    if(first < last)
    {
        int middle = (first + last) / 2; 
        MergeSort(array, first, middle);
        MergeSort(array, middle + 1, last);
        mergeArray(array, first, middle, last);
    }
}

计数排序

时间复杂度为O(n)的排序算法
适用于待排序数有范围,需要较多的辅助空间,空间大小与待排序数范围的大小有关
设定一个计数数组(大小为待排序数最大值加1)
将待排序数读入并给对应值自增
输出到待排序数组完成排序

int get_max(int array[], int length)//获取数组中最大值的函数
{
     if(array == nullptr || length <= 0)
        return -1;
    int max = array[0];
    for(int i = 1; i < length; i++)
    {
        if(array[i] > max)
            max = array[i];
    }
    return max;
}
void CountSort(int array[], int length)
{
    if(array == nullptr || length <= 0)
        return;
    int max = get_max(array, length) + 1;//获取数组中的最大值
    cout << max << endl;
    int *count= new int[max];//分配空间
    for(int i = 0; i < max; i++)//初始化计数数组各位为0
        count[i] = 0;
    // for(int i = 0; i < max; i++)
    //     cout << count[i] << " ";
    // cout <<endl;
    for(int i = 0; i < length; i++)//计数
    {
        count[array[i]]++;
    }
    for(int i = 0; i < max; i++)
        cout << count[i] << " ";
    cout <<endl;
    for(int i = 0, j = 0; i < max; i++)//输出到源数组,完成排序
    {
        for(int k = count[i]; k > 0; k--)
        {
            array[j] = i;
            j++;
        }    
    }
}

桶排序

是对计数排序的一种改进和推广
全依赖“比较”操作的排序算法时间复杂度的一个下界O(N*logN)
这些算法并不是不用“比较”操作,也不是想办法将比较操作的次数减少到 logN。而是利用对待排数据的某些限定性假设 ,来避免绝大多数的“比较”操作。
基本思想

  • 将待排序列以某种映射关系映射到多个桶中,然后对每个桶中的元素进行排序,然后依次枚举输出所有桶中的元素,这样就得到一个有序序列

映射函数

  • 如果关键字k1<k2,那么f(k1)<=f(k2)。也就是说B(i)中的最小数据都要大于B(i-1)中最大数据
  • 数据分段

使用映射函数,减少了几乎所有的比较工作
对N个关键字进行桶排序的时间复杂度分为两个部分

  • (1)循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)
  • (2)利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。

很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到**O(N*logN)**了)。因此,我们需要尽量做到下面两点:

  • (1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
  • (2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。

对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M*(N/M)log(N/M))=O(N+N(logN-logM))=O(N+NlogN-NlogM)
N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。

总结

  • 桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。 当然桶排序的空间复杂度 为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。
  • 适用范围类似于计数排序
int DataMap(int num) //桶排序映射函数
{
    return num / 10;    
}
void BucketSort(int array[], int length)
{
    if(array == nullptr || length <= 0)
        return;
    //vector<list<int>> bucket;//使用双向链表来存储桶内元素,同vector来组织桶
    list<int> bucket[10];//使用双向链表来存储桶内元素,用数组来组织桶
    for(int i = 0; i < length; i++)
    {
       bucket[DataMap(array[i])].push_back(array[i]);//给对应的桶中插入,插入操作O(1)时间复杂度
    }
    for(int i = 0; i < 10; i++)//分别对每个桶中的元素进行排序
    {
        bucket[i].sort();
    }
    for(int i = 0, k = 0; i < 10; i++)//输出桶中的元素到序列中,完成排序
    {
        for(auto j : bucket[i])
        {
            if(k < length)
            {    
                array[k] = j;
                k++;
            }
        }
    }
}

基数排序

BinSort想法非常简单,首先创建数组A[MaxValue];然后将每个数放到相应的位置上(例如17放在下标17的数组位置);最后遍历数组,即为排序后的结果。
问题: 当序列中存在较大值时,BinSort 的排序方法会浪费大量的空间开销。
基数排序是一种借助多关键字排序思想对单逻辑关键字进行排序的方法。
所谓的多关键字排序就是有多个优先级不同的关键字。
如果对数字进行排序,那么个位、十位、百位就是不同优先级的关键字,如果要进行升序排序,那么个位、十位、百位优先级一次增加。基数排序是通过多次的收分配和收集来实现的,关键字优先级低的先进行分配和收集。

void BitCountSort(int array[], int length, int exp)//按位计数排序函数
{
    int range[10];
    int temparr[length];
    for(int i = 0; i <10; i++)
        range[i] = 0;
    
    for(int i = 0; i < length; i++)
    {
        range[(array[i]/exp)%10]++;
    }
    cout << "range :" << endl;
    for(int i = 0; i < 10; i++)
    {
        cout << range[i] << " ";
    }
    cout << endl;
    for(int i = 1; i < 10; i++)
    {
        range[i] += range[i-1];//统计本应出现的位置
    }
    cout << "range :" << endl;
    for(int i = 0; i < 10; i++)
    {
        cout << range[i] << " ";
    }
    cout << endl;
    for(int i = length - 1; i >=0; i--)
    {
        temparr[range[(array[i]/exp)%10] - 1] = array[i];
        range[(array[i]/exp)%10]--;
    }
    for(int i = 0; i < length; i++)
    {
        array[i] = temparr[i];
    }
}

void RadixSort(int array[], int length)
{
    int max = -1;
    //提取最大值
    for(int i = 0; i < length; i++)
    {
        if(array[i] > max)
            max = array[i];
    }
    //提取每一位进行比较位数不足的高位补0
    for(int exp = 1; max/exp > 0; exp *= 10)
        BitCountSort(array, length, exp);
}

总结

简单排序:冒泡排序、选择排序、插入排序
简单排序的变种:快速排序、堆排序、希尔排序
基于分治递归的:归并排序
线性排序:计数排序、桶排序、基数排序

稳定性讨论

我们希望如果关键值相等的时候,先输入的数据应该还是排在前面,而不是随便排

  • 稳定的排序算法有: 插入排序,冒泡排序,合并排序,计数排序,基数排序,桶排序
  • 不稳定的排序算法有:堆排序,快速排序,选择排序,希尔排序。

内存占用讨论

  • In-place sort(不占用额外内存或占用常数的内存):插入排序、选择排序、冒泡排序、堆排序、快速排序、希尔排序。
  • Out-place sort:归并排序、计数排序、基数排序、桶排序。
    当需要对大量数据进行排序时,In-place sort就显示出优点,因为只需要占用常数的内存。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章