数据结构笔记九——各种排序方法(C++)

目录

  • 1、引言——基本概念
  • 2、插入排序
    • 2.1 基本原理
    • 2.2 几种常见的插入排序
      • 2.2.1 直接插入排序
      • 2.2.2 折半插入排序
      • 2.2.3 希尔排序
  • 3、选择排序
    • 3.1 基本原理
    • 3.2 几种常见的选择排序
      • 3.2.1 直接选择排序
      • 3.2.2 堆排序
  • 4、交换排序
    • 4.1 基本原理
    • 4.2 几种常见的交换排序
      • 4.2.1 冒泡排序
      • 4.2.2 快速排序
  • 5、归并排序
  • 6、基数排序
  • 7 、总结

1、引言——基本概念

  • 排序
    把集合中的数据元素按照它们的关键字的非递减或非递增序排成一个序列。
  • 稳定与非稳定排序:
    假定在待排序的集合中存在多个关键字值相同的数据元素。如果经过排序后,这些数据元素的相对次序保持不变则称为稳定排序,否则称为不稳定排序。
  • 内排序与外排序:
    内排序是指被排序的数据元素全部存放在计算机的内存之中,并且在内存中调整数据元素的相对位置。
    外排序是指在排序的过程中,数据元素主要存放在外存储器中,借助于内存储器逐步调整数据元素之间的相对位置。

下面介绍几种排序方法
所用结构体如下:

template<class KEY, class OTHER>
struct SET {
    KEY key;//关键字
    OTHER other;//包含姓名、性别、出生日期等信息
};

2、插入排序

2.1 基本原理

首先将由第一个数据元素组成的序列看成是有序的,然后将剩余的n-1个元素依次插入到前面的已排好序的子序列中去,使得每次插入后的子序列也是有序的。
数据结构笔记九——各种排序方法(C++)_第1张图片

2.2 几种常见的插入排序

2.2.1 直接插入排序

  1. 代码实现
template<class KEY,class OTHER>
void simpleinsertsort(SET<KEY, OTHER>a[], int size) {
	int k;
    SET<KEY, OTHER> tmp;
    for (int j = 1; j < size; ++j) {
        tmp = a[j];
        for (k = j - 1; tmp.key < a[k].key && k >= 0; --k)
            a[k + 1] = a[k];
        a[k + 1] = tmp;
    }
}
  1. 时间复杂度分析
  • 简单插入排序是两层循环嵌套,时间复杂度为 O ( N 2 ) O(N^2) O(N2)
  • 当初始排序为逆序时,达到最大时间复杂度
    1 + 2 + 3 + ⋯ + N − 1 1+2+3+\cdots+N-1 1+2+3++N1
  • 当初始排序为正序时,无需进行数据的移动操作,时间复杂度为 O ( N ) O(N) O(N)
  1. 适用情况
    排序元素数较少,且几乎是已顺序排序的

2.2.2 折半插入排序

利用二分查找法,快速地找到 a [ j ] a[j] a[j]的插入位置。从而达到减少比较次数的目的 ,移动次数不变,故最坏情况下总的比较次数还是 O ( N 2 ) O(N^2) ON2

2.2.3 希尔排序

希尔排序是插入排序算法的改进,直接插入排序的高代价主要由大量的移动产生 ,希尔的想法是避免大量的数据移动,先是比较那些离得稍远些的元素,这样,一次交换就相当于直接插入排序中的多次交换。然后比较那些离得近一点的元素,以此类推,逐步逼近直接的插入排序 。

  1. 执行示例
    数据结构笔记九——各种排序方法(C++)_第2张图片
  2. 时间复杂度分析
  • 不同的增量序列有不同的时间性能。
  • 希尔建议gap从N/2开始平分直到gap为1,之后程序可终止。
  • 希尔排序,时间效率分析比较复杂。
  1. 代码实现
template <class KEY, class OTHER>
void shellSort(SET<KEY, OTHER> a[], int size)
{
    int step, i, j;
    SET<KEY, OTHER> tmp;
 
    for (step = size/2; step > 0; step /= 2)     //step为希尔增量
          for (i = step; i < size; ++i) {
	     tmp = a[i];
                 for (j = i - step; j >= 0 && a[j].key > tmp.key;  j -= step) 
	           a[j+step] = a[j];
	     a[j+step] = tmp;
         }
}

3、选择排序

3.1 基本原理

首先,从n个元素中选出关键字最小的元素。再从剩下的(n-1)个元素中选出关键字最小的元素,依次类推,每次从剩下的元素序列中挑出关键字最小的元素,直至序列中最后只剩下一个元素为止。这样,把每次得到的元素排成一个序列,就得到了按非递减序排列的排序序列。

