【数据结构】第7章 排序

0 总结

排序算法 空间复杂度 最好时间复杂度 平均时间复杂度 最坏时间复杂度 稳定性 线性表适用性
直接插入排序 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) 稳定 顺序、链式
折半插入排序 O ( 1 ) O(1) O(1) O ( n 2 ) O(n^2) O(n2) 稳定 顺序
希尔排序(缩小增量排序) O ( 1 ) O(1) O(1) O ( n 1.3 ) O(n^{1.3}) O(n1.3) 依赖于增量序列的函数 O ( n 2 ) O(n^2) O(n2) 不稳定 顺序
冒泡排序 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) 稳定 顺序、链式
快速排序 递归栈 O ( l o g 2 n ) O({log}_2n) O(log2n)~ O ( n ) O(n) O(n) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) 不稳定 顺序
简单选择排序 O ( 1 ) O(1) O(1) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) 不稳定 顺序、链式
堆排序 O ( 1 ) O(1) O(1) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) 不稳定 顺序
2路归并排序 O ( n ) O(n) O(n) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) 稳定 顺序、链式
基数排序 O ( r ) O(r) O(r) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) 稳定 顺序、链式
排序算法 归属 比较 移动 趟数
直接插入排序 插入排序 最好: n − 1 n-1 n1
最坏: ∑ i = 2 n i = n ( n − 1 ) 2 \sum_{i=2}^ni=\frac{n(n-1)}{2} i=2ni=2n(n1)
最好: 0 0 0
最坏: ∑ i = 2 n ( i + 1 ) = ( n − 1 ) ( n + 4 ) 2 \sum_{i=2}^n(i+1)=\frac{(n-1)(n+4)}{2} i=2n(i+1)=2(n1)(n+4)
n − 1 n-1 n1
折半插入排序 插入排序 O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n) 同上 n − 1 n-1 n1
希尔排序(缩小增量排序) 插入排序
冒泡排序 交换排序 最好: n − 1 n-1 n1
最坏: ∑ i = 1 n − 1 ( n − i ) = n ( n − 1 ) 2 \sum_{i=1}^{n-1}(n-i)=\frac{n(n-1)}{2} i=1n1(ni)=2n(n1)
最好: 0 0 0
最坏: ∑ i = 1 n − 1 3 ( n − i ) = 3 n ( n − 1 ) 2 \sum_{i=1}^{n-1}3(n-i)=\frac{3n(n-1)}{2} i=1n13(ni)=23n(n1)
最好: 1 1 1
最坏: n − 1 n-1 n1
快速排序 交换排序
简单选择排序 选择排序 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n1) 最好: 0 0 0
最坏: 3 ( n − 1 ) 3(n-1) 3(n1)
n − 1 n-1 n1
堆排序 选择排序 建堆 O ( n ) O(n) O(n) 调整 O ( h ) O(h) O(h) n − 1 n-1 n1次调整
k k k路归并排序 ⌈ l o g k n ⌉ ( n − 1 ) ( k − 1 ) \lceil {log}_kn \rceil(n-1)(k-1) logkn(n1)(k1) 与初始序列无关 ⌈ l o g k n ⌉ \lceil {log}_kn \rceil logkn
基数排序 分配 O ( n ) O(n) O(n) 收集 O ( r ) O(r) O(r) d d d

1 排序的概念

  • 排序:重新排列表中的元素,使表中的元素满足按关键字有序的过程
    (1)输入 n n 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
    (2)输出:输入序列的一个重排 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有序排列
    (3)算法的稳定性:经过排序后,能使关键字相同的元素保持原顺序中的相对位置不变,则称这个排序算法是稳定的,否则是不稳定的。算法稳定性并不能衡量算法的优劣
  • 算法分类
    (1)内部排序:在排序期间元素全部存放在内存中的排序
    一般都要进行:比较、移动
    (2)外部排序:在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序

2 插入排序

  • 基本思想:每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成

2.1 直接插入排序

直接插入排序

void InsertSort(ElemType 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];					//复制到插入位置
		}
	}
}
  • 适用于基本有序的排序表和数据量不大的排序表

2.2 折半插入排序

void InsertSort(ElemType A[], int n) {
    
    
	int i, j;
	int 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];						//插入操作
	}
}

