内部排序(二路归并、基数、计数)

内部排序(插入、交换、选择)

一、二路归并排序

1. 算法思想与实现步骤

1)算法思想:
二路归并排序是一种分治算法。它将待排序的序列分为两个子序列,分别对这两个子序列进行排序,然后将两个已排序的子序列归并成一个有序序列。

“归并”的含义是将两个或两个以上的有序表合并成一个新的有序表。假定待排序表含有 n 个记录,则可将其视为 n 个有序的子表,每个子表的长度为 1,然后两两归并,得到 ⌈n / 2⌉ 个长度为 2 或 1 的有序表;继续两两归并……如此重复,直到合并成一个长度为 n 的有序表为止,这种排序方法称为 2 路归并排序。

2)实现步骤:
① 分解:
–> 如果序列长度小于或等于 1,直接返回。
–> 否则,将序列拆分为两个子序列(通常是从中间位置拆分)。
② 递归排序:分别对左侧和右侧的子序列递归调用归并排序。
③ 归并:
–> 创建一个临时数组,用于存放合并后的结果。
–> 使用两个指针分别指向左侧和右侧子序列的起始位置,比较两个指针指向的元素,将较小的元素放入临时数组中,然后移动指针。
–> 将剩余的元素复制到临时数组中,最后再将临时数组中的元素复制回原序列。

2. 算法的C++代码

1)Merge() 的功能是将前后相邻的两个有序表归并为一个有序表。设两段有序表 A[low…mid] 、A [mid + 1…high] 存放在同一顺序表中的相邻位置,先将它们复制到辅助数组 B 中。每次从对应 B 中的两个段取出一个记录进行关键字的比较,将较小者放入 A 中,当数组 B 中有一段的下标超出其对应的表长(即该段的所有元素都已复制到 A 中)时,将另一段中的剩余部分直接复制到 A 中。算法如下:

ElemType *B = (ElemType *)malloc((n+1)*sizeof(ElemType));	// 辅助数组B
void Merge(ElemType A[], int low, int mid, int high) {
// 表中的两段A[low…mid]和A[mid+1…high]各自有序,将它们合并成一个有序表
    int i, j, k;
    for(k=low; k<=high; k++){
        B[k] = A[k]; 		// 将A中所有元素复制到B中
    }
    for(i=low, j=mid+l, k=i; i<=mid && j<=high; k++) {
        if(B[i] <= B[j]) {A[k] = B[i++];}	// 比较B的左右两段中的元素,将较小值复制到A中
        else {A[k] = B[j++];}
    }
    while(i<=mid) {A[k++] = B[i++]; }	// 若第一个表未检测完,复制
    while(j<=high) {A[k++] = B[j++]; }	// 若第二个表未检测完,复制
}

注意:上面的代码中,最后两个while 循环只有一个会执行。

2)一趟归并排序的操作是,调用 ⌈n / 2h⌉ 次算法 Merge(),将 L[1…n] 中前后相邻且长度为 h 的有序段进行两两归并,得到前后相邻、长度为 2h 的有序段,整个归并排序需要进行 ⌈log2n⌉ 趟。递归形式的 2 路归并排序算法是基于分治的,其过程如下:
I、分解:将含有 n 个元素的待排序表分成各含 n / 2 个元素的子表,采用 2 路归并排序算法对两个子表递归地进行排序。
II、合并:合并两个已排序的子表得到排序结果。
算法如下:

void MergeSort(ElemType A[], int low, int high) {
    if(low < high) {
        int mid = (low+high)/2;		// 从中间划分两个子序列
        MergeSort(A, low, mid);		// 对左侧子序列进行递归排序
        MergeSort(A, mid+l, high);	// 对右侧子序列进行递归排序
        Merge(A, low, mid, high);	// 归并
    }
}

3. 示例

下图所示为 2 路归并排序的一个例子,经过三趟归并后合并成了有序序列。

4. 算法的性能分析

1)空间复杂度:
Merge() 操作中,辅助空间刚好为 n 个单元,所以算法的空间复杂度为 O(n)

2)时间复杂度:
每趟归并的时间复杂度为 O(n),共需进行 ⌈log2n⌉ 趟归并,所以算法的时间复杂度为 O(nlog2n)

3)稳定性:
由于 Merge() 操作不会改变相同关键字记录的相对次序,所以 2 路归并排序算法是一种稳定的排序方法。

4)适用性:
归并排序适用于顺序存储和链式存储的线性表

注意:一般而言,对于 N 个元素进行 k 路归并排序时,排序的趟数 m 满足 km = N, 从而 m = logkN,又考虑到 m 为整数,所以 m = ⌈logkN⌉ 。这和前面的 2 路归并是一致的。

