经典排序算法学习笔记

简介

排序算法可以分为内部排序外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
分类:
插入类排序:直接插入排序(Insertion Sort)、折半插入排序(binary insertion sort)、表插入排序(Table Insert Sort)、希尔排序(Shell Sort)
交换类排序:冒泡排序(Bubble Sort)、快速排序(Quick Sort)
选择类排序:简单选择排序(Select Sort)、树形选择排序(Tree Selection Sort)、堆排序(Heap Sort)
归并排序(分治法)(Merge Sort):实现方法 { 2路归并排序、自顶向下、自顶向上…}
分配类排序:桶排序(Bucket Sort)、基数排序(Radix Sort)

这里写图片描述

常见排序算法一般按平均时间复杂度分为两类:
O(n^2):冒泡排序、选择排序、插入排序
O(nlogn):归并排序、快速排序、堆排序等等

两类算法随着排序集合越大,效率差异越大,在数量规模1W以内的排序,两类算法都可以控制在毫秒级别内完成,但当数量规模达到10W以上后,简单排序往往需要以几秒、分甚至小时才能完成排序;而高级排序仍可以在很短时间内完成排序。

各种排序的代码实现

准备工作

首先写几个辅助函数,用于生成数据和测试排序的性能

生成随机数

#include<iostream>
#include<ctime>
#include<cassert>
using namespace std;

namespace sortTestHelper{
        /*
        得到一个长度为n,范围[rangeL,rangeR]的随机数 
        param n 数组大小 rangeL 左区间 rangeR 右区间
        return arr数组 
    */
    int* generateRandomArray(int n,int rangeL,int rangeR){
        assert(rangeL <= rangeR);
        int *arr = new int[n];
        srand(time(NULL));
        for(int i=0 ; i < n ; i ++)
            arr[i] = rand() % (rangeR - rangeL + 1) + rangeL;
        return arr;
    }

     /*
        得到一个长度为n,近乎有序的数组,范围[0,n-1] 
        param n 数组大小,swapTimes 交换次数 
        return arr数组 
    */
    int *generateNearlyOrderedArray(int n,int swapTimes){
        int *arr = new int[n];
        for(int i=0;i<n;i++)
            arr[i] = i;
        srand(time(NULL));
        for(int i=0;i<swapTimes;i++){
            int posx = rand()%n;
            int posy = rand()%n;
            swap(arr[posx],arr[posy]);
         }
         return arr; 
     }

    template<typename T>
    /*
        遍历arr数组里面的元素,打印输出 
        param arr[] 数组,n 数组大小
        return void
    */
    void printArray(T arr[],int n){
        for(int i=0;i<n;i++)
            cout<<arr[i]<<" ";
        cout<<endl;
        return;
     }

     template<typename T>
     /*
        测试排序算法的效率
         param sortName 算法名称,(*sort)(T[],int)函数指针,arr[] 需要排序的数组,n 数组大小
         return void 
     */
     void testSort(string sortName,void(*sort)(T[],int),T arr[],int n){
        clock_t startTime = clock();
        sort(arr,n);
        clock_t endTime = clock();
        cout<<sortName<<" : "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
        return;
     }

     /*
        数组复制
         param a[]源数组, n 数组大小
         return arr 新数组首指针 
     */
     int* copyIntArray(int a[],int n){
        int* arr = new int[n];
        copy(a,a+n,arr);
        return arr;
     }
     /**
     这里使用到了STL中的copy函数,原型:
template<class InIt, class OutIt>
OutIt copy(InIt first, InIt last, OutIt x);
第一个参数是要拷贝元素的首地址,第二个参数是元素最后一个元素的下一个位置,第三个参数是拷贝的目的地址,首地址
     **/
}

下面的所有排序默认从小到大的顺序

冒泡排序

template<typename T>
void bubbleSort(T a[], int n){
    int i, j, temp;
    for (j = 0; j < n - 1; j++)
        for (i = 0; i < n - 1 - j; i++)
            if(a[i] > a[i + 1])
                swap(a[i],a[i+1]);
}

测试:
int a[11]={2,1,4,3,5,4,6,7,1,3,0};
bubbleSort(a,11);

结果:
这里写图片描述

