排序算法的实现及时间复杂度分析——计数排序、选择排序、冒泡排序、插入排序

文章目录

    • 排序算法
    • 计数排序
    • 选择排序
    • 冒泡排序
    • 插入排序

排序算法

排序算法是解决问题中常见且非常重要的一环,针对相应的问题选择相应的排序算法,能够提高问题解决速度,节省时间!!!

常见的排序算法有:

排序算法 关键步骤 时间复杂性
最好 最坏
计数排序 比较 n(n - 1)/2 + n n(n - 1)/2 + 2n - 1
移动 0 6(n - 1)
选择排序(及时终止的) 比较 n - 1 n(n - 1)/2
移动 3 3(n - 1)
冒泡排序(及时终止的) 比较 n - 1 n(n - 1)/2
移动 0 3n(n - 1)/2
插入排序 比较 n - 1 n(n - 1)/2
移动 2(n - 1) 2(n - 1) + n(n - 1)/2

下面就这些排序算法进行实现及分析。

计数排序

  1. 主要思路及实现
    1. 步骤一:先对数组中的元素计算相应的名次,并将相应的名次保存在 rank 数组中;
      • 如何计算名次?
        每一个元素 a [ k ] a[k] a[k] 都和它前面的 k − 1 k - 1 k1 个元素进行比较,若 a [ k ] > a [ i ] ( i ∈ [ 0 , k − 2 ] ) a[k] > a[i](i \in [0, k - 2]) a[k]>a[i](i[0,k2]),则 rank[k]++;反之则 rank[i]++;
        for (int i = 1; i < n; ++i)
            {
                for (int j = 0; j < i; ++j)
                {
                    if (a[i] < a[j])
                        ++rank[j];
                    else
                        ++rank[i];
                }
            }
        
    2. 步骤二:根据对应的名字对原数组中的元素进行原地重排;
      • 怎样进行重排?
        r a n k [ i ] ! = i rank[i] != i rank[i]!=i 时,使用 swap() 函数对 a [ i ] a[i] a[i] a [ r a n k [ i ] ] a[rank[i]] a[rank[i]] 进行交换即可;注意交换不只是 a 数组要进行交换,rank 数组也要跟着一起!!!
        for (int i = 0; i < n; ++i)
            {
                while (rank[i] != i)
                {
                    swap(a[i], a[rank[i]]);
                    swap(rank[i], rank[rank[i]]);
                }
            }
        
    具体实现:
    //计数排序
    template<typename T>
    void countSort(T* a, int n)
    {
        //计算名次
        int* rank = new int[n] ();
    
        for (int i = 1; i < n; ++i)
        {
            for (int j = 0; j < i; ++j)
            {
                if (a[i] < a[j])
                    ++rank[j];
                else
                    ++rank[i];
            }
        }
    
        //按照名次进行原地重排
        for (int i = 0; i < n; ++i)
        {
            while (rank[i] != i)			//注意 swap 中 a 和 rank 都得换哦
            {
                swap(a[i], a[rank[i]]);
                swap(rank[i], rank[rank[i]]);
            }
        }
    
        delete[] rank;
    }
    
  2. 时间复杂性分析
    1. 比较次数

      1. 在计算名次的时候,每一个元素都要和它前面的元素进行比较,因此此时经历了 c o m p a r e 1 = n ( n − 1 ) 2 次比较 compare_1 = \frac{n(n - 1)}{2} \text{次比较} compare1=2n(n1)次比较

      2. 在原地重排时,我们需要考虑最好和最坏两种情况(不考虑循环变量的比较)。
        最好情况下:
        所有元素开始均已有序,因此 r[i] != i 只需要执行 n 次即可;

        最坏情况下:
        每一次交换都会是一个元素到达正确的位置,假设第 1 个位置在不停地进行交换,那么进行 n - 1 次后,所有的元素都会到达其正确的位置。此后只需进行 n 次 r[i] != i 判断即可。因此共 2n - 1 次;

    2. 交换次数与移动次数
      在按照名次进行原地重排时,
      最好情况下:
      一次交换也不做,因此此时交换次数为 0;

      最坏情况下:
      每次都要作交换,并且 n 个元素进行 n - 1 次交换之后,第 n 个元素必有序,此时只需 n - 1 次 swap 即可;

      因此,交换次数: 0 − 2 ( n − 1 ) 0 - 2(n - 1) 02(n1),由于每次交换涉及 3 次移动,所以移动次数: 0 − 3 ∗ 2 ( n − 1 ) 0 - 3 * 2(n - 1) 032(n1)

