8. 排序

考纲内容

  • 排序的基本概念
  • 插入排序
    • 直接插入排序
    • 折半插入排序
    • 希尔排序
  • 交换排序
    • 冒泡排序
    • 快速排序(重点)
  • 选择排序
    • 简单选择排序
    • 堆排序(重点)
  • 2路归并排序(重点)
  • 基数排序
  • 外部排序
  • 各种排序算法的比较
  • 排序算法的应用

1. 排序的基本概念

1. 定义

  • 排序:重新排列表中的元素,使表中的元素满足按关键字有序的过程
  • 算法的稳定性:若待排序表中有两个元素 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前,则称该算法是稳定的
  • 内部排序:在排序期间元素全部存放在内存中的排序
  • 外部排序:在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序
  • 内部排序的一般操作 :比较两个关键字的大小,确定对应元素的前后关系,然后移动元素使之有序
  • 时间复杂度一般由比较和移动的次数决定

2. 插入排序

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

1. 直接插入排序

  • 插入流程—L()表示一个元素,L[]表示一个表

    1. 查出L(i)在L[1…i-1]中的插入位置k
    2. 将L[k…i-1]中的所有元素依次后移一个位置
    3. 将L(i)复制到L(k)
      8. 排序_第1张图片
  • 实现对L[1…n]的排序

    1. 初始L[1]视为一个已排好序的子序列
    2. 依次将L[2]~L[n]插入到前面已排好序的子序列中
  • 算法实现

    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];			//复制到插入位置
    		}
    }
    
  • 性能分析

    • 空间复杂度 O ( 1 ) O(1) O(1)
    • 时间复杂度:比较次数和移动次数取决于待排序表的初始状态
      • best O ( n ) O(n) O(n)(有序,只需比较n次,无需移动)
      • worst O ( n 2 ) O(n^2) O(n2)(逆序,比较 ∑ i = 2 n i \sum_{i=2}^ni i=2ni次,移动 ∑ i = 2 n ( i + 1 ) \sum_{i=2}^n(i+1) i=2n(i+1)次)
      • average O ( n 2 ) O(n^2) O(n2)(总比较和移动次数都约为 n 2 4 \frac{n^2}4 4n2
    • 稳定性:稳定
    • 适用性:顺序存储和链式存储的线性表

2. 折半插入排序

  • 思想

    • 先确定待插入位置后,再统一地向后移动元素
      8. 排序_第2张图片
  • 算法实现

    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];
    	}
    }
    
  • 性能分析

    • 仅减少了比较元素的次数(与表的初始状态无关,仅取决于表中元素个数),约为 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n)
    • 移动次数未改变(依赖于表的初始状态)
    • 时间复杂度 O ( n 2 ) O(n^2) O(n2)
    • 稳定性:稳定
    • 适用性:数据量不是很大的排序表

3. 希尔排序(重点)

  • 思想

    1. 先将待排序表分割成若干形如L[i, i+d, i+2d,···, i+kd]的特殊子表(把相隔某个增量的记录组成一个子表)
    2. 对各个子表分别进行直接插入排序
    3. 当整个表中的元素已基本有序时,再对全体记录进行一次直接插入排序
  • 基本流程

    1. 先取一个小于n的步长 d 1 d_1 d1(n/2),把表中的全部记录分成 d 1 d_1 d1组,所有距离为 d 1 d_1 d1的倍数的记录放在同一组,在各组内进行直接插入排序
    2. 取第二个步长 d 2 ( ⌊ d i / 2 ⌋ ) < d 1 d_2(\lfloor d_i/2 \rfloor) < d_1 d2(di/2)<d1,重复上述过程,直到所取到的 d t = 1 d_t=1 dt=1,即所有记录已放在同一组中
    3. 再进行一次直接插入排序,由于此时已经具有较好的局部有序性,故可以很快得到最终结果
      8. 排序_第3张图片
  • 算法实现

    void ShellSort(int 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[0];		//插入
    			}
    }
    
  • 性能分析

    • 空间复杂度 O ( 1 ) O(1) O(1)
    • 时间复杂度:依赖于增量序列的函数
      • 当n在某个特定范围时,约为 O ( n 1.3 ) O(n^{1.3}) O(n1.3)
      • worst: O ( n 2 ) O(n^2) O(n2)
    • 稳定性:不稳定
    • 适用性:仅适用于线性表为顺序存储的情况

