❂排序总结❂


• 选择排序

• 简单选择排序

  如下图所示为简单选择排序一趟的过程,对于n个元素的序列A[0]~A[n-1]进行n趟操作。每一趟都是从待排序的部分选择最小的元素 与当前待排序序列的第一个元素进行交换,这样就形成了新的有序部分和待排序部分。
在这里插入图片描述
代码如下:

//简单选择排序  
void selectSort(int A[], int n){
    for(int i=0; i<n; i++){
        int min = i;
        //从待排序的部分中找到最小的元素,与A[i]交换
        for(int j=i+1; j<n; j++){
            if(A[j] < A[min]){
                min = j;
            }
        }
        if(min != i){
            swap(A[i], A[min]);
        }
    }
}

可见,简单选择排序的时间复杂度是O(n²),空间复杂度是O(1);并且简单选择排序是一种不稳定的排序算法。(如下图,元素4 和 4*的前后位置发生了变化)

在这里插入图片描述


• 堆排序

  学习堆排序之前,先明确一下什么是小根堆和大根堆:

堆通常是一个可以被看做一棵完全二叉树的。

小根堆:父节点的值小于孩子节点的值;用线性表来表示即为 A[i] ≤ A[2 * i] && A[i] ≤ A[2 * i +1];可见小根堆的根节点是整个序列中最小的值。
在这里插入图片描述
大根堆:父节点的值大于孩子节点的值;用线性表来表示即为A[i]≥A[2* i] && A[i]≥A[2* i +1];可见大根堆的根节点是整个序列中最大的值。
在这里插入图片描述
再了解了大根堆和小根堆的概念以后,我们来看如何初始化一个堆,这里我们以大根堆来举例子,小根堆同理,只需要改变符号即可。

  对于n个序列的堆A[1]~A[n],我们需要从后往前进行堆的初始化,但不是从最后一个元素开始,而是从 n/2(取下界)的位置开始调整。为什么呢?从图中可以看出若是我们想要初始化堆,只需要从最后一个非叶子节点开始进行堆的调整即可,而这个节点就是下标为n/2(取下界)的位置。

具体向下调整的过程,来看一个图解,这里我们假设从k=1的位置进行一趟向下调整(i>1的部分已经调整为大根堆):

  1. 首先用A[0]保存需要调整的值A[k],A[0] = 7;
  2. 令i=2*k找到它的左孩子,它一定是有左孩子的,若有右孩子且右孩子的值大于左孩子的值,则将i++(当前指向左孩子,自增后指向右孩子) ;此时有两种情况:①孩子节点中的最大值小于当前节点的值,说明当前节点已经为大根堆,不需要进行操作了;②孩子节点中的最大值大于当前节点的值,此时不满足大根堆的条件,需要进行调整,将A[k] = A[i],同时调整k为i,应继续判断是否接着向下调整,重复上面的过程;
  3. 最后将A[k] = A[0];即完成一趟向下调整。
    在这里插入图片描述
    堆的初始化代码如下:
//向下调整
void AdjustDown(int A[], int k, int n){
    A[0] = A[k];
    int i;
    for(i=2*k; i<=n; i*=2){
        if(i<n && A[i+1] > A[i]){
            i++;
        }
        if(A[0] > A[i]) {
            break;
        }
        else{
            A[k] = A[i];
            k=i;
        }
    }
    A[k] = A[0];
}
//堆的初始化(大堆)
void heapInit(int A[], int n){
    for(int i=n/2; i>0; i--){
        AdjustDown(A, i, n);
    }
}

明白了堆的初始化,那么堆排序就已经很轻松了,我们这里还是以大堆为例。首先对序列初始化为一个大根堆,大根堆的第一个元素即为该序列中元素的最大值,我们那它与最后一个元素交换,然后对第一个元素进行向下调整(不对已经排好序的元素再次调整,即向下调整的时候忽略后面已经有序的序列),重复此过程,即为堆排序。
下图为一个完整的堆排序过程。
在这里插入图片描述
代码如下:

//向下调整
void AdjustDown(int A[], int k, int n){
    A[0] = A[k];
    int i;
    for(i=2*k; i<=n; i*=2){
        if(i<n && A[i+1] > A[i]){
            i++;
        }
        if(A[0] > A[i]) {
            break;
        }
        else{
            A[k] = A[i];
            k=i;
        }
    }
    A[k] = A[0];
}
//堆的初始化(大堆)
void heapInit(int A[], int n){
    for(int i=n/2; i>0; i--){
        AdjustDown(A, i, n);
    }
}
//堆排序
void heapSort(int A[], int n){
    heapInit(A, n);
    for(int i=n; i>1; i--){
        swap(A[1], A[i]);
        AdjustDown(A, 1, i-1);
    }
}

堆排序时间复杂度是O(nlog2^n);
堆排序的空间复杂度为O(1);
堆排序是一种不稳定的排序算法。


• 交换排序

• 冒泡排序

  顾名思义,冒泡排序就是向金鱼吐泡泡一样,每次将最大泡泡浮到最顶端。对于n个元素的序列,冒泡排序需要进行n-1趟,每趟冒泡排序从第一个元素开始,依次比较相邻两个元素的大小,若前面的值大于后面的值,则进行交换。每趟冒泡都会将一个值放到自己的位置上。
在这里插入图片描述
代码如下:

//冒泡排序
void bubbleSort(int A[], int n){
    for(int i=0; i<n-1; i++){
        bool flag = false;//标志位
        for(int j=1; j<n-i; j++){
            if(A[j-1] > A[j]){
                swap(A[j-1], A[j]);
                flag = true;
            }
        }
        //如果本次冒泡排序未进行交换,说明已经有序,直接返回
        if(flag == false){
            return;
        }
    }
}

冒泡排序排序最好的时间复杂度是O(n);
冒泡排序最坏的时间复杂度是O(n²);
冒泡排序平均的时间复杂度是O(n²);
空间复杂度为O(1);
冒泡排序是一种稳定的排序算法。


• 快速排序

  快速排序是对冒泡排序的一种改进。一趟快速排序的基本思想是:首先要确立一个中心点(pivot),将该序列划分为两个部分,一部分中的所有元素都比pivot值小,另一部分中的所有元素的值都比pivot大。
  我们用low来表示序列的一个元素的下标,high来表示最后一个元素的下标;这里我们为了方便起见,选择每次需要排序的第一个元素(下标low的元素)保存到pivot中,然后先从后先前找,找到第一个比pivot值小的元素放在low的位置处;再从前往后找,找到第一个比pivot值大的元素,将它放到high位置处;重复这个过程,直到low与high最终指向同一个位置,即pivot应该存放的位置。见图解,即为一趟快速排序的过程。
在这里插入图片描述可见,每趟快排的划分都会将一个元素(pivot)确定在它最终的位置上。之后再通过递归的方式将划分出来的两部分继续进行快排。
代码如下:

//快速排序的一趟(快排的主体部分)
int Partition(int A[], int low ,int high){
    int pivot = A[low];
    while(low < high){
        //从后往前
        while(low < high && A[high] >= pivot){
            high--;
        }
        A[low] = A[high];
        //从前往后
        while(low < high && A[low] <= pivot){
            low++;
        }
        A[high] = A[low];
    }
    A[high] = pivot;
    return high;//返回已经确定的位子的下标
}

//快速排序
void quickSort(int A[], int low, int high){
    if(low < high){
        int mid = Partition(A, low, high);
        //递归
        quickSort(A, low, mid-1);
        quickSort(A, mid+1, high);
    }
}

快速排序最好的时间复杂度是O(nlog2^n);
快速排序平均的时间复杂度是O(nlog2^n);
快速排序最坏的时间复杂度是O(n²);

快速排序最好的空间复杂度为O(log2^n);
快速排序平均的空间复杂度为O(log2^n);
快速排序最坏的空间复杂度为O(n);

快速排序是一种不稳定的排序算法(如图可见)。
在这里插入图片描述


• 插入排序