选择排序

  1. “最原始的” 选择排序
    1. 主要思路及实现

      1. 步骤一:在有 n − i ( i ∈ 0 , 1 , 2 , ⋯   , n − 1 ) n - i(i \in {0, 1, 2, \cdots, n - 1}) ni(i0,1,2,,n1) 个元素的数据集中找出最大元素;
      2. 步骤二:将这个最大元素与数据集中最后一个元素进行交换;
      3. 重复步骤一、二,直到得到只有 1 个元素的数据集;

      具体实现:

      //寻找数据集中的最大值对应的索引
      template<typename T>
      int indexOfMax(T* a, int n)
      {
          if (n < 1)
              return -1;
      
          int indexOfMax = 0;
          for (int i = 0; i < n; ++i)
              if (a[indexOfMax] < a[i])
                  indexOfMax = i;
      
          return indexOfMax;
      }
      
      //选择排序
      template<typename T>
      void selectSort(T* a, int n)
      {
          //找到最大元素,然后与最后一个元素进行交换
          for (int i = n; i > 0; --i)
              swap(a[i - 1], a[indexOfMax(a, i)]);
      }
      
    2. 时间复杂性分析:

      1. 比较次数:在对最大值对应的索引进行求取时,每个 i i i 都要在 indexOfMax() 函数中比较 i - 1 次;因此: c o m p a r e 1 = 1 + 2 + ⋯ + n − 1 = n ( n − 1 ) 2 次 compare_1 = 1 + 2 + \cdots + n - 1 = \frac{n(n - 1)}{2} \text{次} compare1=1+2++n1=2n(n1)
      2. 交换次数与移动次数
        同理,进行 n - 1 次交换之后,第 n 个元素必在该在的位置上,因此,交换次数: n − 1 n - 1 n1,移动次数: 3 ( n − 1 ) 3(n - 1) 3(n1)
  2. “及时终止” 的选择排序
    有时候不一定要一直进行排序到只剩下一个元素,当某一次函数执行时数据已经有序了,那么此时便可以不进行下面的过程了。
    1. 主要思路及实现

      1. 步骤一:设置 bool 值 sorted 来表示序列是否已经有序,并赋初始值为 false;
      2. 步骤二:在 for 循环中寻找最大索引时,我们假设序列已经有序,sorted = true;若 if 中条件一直满足,说明假设成立,最终 sorted = true,不再进行下一次循环;
        若序列仍无序,则必会进入 else 语句,sorted = false。从而达到了及时退出的目的。

      具体实现:

      //及时终止的选择排序
      template<typename T>
      void improved_selectSort(T* a, int n)
      {
          bool sorted = false;
          for (int i = n; !sorted && i > 0; --i)
          {
              int indexOfMax = 0;
              //假设已经有序了
              sorted = true;
      
              //找最大索引
              for (int j = 0; j < i; ++j)
              {   //如果是有序的,就会一直走 if,从而 sorted = true,下次不再进入循环;只要有一个不满足,sorted 就会置为false,说明无序
                  if (a[indexOfMax] < a[j])
                      indexOfMax = j;
                  else
                      sorted = false;
              }
              swap(a[i - 1], a[indexOfMax]);
          }
      }
      
    2. 时间复杂性分析

      1. 比较次数(只考虑 a 数组中的元素的比较)
        最好情况下(即序列已经有序):只需进行 if 判断即可,故 c o m p a r e b e s t = ( n − 1 ) 次 compare_{best} = (n - 1)\text{次} comparebest=(n1)
        最坏情况下:每次都要进行 if 判断,共需 c o m p a r e w o r s t = n − 1 + n − 2 + ⋯ + 1 = n ( n − 1 ) 2 次 compare_{worst} = n - 1 + n - 2 + \cdots + 1 = \frac{n(n - 1)}{2}\text{次} compareworst=n1+n2++1=2n(n1)
      2. 交换次数
        最好情况下:只需进行一次交换即可(即第一次的时候),故 s w a p b e s t = 1 次 swap_{best} = 1\text{次} swapbest=1
        最坏情况下:每次都需要进行交换,故需 s w a p w o r s t = ( n − 1 ) 次 swap_{worst} = (n - 1) \text{次} swapworst=(n1)

