瞎几把排序~

算法程序设计——排序

排序的概念

排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。

排序分为内部排序和外部排序。

若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。

反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。

排序的分类

这里主要介绍的八种排序算法均属于内部排序。如果按照策略来分类,大致可分为:交换排序、插入排序、选择排序、归并排序和基数排序。如下图所示:

算法概述

  1. 插入排序
    直接插入排序
    将第一个和第二个元素排好序,然后将第3个元素插入到已经排好序的元素中,依次类推(插入排序最好的情况就是数组已经有序了)

    希尔排序
    因为插入排序每次只能操作一个元素,效率低。希尔排序对待排序的N个元素,取奇数k=N/2,将下标差值为k的数分为一组(一组元素个数看总元素个数决定),在组内构成有序序列,再取k=k/2,将下标差值为k的数分为一组,构成有序序列,直到k=1,然后再进行直接插入排序。

  2. 选择排序
    简单选择排序
    选出最小的数和第一个数交换,再在剩余的数中又选择最小的和第二个数交换,依次类推。

    堆排序
    以升序排序为例,利用小根堆大根堆的性质,不断输出最小元素或移动最大元素,直到堆中元素全部有序。

  3. 交换排序
    冒泡排序
    改进1:如果某次冒泡不存在数据交换,则说明已经排序好了,可以直接退出排序。
    改进2:头尾进行冒泡,每次把最大的沉底,最小的浮上去,两边往中间靠1。

    快速排序
    选择一个基准元素,比基准元素小的放基准元素的前面,比基准元素大的放基准元素的后面,这种动作叫分区,每次分区都把一个数列分成了两部分,每次分区都使得一个数字有序,然后将基准元素前面部分和后面部分继续分区,一直分区直到分区的区间中只有一个元素

  4. 归并排序
    一般归并排序
    将一个无序的数列一直一分为二,直到分到序列中只有一个数的时候,这个序列肯定是有序的,因为只有一个数,然后将两个只含有一个数字的序列合并为含有两个数字的有序序列,这样一直进行下去,最后就变成了一个全部的有序数列,合并时一般需要额外的内存空间用于存放临时数组。

    在位归并排序
    不需要辅助数组即可实现分区的合并。

  5. 基数排序
    找到最大的数,开个比最大的数大一点的数组,遍历每个元素,某个元素为k,则a[k]++,最好遍历数组a,a[k]等于多少就输出多少个k,只能处理整型数。

  6. 计数排序。。。

又或者按照排序稳定性:

不稳定排序:简单选择排序,快速排序,希尔排序,堆排序
稳定排序:冒泡排序,直接插入排序,归并排序,基数排序