3. 交换排序

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

1. 冒泡排序

  • 基本思想

    1. 从后往前(或从前往后)两两比较相邻元素的值,若为逆序(即A[i-1] > A[i]),则交换它们,直到序列比较完(第一趟)
    2. 下一趟冒泡时,前一趟确定的最小元素不再参与比较
      8. 排序_第4张图片
  • 算法实现

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

    • 空间复杂度 O ( 1 ) O(1) O(1)
    • 时间复杂度每趟排序都会将一个元素放置到其最终位置上
      • best O ( n ) O(n) O(n)——(有序)排序趟数:1;移动次数:0;比较次数:n-1
      • worst O ( n 2 ) O(n^2) O(n2)——(逆序)排序趟数:n-1;移动次数: 3 n ( n − 1 ) 2 \frac{3n(n-1)}2 23n(n1);比较次数: n ( n − 1 ) 2 \frac{n(n-1)}2 2n(n1)
    • 稳定性:稳定

2. 快速排序(重点)

  • 基本思想

    • 在待排序表L[1···n]中任取一个元素pivot作为枢轴(通常取首元素),通过一次划分将待排序表划分为独立的两部分L[1···k-1]和L[k+1···n],使得前者元素全部小于pivot,后者元素全部大于等于pivot,则pivot放在了其最终位置L[k]上(一趟快速排序)
    • 分别递归地对两个子表重复上述过程,直到每部分内只有一个元素或空为止
      8. 排序_第5张图片
  • 算法实现

    int Partition(int A[], int low, int high)//一趟划分
    {
    	int 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;				//返回存放枢轴的最终位置
    }
    
    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);
    	}
    }
    
  • 性能分析

    • 空间复杂度:借助一个递归工作栈保存每层递归调用的必要信息(用二叉树分析)
      • best O ( l o g 2 n ) O(log_2 n) O(log2n)——最小高度: ⌊ l o g 2 n ⌋ + 1 \lfloor log_2 n \rfloor+1 log2n+1
      • worst O ( n ) O(n) O(n)——-最大高度: n n n
      • average O ( l o g 2 n ) O(log_2n) O(log2n)
    • 时间复杂度:运行时间主要取决于划分操作的好坏
      • best O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)——平衡划分
      • worst O ( n 2 ) O(n^2) O(n2)——元素基本有序或逆序
    • 稳定性:不稳定
    • 所有内部排序算法中平均性能最优
  • 提高效率的方法

    1. 尽量选取一个可以将数据中分的枢轴元素
    2. 随机地从当前表中选取枢轴元素

3. 算法应用

1. 编写双向冒泡排序算法,在正反两个方向交替进行扫描,即第一趟把关键字最大的元素放在序列的最后面,第二趟把关键字最小的元素放在序列的最前面,如此反复进行

  • 算法实现
    void BubbleSort(int A[], int n)
    {
    	int low = 0, high = n-1;
    	int i, temp;
    	bool flag = true;				//一趟冒泡后记录元素是否交换标志
    	while(low < high && flag)		//flag为false说明已没有逆序
    	{
    		flag = false;
    		for(i = low; i < high; ++i)//从前向后冒泡
    			if(a[i] > a[i+1])
    			{
    				temp = a[i];
    				a[i] = a[i+1];
    				a[i+1] = temp;
    				flag = true;
    			}
    			--high;
    		for(i = high; i > low; --i)//从后往前冒泡
    			if(a[i] < a[i-1])
    			{
    				temp = a[i];
    				a[i] = a[i-1];
    				a[i-1] = temp;
    				flag = true;
    			}
    		++low;
    	}
    }
    