3.2 几种常见的选择排序

3.2.1 直接选择排序

  1. 基本概念
  • 首先在所有元素中用逐个比较的方法选出最小元素,把它与第一个元素交换;然后在剩下的元素中再次用逐个比较的方法选出最小元素,把它与第二个元素交换;以此类推,直到所有元素都放入了正确的位置。
  • 对k个元素而言,每次选出最小元素需要k-1次比较。因此排序一个n个元素组成的序列所需的比较次数为:
    ( n − 1 ) + ( n − 2 ) + … … + 2 + 1 = n ( n − 1 ) 2 = O ( n 2 ) (n-1) + (n-2)+ …… + 2 + 1 = \frac{n(n-1)}{2} = O(n^2) (n1)+(n2)++2+1=2n(n1)=O(n2)
    数据结构笔记九——各种排序方法(C++)_第3张图片
  1. 代码实现
//直接选择排序
template <class KEY, class OTHER>
void simpleSelectSort(SET<KEY, OTHER> a[], int size)
{
   int  i, j, min;
   SET<KEY, OTHER> tmp;

   for (i = 0; i < size - 1; ++i) {
       min = i;
       for (j = i + 1; j < size; ++j)
           if (a[j].key < a[min].key) min = j;//找到最小值
       //最小值与第i位的交换
       tmp = a[i]; 
       a[i] = a[min]; 
       a[min] = tmp;
   }
}

3.2.2 堆排序

  1. 基本概念
  • 直接选择排序在n个元素中选出最小元素需要n-1次比较。而利用基于堆的优先级队列选出最小元素只需要 O ( l o g N ) O(logN) O(logN)的时间
  • 排序N个元素,步骤如下:
    应用buildHeap()对N个元素创建一个优先级队列
    通过调用N次deQueue取出每个项,结果就排好序了。
  • 时间效益:建堆用了 O ( N ) O(N) O(N)的时间,deleteMin是对数时间。因此总的时间是 O ( N l o g N ) O(NlogN) O(NlogN)

【注:关于优先级队列的内容见:数据结构笔记六】

  1. 实现中的主要问题
  • 空间问题:
    N元素的优先级队列需要N个空间,每次deQueue的元素也要放在一个空间中。因此需要两倍的空间。
  • 解决方案:在每一次deQueue后,堆缩小1。因此,在堆中的最后那个位置能被用来存储刚被删去的元素。
  • 假设有一个六个元素的堆。第一次deQueue产生了A1。现在堆中仅有五个元素,我们可以把A1放在位置6。下一个deQueue产生A2。因为堆中现在仅有四个元素,我们可以把A2放在位置5。
    则为了产生递增排序,可以采用最大堆。
  1. 一个例子
    例如,要排序39, 36, 58, 23, 44, 97, 31, 14, 26和77,先将这些元素创建一个堆
    数据结构笔记九——各种排序方法(C++)_第4张图片
    执行一次出队操作后:
    数据结构笔记九——各种排序方法(C++)_第5张图片
    像这样不断进行,既可以得到由小到大排列的数组。

  2. 代码实现
    堆排序和优先级队列中的堆有三个细小的区别:

    • 堆排序用的是最大堆
    • 为了和其他的排序函数保持一致,数据从位置0开始存储。因此,对位置i中的结点,父结点在位置(i – 1)/ 2,左孩子在位置2i + 1,右孩子紧跟着左孩子
    • 在向下过滤时需要告知当前堆的大小
//堆排序
//首先进行堆的构造
template<class KEY, class OTHER>
void heapsort(SET<KEY, OTHER> a[], int size) {
    int i;
    SET<KEY, OTHER> tmp;

    //创建初始的最大化堆
    for (i = size / 2 - 1; i >= 0; i--) {
        percolatedown(a, i, size);//从第一个非叶节点开始进行调整
    }

    //执行n-1次出队
    for (i = size - 1; i > 0; --i) {
        //将a[0]和a[i]交换位置
        tmp = a[0];
        a[0] = a[i];
        a[i] = tmp;
        percolatedown(a, 0, i);//此时需要往下过滤的元素个数只有i个,后面是已经排好序的
    }
}
//percolatedown函数的实现
//此函数共有三个参数:原数组,调整的元素,整个需要调整的树的大小
template <class KEY, class OTHER>
void percolateDown(SET<KEY, OTHER> a[], int hole, int size)
{
    int child;
    SET<KEY, OTHER> tmp = a[hole];

    for (; hole * 2 + 1 < size; hole = child) {//hole*2+1为hole的左孩子的下标
        child = hole * 2 + 1;
        if (child != size - 1 && a[child + 1].key > a[child].key)
            child++;//child为节点值最大的子节点
        if (a[child].key > tmp.key)   a[hole] = a[child];
        else    break;
    }
    a[hole] = tmp;
}