算法原理:通过无序区中相邻记录关键字间的比较和位置的交换,使关键字最小或最大的记录如气泡一般逐渐往上“漂浮”直至“水面”。
当原本数组顺序就是有序的时候,时间复杂度退化为O(n),这是最好情况
当原本数组顺序刚好是逆序的时候,时间复杂度为O(n^2),这是最坏情况

选择排序

template<typename T>
void selectSort(T arr[],int n){
    for(int i=0;i<n;i++){
        int minIndex = i;
        for(int j=i+1;j<n;j++)
            if(arr[j]<arr[minIndex])
                minIndex = j;
        swap(arr[i],arr[minIndex]);
    }
}

算法原理:在一个长度为N的无序数组中,在第一趟遍历N个数据,找出其中最小的数值与第一个元素交换,第二趟遍历剩下的N-1个数据,找出其中最小的数值与第二个元素交换……第N-1趟遍历剩下的2个数据,找出其中最小的数值与第N-1个元素交换,至此选择排序完成。

直接插入排序

template<typename T>
void insertSort(T arr[],int n){
    for(int i=1;i<n;i++){
        T e = arr[i];
        int j;
        for(j=i ; j>0 && arr[j-1]>e ; j--)
            arr[j] = arr[j-1];
        arr[j] = e;
    }
}

算法原理:插入排序就是每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕。
值得注意的是,插入排序能提前终止循环,需要排序的数组越有序,插入排序的效率就越高,最好能达到O(n)的层次,所以对于不同数据针对选择相应的排序算法是最好的

O(n^2)排序算法的效率测试

测试算法:冒泡排序、选择排序、插入排序

调用上面写的辅助函数来创建测试数据,并统计时间

int main(){

    //数据规模
    int n=5000;

    int *arr = sortTestHelper::generateRandomArray(n,0,n);
    int *arr2 =  sortTestHelper::copyIntArray(arr,n);
    int *arr3 =  sortTestHelper::copyIntArray(arr,n);
    sortTestHelper::testSort("select sort",selectSort,arr,n);
    sortTestHelper::testSort("bubble sort", bubbleSort,arr2,n);
    sortTestHelper::testSort("insert sort", insertSort,arr3,n);
    delete[] arr;
    delete[] arr2;
    delete[] arr3;
    return 0;
}

数据量5000的随机数
这里写图片描述
数据量20000的随机数
这里写图片描述
数据量50000的随机数
这里写图片描述
在数据量30000,近乎有序的随机数下
这里写图片描述
可见插入排序在近乎有序的随机数下排序最快,这也是插入排序的一个重要的性质

最后将STL模板里的sort函数拿来测试,它属于快速排序
SO。。
数据量100000的随机数
这里写图片描述
可见正常情况下快排比O(n^2)的排序快太多太多了。。

下面是高级排序算法

归并排序

这里写图片描述

//将arr[l...mid]和arr[mid+1...r]两部分进行归并 
template<typename T>
void __merge(T arr[],int l,int mid,int r){
//  T aux[r-l+1];
    T* aux = new T[r-l+1];
    for(int i=l;i<=r;i++)
        aux[i-l] = arr[i];
    int i = l,j = mid + 1;
    for(int k=l;k<=r;k++){
        if(i>mid){ //第一个数组没有元素的时候 
            arr[k] = aux[j-l];
            j++;
        }
        else if(j>r){ //第二个数组没有元素的时候 
            arr[k] = aux[i-l];
            i++;
        }
        else if(aux[i-l]<aux[j-l]){
            arr[k] = aux[i-l];
            i++;
        }
        else{
            arr[k] = aux[j-l];
            j++;
        }
    } 
}

//递归使用归并排序,对arr[l...r]的范围进行排序
template<typename T>
void __mergeSort(T arr[],int l,int r){
//  if(l>=r)
//      return;
//为提高效率,这里当递归数组长度小于60时,转为插入排序,所有高级排序算法都可以这样,然后将数组一分为二,递归处理,这里仅当数组第一部分大于数组第二部分才合并处理,因为如果小于的话就已经是有序的了
    if(r-l<=60){
        insertSort(arr,l,r);
        return;
    }
    int mid = (l+r)/2;
    __mergeSort(arr,l,mid);
    __mergeSort(arr,mid+1,r);
    if(arr[mid]>arr[mid+1]) 
        __merge(arr,l,mid,r); 

} 