2. 已知线性表按顺序存储,且每个元素都是不相同的整数型元素,设计把所有奇数移动到偶数前边的算法(要求时间最少,辅助空间最少)

  • 算法实现
    void move(int A[], int len)
    {
    	int i = 0, j = len-1;
    	while(i < j)
    	{
    		while(i < j && A[i]%2 != 0)
    			++i;
    		while(i < j && A[i]%2 != 1)
    			--j;
    		if(i < j)
    		{
    			temp = A[i];
    			A[i] = A[j];
    			A[j] = temp;
    			++i, --j;
    		}
    	}
    }
    

3. 试编写一个算法,使之能够在数组L[1…n]中找出第k小的元素(即从小到大排序后处于第k个位置的元素)

  • 算法思想:从数组L[1…n]中选择枢轴pivot(随机或直接取第一个)进行和快速排序一样的划分操作后,表L[1…n]被划分为L[1…m-1]和L[m+1…n],其中L(m)=pivot,讨论m与k的大小关系
    1. m=k,显然pivot就是所要寻找的元素,直接返回pivot即可
    2. m>k,所要寻找的元素一定落在L[1…m-1]中,因此可对L[1…m-1]递归地查找第k小的元素
    3. m
  • 算法实现
    int kth_elem(int a[], int low, int high, int k)
    {
    	int pivot = a[low];
    	int low_temp = low;		//由于下面会修改low与high,递归时又需要用到它们,故用此暂存
    	int high_temp = high;
    	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;
    	if(low == k)
    		return a[low];
    	else if(low > k)
    		return kth_elem(a, low_temp, low-1, k);
    	else
    		return kth_elem(a, low+1, high_temp, k);
    }
    

4. 已知由n( n ≥ 2 n\ge2 n2)个正整数构成的集合 A = { a k ∣ 0 ≤ k < n } A=\{a_k|0\le kA={ak0k<n} ,将其划分为两个不相交的子集 A 1 A_1 A1 A 2 A_2 A2,元素个数分别是 n 1 n_1 n1 n 2 n_2 n2 A 1 A_1 A1 A 2 A_2 A2中的元素之和分别是 S 1 S_1 S1 S 2 S_2 S2,设计一个尽可能高效的划分算法,满足 ∣ n 1 − n 2 ∣ |n_1-n_2| n1n2最小且 ∣ S 1 − S 2 ∣ |S_1-S_2| S1S2最大

  • 算法思想:将最小的 ⌊ n 2 ⌋ \lfloor\frac{n}2\rfloor 2n个元素放在

5. 设有一个仅有红、白、蓝三种颜色的条块组成的条块序列,请编写一个时间复杂度为 O ( n ) O(n) O(n)的算法,使得这些条块按红、白、蓝的顺序排好

  • 算法思想:顺序扫描线性表,将红色条块交换到线性表的最前面,蓝色条块交换到线性表的最后面。为此,设立三个指针,其中,j为工作指针,表示当前扫描的元素,i以前的元素全部为红色,k以后的元素全部为蓝色,根据j所指示元素的颜色,决定将其交换到序列的前部或尾部
  • 算法实现
    typedef Enum{RED, WHITE, BLUE} color;		//设置枚举数组
    void Flag_Arrange(color a[], int n)
    {
    	int i = 0, j = 0, k = n-1, temp;
    	while(j <= k)
    		switch(a[j])						//判断条块的颜色
    		{
    			case RED:
    				temp = a[i];
    				a[i] = a[j];
    				a[j] = temp;
    				++i,++j;
    				break;
    			case WHITE:
    				++j;
    				break;
    			case BLUE:
    				temp = a[j];
    				a[j] = a[k];
    				a[k] = temp;
    				--k;			//没有j++语句,防止交换后a[j]仍是蓝色的情况
    		}
    }
    
    

4. 选择排序

基本思想:每一趟在后面n-i+1(i=1,2,···,n-
1)个待排序元素中选取关键字最小的元素,作为有序子序列的第i个元素,直到n-1趟做完

1. 简单选择排序

  • 算法思想:假设排序表为L[1…n],第i趟排序即从L[i…n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,经过n-1趟排序就可使得整个排序表有序
    8. 排序_第6张图片

  • 算法实现

    void SelectSort(int A[], int n)
    {
    	int i, j, temp, min;
    	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)				//将当前最小值放到最终位置,共移动3次
    		{
    			temp = A[min];
    			A[min] = A[i];
    			A[i] = temp;
    		}
    	}
    }
    
  • 性能分析

    • 空间复杂度 O ( 1 ) O(1) O(1)
    • 时间复杂度 O ( n 2 ) O(n^2) O(n2)
    • 移动次数有序:0次;逆序:不超过3(n-1)次
    • 比较次数:与序列的初始状态无关,始终是 n ( n − 1 ) 2 \frac{n(n-1)}2 2n(n1)
    • 稳定性:不稳定