2.3 希尔排序(缩小增量排序)

希尔排序

void ShellSort(ElemType A[], int n) {
    
    
	/*A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到*/
	for (dk = n / 2; dk >= 1; dk = dk / 2)		//步长变化
		for (i = dk + 1; i <= n; i++) 
			if (A[i] < A[i - dk]) {
    
    				//需将A[i]插入有序增量子表
				A[0] = A[i];					//暂存在A[0]
				for (j = i - dk; j > 0 && A[0] < A[j]; j -= dk)
					A[j + dk] = A[j];			//记录后移,查找插入的位置
				A[j + dk] = A[0];				//插入
			}
}

3 交换排序

  • 基本思想:根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置

3.1 冒泡排序

冒泡排序

void BubbleSort(ElemType A[], int n) {
    
    
	for (i = 0; i < n - 1; i++) {
    
    
		flag = false;							//表示本趟冒泡是否发生交换的标志
		for (j = n - 1; j > i; j--)				//一趟冒泡过程
			if (A[j - 1] > A[j]) {
    
    				//若为逆序
				swap(A[j - 1], A[j]);			//交换
				flag = true;
			}
		if (flag = false)
			return;								//本趟遍历后没有发生交换,说明表已经有序
	}
}

双向起泡

/*双向起泡排序,交替进行正反两个方向的起泡过程*/
void BubbleSort(ElemType A[], int n) {
    
    
	int low = 0, high = n - 1;
	_Bool flag = true;							//一趟冒泡后记录元素是否交换标志
	while (low < high && flag) {
    
    				//循环跳出条件,当flag为false说明已没有逆序
		flag = false;							//每趟初始置flag为false
		for (i = low; i < high; i++)			//从前向后起泡
			if (a[i] > a[i + 1]) {
    
    				//发生逆序
				swap(a[i], a[i + 1]);			//交换
				flag = true;					//置flag
			}
		high--;									//更新上界
		for (i = high; i > low; i--)			//从后往前起泡
			if (a[i] < a[i + 1]) {
    
    				//发生逆序
				swap(a[i], a[i - 1]);			//交换
				flag = true;					//置flag
			}
		low++;									//修改下界
	}
}

3.2 快速排序【分治】

  • 一趟快速排序(一次划分):小 枢轴 大
    快速排序
void QuickSort(ElemType A[], int low, int high) {
    
    
	if (low < high) {
    
    							//递归跳出的条件
		int pivotpos = Partition(A, low, high);	//划分
		QuickSort(A, low, pivotpos - 1);		//依次对两个子表进行递归排序
		QuickSort(A, pivotpos + 1, high);
	}
}

/*划分,将表A[low...high]划分为满足上述条件的两个子表*/
int Partition(ElemType A[], int low, int high) {
    
    
	ElemType pivot = A[low];					//将当前表中第一个元素设为枢轴,对表进行划分
	while (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;									//返回存放枢轴的最终位置
}
  • 适用于待排序数据分布较为随机时

4 选择排序

  • 每一趟在后面 n − i + 1 n-i+1 ni+1 i = 1 , 2 , ⋅ ⋅ ⋅ , n − 1 i=1,2,···,n-1 i=1,2,,n1)个待排序元素中选取关键字最小的元素,作为有序子序列的第 i i i个元素,直到第 n − 1 n-1 n1趟做完,待排序元素只剩下 1 1 1个,就不用再选了。

4.1 简单选择排序

简单选择排序