//入口,将其分为左右两个值传入子函数
template<typename T>
void mergeSort(T arr[],int n){
    __mergeSort(arr,0,n-1);
}

代码分析,首先把需要排序的数组复制到辅助空间,这里应该注意aux的偏移量,然后i为数组一的第一个元素,j为数组二的第二个元素,现在需要对数组一和数组二进行归并,所以遍历整个辅助数组,根据大小重新排列进arr数组里
注意,如果数组过大,使用T aux[r-l+1]这种方式声明,属于局部变量,存放在栈上,而栈的默认内存空间只有几M,过大的数组(如100W,大小1000000*4(int)=4M)会导致内存溢出,所以这里使用了T* aux = new T[r-l+1]开辟空间,以免出现溢出,它存放在堆里,大小取决于物理上的内存。也可以声明在外部作为全局变量,防止溢出。
算法原理:
分治法,可以是自上到下,自下到上,这里是自上到下递归实现,先把数组二分,然后分别排序,排序的时候又分,直到一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
根据归并排序的算法,还可以用来求逆序对、求数组中第n大的元素。

希尔排序

void shellSort(int a[], int n) {  
    int i, j, gap;  

    for (gap = n / 2; gap > 0; gap /= 2)  
        for (i = gap; i < n; i++)  
            for (j = i - gap; j >= 0 && a[j] > a[j + gap]; j -= gap)  
                swap(a[j], a[j + gap]);  
} 

算法原理:实质就是分组插入排序,先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

如:
第一次 gap = 10 / 2 = 5
49 38 65 97 26 13 27 49 55 4
49和13相比,然后38与27,65与49,直到末尾
第二次 gap = 5 / 2 = 2
13 27 49 55 4 49 38 65 97 26
13与49、4、38、97相比,然后27与55、49、65、26相比
第三次 gap = 2 / 2 = 1
4 26 13 27 38 49 49 55 97 65
每个比较
第四次 gap = 1 / 2 = 0 排序完成得到数组:
4 13 26 27 38 49 49 55 65 97

快速排序

随机化快排

这里写图片描述
快速排序的基本思路就是从当前数组中选择一个元素作为基点,如图所示,选择了4为基点,然后使小于4的数全在它左边,大于4的数全在它右边,然后对小于4的这部分和大于4的这部分分别继续使用快速排序的思路进行排序,逐渐递归下去,完成整个排序过程。

partition过程
这里写图片描述

代码实现

template<typename T>
int __partition(T arr[],int l,int r){
    //随机化一个基点
    swap(arr[rand()%(r-l+1)+l],arr[l]);
    T v = arr[l];
    int j = l;
    for(int i=l+1;i<=r;i++){
        if(arr[i]<v){
            swap(arr[j+1],arr[i]);
            j++;
        }
    }
    swap(arr[l],arr[j]);
    return j;
}
template<typename T>
void __quickSort(T arr[],int l,int r){
//  if(l>=r)
//      return;
//思路跟上面一样,当数据规模小于一定程度后使用基本排序算法
    if(r-l<=80){
        insertSort(arr,l,r);
        return;
    } 
    int p = __partition(arr,l,r);
    __quickSort(arr,l,p-1);
    __quickSort(arr,p+1,r);
} 
//入口,将其分为左右两个值传入子函数,并且设置随机数种子
template<typename T>
void quickSort(T arr[],int n){
    srand(time(NULL));
    __quickSort(arr,0,n-1);
}

代码分析,先将arr第一个元素与arr中随机一个元素交换,以免在近乎有序的情况下时间复杂度退化为O(n^2),这样交换之后,算法的时间复杂度为O(n^2)的几率就会大大降低了。然后将第一个元素设置为关键数据,for循环里从第二个元素开始遍历,直到最后一个元素,如果arr[i]小于关键元素将其交换,然后索引j++,for执行完成后,此时数组中位置是,arrl是第一个元素,接着是小于arr[l]的元素,然后是大于arr[l]的元素,注意到arr[j]是小于arr[l]的元素,arr[j+1]是大于arr[l]的元素,最后将arr[l],与arr[j]交换,一趟partition完成。
算法原理:快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用。快速排序思想是分治法。先从数列中取出一个数作为基准数,分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边,再对左右区间重复第二步,直到各区间只有一个数。

