排序算法大总结

这篇博客总结了多种排序算法,包括选择排序、插入排序(基础版与改进版)、冒泡排序和希尔排序,这些算法的时间复杂度均为O(n^2)。接着介绍了归并排序,它是O(nlogn)级别的算法,通过递归与合并操作实现排序。文章还讨论了不同排序算法在处理近乎有序数组时的效率差异,并提出了在特定情况下采用插入排序或改进版归并排序的优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

选择排序

思路:

从最左边的元素开始,逐个遍历,找到这一趟中的最小值,存到minIndex里面,最后这一趟结束,将它和这一趟开始遍历的位置i上的元素swap,直到最后一个位置上也就位。相当于选择出每一趟最小的元素,然后把它放到这一趟开始的地方。

基础版:
void selectionSort(int arr[], int n){

    for(int i = 0 ; i < n ; i ++){
        // 寻找[i, n)区间里的最小值
        int minIndex = i;
        for( int j = i + 1 ; j < n ; j ++ )
            if( arr[j] < arr[minIndex] )
                minIndex = j;

        swap( arr[i] , arr[minIndex] );
    }

}

插入排序

思路:

由于第一个元素可以默认为已排好序,因此可以从第二个元素开始遍历,每一趟使一个元素正确的插入到前面已排好序的数组中。如果这个元素比它前面的元素小,则swap一下,如果大于等于它前面的元素,说明它以及它前面的元素已经排好序了,这一趟结束。

插入排序和选择排序一个很大的区别就是插入排序每一趟有可能提前结束,即它只要到达一个位置大于等于它前面的元素,那么这一趟就已经排好了;而选择排序每一趟必须遍历完这一趟所有的元素,找到最小的那个,然后再把最小的那个和这一趟开始的元素进行swap,是不能提前结束的。

对于近乎有序的数组,适合用插入排序。
对于完全有序的数组,插入排序复杂度为O(n)O(n)

基础版:
void insertionSort(T arr[], int n){

    for( int i = 1 ; i < n ; i ++ ) {

        // 寻找元素arr[i]合适的插入位置
        // 写法1
        for( int j = i ; j > 0 ; j-- ) //注意这里j遍历到下标为1的位置就好了,因为下面是将位置j和位置j-1的元素进行比较
            if( arr[j] < arr[j-1] )
                swap( arr[j] , arr[j-1] );
            else
                break;

        // 写法2
       // for( int j = i ; j > 0 && arr[j] < arr[j-1] ; j -- )
       //     swap( arr[j] , arr[j-1] );

    }

    return;
}
改进版:

将原本一次次的swap操作,变成一次次比较,而在一趟的最后进行赋值。

插入排序算法针对完全排好序的数组是O(n)级别的算法,每次判断一下当前数就是正确位置,这一趟就退出了

改进后的插入排序避免了很多次unnecessary copy,由此非常适合基本排好序而不需要很多次变动的数组。

void insertionSort(T arr[], int n){

    for( int i = 1 ; i < n ; i ++ ) {
        // 写法3
        T e = arr[i];// 声明一个变量保存要插入的元素
        int j; // j保存元素e应该插入的位置
        for (j = i; j > 0 && arr[j-1] > e; j--)//如果这个元素前面一个元素比它要大且未遍历到边界
            arr[j] = arr[j-1];把这个位置前面一个元素往这个位置挪
        arr[j] = e;//找到位置,赋值
    }

    return;
}

冒泡排序

思路:

i从最左边的元素开始遍历,一共要遍历n-1趟,因为第n-1趟遍历完成后,最后一个元素也就位了。在每一趟里面,j用来从第二个元素开始遍历到这一趟的最后一个元素,然后将它与它前面的元素比较,如果比前面的元素小就交换,然后j++;如果大于等于前面的元素就不交换,然后j++。直到遍历到这一趟的最后一个元素为止。
如,
第一趟:i = 0, j下标从1遍历到n-1,最后一个元素就位;
第二趟:i = 1, j从1遍历到n-2,最后一个元素就位

第m趟:i = m-1, j从1遍历到n-1-i

第n-1趟:i = n-2, j从1遍历到n-1-n+2=1

基础版:
void bubbleSort(T arr[], int n){
    for(int i = 0; i < n-1 ; i++){ // n-1趟即可
        for(int j = 1; j < n-i; j++){
            if(arr[j] < arr[j-1])
                swap(arr[j], arr[j-1]);
        }
    }
}

希尔排序

思路:

希尔排序的思路实际上是插入排序的延伸。插入排序中每一次都和之前的一个元素比较,而希尔排序每一次对之前的第h个元素进行比较,这样通过将h一个很大的值,逐渐变成一个很小的值,直到等于1,此时整个数组也排好了。