void SelectSort(ElemType A[], int n) {
    
    
	for (i = 0; i < n - 1; i++) {
    
    				//一共进行n-1趟
		min = i;								//记录最小元素位置
		for (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次
	}
}

4.2 堆排序

  • 当前仅当 n n n个关键字序列 L [ 1... n ] L[1...n] L[1...n]满足以下条件之一时为:( 1 ≤ i ≤ ⌊ n / 2 ⌋ 1≤i≤\lfloor n/2 \rfloor 1in/2
    (1)大根堆(大顶堆) L ( i ) ≥ L ( 2 i ) 且 L ( i ) ≥ L ( 2 i + 1 ) L(i)≥L(2i)且L(i)≥L(2i+1) L(i)L(2i)L(i)L(2i+1)
    (2)小根堆(小顶堆) L ( i ) ≤ L ( 2 i ) 且 L ( i ) ≤ L ( 2 i + 1 ) L(i)≤L(2i)且L(i)≤L(2i+1) L(i)L(2i)L(i)L(2i+1)
  • 堆相当于一棵完全二叉树
    堆排序
/*堆排序*/
void HeapSort(ElemType A[], int len) {
    
    
	BuildMaxHeap(A, len);						//初始建堆
	for (i = len; i > 1; i--) {
    
    					//n-1趟的交换和建堆过程
		swap(A[i], A[1]);						//输出堆顶元素(和堆底元素交换)
		HeadAdjust(A, 1, i - 1);				//调整,把剩余的i-1个元素整理成堆
	}
}

/*建立大根堆*/
void BuildMaxHeap(ElemType A[], int len) {
    
    
	for (i = len / 2; i > 0; i--)				//从i=[n/2]~1,反复调整堆
		HeadAdjust(A, i, len);
}

/*将元素k为根的子树进行调整*/
void HeadAdjust(ElemType A[], int k, int len) {
    
    
	A[0] = A[k];								//A[0]暂存子树的根结点
	for (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];								//被筛选结点的值放入最终位置
}
  • 适用于关键字较多的情况
  • 通常,取一大堆数据中的 k k k个最大(最小)的元素时,都优先采用堆排序

5 归并排序

  • 基本思想:将两个或两个以上的有序表组合成一个新的有序表
  • 2路归并排序:两两归并【分治】
    2路归并排序
ElemType* B = (ElemType*)malloc((n + 1) * sizeof(ElemType));	//辅助数组

/*将前后相邻的两个有序表归并为一个有序表*/
void Merge(ElemType A[], int low, int mid, int high) {
    
    
	/*表A的两段A[low...mid]和A[mid...high]各自有序,将它们合并成一个有序表*/
	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])						//比较B的左右两段中的元素
			A[k] = B[i++];						//将较小值复制到A中
		else
			A[k] = B[j++];
	}
	while (i <= mid)
		A[k++] = B[i++];						//若第一个表未检测完,复制
	while (j <= high)
		A[k++] = B[j++];						//若第二个表未检测完,复制
}

/*合并两个已排序的子表得到排序结果*/
void MergeSort(ElemType 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);				//归并
	}
}

6 基数排序

  • 基本思想:借助多关键字排序的思想对单逻辑关键字进行排序

假设长度为 n n n的线性表中每个结点 a j a_j aj的关键字由 d d d元组 ( k j d − 1 , k j d − 2 , ⋅ ⋅ ⋅ , k j 1 , k j 0 ) (k_j^{d-1},k_j^{d-2},···,k_j^{1},k_j^{0}) (kjd1,kjd2,,kj1,kj0)组成,满足 0 ≤ k j i ≤ r − 1 0≤k_j^i≤r-1 0kjir1 0 ≤ j < n , 0 ≤ i ≤ d − 1 0≤j<n,0≤i≤d-1 0jn0id1
其中, k j d − 1 k_j^{d-1} kjd1为最主位关键字, k j 0 k_j^{0} kj0为最次位关键字

  • 最高位优先(MSD)法:按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列
  • 最低位优先(LSD)法:按关键字权重递增依次进行排序,最后形成一个有序序列
  • 一次稳定的基数排序过程:对 i = 0 , 1 , ⋅ ⋅ ⋅ , d − 1 i=0,1,···,d-1 i=0,1,,d1,依次做一次“分配”和“收集”
  • LSD排序过程:使用 r r r个队列 Q 0 , Q 1 , ⋅ ⋅ ⋅ , Q r − 1 Q_0,Q_1,···,Q_{r-1} Q0,Q1,,Qr1
    1° 分配:开始时,把 Q 0 , Q 1 , ⋅ ⋅ ⋅ , Q r − 1 Q_0,Q_1,···,Q_{r-1} Q0,Q1,,Qr1各个队列置成空队列,然后依次考察线性表中的每个结点 a j a_j aj j = 0 , 1 , ⋅ ⋅ ⋅ , n − 1 j=0,1,···,n-1 j=0,1,,n1),若 a j a_j aj的关键字 k j i = k k_j^i=k kji=k,就把 a j a_j aj放进 Q k Q_k Qk队列中
    2° 收集:把 Q 0 , Q 1 , ⋅ ⋅ ⋅ , Q r − 1 Q_0,Q_1,···,Q_{r-1} Q0,Q1,,Qr1各个队列中的结点依次首尾相接,得到新的结点序列,从而组成新的线性表