利用快排这种思想,可以在O(n)的时间复杂度求出一组数第n大的元素。。

算法改进:当我们排序的是一个近乎有序的序列时,快速排序会退化到一个O(n^2)级别的排序算法,而对此的改进就是引入了随机化快速排序算法;但是当我们排序的是一个数值重复率非常高的序列时,此时随机化快速排序算法就不再起作用了,而将会再次退化为一个O(n^2)级别的排序算法

这里写图片描述
如上图所示就是之前分析的快速排序算法的partition的操作原理,我们通过判断此时i索引指向的数组元素e>v还是<v,将他放在橙色或者是紫色两个不同的位置,然后将整个数组分成两个部分递归下去,但是这里其实我们是没有考虑=v的情况,当有大量重复元素的时候(e=v),无论是将这个元素放入小于v的区间里还是大于v的区间里,最终还是有可能会失衡
这里写图片描述
从这里可以看出,不管是>=v还是<=v,当我们的序列中存在大量重复的元素时,排序完成之后就会将整个数组序列分成两个极度不平衡的部分这样这个快速排序算法会退化成为O(n^2)级别,解决方法可以使用双路快排

双路快排

双路快排的partition
这里写图片描述

双路快速排序算法使用两个索引值(i、j)用来遍历我们的序列,将<v的元素放在索引i所指向位置的左边,而将>v的元素放在索引j所指向位置的右边
这样的partition操作可以将e=v的元素分散到了左右两部分

代码实现:

template<typename T>
int __partition2(T arr[],int l,int r){
    swap(arr[rand()%(r-l+1)+l],arr[l]);
    T v = arr[l];
    //arr[l+1…i)<=v;arr(j…r]>=v
    int i=l+1,j=r;
    while(true){
        while(i<=r&&arr[i]<v)i++;
        while(j>=l+1&&arr[j]>v)j--;
        if(i>j)break;
        swap(arr[i],arr[j]);
        i++;
        j--;
    } 
    swap(arr[l],arr[j]);
    return j;
}
template<typename T>
void __quickSort2(T arr[],int l,int r){
//  if(l>=r)
//      return;
    if(r-l<=80){
        insertSort(arr,l,r);
        return;
    } 
    int p = __partition2(arr,l,r);
    __quickSort2(arr,l,p-1);
    __quickSort2(arr,p+1,r);
} 
template<typename T>
void quickSort2(T arr[],int n){
    srand(time(NULL));
    __quickSort2(arr,0,n-1);
}

通过这个思路,我们可以进一步优化,提出三路快排的思想

三路快排

这里写图片描述

三路快排的基本思想就是,在对数据进行分区的时候分成左中右三个部分,中间都是相同的值,左侧小于中间,右侧大于中间。
三路快排的复杂度比普通快排小,主要取决于数据中重复数据的数量。重复数据越多,三路快排的复杂度就越接近于N。

代码实现:


template<typename T>
void __quickSort3(T arr[],int l,int r){
//  if(l>=r)
//      return;
    if(r-l<=80){
        insertSort(arr,l,r);
        return;
    }
    //partition 
    swap(arr[rand()%(r-l+1)+l],arr[l]);
    T v = arr[l];
    int lt = l;//arr[l+1lt]<v
    int gt = r+1;//arr[gt…r]>v
    int i = l+1;//arr[lt+1…i)==v
    while(i<gt){
        if(arr[i]<v){
            swap(arr[i],arr[lt+1]);
            lt++;
            i++;
        }else if(arr[i]>v){
            swap(arr[i],arr[gt-1]);
            gt--;
        }else{
            i++;
        }
    } 
    swap(arr[l],arr[lt]);
    __quickSort3(arr,l,lt-1);
    __quickSort3(arr,gt,r);
} 
template<typename T>
void quickSort3(T arr[],int n){
    srand(time(NULL));
    __quickSort3(arr,0,n-1);
}

堆排序

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值