数据结构与算法(十二)——排序算法

iwehdio的博客园:https://www.cnblogs.com/iwehdio/

1、快排

  • 快速排序:

    • 将子序列划分为两个部分S1、S2,并且满足S1中最大值小于S2中最小值。

    • 这样,在子序列分别递归的排序之后,原序列自然有序。

    • 轴点:左 / 右侧的元素,均不比它更大 / 小。

    • 轴点自然的将序列分为了符合之前条件的两个部分,剩下的部分只需递归完成即可。

      template <typename T> 
      void Vector<T>::quickSort ( Rank lo, Rank hi ) { 
         if ( hi - lo < 2 ) return; //单元素区间自然有序,否则...
         Rank mi = partition ( lo, hi ); //在[lo, hi)内构造轴点
         quickSort ( lo, mi ); //对前缀递归排序
         quickSort ( mi + 1, hi ); //对后缀递归排序
      }
      
      
      
    • 但是,轴点在原始序列中未必存在。轴点存在的必要条件是,轴点在序列中所在的位置必定是其排序后所在的位置(就位)。

    • 在有序序列中,所有元素都是轴点。因此快排就是将所有元素逐个转换为轴点的过程。通过适当的交换,可使任一元素转换为轴点。

    • 轴点的构造:

      • 以首元素m为轴点为例,使用lo和hi两个指针,将序列分为三部分。L是从前往后的小于等于m的部分,G是从后往前的大于等于m的部分,U是未扫描的部分。lo和hi指向U的边界。

      • lo和hi向中间扫描,最终重合的位置就是m作为轴点所在的位置。

      • 具体的,在算法进行的开始,先去除m并备份。这样原来m的位置就空闲了,并由lo指向。

      • 然后hi开始扫描,遇到大于等于m的就越过,遇到小于m的就将其移至lo指向的空闲位置。然后改为lo扫描,hi空闲。

      • 最后扫描完成后,将备份的m移入空闲位置。

    • 时间复杂度最好O(nlog n),最坏O(n^2),平均O(nlog n)。

  • 快速排序的变种:

    • 同样的将整个序列分为四部分,只不过待处理部分U在最后端,而P也不需要取出备份。
    • k指针从头后移,遇到大于等于轴点的元素就只后移,相当于将该元素归入G。遇到小于轴点的元素,则与G中的首元素交换位置,相当于将该元素归入L,然后再后移。
    • 最后U的规模为0时,将位于序列首端的轴点P与L中的最后一个元素交换位置。

    • 实现:

      template <typename T> //轴点构造算法:通过调整元素位置构造区间[lo, hi)的轴点,并返回其秩
      Rank Vector<T>::partition ( Rank lo, Rank hi ) { //版本C
         swap ( _elem[lo], _elem[ lo + rand() % ( hi - lo ) ] ); //任选一个元素与首元素交换
         T pivot = _elem[lo]; //以首元素为候选轴点——经以上交换,等效于随机选取
         int mi = lo;
         for ( int k = lo + 1; k < hi; k++ ) //自左向右扫描
            if ( _elem[k] < pivot ) //若当前元素_elem[k]小于pivot,则
               swap ( _elem[++mi], _elem[k] ); //将_elem[k]交换至原mi之后,使L子序列向右扩展
         swap ( _elem[lo], _elem[mi] ); //候选轴点归位
         return mi; //返回轴点的秩
      }
      