基数排序

7 内部排序算法的应用

  • 选取排序方法应考虑的因素
    (1)待排序的元素数目 n n n
    (2)元素本身信息量的大小
    (3)关键字的结构及其分布情况
    (4)稳定性的要求
    (5)语言工具的条件,存储结构及辅助空间的大小等
  • 排序算法选择
    (1)若 n n n较小: O ( n 2 ) O(n^2) O(n2)
      ① 记录本身信息量较大 —— 简单选择排序
      ② 记录本身信息量较小 —— 直接插入排序
      ③ 关键字初始基本有序 —— 直接插入排序、冒泡排序
    (2)若 n n n中等规模     —— 希尔排序
    (3)若 n n n较大: O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n)
      ① 关键字随机分布时  —— 快速排序【基于比较的内部排序方法中最好的方法】
      ② 所需辅助空间少   —— 堆排序
      ③ 稳定        —— 归并排序【最好结合直接插入排序
    (4)若 n n n很大,关键字位数较少且可以分解时 —— 基数排序
  • 排序算法小结
    (1)当文件的 n n n个关键字随机分布时,任何借助于“比较”的排序算法,至少需要 O ( n l o g 2 n ) O(n{log}_2n) O(nlog2n)的时间
    (2)当记录本身信息量较大时,为避免耗费大量时间移动记录,可用链表作为存储结构

8 外部排序

  • 外部排序指待排序文件较大,内存一次放不下,需存放在外存的文件的排序
  • 为减少平衡归并中外存读写次数所采取的方法:增大归并路数和减少归并段个数
  • 利用败者树增大归并路数
  • 利用置换 - 选择排序增大归并段长度来减少归并段个数
  • 由长度不等的归并段,进行多路平衡归并,需要构造最佳归并树
  • 时间代价主要考虑访问磁盘的次数,即I/O次数
  • 通常采用多路平衡归并排序,包括两个相对独立的阶段:
    (1)生成初始归并段阶段。根据内存缓冲区大小,将外存上的文件分成若干长度为 l l l的子文件,依次读入内存并利用内部排序方法对它们进行排序,并将排序后得到的有序子文件归并段顺串)重新写回外存
    (2)多趟归并排序阶段。对这些归并段进行逐趟归并,使归并段逐渐由小到大,直至得到整个有序文件为止
  • 外 部 排 序 的 总 时 间 = 内 部 排 序 所 需 的 时 间 + 外 存 信 息 读 写 的 时 间 + 内 部 归 并 所 需 的 时 间 外部排序的总时间=内部排序所需的时间+外存信息读写的时间+内部归并所需的时间 =++
  • 只要增大归并路数 k k k,或减少初始归并段个数 r r r,都能减少归并趟数 S S S,进而减少读写磁盘的次数,达到提高外部排序速度的目的

一般地,对 r r r个初始归并段,做 k k k路平衡归并,归并树可用严格 k k k叉树(即只有度为 k k k与度为 0 0 0的结点的 k k k叉树)来表示

  • 第一趟可将 r r r个初始归并段归并为 ⌈ r / k ⌉ \lceil r/k \rceil r/k个归并段,以后每趟归并将 m m m个归并段归并为 ⌈ m / k ⌉ \lceil m/k \rceil m/k个归并段,直至最后形成一个大的归并段为止
  • 树 的 高 度 = ⌈ l o g k r ⌉ = 归 并 趟 数 S 树的高度=\lceil {log}_kr \rceil=归并趟数S =logkr=S
  • 内部归并排序的缺点:
    (1)内部归并时间与归并路数 k k k有关
    (2)初始归并段长度依赖于内部排序时可用内存工作区的大小

