【数据结构】第八章:排序
本篇笔记课程来源:王道计算机考研 数据结构
【数据结构】第八章:排序
- 一、基本概念
- 1. 基本概念
- 2. 评价指标
- 二、插入排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 三、希尔排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 四、冒泡排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 五、快速排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 六、简单选择排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 七、堆排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 4. 插入和删除
- 八、归并排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 九、基数排序
- 1. 算法思想
- 2. 算法性能分析
- 十、外部排序
- 1. 算法思想
- 2. 算法实现
- 3. 算法性能分析
- 4. k 路平衡归并
- 5. 败者树(减少归并趟数)
- 6. 置换-选择排序(减少初始归并段数量)
- 十一、最佳归并树
- 1. 归并树的性质
- 2. 构造最佳归并树
一、基本概念
1. 基本概念
- 排序(sort):就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。
- 输入:n 个记录 R 1 , R 2 , . . . , R n R_1,R_2,...,R_n R1,R2,...,Rn,对应的关键字为 k 1 , k 2 , . . . , k n k_1,k_2,...,k_n k1,k2,...,kn
- 输出:输入序列的一个重排 R 1 ′ , R 2 ′ , . . . , R n ′ R_1^′,R_2^′,...,R_n^′ R1′,R2′,...,Rn′,使得有 k 1 ′ ≤ k 2 ′ ≤ . . . ≤ k n ′ k_1^′≤k_2^′≤...≤k_n^′ k1′≤k2′≤...≤kn′(也可递减)
- 算法分类:
- 内部排序:数据都在内存中
- 外部排序:数据太多,无法全部放入内存
2. 评价指标
- 稳定性:若待排序表中有两个元素 R i R_i Ri 和 R j R_j Rj,其对应的关键字相同即 k e y i = k e y j key_i=key_j keyi=keyj,且在排序前 R i R_i Ri 在 R j R_j Rj 的前面,若使用某一排序算法排序后, R i R_i Ri 仍然在 R j R_j Rj 的前面,则成这个排序算法使稳定的,否则称排序算法是不稳定的。
二、插入排序
1. 算法思想
- 直接插入排序算法思想:每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
- 折半插入排序算法思想:先用折半查找找到应该插入的位置,在移动元素。
- 当
low > high
时折半查找停止,应将[low, i-1]
内的元素全部右移,并将 A[0] 复制到 low 所指位置 - 当
A[mid] == A[0]
时,为了保证算法的稳定性,应继续在 mid 所指位置右边寻找插入位置
- 当
2. 算法实现
- 直接插入排序
void InsertSort(int A[],int n) {int i, j, temp;for (i = 1; i < n; i++){ // 将各个元素插入已排好序的序列中if (A[i] < A[i - 1]) { // 若 A[i] 关键字小于前驱temp = A[i]; // 用 temp 暂存 A[i]for (j = i - 1; j >= 0 && A[j] > temp; --j) // 检查所有前面已排好序的元素A[j + 1] = A[j]; // 所有大于 temp 的元素都向后挪位A[j + 1] = temp; // 复制到插入位置} } }
- 直接插入排序(带哨兵)
void InsertSort(int A[], int n){int i, j;for (i = 2; i <= n; i++){ // 依次将 A[2]~A[n] 插入到前面已排序序列if (A[i] < A[i - 1]){ // 若 A[i] 关键码小于其前驱,将 A[i] 插入有序表A[0] = A[i]; // 复制为哨兵,A[0] 不存放元素for (j = i - 1; A[0] < A[j]; --j) // 从后往前查找待插入位置A[j + 1] = A[j]; // 向后挪位A[j + 1] = A[0]; // 复制到插入位置} } }
- 折半插入排序
void InsertSort(int A[], int n) {int i, j, low, high, mid;for (i = 2; i <= n; i++) { // 依次将 A[2]~A[n] 插入前面的已排序序列A[0] = A[i]; // 将 A[i] 暂存到 A[0]low = 1; // 设置折半查找的范围high = i - 1;while (low <= high) { // 折半查找(默认递增有序)mid = (low + high) / 2; // 取中间点if (A[mid] > A[0]) // 查找左半子表high = mid - 1;else // 查找右半子表low = mid + 1;}for (j = i - 1; j >= high + 1; j--)A[j + 1] = A[j]; // 统一后移元素,空出插入位置A[high + 1] = A[0]; // 插入操作} }
3. 算法性能分析
- 空间复杂度: O ( 1 ) O(1) O(1)
- 时间复杂度:
- 最好时间复杂度: O ( n ) O(n) O(n)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 平均时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:稳定
三、希尔排序
1. 算法思想
- 希尔排序(Shell Sort):先追求表中元素部分有序,在逐渐逼近全局有序
- 先将待排序表分割成若干形如 L [ i , i + d , i + 2 d , . . . , i + k d ] L[i,i+d,i+2d,...,i+kd] L[i,i+d,i+2d,...,i+kd] 的特殊子表,对各个子表分别进行直接插入排序。缩小增量 d ,重复上述过程,直到 d = 1 d = 1 d=1 为止。
2. 算法实现
- 希尔排序
void ShellSort(int A[], int n) {int d, i, j;// A[0] 只是暂存单元,不是哨兵,当 j<=0 时,插入位置已到for (d = n / 2; d >= 1; d = d / 2) { // 步长变化for (i = d + 1; i <= n; i++) {if (A[i] < A[i - d]) { // 需将 A[i] 插入有序增量子表A[0] = A[i]; // 暂存在 A[0]for (j = i - d; j > 0 && A[0] < A[j]; j -= d) {A[j + d] = A[j]; // 记录后移,查找插入的位置}A[j + d] = A[0]; // 插入}}} }
3. 算法性能分析
- 空间复杂度: O ( 1 ) O(1) O(1)
- 时间复杂度:和增量序列 d 1 , d 2 , d 3 , . . . d_1,d_2,d_3,... d1,d2,d3,... 的选择有关,最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2),当 n n n 在某个范围内时,可达 O ( n 1.3 ) O(n^{1.3}) O(n1.3)
- 稳定性:不稳定
- 适用性:仅适用于顺序表,不适用于链表
四、冒泡排序
1. 算法思想
- 属于交换排序的一种。
- 交换排序:根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
- 冒泡排序:从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即 A [ i − 1 ] > A [ i ] A[i-1]>A[i] A[i−1]>A[i]),则交换它们,直到序列比较完。称这样过程为 “一趟” 冒泡排序。总共需要进行 n − 1 n-1 n−1 趟冒泡。
2. 算法实现
- 冒泡排序
// 交换 void swap(int &a, int &b) {int temp = a;a = b;b = temp; }void BubbleSort(int arr[], int n) {for (int i = 0; i < n - 1; i++) {bool flag = false; // 表示本趟冒泡是否发生交换的标志for (int j = n - 1; j > i; j--) { // 一趟冒泡过程if (arr[j - 1] > arr[j]) { // 若为逆序swap(arr[j], arr[j - 1]); // 交换flag = true;}}if (flag == false) return; // 本趟遍历后没有发生任何变化,说明表已经有序} }
3. 算法性能分析
- 空间复杂度: O ( 1 ) O(1) O(1)
- 时间复杂度:
- 最好时间复杂度: O ( n ) O(n) O(n)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 平均时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:稳定。
五、快速排序
1. 算法思想
- 和冒泡排序一样,也是属于交换排序的一种。
- 算法思想:
- 在待排序表 L [ 1... n ] L[1...n] L[1...n] 中任取一个元素
pivot
作为枢轴(或基准,通常取首元素) - 通过一趟排序将待排序表划分为独立的两部分 L [ 1... k − 1 ] L[1...k-1] L[1...k−1] 和 L [ k + 1... n ] L[k+1...n] L[k+1...n],使得 L [ 1... k − 1 ] L[1...k-1] L[1...k−1] 中的所有元素小于
pivot
, L [ k + 1... n ] L[k+1...n] L[k+1...n] 中的所有元素大于等于pivot
,pivot 放在了其最终位置 L ( k ) L(k) L(k) 上,这个过程称为依次 “划分”。 - 然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了其最终位置上。
- 在待排序表 L [ 1... n ] L[1...n] L[1...n] 中任取一个元素
- 快速排序算法优化 —— 尽量选择可以把数据中分的枢轴元素,例如:
- 选头、中、尾三个位置的元素,取中间值作为枢轴元素
- 随机选一个元素作为枢轴元素
2. 算法实现
-
快速排序
// 用第一个元素将待排序序列划分为左右两个部分 int Partition(int A[], int low, int high) {int pivot = A[low]; // 第一个元素作为枢轴while (low < high) { // 用 low、high 搜索枢轴的最终位置while (low < high && A[high] >= pivot)high--;A[low] = A[high]; // 比枢轴小的元素移动到左端while (low < high && A[low] <= pivot)low++;A[high] = A[low]; // 比枢轴大的元素移动到右端}A[low] = pivot; // 枢轴元素存放到最终位置return low; // 返回存放枢轴的最终位置 }void QuickSort(int A[], int low, int high) {if (low < high) { // 递归跳出的条件int pivotpos = Partition(A, low, high); // 划分QuickSort(A, low, pivotpos - 1); // 划分左子表QuickSort(A, pivotpos + 1, high); // 划分右子表} }
3. 算法性能分析
- 时间复杂度: O ( n × 递归层数 ) O(n×递归层数) O(n×递归层数)
- 最好时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- 最坏时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 平均时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- 空间复杂度: O ( 递归层数 ) O(递归层数) O(递归层数)
- 最好空间复杂度: O ( l o g 2 n ) O(log_2n) O(log2n)
- 最坏空间复杂度: O ( n ) O(n) O(n)
- 若每次选择的枢轴将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高
- 若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)
- 稳定性:不稳定
六、简单选择排序
1. 算法思想
- 属于选择排序的一种。
- 选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。
2. 算法实现
- 简单选择排序
void SelectSort(int A[], int n) {for (int i = 0; i < n - 1; i++){ // 一共进行 n - 1 趟int min = i; // 记录最小元素位置for (int j = i + 1; j < n; j++) // 在 A[i…n-1] 中选择最小的元素if (A[j] < A[min])min = j; // 更新最小元素位置if (min != i) swap(A[i], A[min]); // 封装的 swap 函数共移动元素3次} }
3. 算法性能分析
- 空间复杂度: O ( 1 ) O(1) O(1)
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 稳定性:不稳定
- 适用性:既可用于顺序表,也可用于链表
七、堆排序
1. 算法思想
- 也是选择排序中的一种。
- 堆(Heap):若 n n n 个关键字序列 L [ 1 … n ] L[1…n] L[1…n] 满足下面某一条性质,则称为堆:
- 大根堆:满足 L ( i ) ≥ L ( 2 i ) 且 L ( i ) ≥ L ( 2 i + 1 ) (1≤i≤n/2) L(i)≥L(2i)\quad且\quad L(i)≥L(2i+1) \tag{1≤i≤n/2} L(i)≥L(2i)且L(i)≥L(2i+1)(1≤i≤n/2)即在完全二叉树中,根 ≥ 左、右
- 小根堆:满足 L ( i ) ≤ L ( 2 i ) 且 L ( i ) ≤ L ( 2 i + 1 ) (1≤i≤n/2) L(i)≤L(2i)\quad且\quad L(i)≤L(2i+1) \tag{1≤i≤n/2} L(i)≤L(2i)且L(i)≤L(2i+1)(1≤i≤n/2)即在完全二叉树中,根 ≤ 左、右
- 建立大根堆:
- 思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
- 检查当前节点是否满足 根 ≥ 左、右,若不满足,将当前结点与更大的一个孩子呼唤
- 若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断下坠)
- 基于大根堆进行排序:
- 每一趟将堆顶元素加入有序子序列(与待排序序列中最后一个元素交换)
- 并将待排序元素序列再次调整为大根堆(小元素不断下坠)
- 基于大根堆的堆排序得到递增序列,基于小根堆的堆排序得到递减序列
2. 算法实现
- 建立大根堆
void BuildMaxHeap(int A[], int len) {for (int i = len / 2; i > 0; i--) // 从后往前调整所有非终端结点HeadAdjust(A, i, len); }void HeadAdjust(int A[], int k, int len) {A[0] = A[k]; // A[0] 暂存子树的根结点for (int i = 2 * k; i <= len; i *= 2) { // 沿 key 较大的子节点向下筛选if (i < len && A[i] < A[i + 1])i++; // 取 key 较大的子节点的下标if (A[0] >= A[i])break; // 筛选结束else {A[k] = A[i]; // 将 A[i] 调整到父节点上k = i; // 修改 k 值,以便继续向下筛选}}A[k] = A[0]; // 被筛选结点的值放入最终位置 }
- 大根堆排序
void HeapSort(int A[], int len) {BuildMaxHeap(A, len); // 初始建堆for (int i = len; i > 1; i--) { // n-1 趟的交换和建堆过程swap(A[i], A[1]); // 堆顶元素和堆底元素交换HeadAdjust(A, 1, i - 1); // 把剩余的待排序元素整理成堆} }
3. 算法性能分析
- 初始建堆的过程,关键字对比次数不超过 4 n 4n 4n,建堆时间复杂度为 O ( n ) O(n) O(n)
- 排序的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- 堆排序总的时间复杂度为: O ( n ) + O ( n l o g 2 n ) = O ( n l o g 2 n ) O(n)+O(nlog_2n)=O(nlog_2n) O(n)+O(nlog2n)=O(nlog2n)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 稳定性:不稳定
4. 插入和删除
- 插入:对于小根堆,新元素放到表尾,与父节点对比,若新元素比父节点更小,则二者互换,新元素就这样一路上升,直到无法继续上升为止。
- 删除:被删除的元素用堆底元素替代,然后让该元素不断下坠,直到无法下坠为止。
八、归并排序
1. 算法思想
- 归并:把两个或多个已经有序的序列合并成一个。
- m 路归并,每选出一个元素需要对比关键字 m-1 次。
- 归并排序:在内部排序中一般采用二路归并,把数组内的两个有序序列归并为一个
2. 算法实现
- 归并排序
int *B = (int *)malloc(n * sizeof(int));// A[low...mid] 和 A[mid+1...high] 各自有序,将两个部分归并 void Merge(int A[], int low, int mid, int high) {int i, j, k;for (k = low; k <= high; k++)B[k] = A[k]; // 将 A 中所有元素复制到 B 中for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {if (B[i] <= B[j]) // 将较小值复制到 A 中A[k] = B[i++];elseA[k] = B[j++];}while (i <= mid) A[k++] = B[i++];while (j <= high) A[k++] = B[j++]; }void MergeSort(int A[], int low, int high) {if (low < high) {int mid = (low + high) / 2; // 从中间划分MergeSort(A, low, mid); // 对左半部分归并排序MergeSort(A, mid + 1, high); // 对右半部分归并排序Merge(A, low, mid, high); // 归并} }
3. 算法性能分析
- 二路归并的归并树,形态上是一颗倒立的二叉树
- n 个元素进行二路归并排序,归并趟数为 ⌈ l o g 2 n ⌉ ⌈log_2n⌉ ⌈log2n⌉,每趟归并时间复杂度为 O ( n ) O(n) O(n),则总的算法时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
- 空间复杂度为 O ( n ) O(n) O(n)
- 稳定性:稳定
九、基数排序
1. 算法思想
- 基数:假设长度为 n 的线性表中每个结点 a j a_j aj 的关键字由 d d d 元组 ( k j d − 1 , k j d − 2 , k j d − 3 , . . . , k j 1 , k j 0 ) (k^{d-1}_j,k^{d-2}_j,k^{d-3}_j,...,k^1_j,k^0_j) (kjd−1,kjd−2,kjd−3,...,kj1,kj0) 组成,其中 0 ≤ k j i ≤ r − 1 ( 0 ≤ j < n , 0 ≤ i ≤ d − 1 ) 0≤k_j^i≤r-1(0≤j<n,0≤i≤d-1) 0≤kji≤r−1(0≤j<n,0≤i≤d−1), r r r 称为基数
- 基数排序得到递减序列过程:
- 初始化:设置 r r r 个空队列, Q r − 1 , Q r − 2 , . . . , Q 0 Q_{r-1},Q_{r-2},...,Q_0 Qr−1,Qr−2,...,Q0
- 按照各个关键字位权重递增的次序(个、十、百),对 d d d 个关键字位分别做分配和收集
- 分配:顺序扫描各个元素,若当前处理的关键字位 = x = x =x,则将元素插入 Q x Q_x Qx 队尾
- 收集:把 Q r − 1 , Q r − 2 , . . . , Q 0 Q_{r-1},Q_{r-2},...,Q_0 Qr−1,Qr−2,...,Q0 各个队列中的结点依次出队并链接
2. 算法性能分析
- 空间复杂度: O ( r ) O(r) O(r)
- 时间复杂度:一趟分配 O ( n ) O(n) O(n),一趟收集 O ( r ) O(r) O(r),总共 d d d 趟分配、收集,总的时间复杂度为 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))
- 稳定性:稳定
- 适用场景:
- 数据元素的关键字可以方便地拆分为 d d d 组,且 d d d 较小
- 每组关键字的取值范围不大,即 r r r 较小
- 数据元素个数 n n n 较大
十、外部排序
1. 算法思想
- 使用 “归并排序” 的方法,最少只需在内存中分配 3 块大小的缓冲区即可对任意一个大文件进行排序。
- 外部排序:数据元素太多,无法一次全部读入内存进行排序。
2. 算法实现
- 若要进行 k 路归并排序,则需要在内存中分配 k 个输入缓冲区和 1 个输出缓冲区。
- 生成 r 个初始归并段(对 L 个记录进行内部排序,组成一个有序的初始归并段)
- 进行 S 趟 k 路归并, S = ⌈ l o g k r ⌉ S= ⌈log_kr⌉ S=⌈logkr⌉
- 将 k 个归并段的块读入 k 个输入缓冲区
- 用 “归并排序” 的方法从 k 个归并段中选出几个最小记录暂存到输出缓冲区中
- 当输出缓冲区满时,写出外存
3. 算法性能分析
- 外部排序时间开销 = 读写外存的时间 + 内部排序所需时间 + 内部归并所需时间
- 对 r 个初始归并段,做 k 路归并,则归并树可用 k 叉树表示;若树高为 h,则归并趟数 = h − 1 = ⌈ l o g k r ⌉ = h - 1 = ⌈log_kr⌉ =h−1=⌈logkr⌉
- k 叉树第 h 层最多有 k h − 1 k^{h-1} kh−1 个结点,则 r ≤ k h − 1 , ( h − 1 ) 最小 = ⌈ l o g k r ⌉ r ≤k^{h-1},(h-1)_{最小}=⌈log_kr⌉ r≤kh−1,(h−1)最小=⌈logkr⌉
- k 越大,r 越小,归并趟数越小,读写磁盘次数越少
- 优化:
- 减少读写外存的趟数
- 采用多路归并可以减少归并趟数,从而减少磁盘 IO 次数。
- 负面影响:
- k 路归并时,需要开辟 k 个输入缓冲区,内存开销增加
- 每挑选一个关键字需要对比关键字(k-1)次,内部归并所需时间增加。
- 减少初始归并段数量
- 生成初始归并段的 “内存工作区” 越大,初始归并段越长。
- 若能增加初始归并段的长度,则可减少初始归并段数量 r
- 减少读写外存的趟数
4. k 路平衡归并
- 最多只能有 k 个段归并为一个;
- 每一趟归并中,若有 m 个归并段参与归并,则经过这一趟处理得到 ⌈ m / k ⌉ ⌈m/k⌉ ⌈m/k⌉ 个新的归并段。
5. 败者树(减少归并趟数)
- 败者树 —— 可视为一颗完全二叉树(多了个头头)。
- k 个叶结点分别是当前参加比较的元素,每个叶子结点对应一个归并段
- 非叶子结点用来记忆左右子树中的 “失败者”,记录败者来自哪个归并段
- 而让胜者往上继续进行比较,一直到根结点,根结点记录冠军来自哪个归并段
- 败者树解决的问题:使用多路平衡归并可减少归并趟数,但是用一一比较的方法从 k 个归并段选出一个最小(大)元素需要对比关键字 k − 1 k-1 k−1 次,构造败者树可以使关键字对比次数减少到 ⌈ l o g 2 k ⌉ ⌈log_2k⌉ ⌈log2k⌉ 次。
- 对于 k 路归并,第一次构造败者树需要对比关键字 k − 1 k-1 k−1 次;构造好败者树后,每次选出最大(小)元素只需对比关键字 ⌈ l o g 2 k ⌉ ⌈log_2k⌉ ⌈log2k⌉ 次。
6. 置换-选择排序(减少初始归并段数量)
- 优化前:用于内部排序的内存工作区(WA:Work Area)可容量 l l l 个记录,则每个初始归并段也只能包含 l l l 个记录,若文件共有 n n n 个记录,则初始归并段数量为 r = n / l r=n/l r=n/l。
- 设初始待排文件为 FI(File Input),初始归并段输出文件为 FO(File Output),内存工作区为 WA,FO 和 WA 的初始状态为空,WA 可容纳 w w w 个记录。置换-选择排序算法步骤:
- 从 FI 输入 w w w 个记录到工作区 WA
- 从 WA 中选出其中关键字取最小值的记录,记为 MINIMAX 记录
- 将 MINIMAX 记录输出到 FO 中去
- 若 FI 不空,则从 FI 输入下一个记录到 WA 中
- 从 WA 中所有关键字比 MINIMAX 记录的关键字大的记录中选出最小关键字记录,作为新的 MINIMAX 记录
- 重复 3 ~ 5,直到在 WA 中选不出新的 MINIMAX 记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到 FO 中去
- 重复 2 ~ 6,直到 WA 为空。由此得到全部初始归并段
十一、最佳归并树
1. 归并树的性质
- 每个初始归并段看作一个叶子结点,归并段的长度作为结点权值,则归并过程中的磁盘 IO 次数 = 归并数的带权路径长度(WPL)* 2
2. 构造最佳归并树
- 对于 k 叉归并,若初始归并段的数量无法构成严格的 k 叉归并树,则需要补充几个长度为 0 的 “虚段”,再进行 k 叉哈夫曼树的构造。
- k 叉的最佳归并树一定是一颗严格的 k 叉树,即树中只包含度为 k 或 0 的结点。设度为 k 的结点有 n k n_k nk 个,度为 0 的结点有 n 0 n_0 n0 个,归并数总结点数为 n,则:
{ n = n 0 + n k k n k = n − 1 ⟹ n 0 = ( k − 1 ) n k + 1 ⟹ n k = ( n 0 − 1 ) ( k − 1 ) , n k , n 0 ∈ N ∗ \begin{cases} n=n_0+n_k\\ kn_k=n-1 \\ \end{cases} \implies n_0=(k-1)n_k+1\implies n_k=\frac{(n_0-1)}{(k-1)},n_k,n_0∈ℕ* {n=n0+nkknk=n−1⟹n0=(k−1)nk+1⟹nk=(k−1)(n0−1),nk,n0∈N∗因此:- 若 ( 初始归并段数量 − 1 ) % ( k − 1 ) = 0 (初始归并段数量 - 1)\%(k-1)=0 (初始归并段数量−1)%(k−1)=0,说明刚好可以构成严格 k 叉树,此时不需要添加虚段
- 若 ( 初始归并段数量 − 1 ) % ( k − 1 ) = u ≠ 0 (初始归并段数量 - 1)\%(k-1)=u≠0 (初始归并段数量−1)%(k−1)=u=0,则需要补充 ( k − 1 ) − u (k-1)-u (k−1)−u 个虚段