• 直接插入排序

  对n个元素的序列A[1]~A[n],进行n-1趟排序。插入排序从第二个元素A[2]开始进行,每一趟插入排序的过程是:先将此时需要插入的元素A[i]保存到哨兵当中,然后在已有序部分进行查找,为了保证元素后移的时候不被覆盖掉,这里我们从有序部分的后面向前查找,若有序部分的元素大于哨兵中的值,则将这个元素后移,直到找到合适的位子,再将需要插入的元素放进去即可。
  这里设置哨兵不仅能存放当前需要插入元素的值,以免元素后移时将它覆盖;还可以作为判断的条件,因为我们再后移的时候总是要求比该元素大的时候移动,这样即使比较到了A[0]的位置,由于哨兵中存放的与我们需要插入的元素相等,因此可以直接作为判断的条件,而不用判断是否比较到第一个节点。
在这里插入图片描述代码如下:

// 直接插入排序
void insertSort(int A[], int n){
    int i,j;
    for(i=2; i<=n; i++){
        A[0] = A[i];//A[0]为哨兵
        j=i-1;
        while(A[j] > A[0]){
            A[j+1] = A[j];
            j--;
        }
        A[j+1] = A[0];
    }
    
}

直接插入排序最好的时间复杂度是O(n); 因为当序列为正序的时候不需要移动元素。
直接插入排序最坏的时间复杂度是O(n²);
直接插入排序平均的时间复杂度是O(n²);
空间复杂度为O(1); 并且直接插入排序是一种稳定的排序算法。


• 折半插入排序

  顾名思义,就是在插入排序的过程中,使用二分查找来更快速地定位到该插入的位子,其余部分与插入排序一样,就不赘述了。

代码如下:

//折半插入排序
void BinsertSort(int A[], int n){
    int i,j;
    for(i=2; i<=n; i++){
        A[0] = A[i];
        int low=1, high=i-1;
        while(low <= high){
            int mid= (low+high)/2;
            if(A[mid] > A[0]){
                high = mid-1;
            }
            else{
                low = mid+1;
            }
        }
        for(j=i-1; j>=high; j--){
            A[j+1] = A[j];
        }
        A[high+1] = A[0];
    }
}

折半插入排序的时间复杂度是O(n²);
折半插入排序的空间复杂度是O(1);

并且折半插入排序是一种稳定的排序算法。


• 希尔排序

  希尔排序又称为缩小增量排序,希尔排序的思想是把待排序的序列一个步长d 分为一组,先进行组内的插入排序,然后逐步缩小步长d,当整个表中的元素基本有序时,再对整体进行一次插入排序。
在这里插入图片描述代码如下:

//希尔排序
void shellSort(int A[], int n){
    for(int d=n/2; d>=1; d=d/2){
        int i,j;
        //分组内直接插入排序
        for(i=d+1; i<=n; i++){
            if(A[i] < A[i-d]){
                A[0] = A[i];//哨兵
                for(j=i-d; j>0 && A[0]<A[j]; j-=d){
                    A[j+d] = A[j];
                }
                A[j+d] = A[0];
            }
        }
    }
}

希尔排序的时间复杂度是O(n^1.3);
希尔排序的空间复杂度是O(1);
希尔排序是一个不稳定的排序算法(如下图所示)。
在这里插入图片描述


• 归并排序

归并排序是一种稳定的排序方法。

//合并两个有序的线性表(这里把一个线性表分为前后两个部分,来进行合并)
//low,mid,high三个下标将线性表分为两部分【low~mid】【mid+1~high】
void Merge(int A[], int low, int mid, int high){
    //1.申请一个与A数组大小相等的辅助数组B(下标为0不使用)
    int n = high-low+1;
    int* B = new int[n];
    //2.将A中的数据按下标复制过去
    for(int i=low; i<=high; i++){
        B[i] = A[i];
    }
    //3.合并
    int i,j,k;
    for(i=low,j=mid+1,k=low; i<=mid && j<=high; k++){
        if(B[i]<B[j]){
            A[k] = B[i++];
        }
        else{
            A[k] = B[j++];
        }
    }
    //4.将剩余的直接赋过去
    while(i<=mid){
        A[k++] = B[i++];
    }
    while(j<=high){
        A[k++] = B[j++];
    }
}


//归并排序
void MergeSort(int A[], int low, int high){
    if(low<high) {
        int mid = (low+high)/2;
        MergeSort(A, low, mid);
        MergeSort(A, mid+1, high);
        Merge(A, low, mid, high);
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值