8.1 败者树

  • 增加归并路数 k k k → 内部归并时间增加 → 解决:引入败者树
  • 败者树:可视为完全二叉树
    (1) k k k个叶结点分别存放 k k k个归并段在归并过程中当前参加比较的记录
    (2)内部结点用于记忆左右子树中的“失败者”,而上胜者往下继续进行比较,一直到根结点
    (3)若比较两个数,大的为失败者,小的为胜利者,则根结点指向的数为最小数

k k k路归并的败者树深度 = ⌈ l o g 2 k ⌉ =\lceil {log}_2k \rceil =log2k
总的比较次数 = S ( n − 1 ) ⌈ l o g 2 k ⌉ = ⌈ l o g k r ⌉ ( n − 1 ) ⌈ l o g 2 k ⌉ = ( n − 1 ) ⌈ l o g 2 r ⌉ =S(n-1)\lceil {log}_2k \rceil=\lceil {log}_kr \rceil (n-1)\lceil{log}_2k \rceil=(n-1)\lceil {log}_2r \rceil =S(n1)log2k=logkr(n1)log2k=(n1)log2r
趟数 S = ⌈ l o g k r ⌉ S=\lceil {log}_kr \rceil S=logkr
k k k个元素中选择关键字最小的记录需要比较 ⌈ l o g 2 k ⌉ \lceil{log}_2k \rceil log2k次【此处为改进的地方(原: k − 1 k-1 k1
每趟归并 n n n个元素需要做 ( n − 1 ) ⌈ l o g 2 k ⌉ (n-1)\lceil {log}_2k \rceil (n1)log2k次比较

  • 使用败者树后,内部归并的比较次数与 k k k无关
  • 注意:归并路数 k k k并不是越大越好, k k k值过大时,读写外存次数会增加
    缓冲区

8.2 置换 - 选择排序(生成初始归并段)

  • 减少初始归并段个数 r r r → 置换 - 选择算法生成长度不等的初始归并段

设初始待排文件为FI,初始归并段输出文件为FO,内存工作区为WA,FO和WA的初始状态为空,WA可容纳 w w w个记录

  • 置换 - 选择算法步骤:
    从FI输入 w w w个记录到工作区WA
    从WA中选出其中关键字取最小值的记录,记为MINIMAX记录
    将MINIMAX记录输出到FO中去
    若FI不空,则从FI输入下一个记录到WA中
    从WA中所有关键字比MINIMAX记录的关键字大的记录中选出最小关键字记录,作为新的MINMAX记录
    重复3°~5°,直至在WA中选不出新的MINIMAX记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到FO中去
    重复2°~6°,直至WA为空。由此得到全部初始归并段
  • 在WA中选择MINIMAX记录的过程需利用败者树来实现

8.3 最佳归并树

  • 设计 m m m路归并排序的优化方案
  • 最佳归并树 m m m叉哈夫曼树
    (1)让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,使得总的I/O次数最少
    (2)若初始归并段不足以构成一棵严格 k k k叉树时,需要添加长度为 0 0 0的“虚段”

设度为 0 0 0的结点有 n 0 n_0 n0 = n =n =n)个,度为 k k k的结点有 n k n_k nk个,则对严格 k k k叉树有 n 0 = ( k − 1 ) n k + 1 n_0=(k-1)n_k+1 n0=(k1)nk+1,即 n k = ( n 0 − 1 ) / ( k − 1 ) n_k=(n_0-1)/(k-1) nk=(n01)/(k1)

  • 判定添加虚段的数目:
    1° 若 ( n 0 − 1 ) % ( k − 1 ) = 0 (n_0-1)\%(k-1)=0 (n01)%(k1)=0,则说明这 n 0 n_0 n0个叶结点正好可以构造 k k k叉归并树。此时,内结点有 n k n_k nk
    2° 若 ( n 0 − 1 ) % ( k − 1 ) = u ≠ 0 (n_0-1)\%(k-1)=u≠0 (n01)%(k1)=u=0,则说明对于这 n 0 n_0 n0个叶结点,其中有 u u u个多余,不能包含在 k k k叉归并树中。则需要再加上 k − u − 1 k-u-1 ku1个空归并段来建立归并树。此时,内结点有 n k + 1 n_k+1 nk+1

9 参考

王道

猜你喜欢

转载自blog.csdn.net/qq_44714521/article/details/107430314