冒泡排序

  1. “最原始的” 冒泡排序
    1. 主要思路及实现

      1. 步骤一:每次对 n − i ( i ∈ 0 , 1 , 2 , ⋯   , n − 1 ) n - i(i \in {0, 1, 2, \cdots, n - 1}) ni(i0,1,2,,n1) 个元素集中的相邻元素进行比较,若不满足顺序,则交换;反之保持原样。每次冒泡过程结果为对应数据集中的最大值被放到序列的最右端
      2. 步骤二:重复步骤一 n - 1 次即可;

      具体实现:

      //冒泡排序
      //一次冒泡过程
      template<typename T>
      void bubble(T* a, int n)    //一次冒泡过程
      {
          for (int i = 0; i < n; ++i)
          {
              if (a[i] > a[i + 1])
                  swap(a[i], a[i + 1]);
          }
      }
      
      template<typename T>
      void bubbleSort(T* a, int n)
      {
          for (int i = n; i > 0; --i)
              bubble(a, i);
      }
      
    2. 时间复杂性分析

      1. 比较次数(针对 a 数组中的元素)
        这样的比较主要发生在一次冒泡过程中,每一次都有 n − i ( i ∈ 0 , 1 , 2 , ⋯   , n − 1 ) n - i (i \in {0, 1, 2, \cdots, n - 1}) ni(i0,1,2,,n1) 进行比较,因此: c o m p a r e = n − 1 + n − 2 + ⋯ + 2 + 1 = n ( n − 1 ) 2 次 compare = n - 1 + n - 2 + \cdots + 2 + 1 = \frac{n(n - 1)}{2} \text{次} compare=n1+n2++2+1=2n(n1)
      2. 交换次数
        最好情况下,所有元素开始就已经有序,无需交换,故: c o m p a r e b e s t = 0 次 compare_{best} = 0\text{次} comparebest=0
        最坏情况下,所有元素都得两两进行比较,故: c o m p a r e w o r s t = n − 1 + n − 2 + ⋯ + 2 + 1 = n ( n − 1 ) 2 次 compare_{worst} = n - 1 + n - 2 + \cdots + 2 + 1 = \frac{n(n - 1)}{2}\text{次} compareworst=n1+n2++2+1=2n(n1)
  2. “及时终止的” 冒泡排序
    1. 主要思路及实现

      1. 与 “及时终止的” 选择排序一样,也是用一个 bool 值变量在 if 循环中控制循环的进行;

      具体实现:

      //及时终止的冒泡排序
      template<typename T>
      void improved_bubbleSort(T* a, int n)
      {
          bool stopped = false;
          for (int i = n; !stopped && i > 0; --i)
          {
              stopped = true;
              for (int j = 0; j < i; ++j)
              {
                  if (a[j] > a[j + 1])
                      swap(a[j], a[j + 1]);
                  else
                      stopped = false;
              }
          }
      }
      
    2. 时间复杂性分析

      1. 比较次数
        最好情况下,只需在第一趟冒泡时进行元素的比较即可,因此: c o m p a r e b e s t = ( n − 1 ) 次 compare_{best} = (n - 1)\text{次} comparebest=(n1)
        最坏情况下,每一次都会进行比较,因此: c o m p a r e w o r s t = n ( n − 1 ) 2 次 compare_{worst} = \frac{n(n - 1)}{2}\text{次} compareworst=2n(n1)
      2. 交换次数
        交换次数和 “最原始的” 冒泡排序一样,并无变化。

插入排序

  1. 主要思路及实现

    1. 步骤一:假设第 k k k 个元素 x 之前的序列是有序的,将第 k k k 个元素 x 它们进行比较;
    2. 步骤二:若 x < a[i],则 a[i] 及之后的元素都向后移,直接某个元素 < x,那么 x 就应该插入在第 i + 1 个位置;

    具体实现:

    //插入排序
    template<typename T>
    void insert(T* a, int n, int key)
    {
        int i;
        for (i = n - 1; i >= 0 && key < a[i]; --i)
            a[i + 1] = a[i];
        a[i + 1] = key;
    }
    
    template<typename T>
    void insertSort(T* a, int n)
    {
        for (int i = 1; i <= n; ++i)
            insert(a, i, a[i]);
    }
    
    1. 时间复杂性分析
      1. 比较次数
        最好情况下,只需与最后一个元素进行比较即可,故 c o m p a r e b e s t = n − 1 次 compare_{best} = n - 1\text{次} comparebest=n1
        最坏情况下,每个元素均需进行比较,故 c o m p a r e w o r s t = n − 1 + n − 2 + ⋯ + 2 + 1 = n ( n − 1 ) 2 次 compare_{worst} = n - 1 + n - 2 + \cdots + 2 + 1 = \frac{n(n - 1)}{2}\text{次} compareworst=n1+n2++2+1=2n(n1)
      2. 移动次数
        最好情况下,只有在传参和最后 key 的赋值时进行了移动,因此: m o v e b e s t = 2 ( n − 1 ) 次 move_{best} = 2(n - 1)\text{次} movebest=2(n1)
        最坏情况下,在 c o m p a r e b e s t compare_{best} comparebest 的基础上,还要进行 a 数组元素向后移的操作,因此: m o v e w o r s t = m o v e b e s t + n − 1 + n − 2 + ⋯ + 2 + 1 = 2 ( n − 1 ) + n ( n − 1 ) 2 次 move_{worst} = move_{best} + n - 1 + n - 2 + \cdots + 2 + 1 \\= 2(n - 1) + \frac{n(n - 1)}{2}\text{次} moveworst=movebest+n1+n2++2+1=2(n1)+2n(n1)

后续的排序方法还会进行补充!
请添加图片描述

你可能感兴趣的:(数据结构,C++,算法,数据结构)