5. 例题

① 【2013 统考真题】已知两个长度分别为 m 和 n 的升序列表,若将它们合并为长度为 m + n 的一个降序链表,则最坏情况下的时间复杂度是( D )。
A. O(n)
B. O(mn)
C. O(min(m, n))
D. O(max(m, n))

② 【2022 统考真题】使用二路归并排序对含 n 个元素的数组 M 进行排序时,二路归并操作的功能是( A )。
A. 将两个有序表合并为一个新的有序表
B. 将 M 划分为两部分,两部分的元素个数大致相等
C. 将 M 划分为 n 个部分,每个部分中仅含有一个元素
D. 将 M 划分为两部分,一部分元素的值均小于另一部分元素的值

③ 将两个元素个数分别为 M 和 N 的有序表合并成一个有序表,最少的比较次数是(min(M, N)),最多的比较次数是(M + N - 1)。

二、基数排序

1. 算法思想与实现步骤

1)算法思想:
基数排序是一种很特别的排序方法,它不基于比较和移动进行排序,而基于关键字各位的大小进行排序。基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。它将整数按每位数字分组,从最低位到最高位依次进行排序。通常使用计数排序作为每位数字的稳定排序算法。

2)实现步骤:
① 找出最大值:找到待排序数组中最大的数,以确定需要排序的位数。
② 按位排序:
–> 从最低位到最高位进行排序:
–> 针对当前位,使用计数排序对数组进行排序。
–> 以每一位上的数字为关键字,保持前面已排序的结果。
③ 重复步骤 ② :继续处理下一个更高一位,直到所有位数都处理完为止。

2. 示例

假设长度为 n 的线性表中每个结点 aj 的关键字由 d 元组(kjd-1, kjd-2, … , kj1, kj0)组成,满足 0 <= kji <= r - 1(0 <= j < n, 0 <= i <= d - 1)。其中 kjd-1 为最主位关键字,kj0 为最次位关键字。

为实现多关键字排序,通常有两种方法:

  • 第一种是最高位优先(MSD)法,按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列。
  • 第二种是最低位优先(LSD)法,按关键字位权重递增依次进行排序,最后形成一个有序序列。

下面描述以 r 为基数的最低位优先(LSD)基数排序的过程,在排序过程中,使用 r 个队列 Q0, Q1, … , Qr-1 。基数排序的过程如下:
对 i = 0, 1, … , d-1,依次做一次“分配”和“收集”(其实是一次稳定的排序过程)。

  • 分配:开始时,把 Q0, Q1, … , Qr-1 各个队列置成空队列,然后依次考察线性表中的每个结点aj(j = 0, 1, … , n - 1),若 aj 的关键字 kji = k,就把 aj 放进 Qk 队列中。
  • 收集:把 Q0, Q1, … , Qr-1 各个队列中的结点依次首尾相接,得到新的结点序列,从而组成新
    的线性表。

1)第一趟:每个关键字是 1000 以下的正整数,基数 r = 10,在排序过程中需要借助 10 个链队列,每个关键字由 3 位子关键字构成—— K1K2K3 ,分别代表百位、十位和个位,一共需要进行三趟“分配”和“收集“操作。第一趟分配用最低位子关键字 K3 进行,将所有最低位子关键字(个位)相等的记录分配到同一个队列,如下图(a)所示,然后进行收集操作,第一趟收集后的结果如下图(b)所示。

2)第二趟:分配用次低位子关键字 K2 进行,将所有次低位子关键字(十位)相等的记录分配到同一个队列,如下图(a)所示。第二趟收集后的结果如下图(b)所示。

3)第三趟:分配用最高位子关键字 K1 进行,将所有最高位子关键字(百位)相等的记录分配到同一个队列,如下图(a)所示,第三趟收集后的结果如下图(b)所示,至此整个排序结束。

3. 算法的性能分析

1)空间复杂度:
一趟排序需要的辅助存储空间为 t(t 个队列: t 个队头指针和 t 个队尾指针),但以后的排序中会重复使用这些队列,所以基数排序的空间复杂度为 O(t)

2)时间复杂度:
基数排序需要进行 d 趟分配和收集,一趟分配需要 O(n) ,一趟收集需要 O® ,所以基数排序的时间复杂度为 O(d(n + r)) ,它与序列的初始状态无关。

3)稳定性:
对于基数排序算法而言,很重要一点就是按位排序时必须是稳定的。因此,这也保证了基数排序的稳定性。

4)适用性:
基数排序适用于顺序存储和链式存储的线性表

4. 例题