2. 堆排序(重点)

  • 定义
    • 大根堆:根 ≥ \ge 左、右(用完全二叉树理解)
      • L ( i ) ≥ L ( 2 i ) 且 L ( i ) ≥ L ( 2 i + 1 ) , 1 ≤ i ≤ ⌊ n 2 ⌋ L(i)\ge L(2i)且L(i)\ge L(2i+1), 1\le i\le \lfloor\frac{n}2\rfloor L(i)L(2i)L(i)L(2i+1),1i2n
    • 小根堆:根 ≤ \le 左、右
      • L ( i ) ≤ L ( 2 i ) 且 L ( i ) ≤ L ( 2 i + 1 ) , 1 ≤ i ≤ ⌊ n 2 ⌋ L(i) \le L(2i) 且 L(i)\le L(2i+1), 1\le i\le\lfloor\frac{n}2\rfloor L(i)L(2i)L(i)L(2i+1),1i2n
        8. 排序_第7张图片
  • 基本思想(大根堆为例)
    • 将无序序列建成一个二叉树,根据升序(降序)选择大根堆(小根堆 )
    • 将堆顶元素与末尾元素交换,将最大(小)元素按序存入数组末端
    • 将堆顶元素向下调整使其继续保持大根堆(小根堆)的性质
    • 如此重复,直到堆中仅剩一个元素为止
      8. 排序_第8张图片
  • 构造初始堆——n个结点的完全二叉树,最后一个结点是第 ⌊ n 2 ⌋ \lfloor\frac{n}2\rfloor 2n个结点的孩子
    • 先按照序列构造二叉树
    • ⌊ n 2 ⌋ \lfloor\frac{n}2\rfloor 2n个结点为根的子树筛选,若根结点的关键字小于左右孩子中关键字较大者,则交换,使该子树成为堆
    • 向前依次对各结点( ⌊ n 2 − 1 \lfloor\frac{n}2-1 2n1~ 1 1 1)为根的子树进行筛选,此时可能会破坏下一级的堆,于是继续采用上述该方法构造下一级的堆,直到以该结点为根的子树构成堆为止
    • 反复利用上述调整堆的方法建堆,直到根结点
  • 插入
    • 新元素放到表尾(堆底)
    • 根据大/小根堆的要求,新元素不断上升(只与父结点对比一次),直到无法上升为止
      8. 排序_第9张图片
  • 删除
    • 被删除元素用表尾元素代替
    • 根据大/小根堆的要求,元素不断下坠(左右孩子先对比,再与父结点对比),直到无法下坠为止
  • 算法实现
    //建立大根堆
    void BuildMaxHeap(int A[], int len)
    {
    	for(int i = len/2; i > 0; --i)	//从i=[n/2]~1,反复调整堆
    		HeadAdjust(A, i, len);
    }
    void HeadAdjust(int 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];					//被筛选结点的值放入最终位置
    }
    
    //堆排序算法
    void HeapSort(int A[], int len)
    {
    	BuildMaxHeap(A, len);			//初始建堆
    	for(i = len; i > 1; --i)		//n-1趟的交换和建堆过程
    	{
    		temp = A[i];				//输出堆顶元素(和堆底元素交换) 
    		A[i] = A[1];
    		A[1] = temp;
    		HeadAdjust(A, 1, i-1);		//调整,把剩余的i-1个元素整理成堆
    	}
    ]
    
  • 性能分析
    • 空间复杂度 O ( 1 ) O(1) O(1)
    • 时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
      • 建堆时间 O ( n ) O(n) O(n),总比较次数不超过4n
      • n-1次调整时间,每次调整的时间复杂度为 O ( h ) O(h) O(h)
    • 稳定性不稳定
    • 适用性:适合关键字较多的情况