2、选取

  • 众数:这里指的是出现次数超过序列长度一半以上的元素。

  • 这种定义下,如果存在众数,则其必然也是中位数。

  • 减而治之:

    • 若在向量A的前缀P(P长度为偶数)中,元素x出现的次数恰占半数,则A有众数仅当,对应的后缀A –Р有众数m,且m就是A的众数。
    • 若x= m,则在排除前缀P之后,m与其它元素在数量上的差距保持不变。
    • 若x ≠ m,则在排除前缀Р之后,m与其它元素在数量上的差距不致缩小。

  • 实现:

    • 用一个计数器记录众数的候选者与其他元素的数量差。
    template <typename T> T majEleCandidate ( Vector<T> A ) { //选出具备必要条件的众数候选者
       T maj; //众数候选者
    // 线性扫描:借助计数器c,记录maj与其它元素的数量差额
       for ( int c = 0, i = 0; i < A.size(); i++ )
          if ( 0 == c ) { //每当c归零,都意味着此时的前缀P可以剪除
             maj = A[i]; c = 1; //众数候选者改为新的当前元素
          } else //否则
             maj == A[i] ? c++ : c--; //相应地更新差额计数器
       return maj; //至此,原向量的众数若存在,则只能是maj —— 尽管反之不然
    }
    
  • 选取的通用算法:

    • 选取的目标是,取出序列中经排序后秩为k的元素。

    • quickSelect():

      • 借鉴快排的思想,如果轴点的秩恰好为k,则直接返回。
      • 如果轴点的秩不是k,但可通过与轴点的秩比较,得出k是在L中还是在G中。

    • linearSelect():

      • 设定一个常量Q,当序列A短于Q时,则为递归基,使用平凡的选取算法(比如排序)。
      • 否则,将序列分为n/Q个长为Q的序列。
      • 对于每个序列进行排序,并选出各自的中位数。递归的选取出这些中位数的中位数,记为M。
      • 将整个序列分为,L:小于M的;E:等于M的;G:大于M的。
      • 如果目标的秩k落在L或G,则递归,若落在E则返回M。

      • 对于M,至少各有n/4个元素,不小于 / 不大于M。即白色部分占n/4,且必然大于等于M,黑色部分占n/4,且必然小于等于M。

      • 时间复杂度与Q的取值有关,至少应大于4。时间复杂度O(n)。

3、希尔排序

  • 希尔排序:
    • 将整个序列视为一个矩阵,逐列各自排序。矩阵的宽度逐渐缩减,最后变成一列。对于分为h列的排序,称为h-sorted。
    • 需要一个步长序列,存储了矩阵的宽度变化。
    • 对于每一列的排序算法,需要使用输入敏感的算法,以保证在有序性持续改善的情况下,计算成本较低。
    • 有序性可以通过逆序对的数量表征。
    • shell序列:
      • 从1开始,以2为比的等比数列。
      • 因为除了第一项都是偶数,因此两次分列时没有进行元素的交换。
      • 应该尽量使得相邻的列数互质。
    • 如果一个序列中有,s[i]<=s[i+g],则称为这个序列为g-ordered。
    • 已经证明,对于一个g-ordered序列,经过h-sorted后,依然是g-ordered的。
    • 这就说明了希尔排序在不同宽度的列进行排序后,会成为所有矩阵宽度的h-ordered。
    • 而且所有h-ordered的序列,都是可以线性组合的。
    • 又因为,对于互质的数g和h,其无法完成线性组合的最大值为gh-g-h。
    • 因此对于一个h-ordered和g-ordered序列,s[i]的逆序对只可能存在于其后的(g-1)(h-1)个位置内。这个范围会在排序过程中逐渐缩减。最终用于插入排序的序列逆序对很少。

4、十大排序算法

  • 学习自:十大经典排序算法

  • 排序算法分类:

    • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlog n),因此也称为非线性时间比较类排序。
    • 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

  • 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面。

  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