① 【2013 统考真题】对给定的关键字序列 110, 119, 007, 911, 114, 120, 122 进行基数排序,第 2 趟分配收集后得到的关键字序列是( C )。
A. 007, 110, 119, 114, 911, 120, 122
B. 007, 110, 119, 114, 911, 122, 120
C. 007, 110, 911, 114, 119, 120, 122
D. 110, 120, 911, 122, 114, 007, 119

② 【2015 统考真题】下列排序算法中,元素的移动次数与关键字的初始状态无关的是( C )。
A. 直接插入排序
B. 冒泡排序
C. 基数排序
D. 快速排序

③ 【2021 统考真题】设数组 S[ ] = {93, 946, 372, 9, 146, 151, 301, 485, 236, 327, 43, 892} ,采用最低位优先(LSD))基数排序将 S 排列成升序序列。第一趟分配、收集后,元素 372 之前、之后紧邻的元素分别是( C )。
A. 43, 892
B. 236, 301
C. 301, 892
D. 485, 301

④ 设线性表中每个元素有两个数据项 k1 和 k2,现对线性表按以下规则进行排序:先看数据项 k1,k1值小的元素在前,大的元素在后;在 k1 值相同的情况下,再看 k2,k2值小的元素在前,大的元素在后。满足这种要求的排序算法是( D )。
A. 先按 k1 进行直接插入排序,再按 k 2 进行简单选择排序
B. 先按 k2 进行直接插入排序,再按 k 1 进行简单选择排序
C. 先按 k1 进行简单选择排序,再按 k 2 进行直接插入排序
D. 先按 k2 进行简单选择排序,再按 k 1 进行直接插入排序

⑤ 有 n 个十进制整数进行基数排序,其中最大的整数为 5 位,则基数排序过程中临时建立的队列个数是( D )。
A. n
B. 2
C. 5
D. 10
【基数排序中建立的队列个数等于进制数】

三、*计数排序

1. 算法思想与实现步骤

1)算法思想:
计数排序也是一种不基于比较的排序算法,适用于范围有限且元素个数较少的整数集合。它通过计算每个元素出现的次数(频率)来确定元素的位置。计数排序的思想是:对每个待排序元素 x,统计小于 x 的元素个数,利用该信息就可确定 x 的最终位置。当有几个元素相同时,该排序方案还需做一定的优化。

2)实现步骤:
① 确定范围:找到待排序数组中的最大值和最小值,以确定计数数组的大小。
② 创建计数数组:创建一个计数数组(长度为最大值与最小值之差加 1),初始化所有元素为 0 。
③ 统计频率:遍历待排序数组,统计每个元素出现的次数,并将频率存入计数数组中。
④ 累加计数:将计数数组中的元素进行累加,以确定每个元素在排序后的最终位置。
⑤ 输出结果:创建一个输出数组,遍历计数数组,将每个元素根据其计数放入输出数组中。
⑥ 复制回原数组:将输出结果复制回原数组。

2. 算法的C++代码

在计数排序算法的实现中,假设输入是一个数组 A[n],序列长度为 n,我们还需要两个数组:B[n]存放输出的排序序列,C[k]存储计数值。用输入数组 A 中的元素作为数组 C 的下标(索引),而该元素出现的次数存储在该元素作为下标的数组 C 中。算法如下:

void CountSort(ElemType A[], ElemType B[], int n, int k){
    int i C[k];
    for(i=0; i<k; i++) {C[i] = 0;}		// 初始化计数数组
    for(i=0; i<n; i++) {C[A[i]]++;}		// 遍历输入数组,统计每个元素出现的次数,C[A[i]]保存的是等于A[i]的元素个数
    for(i=l; i<k; i++) {C[i] = C[i]+C[i-1];}	// C[x]保存的是小于或等于x的元素个数
    for(i=n-l; i>=0; i--){		// 从后往前遍历输入数组
        B[C[A[i]]-1] = A[i];	// 将元素A[i]放在输出数组B[]的正确位置上
        C[A[i]] = C[A[i]]-1;
    }
}
  • 第一个 for 循环执行完后,数组 C 的值初始化为 0。
  • 第二个 for 循环遍历输入数组 A,若一个输入元素的值为 x,则将 C[x] 值加 1,该 for 循环执行完后,C[x] 中保存的是等于 x 的元素个数。
  • 第三个 for 循环通过累加计算后,C[x] 中保存的是小于或等于 x 的元素个数。
  • 第四个 for 循环从后往前遍历数组 ,把每个元素 A[i] 放入它在输出数组 B 的正确位置上。