3. 算法应用

1. 编写一个算法,在基于单链表的待排序关键字序列上进行简单选择排序

  • 算法实现
    void selectSort(LNode *&L)
    {
    	LNode *pre, *p, *max, *maxpre, *h = L->next;
    	L->next = NULL;
    	while(h != NULL)
    	{
    		p = max = h, pre = maxpre = NULL;	//p为工作指针
    		while(p != NULL)
    		{
    			if(p->data > max->data)
    			{
    				maxpre = pre;			//找到更大的,记忆它和它的前驱
    				max = p;
    			}
    			pre = p;
    			p = p->next;
    		}
    		maxpre->next = max->next;		//头插法,将较大的数据先插入,最终得到非递减序列
    		max->next = L->next;
    		L->next = max;
    	}
    }
    

2. 设计一个算法,判断一个数据序列是否构成一个小根堆

  • 算法实现
    bool IsMinHeap(int A[], int len)
    {									//数组下标从1开始存储
    	if(len % 2 == 0)				//len为偶数,又一个单分支结点
    	{
    		if(A[len/2] > A[len])		//判断单分支结点
    			return false;
    		for(i = len/2-1; i >= 1; --i)//判断所有双分支结点
    			if(A[i] > A[2*i] || A[i] > A[2*i+1])
    				return false;
    	}
    	else							//len为奇数时,没有单分支结点
    	{
    		for(i = len/2; i >= 1; --i) //判断所有双分支结点
    			if(A[i] > A[2*i+1] || A[i] > A[2*i+1])
    				return false;
    	}
    	return true;
    }
    

5. 归并排序和基数排序