4、交换排序

4.1 基本原理

  • 交换排序就是根据序列中两个数据元素的比较结果来确定是否要交换这两个数据元素在序列中的位置。
  • 交换排序的特点是:通过交换,将关键字值较大的数据元素向序列的尾部移动,关键字值较小的数据元素向序列的头部移动。

4.2 几种常见的交换排序

4.2.1 冒泡排序

  1. 基本思路
  • 从头到尾比较相邻的两个元素,将小的换到前面,大的换到后面。经过了从头到尾的一趟比较,就把最大的元素交换到了最后一个位置。这个过程称为一趟起泡。
  • 然后再从头开始到倒数第二个元素进行第二趟起泡。经过了第二趟比较,又将第二大的元素放到了倒数第二个位置。
  • 依次类推,经过第n-1趟起泡,将倒数第n-1个大的元素放入第2个单元。
    数据结构笔记九——各种排序方法(C++)_第6张图片
  1. 代码实现
//冒泡排序
template<class KEY, class OTHER>
void bubblesort(SET<KEY, OTHER>a[], int size) {
    SET<KEY, OTHER> tmp;
    bool flag = true;//记录一次起泡过程中是否发生交换
    for (int i = 1; i < size; ++i) {//最多起泡size-1次
        flag = false;
        for (int j = 0; j < size - i; ++j) {
            if (a[j + 1].key < a[j].key) {
                tmp = a[j];
                a[j] = a[j+1];
                a[j + 1] = tmp;
                flag = true;
            }
        if (!flag) break;//如果在一次起泡中并未发生数据的交换说明已经排好了序
        }
    }
}
  1. 时间性能分析
  • 当原始序列有序时,冒泡排序出现最好的情况。此时,只需要一次起泡,执行n-1次的比较。因此,最好情况下的时间复杂度为 O ( N ) O(N) ON
  • 当原始序列正好是逆序时,冒泡排序出现最坏的情况。此时,需要n-1趟起泡,第i趟起泡需要做(n-i)次比较以及(n-i)次交换。所以时间复杂度是 O ( N 2 ) O(N^2) ON2的。
  • 冒泡排序法适合那些原始数据本来就比较有序的情况。

4.2.2 快速排序