若数组 A 中不存在相同的元素,则 C[A[i]] - 1 就是 A[i] 在数组 B 中的最终位置,这是因为共有 C[A[i]] 个元素小于或等于 A[i] 。若数组 A 中存在相同的元素,将每个元素 A[i] 放入数组 B[ ] 后,都要将 C[A[i]] 减 1,这样,当遇到下一个等于 A[i] 的输入元素(若存在)时,该元素就可放在数组 B 中 A[i] 的前一个位置上。

3. 示例

假设输入数组 A[ ] = {2, 4, 3, 0, 2, 3},第二个 for 循环执行完后,辅助数组 C 的情况如下图(a)所示;第三个 for 循环执行完后,辅助数组 C 的情况如下图(b)所示。图(c)至图(h)分别是第四个 for 循环每迭代一次后,输出数组 B 和辅助数组 C 的情况。

由上面的过程可知,计数排序的原理是:数组的索引(下标)是递增有序的,通过将序列中的元素作为辅助数组的索引,其个数作为值放入辅助数组,遍历辅助数组来排序。

4. 算法的性能分析

1)空间复杂度:
计数排序是一种用空间换时间的做法。输出数组的长度为 n;辅助的计数数组的长度为 k,空间复杂度为 O(n + k) 。若不把输出数组视为辅助空间,则空间复杂度为 O(k) 。

2)时间复杂度:
上述代码的第 1 个和第 3 个 for 循环所花的时间为 O(k),第 2 个和第 4 个 for 循环所花的时间为 O(n),总时间复杂度为 O(n + k) 。因此,当 k = O(n) 时,计数排序的时间复杂度为 O(n);但当 k > O(nlogn) 时,其效率反而不如一些基于比较的排序(如快速排序、堆排序等)。

3)稳定性:
上述代码的第 4 个 for 循环从后往前遍历输入数组,相同元素在输出数组中的相对位置不会改变,因此计数排序是一种稳定的排序算法。

4)适用性:
计数排序更适用于顺序存储的线性表。计数排序适用于序列中的元素是整数且元素范围(0 ~ k - 1)不能太大,否则会造成辅助空间的浪费。

四、各种内部排序算法的比较及应用

1. 比较

1)时间复杂度方面

  • 简单选择排序、直接插入排序和冒泡排序平均情况下的时间复杂度都为 O(n2),且实现过程也较为简单,但直接插入排序和冒泡排序最好情况下的时间复杂度可以达到 O(n),而简单选择排序则与序列的初始状态无关。
  • 希尔排序作为插入排序的拓展,对较大规模的数据都可以达到很高的效率,但由于希尔排序的时间复杂度依赖于增量函数,所以目前无法准确给出其时间复杂度。
  • 堆排序利用了一种称为堆的数据结构,可以在线性时间内完成建堆,且在 O(nlog2n) 内完成排序过程。
  • 快速排序基于分治的思想,虽然最坏情况下的时间复杂度会达到 O(n2),但快速排序的平均性能可以达到 O(nlog2n),在实际应用中常常优于其他排序算法。
  • 归并排序同样基于分治的思想,但由于其分割子序列与初始序列的排列无关,因此它的最好、最坏和平均时间复杂度均为 O(nlog2n) 。

2)空间复杂度方面

  • 简单选择排序、插入排序、冒泡排序、希尔排序和堆排序都仅需借助常数个辅助空间。
  • 快速排序需要借助一个递归工作栈,平均大小为 O(Iog2n),当然在最坏情况下可能会增长到 O(n) 。
  • 2 路归并排序在合并操作中需要借助较多的辅助空间用于元素复制,大小为 O(n),虽然有方法能克服这个缺点,但其代价是算法会很复杂而且时间复杂度会增加。

3)稳定性方面

  • 插入排序、冒泡排序、归并排序和基数排序是稳定的排序方法。
  • 简单选择排序、快速排序、希尔排序和堆排序都是不稳定的排序方法。
  • 平均时间复杂度为 O(nlog2n) 的稳定排序算法只有归并排序。

对于不稳定的排序方法,只需举出一个不稳定的实例即可。

4)适用性方面

  • 折半插入排序、希尔排序、快速排序和堆排序适用于顺序存储。
  • 直接插入排序、冒泡排序、简单选择排序、归并排序和基数排序既适用于顺序存储,又适用于链式存储。

2. 应用

通常情况,对排序算法的比较和应用应考虑以下情况:

1)选取排序方法需要考虑的因素

① 待排序的元素数目 n 。
② 待排序的元素的初始状态。
③ 关键字的结构及其分布情况。
④ 稳定性的要求。
⑤ 存储结构及辅助空间的大小限制等。

2)排序算法小结