1. 归并排序

  • 算法思想
    8. 排序_第10张图片

    • Merge——设两段有序表A[low…mid]、A[mid+1…high]放在同一顺序表中的相邻位置
    1. 将它们复制到辅助数组B中
    2. 每次从对应B中的两个段中取出一个记录进行关键字的比较,将较小者放入A中
    3. 当数组B中有一段的下标超出其对应的表长(即该段的所有元素都已复制到A中)时,将另一段中的剩余部分直接复制到A中
    • MergeSort——每趟归并排序的操作就是调用 ⌈ n 2 h ⌉ \lceil\frac{n}{2h}\rceil 2hn次Merge()
    1. 假定待排序表有n个记录,则可将其视为n个有序的子表,每个子表的长度为1
    2. 两两归并,得到 ⌈ n 2 ⌉ \lceil\frac{n}2\rceil 2n个长度为2或1的有序表
    3. 继续两两归并,直到合并成一个长度为n的有序表为止
  • 算法实现

    int *B = (int *)malloc((n+1)*sizeof(int));
    void Merge(int A[], int low, int mid, int high)
    {	//表A的两段A[low…mid]和A[mid+1…high]各自有序,将它们合并成一个有序表
    	for(int k = low; k <= high; ++k)
    		B[k] = A[k];			//将A中所有元素复制到B中
    	for(int 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(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);	//归并
    	}
    }
    
  • 性能分析

    • 空间复杂度 O ( n ) O(n) O(n)(辅助空间 O ( n ) O(n) O(n)+递归栈 O ( l o g 2 n O(log_2n O(log2n))
    • 时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)(每趟归并的时间复杂度为 O ( n ) O(n) O(n),共需归并 ⌊ l o g 2 n ⌋ \lfloor log_2n\rfloor log2n
    • 稳定性稳定
    • N个元素k路归并:排序趟数m满足 k m = N , m = ⌈ l o g k N ⌉ k^m=N , m=\lceil log_kN\rceil km=N,m=logkN

2. 基数排序

  • 算法思想:基于关键字各位的大小排序,借助多关键字排序(分配+收集)的思想对单逻辑关键字进行排序
    • 最高位优先(MSD):按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列
    • 最低位优先(LSD):按关键字权重递增次序依次进行排序,最后形成一个有序序列
  • 实例:以r为基数的最低位优先基数排序,使用r个队列 Q 0 , Q 1 , … , Q r − 1 Q_0, Q_1, …, Q_{r-1} Q0,Q1,,Qr1
    • i = 0 , 1 , … , d − 1 i=0,1,…,d-1 i=0,1,,d1,依次做一次“分配”和“收集”
    • 分配:开始时,把 Q 0 , Q 1 , … , Q r − 1 Q_0, Q_1,…,Q_{r-1} Q0,Q1,,Qr1各个队列置为空队列,然后依次考察线性表中的每个结点 a j ( j = 0 , 1 , … , n − 1 ) a_j(j=0,1,…,n-1) aj(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队列中
    • 收集:把 Q 0 , Q 1 , … , Q r − 1 Q_0, Q_1,…,Q_{r-1} Q0,Q1,,Qr1各个队列中的结点依次首尾相接,得到新的结点序列,从而组成新的线性表
      8. 排序_第11张图片
  • 性能分析
    • 空间复杂度 O ( r ) O(r) O(r)(r个队列:r个队头指针+r个队尾指针,但会重复使用)
    • 时间复杂度 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))(d趟分配 O ( n ) O(n) O(n)和收集 O ( r ) O(r) O(r),与序列的初始状态无关)
    • 稳定性:稳定
    • 适用性
      1. 数据元素的关键字可以方便地拆分为d组,且d较小
      2. 每组关键字的取值范围不大,即r较小
      3. 数据元素个数n较大

6. 内部排序算法的比较与应用

1. 各种排序算法的比较

8. 排序_第12张图片

  • 一趟排序有元素处于最终位置:冒泡、快排、简单选择、堆
  • 比较次数与初始序列无关:简单选择、折半插入
  • 排序趟数与初始序列有关:(交换类)冒泡、快排

2. 各种排序算法的应用

  • n较小(1000以下),(记录本身信号量大)简单选择排序,直接插入排序,冒泡排序
  • 基本有序,直接插入排序,冒泡排序
  • n中等(1000左右),希尔排序
  • n较大(1000以上),快速排序,堆排序,归并排序,基数排序(关键字位数少且可分解)

7. 外部排序

  • 需要将待排序的记录存储在外存上,排序时再把数据一部分一部分地掉入内存进行排序,在排序过程中需要多次内存和外存之间的交换

1. 外部排序的方法

  • 归并排序步骤
    • 根据内存缓冲区大小,将外存上的文件分成若干长度为 l l l 的子文件,依次读入内存并利用归并排序方法对它们进行排序,并将排序后得到的有序子文件重新写会外存
    • 对这些归并段进行逐趟归并,使归并段(有序子文件)逐渐由小到大,直至得到整个有序文件为止
  • 时间复杂度:主要为I/O时间
    • 外部排序总时间 = 内部排序所需时间 + 外存信息读写时间 + 内部归并所需时间
    • 树的高度-1 = ⌈ l o g k r ⌉ \lceil log_kr\rceil logkr = 归并趟数S
    • 提高效率方法(减少归并趟数)
      • 增大归并路数k
      • 减少初始归并段个数r

2. 多路平衡归并与败者树

  • S趟归并所需总比较次数
    • k个元素选择关键字最小的记录需要比较 k − 1 k-1 k1
    • 每趟归并n个元素需要比较 ( n − 1 ) ( k − 1 ) (n-1)(k-1) (n1)(k1)
      S ( n − 1 ) ( k − 1 ) = ⌈ l o g k r ⌉ ( n − 1 ) ( k − 1 ) = ⌈ l o g 2 r ⌉ ( n − 1 ) ( k − 1 ) ⌈ l o g 2 k ⌉ S(n-1)(k-1) = \lceil log_kr\rceil(n-1)(k-1)=\frac{\lceil log_2r\rceil(n-1)(k-1)}{\lceil log_2k\rceil} S(n1)(k1)=logkr(n1)(k1)=log2klog2r(n1)(k1)
    • 内部归并时间随k的增长而增长,将抵消由于增大k而减少外存访问次数所得到的效益
  • 败者树:k个叶结点分别存放在k个归并段在归并过程中当前参加比较的记录,内部结点用来记忆左右子树中的”失败者“,而让胜利者继续进行比较,一直到根结点。若比较两个数,大的为失败者,小的为胜利者,则根结点指向的数为最小数
    8. 排序_第13张图片
    8. 排序_第14张图片
    • k路归并的败者树深度为: ⌈ l o g 2 k ⌉ \lceil log_2 k\rceil log2k
    • 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
    • 只要内存空间允许,增大归路并数k将有效地减少归并树的高度,从而减少I/O次数,提高外部排序的速度
    • 归并路数不是越大越好,归并路数k增大时,相应地需要增加输入缓冲区的个数,若可供使用的内存空间不变,势必要减少每个输入缓冲区的容量,使得内存、外存交换数据的次数增大

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

  • 算法步骤:设舒适文件为FI(初始为空),初始归并段输出文件为FO(初始为空),内存工作区为WA(可容纳w个记录)
    1. 从FI输入w个记录到工作区WA
    2. 从WA中选出其中关键字取最小值的记录,记为MINIMAX记录
    3. 将MINIMAX记录输出到FO中去
    4. 若FI不空,则从FI输入下一个记录到WA中
    5. 从WA中所有关键字比MINIMAX记录的关键字大的记录中选出最小关键字记录,作为新的MINIMAX记录
    6. 重复3~5,直至在WA中选不出新的MINIMAX记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到FO中去
    7. 重复2~6,直至WA为空。由此得到全部初始归并段
      8. 排序_第15张图片

5. 最佳归并树

  • 定义:在归并树中,让记录数少的初始归并段最先归并,记录数多的初始归并段最晚归并,就可以建立总的I/O次数最少的最佳归并树
  • 注意
    • 若初始归并段不足以构成一棵严格k叉树时,需要添加长度为0的”虚段“
    • 归并过程中的磁盘I/O次数 = 归并树的WPL * 2
    • k叉归并的最佳归并树一定是严格二叉树,即树中只有度为k、度为0的结点
  • 确定添加虚段数目
    • 设度为0的结点有 n 0 ( = n ) n_0(=n) n0(=n)个,度为k的结点有 n k n_k nk个,则对严格k叉树有 n 0 = ( k − 1 ) n k + 1 , n k = n 0 − 1 k − 1 n_0=(k-1)n_k+1,n_k = \frac{n_0-1}{k-1} n0=(k1)nk+1nk=k1n01
    • ( n 0 − 1 ) % ( k − 1 ) = 0 (n_0-1)\%(k-1) = 0 (n01)%(k1)=0,则正好可以构成k叉归并树,内结点 n k n_k nk
    • ( n 0 − 1 ) % ( k − 1 ) = u ≠ 0 (n_0-1)\%(k-1)=u\ne0 (n01)%(k1)=u=0,则说明有u个多余,再增加一个内结点(代替一个叶结点的位置),此时需要加上 k − u − 1 k-u-1 ku1个空归并段
      8. 排序_第16张图片

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