代码分析

  1. 直接插入排序(Insertion Sort)
    算法思想:直接插入排序的核心思想就是:将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过 因此,从上面的描述中我们可以发现,直接插入排序可以用两个循环完成:
    第一层循环:遍历待比较的所有数组元素
    第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。如果:selected > ordered,那么将二者交换。

    void InsertSort(int a[], int n) {
    for(int i= 1; i<n; i++) {
    if(a[i] < a[i-1]) { //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
    int j= i-1;
    int x = a[i]; //复制为哨兵,即存储待排序元素
    a[i] = a[i-1]; //先后移一个元素
    while(x < a[j]){ //查找在有序表的插入位置
    a[j+1] = a[j];
    j--; //元素后移
    }
    a[j+1] = x; //插入到正确位置
    }
    }
    }
  2. 希尔排序(Shell‘s Sort)

    算法思想:希尔排序也称 递减增量排序 算法,是插入排序的一种更高效的改进版本,但希尔排序是非稳定排序算法。希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

    算法步骤:

    • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

    • 按增量序列个数k,对序列进行k 趟排序;

    • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

    void ShellInsertSort(int a[], int n, int dk) {
    for(int i= dk; i<n; ++i) {
    if(a[i] < a[i-dk]) { //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入
    int j = i-dk;
    int x = a[i]; //复制为哨兵,即存储待排序元素
    a[i] = a[i-dk]; //首先后移一个元素
    while(x < a[j]) { //查找在有序表的插入位置
    a[j+dk] = a[j];
    j -= dk; //元素后移
    }
    a[j+dk] = x; //插入到正确位置
    }
    }
    }
    // 先按增量d(n/2,n为要排序数的个数进行希尔排序
    void shellSort(int a[], int n) {
    int dk = n/2;
    while( dk >= 1 ){
    ShellInsertSort(a, n, dk);
    dk = dk/2;
    }
    }

    关于增量序列的选择:

  1. 简单选择排序(Selection Sort)

    算法思想:简单选择排序的实现思想:比较+交换

    1. 从待排序序列中,找到关键字最小的元素;
    2. 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换;
    3. 从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。因此我们可以发现,简单选择排序也是通过两层循环实现。第一层循环:依次遍历序列当中的每一个元素 第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。
    int SelectMinKey(int a[], int n, int i) {
    int k = i;
    for(int j=i+1 ; j< n; ++j)
    if(a[k] > a[j])
    k = j;
    return k;
    }
    void selectSort(int a[], int n) {
    int key, tmp;
    for(int i = 0; i< n; ++i) {
    key = SelectMinKey(a, n,i); //选择最小的元素
    if(key != i) {
    tmp = a[i];
    a[i] = a[key];
    a[key] = tmp; //最小元素与第i位置元素互换
    }
    }
    }
  2. 堆排序(Heap Sort)

    堆的概念本质是一种数组对象。特别重要的一点性质:任意的叶子节点小于(或大于)它所有的父节点。对此,又分为大顶堆和小顶堆:

    • 大顶堆要求节点的元素都要大于其孩子。

    • 小顶堆要求节点元素都小于其左右孩子。

    两者对左右孩子的大小关系不做任何要求。

    利用堆排序,就是基于大顶堆或者小顶堆的一种排序方法,下面通过大顶堆来实现。

    基本思想:堆排序可以按照以下步骤来完成:

    1. 首先将序列构建称为大顶堆;(这样满足了大顶堆的性质:位于根节点的元素一定是当前序列的最大值)

    2. 取出当前大顶堆的根节点,将其与序列末尾元素进行交换;(此时:序列末尾的元素为已排序的最大值;由于交换了元素,当前位于根节点的堆并不一定满足大顶堆的性质)

    3. 对交换后的n-1个序列元素进行调整,使其满足大顶堆的性质;

    4. 重复2.3步骤,直至堆中只有1个元素为止

    /**
    * 已知H[s…m]除了H[s] 外均满足堆的定义
    * 调整H[s],使其成为大顶堆.即将对第s个结点为根的子树筛选,
    *
    * @param H是待调整的堆数组
    * @param s是待调整的数组元素的位置
    * @param length是数组的长度
    */
    void HeapAdjust(int H[],int s, int length) {
    int tmp = H[s];
    int child = 2*s+1; //左孩子结点的位置。(i+1 为当前调整结点的右孩子结点的位置)
    while (child < length) {
    if(child+1 <length && H[child]<H[child+1]) { // 如果右孩子大于左孩子(找到比当前待调整结点大的孩子结点)
    ++child ;
    }
    if(H[s]<H[child]) { // 如果较大的子结点大于父结点
    H[s] = H[child]; // 那么把较大的子结点往上移动,替换它的父结点
    s = child; // 重新设置s ,即待调整的下一个结点的位置
    child = 2*s+1;
    } else { // 如果当前待调整结点大于它的左右孩子,则不需要调整,直接退出
    break;
    }
    H[s] = tmp; // 当前待调整的结点放到比其大的孩子结点位置上
    }
    }
    void HeapSort(int H[],int length) {
    //建立初始堆
    //最后一个有孩子的节点的位置 i= (length -1) / 2
    for (int i = (length -1) / 2 ; i >= 0; --i)
    HeapAdjust(H,i,length);
    //从最后一个元素开始对序列进行调整
    for (int i = length - 1; i > 0; --i) {
    //交换堆顶元素H[0]和堆中最后一个元素
    int temp = H[i];
    H[i] = H[0];
    H[0] = temp;
    //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
    HeapAdjust(H,0,i);
    }
    }
  3. 冒泡排序(Bubble Sort)

    算法思想:冒泡遍历所有的数据,每次对相邻元素进行两两比较,如果顺序和预先规定的顺序不一致,则进行位置交换;这样一次遍历会将最大或最小的数据上浮到顶端,之后再重复同样的操作,直到所有的数据有序。

    这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端,冒泡排序算法的运作如下:

    • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

    • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

    • 针对所有的元素重复以上的步骤,除了最后一个。

    • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

    void bubbleSort(int a[], int n) {
    for(int i =0 ; i< n-1; ++i) {
    for(int j = 0; j < n-i-1; ++j) {
    if(a[j] > a[j+1]) {
    int tmp = a[j] ;
    a[j] = a[j+1] ;
    a[j+1] = tmp;
    }
    }
    }
    }
  4. 快速排序(Quick Sort)

    算法思想:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n logn)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。

    事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

    快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

    算法步骤:

    1. 从数列中挑出一个元素,称为 “基准”(pivot)。
    2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
    3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。
    void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
    }
    int partition(int a[], int low, int high) {
    int privotKey = a[low]; //基准元素
    while(low < high) { //从表的两端交替地向中间扫描
    while(low < high && a[high] >= privotKey)
    --high; //从high 所指位置向前搜索,至多到low+1 位置。将比基准元素小的交换到低端
    swap(&a[low], &a[high]);
    while(low < high && a[low] <= privotKey )
    ++low;
    swap(&a[low], &a[high]);
    }
    return low;
    }
    void quickSort(int a[], int low, int high) {
    if(low < high) {
    int privotLoc = partition(a, low, high); //将表一分为二
    quickSort(a, low, privotLoc -1); //递归对低子表递归排序
    quickSort(a, privotLoc + 1, high); //递归对高子表递归排序
    }
    }
  5. 归并排序(Merge Sort)

    算法思想:归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

    算法步骤:

    1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
    2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
    3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
    4. 重复步骤3直到某一指针达到序列尾;
    5. 将另一序列剩下的所有元素直接复制到合并序列尾。
    //将r[i…m]和r[m +1 …n]归并到辅助数组rf[i…n]
    void Merge(ElemType *r,ElemType *rf, int i, int m, int n) {
    int j,k;
    for(j=m+1,k=i; i<=m && j <=n ; ++k) {
    if(r[j] < r[i])
    rf[k] = r[j++];
    else
    rf[k] = r[i++];
    }
    while(i <= m)
    rf[k++] = r[i++];
    while(j <= n)
    rf[k++] = r[j++];
    }
    void MergeSort(ElemType *r, ElemType *rf, int lenght) {
    int len = 1;
    ElemType *q = r ;
    ElemType *tmp ;
    while(len < lenght) {
    int s = len;
    len = 2 * s ;
    int i = 0;
    while(i+ len <lenght) {
    Merge(q, rf, i, i+ s-1, i+ len-1 ); //对等长的两个子表合并
    i = i+ len;
    }
    if(i + s < lenght) {
    Merge(q, rf, i, i+ s -1, lenght -1); //对不等长的两个子表合并
    }
    tmp = q;
    q = rf;
    rf = tmp; //交换q,rf,以保证下一趟归并时,仍从q 归并到rf
    }
    }

    一般在提到Merge Sort时,大都想到上面分治Divide-and-Conqure算法, 它一般是O(nlgn)的时间复杂度以及额外的O(n)空间。,但实际上O(n)的额外空间这一缺点是完全可以克服的,完全可以实现O(nlgn) 时间效率以及 O(1) 额外空间 的归并排序。对于这种不用额外空间(即常数大小的额外空间)的算法,有一个通用的名字叫做In-place Algorithms,因此我们称该归并算法为 In-place merge sort ,也就是在位归并排序

    在位归并排序

    在C++头文件中有在位归并排序的模板实现:#include<algorithm>void std::inplace_merge(_BIter, _BIter, _BIter);

    #include<algorithm>
    int num[arrayLength];
    mergesort(0, arrayLength-1); // main()函数调用
    void mergesort(int left,int right) {
    if(left>=right)
    return;
    int mid=(left+right)/2;
    mergesort(left,mid);
    mergesort(mid+1,right);
    inplace_merge(number+left, number+mid+1, number+right+1);
    }

    另外,还可以通过 循环左移 来实现归并操作:

    //将长度为n的数组逆序
    void reverse(int *arr,int n) {
    int i=0,j=n-1;
    while(i<j) {
    swap(arr[i],arr[j]);
    i++;
    j--;
    }
    }
    //将数组向左循环移位i个位置
    void exchange(int *arr,int n,int i) {
    reverse(arr,i);
    reverse(arr+i,n-i);
    reverse(arr,n);
    }
    //数组两个有序部分的归并
    void merge(int *arr,int begin,int mid,int end) {
    int i=begin,j=mid,k=end;
    while(i<j && j<=k) {
    int step=0;
    while(i<j && arr[i]<=arr[j])
    ++i;
    while(j<=k && arr[j]<=arr[i]) {
    ++j;
    ++step;
    }
    exchange(arr+i,j-i,j-i-step);
    i=i+step;
    }
    }
    void MergeSort(int *arr,int l,int r) {
    if(l<r) {
    int mid=(l+r)/2;
    MergeSort(arr,l,mid);
    MergeSort(arr,mid+1,r);
    merge(arr,l,mid+1,r);
    }
    }
  6. 基数排序(Radix Sort)

    算法思想:基数排序 通过序列中各个元素的值,对排序的N个元素进行若干趟的“分配”与“收集”来实现排序。

    分配:将 L[i]中的元素取出,首先确定其个位上的数字,根据该数字分配到与之序号相同的桶中 。

    收集:当序列中所有的元素都分配到对应的桶中,再按照顺序依次将桶中的元素收集形成新的一个待排序列 L[ ] 。

    对新形成的序列L[]重复执行分配和收集元素中的十位、百位...直到分配完该序列中的最高位,则排序结束。

    void RadixSort(Node L[],length,maxradix) {
    int m,n,k,lsp;
    k=1;
    m=1;
    int temp[10][length-1];
    Empty(temp); //清空临时空间
    while(k<maxradix) { //遍历所有关键字
    for(int i=0; i<length; i++) { //分配过程
    if(L[i]<m)
    Temp[0][n]=L[i];
    else
    Lsp=(L[i]/m)%10; //确定关键字
    Temp[lsp][n]=L[i];
    n++;
    }
    CollectElement(L,Temp); //收集
    n=0;
    m=m*10;
    k++;
    }
    }