① 若 n 较小,可采用直接插入排序或简单选择排序。由于直接插入排序所需的记录移动次数较简单选择排序的多,因而当记录本身信息量较大时,用简单选择排序较好。
② 若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。
③ 若 n 较大,则应采用时间复杂度为 O(nlog2n) 的排序方法:快速排序、堆排序或归并排序。当待排序的关键字随机分布时,快速排序被认为是目前基于比较的内部排序方法中最好的方法(平均时间最短)。堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况,这两种排序都是不稳定的。若要求排序稳定且时间复杂度为 O(nlog2n),则可选用归并排序。
④ 在基于比较的排序方法中,每次比较两个关键字的大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的 n 个关键字随机分布时,任何借助于“比较”的排序算法,至少需要 O(nlog2n) 的时间。
⑤ 若 n 很大,记录的关键字位数较少且可以分解时,采用基数排序较好。
⑥ 当记录本身信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构。

3)补充

  • 每一趟排序都能至少确定一个元素在其最终位置上的排序算法有:快速排序、简单选择排序、堆排序、冒泡排序。其中简单选择排序、堆排序、冒泡排序在每趟处理后都能产生当前的最大值或最小值。
  • 元素的移动次数与关键字的初始状态无关的是:基数排序。
  • 元素的比较次数与关键字的初始状态无关的是:简单选择排序、堆排序、归并排序、折半插入排序。
  • 算法的时间复杂度与关键字的初始状态无关的是:简单选择排序、堆排序、归并排序、基数排序。
  • 算法的排序趟数与关键字的初始状态无关的是:直接插入排序、简单选择排序、归并排序、基数排序。其中,对 n 个元素进行直接插入排序或简单选择排序的排序趟数肯定为 n - 1 。
  • 算法的排序趟数与关键字的初始状态有关的是:冒泡排序、快速排序。
  • m 路归并,每选出一个元素需要比较关键字 m - 1 次。
  • 基数排序是唯一一个不是基于比较的排序算法。

有关元素个数规模的总结:

I、直接插入排序、冒泡排序和简单选择排序是基本的排序方法,它们主要用于元素个数 n 不是很大(n < 10000)的情形。

II、对于中等规模的元素序列(n <= 1000),希尔排序是一种很好的选择。

III、对于元素个数 n 很大的情况,可以采用快排、堆排序、归并排序或基数排序,其中快排和堆排序都是不稳定的,而归并排序和基数排序是稳定的排序算法。

3. 例题

① 一台计算机具有多核 CPU,可以同时执行相互独立的任务。归并排序的各个归并段可以并行执行,在下列排序算法中,不可以并行执行的有( A )。
I. 基数排序 II. 快速排序 III. 冒泡排序 IV. 堆排序
A. I、III
B. I、II
C. I、III、IV
D. II、IV

② 【2017 统考真题】下列排序方法中,若将顺序存储更换为链式存储,则算法的时间效率会降低的是( D )。
I. 插入排序 II . 选择排序 Ill. 起泡排序 IV. 希尔排序 V. 堆排序
A. 仅 I 、II
B. 仅 II 、III
C. 仅 III 、IV
D. 仅 IV 、V

③ 【2017 统考真题】在内部排序时,若选择了归并排序而未选择插入排序,则可能的理由是( B )。
I. 归并排序的程序代码更短
II . 归并排序的占用空间更少
Ill. 归并排序的运行效率更高
A. 仅 II
B. 仅 III
C. 仅 I 、II
D. 仅 I 、III

④ 【2019 统考真题】选择一个排序算法时,除算法的时空效率外,下列因素中,还需要考虑的是( D )。
I. 数据的规模 II. 数据的存储方式 III .算法的稳定性 IV. 数据的初始状态
A. 仅 III
B. 仅 I 、II
C. 仅 II 、III 、IV
D. I 、II 、III 、IV

⑤ 【2020 统考真题】对大部分元素已有序的数组排序时,直接插入排序比简单选择排序效率更高,其原因是( A )。
I. 直接插入排序过程中元素之间的比较次数更少
II. 直接插入排序过程中所需的辅助空间更少
III. 直接插入排序过程中元素的移动次数更少
A. 仅 I
B. 仅 III
C. 仅 I 、II
D. I 、II 和III

⑥ 【2022 统考真题】对数据进行排序时,若采用直接插入排序而不采用快速排序, 则可能的原因是( D )。
I. 大部分元素已有序
II . 待排序元素数量很少
III. 要求空间复杂度为 O(1)
IV. 要求排序算法是稳定的
A. 仅 I 、II
B. 仅 III 、IV
C. 仅 I 、II 、IV
D. I 、II 、III 、IV

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值