基础版:
void shellSort(T arr[], int n){

    // 计算 increment sequence: 1, 4, 13, 40, 121, 364, 1093...
    int h = 1;
    while( h < n/3 )
        h = 3 * h + 1;

    while( h >= 1 ){

        // h-sort the array
        for( int i = h ; i < n ; i ++ ){

            // 对 arr[i], arr[i-h], arr[i-2*h], arr[i-3*h]... 使用插入排序
            T e = arr[i];
            int j;
            for( j = i ; j >= h && e < arr[j-h] ; j -= h )
                arr[j] = arr[j-h];
            arr[j] = e;
        }

        h /= 3;
    }
}

// 比较SelectionSort, InsertionSort和BubbleSort和ShellSort四种排序算法的性能效率
// ShellSort是这四种排序算法中性能最优的排序算法
上面的排序算法都是O(n2)O(n2)级别的算法,下面介绍的归并排序和快速排序是O(nlogn)O(nlogn)级别的,那么O(n2)O(n2)O(nlogn)O(nlogn)有什么不同呢?

这里写图片描述

归并排序

思路:

归并排序利用递归的思想,把数组分成两组,而后再在组内再分成两组,以此类推直到每组只有一个元素,此时每组都是排好序的。之后再两组两组进行merge操作。
需要申请额外空间,使得归并的时候时间复杂度是O(n)。
注意数组越界和范围定义的问题。

基础版:
template<typename T>

void mergeSort(T arr[], int l, int r){ //递归使用归并排序,对arr[l...r]范围进行排序

    if(l >= r)
        return;

    int mid = l + (r-l)/2;
    mergeSort(arr,l,mid);
    mergeSort(arr,mid+1,r);

    merge(arr, l, mid, r);
}

void merge(T arr[], int l, int mid, int r){ // 将arr[l...mid]和arr[mid+1,r]两部分进行归并

    T aux[r-l+1];
    for(int i = l ; i <= r; i++)
        aux[i-l] = arr[i]; //aux数组和arr数组的偏移量是l

    int i =  l, j = mid + 1;
    for(int k = l; k <= r; k++){

        //要考虑数组越界:越界情况是i>mid和j>r
        if(i > mid){
            arr[k] = aux[j-l];
            j++;
        }
        else if ( j > r){
            arr[k] = aux[i-l];
            i++;
        }

        //都没有越界的情况
        if(aux[i-l] < aux[j-l]){
            arr[k] = aux[i-l];
            i++;
        }

        else{
            arr[k] = aux[j-l];
            j++;
        }
    }
}
改进版:
  1. 对于近乎有序的数组,适合用插入排序,此时用归并排序反而效率很低,因此我们要对它进行优化。在mergeSort函数里,当两个子数组都sort了之后,且arr[mid]<=arr[mid+1],则无需进行merge操作,因为arr[l…mid]和arr[mid+1…r]都已经排好序了,而arr[mid]又小于等于arr[mid+1],此时整个数组就已经排好序了。在代码中只需加一行判断即可。
void mergeSort(T arr[], int l, int r){

    if(l >= r)
        return;
    int mid = l + (r-l)/2;

    mergeSort(arr, l, mid);
    mergeSort(arr, mid+1, r);

    if(arr[mid] > arr[mid+1])//merge之前判断
        merge(arr,l,mid,r);
}
  1. 当subarray比较小的时候,我们可以转而使用插入排序而非归并排序来提高性能,这是因为此时很大概率这个subarray是近乎有序的数组。
void mergeSort(T arr[], int l, int r){

    if(r - l <= 15){//数组元素少的时候采用插入排序
        insertionSort(arr, l, r);
        return;
    }
    int mid = l + (r-l)/2;

    mergeSort(arr, l, mid);
    mergeSort(arr, mid+1, r);

    if(arr[mid] > arr[mid+1])//merge之前判断
        merge(arr,l,mid,r);
}

之前的插入排序是从0…n-1进行排序的,这里的插入排序是从l到r进行排序的,细节上略有不同:

void insertionSort(T arr[], int l, int r){

    for(int i = l+1; i <= r; i++){
        T e = arr[i];
        int j;
        for(j = i; j > l && arr[j-1] > e; j--)
            arr[j] = arr[j-1];
        arr[j] = e;
    }
    return;
}

总结

这里写图片描述
需要注意的是,快速排序的空间复杂度是O(logn)O(logn),这是因为它使用递归需要开辟额外的栈空间来存储上一状态,递归的次数是lognlogn级别的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值