总结分析

各算法の时间复杂度函数O(n)的增长情况如图:

所以对n较大的排序记录,一般的选择都是时间复杂度为O(nlog2n)的排序方法。

  1. 平方阶(O(n2))排序 

    各类简单排序:直接插入、直接选择和冒泡排序;

  2. 线性对数阶(O(nlog2n))排序

    快速排序、堆排序和归并排序;

  3. O(n1+§))排序,§是介于0和1之间的常数。

    希尔排序

  4. 线性阶(O(n))排序

    基数排序,此外还有桶排序、箱排序。

因此

  • 当原表有序或基本有序时,直接插入排序冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n);

  • 快速排序则相反,当原表基本有序时,将蜕化为冒泡排序,时间复杂度提高为O(n2)

  • 原表是否有序,对简单选择排序、堆排序、归并排序和基数排序的时间复杂度影响不大。

排序算法的稳定性: 若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。

稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。

  • 基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。另外,如果排序算法稳定,可以避免多余的比较;

  • 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序

  • 不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序

何种排序算法?

考虑的因素有以下四点:

  1. 待排序的记录数目n的大小;

  2. 记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;

  3. 关键字的结构及其分布情况;

  4. 对排序稳定性的要求。

具体来说:设待排序元素的个数为n

1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序堆排序归并排序

  • 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;

  • 堆排序 : 如果内存空间允许且要求稳定性的,

  • 归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。

2) 当n较大,内存空间允许,且要求稳定性:归并排序

3)当n较小,可采用直接插入或直接选择排序。

  • 直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。

  • 直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序

4)一般不使用或不直接使用传统的冒泡排序。

5)基数排序

  • 它是一种稳定的排序算法,但有一定的局限性:
    1. 关键字可分解。
    2. 记录的关键字位数较少,如果密集更好。
    3. 如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。
posted @   awysl  阅读(67)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示