在待排序的序列中选择一个数据元素,以该元素为标准,将所有数据元素分为两组,第一组的元素均小于或等于标准元素,第二组的数据元素均大于标准元素。第一组的元素放在数组的前面部分,第二组的数据元素放在数组的后面部分,标准元素放在中间。这个位置就是标准元素的最终位置。这称为一趟划分。然后对分成的两组数据重复上述过程,直到所有的元素都在适当的位置为止。(注意其中的递归思想

如何来找到中心点?

  • 用待排序序列的第一个元素作为标准元素。如果输入是随机的,这个选择是可接受的。但是如果输入已经有序,这样的标准元素提供一个糟糕的划分,所有的待排序数据都在标准元素的一边。因此,这种选择在最坏情况下的时间复杂度是平方级的。
  • 随机选择一个标准元素,这样很少有可能选到最差的元素。但随机数的选择也要花相当多的时间。
  • N个数的中值是第N/2小的数。对标准元素的最好选择显然是这个中值,因为它保证对元素的均匀划分。中值可以通过采样来得到。

如何对数据进行划分?

  • 从右向左开始检查。如果high的值大于k,该位置中的值位置正确,high减1,继续往前检查,直到遇到一个小于k的值。
  • 将小于k的这个值放入low的位置。此时high的位置又空出来了。然后从low位置开始从左向右检查,直到遇到一个大于k的值。
  • 将low位置的值放入high位置,重复第一步,直到low和high重叠。将k放入此位置。

一个例子
数据结构笔记九——各种排序方法(C++)_第7张图片
代码实现:

//快速排序
//划分函数的实现
//先分堆后排序
template <class KEY, class OTHER>
int divide(SET<KEY, OTHER> a[], int low, int high)
{
    SET<KEY, OTHER>  k = a[low];
    do {
        while (low < high && a[high].key >= k.key) --high;
        if (low < high) { a[low] = a[high]; ++low; }
        while (low < high && a[low].key <= k.key) ++low;
        if (low < high) { a[high] = a[low]; --high; }
    } while (low != high);
    a[low] = k;
    return low;//返回划分点所在位置
}
//快速排序的实现
//一次划分,两次递归
template <class KEY, class OTHER>
void quickSort(SET<KEY, OTHER> a[], int low, int high)
{
    int mid;

    if (low >= high) return;
    mid = divide(a, low, high);//即划分所返回的数值
    quickSort(a, low, mid - 1);//排序左一半
    quickSort(a, mid + 1, high);//排序右一半
}
//快速排序的包裹函数
//为了和其它的排序算法参数一致
//因为采用了递归算法,所以在参数上会多加了两个控制其起始位置的参数
template <class KEY, class OTHER>
void quickSort(SET<KEY, OTHER> a[], int size)
{
    quickSort(a, 0, size - 1);
}

时间复杂度分析
T ( N ) = T ( i ) + T ( N − i − 1 ) + c N ( N 表 示 进 行 了 N 次 比 较 ) T ( 0 ) = T ( 1 ) = 1 \begin{array}{ll} T(N) = T(i) + T(N-i-1) + cN (N表示进行了N次比较)\\ T(0) = T(1) = 1 \end{array} T(N)=T(i)+T(Ni1)+cNNNT(0)=T(1)=1
由此公式可得:

  • 最差的情况:中心点最小或最大 —— Q ( N 2 ) Q(N^2) Q(N2)
  • 最好的情况:中心点在中间—— O ( N l o g N ) O(NlogN) O(NlogN)

5、归并排序

基本概念
——合并两个已排序的有序表
顺序比较两者的相应元素,小者移入另一表中,反复如此,直至其中一表为空为止,将另一表中剩余结点自左至右复制到表C的剩余位置。
一个例子
数据结构笔记九——各种排序方法(C++)_第8张图片数据结构笔记九——各种排序方法(C++)_第9张图片数据结构笔记九——各种排序方法(C++)_第10张图片
代码实现:

//归并排序
//归并
template <class KEY, class OTHER>
void merge(SET<KEY, OTHER> a[], int left, int mid, int right)
{
    SET<KEY, OTHER>* tmp = new SET<KEY, OTHER>[right - left + 1];
    int i = left, j = mid, k = 0;

    while (i < mid && j <= right)                          //两表都未结束
        if (a[i].key < a[j].key)     tmp[k++] = a[i++];
        else tmp[k++] = a[j++];

    while (i < mid)  tmp[k++] = a[i++];       //前半部分没有结束
    while (j <= right)  tmp[k++] = a[j++];    //后半部分没有结束

    for (i = 0, k = left; k <= right; ) a[k++] = tmp[i++];//再把这个腾回去
    delete[] tmp;
}
//归并排序法
//递归思想,n=1为截止条件,否则对前一半和后一半分别调用归并排序,并再次归并两个已排序的数组
template <class KEY, class OTHER>
void mergeSort(SET<KEY, OTHER> a[], int left, int right)
{
    int mid = (left + right) / 2;

    if (left == right) return;
    mergeSort(a, left, mid);
    mergeSort(a, mid + 1, right);
    merge(a, left, mid + 1, right);
}
//归并排序的包裹函数
//为了和其它的排序算法参数一致
//因为采用了递归算法,所以在参数上会多加了两个控制其起始位置的参数
template <class KEY, class OTHER>
void mergeSort(SET<KEY, OTHER> a[], int size)
{
    mergeSort(a, 0, size - 1);
}

时间复杂度分析
T ( 1 ) = 1 T ( N ) = 2 T ( N / 2 ) + N \begin{array}{ll} T(1) = 1\\ T(N) = 2T(N/2) + N \end{array} T(1)=1T(N)=2T(N/2)+N
同上分析得时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)

6、基数排序

(唔,貌似我们不要求掌握)
基本概念

  • 基数排序又称为口袋排序法
  • 通过分配的方法对整数进行排序
  • 以排序十进制非负整数为例,可设置10个口袋
  • 首先将元素按个位数分别放入十个口袋,然后将每个口袋中的元素倒出来按元素的十位数分别放入十个口袋。然后把它们倒出来,再按百位数分配到最后一次倒出来时,元素就已经排好了序
    数据结构笔记九——各种排序方法(C++)_第11张图片数据结构笔记九——各种排序方法(C++)_第12张图片

7 、总结

  • 直接插入、直接选择排序适合于非常少量的数据
  • 希尔排序对中等的数据量是一个好选择
  • 冒泡排序适合原来就较有次序的序列
  • 归并排序、快速排序、堆排序都有 O ( N l o g N ) O(N log N) ONlogN的性能。

(wu 要考试了,本来说跟着课程进度更新这一个系列,貌似时间不太够啦,可能要拖到寒假了bye~)

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