冒泡排序Bubble

  • 描述:

    • 比较相邻的两个元素,如果后边的元素较大则交换位置。
    • 从前往后扫描,n次扫描后这样最大的n个元素必然位于最后。
    • 设置一个标志位,如果这次扫描没有发生交换则说明排序完成。
    • 用一个递增变量i来记录扫描次数,不扫描最后的有序部分。
  • 实现:

    int temp;
    for (int i = 0; i < arr.length; i++) {
        for (int j = 0; j < arr.length-1-i; j++) {
            if (arr[j] > arr[j+1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    
    //-----------------------------------
    
    boolean flag = true;   //用于在没有发生交换时提前退出
    int j=arr.length;       //用于每次扫描不遍历有序部分
    int temp;
    while (flag) {
        flag = false;
        for (int i = 0; i < j-1; i++) {
            if (arr[i] > arr[i+1]) {
                temp = arr[i];
                arr[i] = arr[i+1];
                arr[i+1] = temp;
                flag = true;
            }
        }
        j--;
      }
    

选择排序Selection

  • 描述:

    • 以有序序列在尾部为例(这样每次扫描从头开始),每次选择无序部分最大的元素,存放在有序部分的最后。
    • 用一个指针记录有序序列下一个插入位置,另一个指针进行扫描。
  • 实现:

    int max;        //记录最大值及其位置
    int index;
    for (int i = arr.length-1; i >=0; i--) {     //从后往前,插入位置
        max = arr[i]; index = i;
        for (int j = 0; j <= i; j++) {       //从前往后扫描
            if (max < arr[j]){
                max = arr[j];
                index = j;
            }
        }
        arr[index] = arr[i];
        arr[i] = max;
    }
    

插入排序Insertion

  • 描述:

    • 以有序序列在前为例,从前往后扫描,将元素插入到有序序列中的合适位置。
    • 该元素从后往前与有序序列中的元素比较,如该元素较小则有序序列中的元素后移一位。
  • 实现:

    int j;
    int temp;
    for (int i = 1; i < arr.length; i++) {      //从前往后扫描
        temp = arr[i];
        j = i;
        while (j>0 && temp < arr[j-1]) {      //从有序序列最后i-1开始,j是插入位置
            arr[j] = arr[j-1];
            j--;
        }
        arr[j]=temp;
    }
    

希尔排序Shell

  • 描述:

    • 相当于对不同的间隔的元素序列进行排序,具体进行排序的算法可以为插入排序。
    • 每一个间隔从gap开始,相当于插入排序从1开始。遍历一遍是对不同分组交替插入。
  • 实现:

    int temp;
    int j;
    //gap是间隔(矩阵宽度),也可以作为输入的其他值。
    for (int gap = (int) Math.floor(arr.length/2); gap > 0; gap= (int) Math.floor(gap/2)) {
        //从gap开始(矩阵中这一列的第二个)开始向后扫描。
        // 对间隔为gap的序列进行插入排序。事实上每次+1前后进行的是不同分组的排序。
        for (int i = gap; i < arr.length; i++) {
            j = i;              //j是插入位置
            temp=arr[i];
            while (j-gap>=0 && temp < arr[j-gap]){
                arr[j] = arr[j-gap];
                j-=gap;
            }
            arr[j] = temp;
        }
    }
    

归并排序Merge

  • 描述:

    • 单个元素自然有序,将两个有序序列合并为一个有序序列。
    • 进行归并时,可以只复制前半部分的数组,后半部分使用原来的数组空间。
  • 实现:

    static void mergeSort(int[] arr, int lo, int hi) {
        if (hi - lo == 0) {
            return;
        }
        int mid = (lo + hi) / 2;
        mergeSort(arr, lo, mid);
        mergeSort(arr, mid+1, hi);
        merge(arr, lo, mid, hi);
    }
    
    static void merge(int[] arr,int lo, int mid, int hi) {
        int[] temp = new int[mid-lo+1];     //只复制数组的前半部分
        for (int i=lo; i<=mid; i++) {
            temp[i-lo] = arr[i];
        }
        int i=0; int j=mid+1; int k=lo;
        while (i<=mid-lo && j<=hi) {
            if (temp[i]<arr[j]){
                arr[k++]=temp[i++];
            } else {
                arr[k++]=arr[j++];
            }
        }
        if (j>hi) {                     //如果是前半部分先排列完毕,则后半部分自然有序
            while (i<=mid-lo) {
                arr[k++]=temp[i++];
            }
        }
    }
    

快速排序Quick

  • 描述:

    • 用一个轴点将序列分为两部分,左侧的都比轴点小,右侧的都比轴点大。
    • mid指向小于轴点分区中的最后一个元素,所以插入前需要+1。k指向当前扫描元素。
    • 在不同的算法中,hi可能有不同的含义(末元素或尾哨兵),看边界是否-1,在这里是尾哨兵。
  • 实现:

    static void quickSort(int[] arr, int lo, int hi) {
        if (hi - lo <= 1) {         //这里的hi是末元素的下一个,也就是结尾哨兵
            return;
        }
        int mid = partition(arr, lo, hi);
        quickSort(arr, lo, mid);
        quickSort(arr, mid+1, hi);
    }
    
    static int partition(int[] arr,int lo, int hi) {
        int mid = new Random().nextInt(hi-lo) + lo;     //随机选取轴点
        swap(arr, lo, mid);
        mid = lo;
        for (int k = lo+1; k <hi ; k++) {
            if (arr[k] < arr[lo]) {
                swap(arr, ++mid, k);
            }
        }
        swap(arr, lo, mid);
        return mid;
    }
    
    static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    

堆排序Heap

  • 描述:

    • 使用对叶子节点自下而上的下滤快速建堆。
    • 对于向量组织的堆,父节点秩为i,则子节点秩为2i+1和2i+2。
    • 每次取出栈顶元素后,将其放在有序序列的最前。同时维护堆序性。
  • 实现:

    static void heapSort(int[] arr, int lo, int hi) {
        buildHeap(arr, lo, hi);
        for (int i = hi -1; i>lo; i--) {
            swap(arr, lo, i);
            percolateDown(arr, lo, lo, i);      //取出堆顶元素
        }
    }
    
    static void buildHeap(int[] arr, int lo, int hi) {      //快速建堆
        for (int i = (int) Math.floor((hi-lo)/2); i >=0; i--) {     //从非叶子节点开始自下而上的下滤
            percolateDown(arr, i, lo, hi);
        }
    }
    
    static void percolateDown(int[] arr, int parent, int lo, int hi) {  //下滤
        int left = lo + (parent-lo) * 2 + 1;
        int right = lo + (parent-lo+1) * 2;
        int choice = parent;
        if (left < hi && arr[left] > arr[choice]) {
            choice = left;
        }
        if (right < hi && arr[right] > arr[choice]) {
            choice = right;
        }
        if (choice != parent) {
            swap(arr, choice, parent);
            percolateDown(arr, choice, lo, hi);     //如果更新了,继续考察下滤后的节点
        }
    }
    
    static void swap(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }
    

计数排序Counting

  • 描述:

    • 将输入的数值存储在新的数组空间中。索引为数值的大小,值为该数值出现的次数。
    • 在排序前需要知道数组中数值的大小范围。
    • 映射完成后,再反向填充原数组。
  • 实现:

    int[] count = new int[maxNum-minNum+1];     //需要知道数组的数值范围
    for (int i = 0; i < arr.length; i++) {
        count[arr[i]-minNum]++;     //计数
    }
    int j = 0;
    for (int i = minNum; i <= maxNum; i++) {
        while (count[i-minNum]-- > 0)
            arr[j++] = i;           //反向填充
    }
    

桶排序Bucket

  • 描述:

    • 桶排序是计数排序的升级版。不简单使用数值而是使用一个映射函数生成索引。
    • 在每个索引的空间称为一个桶,在一个桶中再进行排序。
    • 最后把不是空的桶中的数据拼接起来。
  • 实现:

    int bucketSize = 5;         //每个桶中存储的范围
    int bucketCount = (int) Math.floor((maxNum-minNum)/bucketSize)+1;   //桶的个数,也可以用其他映射函数,保证大小次序不变即可
    LinkedList<Integer>[] bucket = new LinkedList[bucketCount];
    
    int index;
    for (int i = 0; i < arr.length; i++) {
        index = (int) Math.floor((arr[i]-minNum)/bucketSize);       //装入桶中
        if (bucket[index]==null) {
            bucket[index] = new LinkedList<>();
        }
        bucket[index].add(arr[i]);
    }
    int j = 0;
    for (int i = 0; i < bucketCount; i++) {
        if (bucket[i]!=null){
            insertSort(bucket[i]);              //桶中元素排序
            for (Integer ele : bucket[i]) {     //输出有序序列
                arr[j++] = ele;
            }
        }
    }
    

基数排序Radix

  • 描述:

    • 先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
    • 对整数排序可按从低往高位上的数字大小作为优先级。可以取除以其他数,能覆盖元素范围即可。
    • 对于每个桶,添加元素与取出元素的位置应该相反。如从尾部添加,就从头部取出,不然会破坏顺序。
  • 实现:

    int dev = 1; int mul = 10;          //所要除的是哪一位;进制数。这里取某一位上的大小为优先级
    int maxpriority = 2;        //这里取十进制的个位和十位,选取时能覆盖数组中的元素即可
    int bucketCount = mul;      //桶的个数
    LinkedList<Integer>[] bucket = new LinkedList[bucketCount];
    int index;
    for (int i=0; i<maxpriority; dev*=mul,i++){         //每个优先级遍历一遍
        for (int j = 0; j < arr.length; j++) {
            index = (arr[j] % (dev*mul))/dev;       //取余得到优先级
            if (bucket[index]==null){
                bucket[index] = new LinkedList<>();
            }
            bucket[index].add(arr[j]);
        }
        int k=0;
        for (int j = 0; j < bucket.length; j++) {      //计数排序
            if (bucket[j]!=null) {
                while (!bucket[j].isEmpty()) {
                    arr[k++] = bucket[j].pop();     //注意这里与上边添加元素的位置相反
                }
            }
        }
    }
    

iwehdio的博客园:https://www.cnblogs.com/iwehdio/
原文地址:https://www.cnblogs.com/iwehdio/p/14193265.html