zoukankan      html  css  js  c++  java
  • 数据结构与算法(十二)——排序算法

    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/
  • 相关阅读:
    websocket
    svg vs canvas
    nw
    web sql
    web worker
    【转载】磁盘阵列详解
    【识记】开源软件系列
    【转载】从1.5K到18K 一个程序员的5年成长之路
    SQL中in和not in
    SQL Server select count(distinct *)
  • 原文地址:https://www.cnblogs.com/iwehdio/p/14193265.html
Copyright © 2011-2022 走看看