NO.13数据结构排序|直接插入|折半插入|简单选择排序|堆排序|冒泡排序|快速排序|归并排序|基数排序|外部归并排序|败树者|置换选择排序|最佳归并树

![[Pasted image 20251003054725.png]]

排序的基本概念

定义

将一批乱序的记录(数据)重新排列成按关键字有序的记录序列的过程称为排序。

算法的稳定性

若待排序表中有两个元素 RiR_{i}RiRjR_{j}Rj, 其对应的关键字相同即 keyi=keyjkey_{i}=key_{j}keyi=keyj,且在排序前 RiR_{i}RiRjR_{j}Rj的前面, 若使用算法排序之后, RiR_{i}Ri仍然在 RjR_{j}Rj前面, 则称该算法是稳定的。
Tips: 稳定性通俗一点讲, 就是关键值相等的元素, 排序之后相对位置不改变。 算法稳定并不用来判断算法的优劣, 一般用时间空间复杂度来判断。

插入排序

定义

基本思想是将一个记录插入到已经排好序的有序表中, 从而一个新的、记录数增加 1 的有序表。
根据查找插入位置的方法不同,有直接插入和折半插入排序两种
![[Pasted image 20251003083115.png]]

直接插入排序
  • 基本思路
    假设初始 L[0…i−1]L[0…i-1]L[0i1]是一个排好序的子序列,
    对于元素 L[i]L[i]L[i]插入到前面已经排好序的子序列当中,
  1. 查找出 L[i]L[i]L[i]在有序序列 L[0...i−1]L[0...i-1]L[0...i1]中的插入位置 k,
  2. L[k...i−1]L[k...i-1]L[k...i1]中所有元素后移一个位置,
  3. L[i]L[i]L[i]复制到 L[k]L[k]L[k]
    ![[Pasted image 20251003055211.png]]

初始 L[1]L[1]L[1]认为是排好的序列, 因此, 只需要 n-1 次操作就可以得到一个有序的表。

void InsertSort(ElemType A[], int n){
	int i, j;
	for(i = 2; i <= n; i++){
		if(A[i] < A[i-1]){//是否需要向前比较 
			A[0] = A[i];I/哨兵
			for(j=i-1; A[0] < A[j]; j--){ //查找插入位置
				A[j+1] = A[j]; //向后移动元素
			}
			A[j+1] = A[0];
		}
	}
}
  • 性能分析
    空间效率: 仅使用了常数辅助单元, 空间复杂度为 O(1)
    时间复杂度: 查看代码, 每次排序需要经过 n-趟, 每趟都需要比较关键字和移动元素, 而比较次数和移动次数取决于待排序表的初始状态。
    最好情况下, 表中元素都是有序的, 则每趟都是比较一次, 不需要移动,复杂度为 O(n)。 最坏情况下, 表中元素刚好与排序结果相反, 总的比较次数最大, 复杂度为 O(n^2)。 取最好和最坏的平均值作为平均情况下的时间复杂度, 为 O(n^2) .
    稳定性: 稳定
折半插入排序
  • 基本思路
    直接插入排序, 进行两个工作,
  1. 从前面的子表中找出待插入元素应该被插入的位置给插入位置腾出空间,
  2. 将插入元素复制到插入位置。
    由于是顺序存储的线性表, 查找有序子表可以用折半查找来实现。 确定顺序后统一移动元素。 这就是折半插入排序。
void InsertSort(ElemType A[],int n){
	int i, j, low, high, mid; 
	for(i = 2; i <= n; i++){ 
		A[0] = A[i];
		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[];//移动元素
		A[high+1] = A[0];//插入
	}
}
  • 性能分析
    空间效率: 空间复杂度为 O(1)
    时间复杂度: 减少了比较次数, 为 O(nlogn), 但是移动次数没变,因此复杂度仍然为 O(n^2)
    稳定性: 稳定
希尔排序
  • 基本思路
    先将待排序表分割成若干形如 L[i,i+d,i+2d,...,i+kd]L[i,i+d,i+2d,...,i+kd]L[i,i+d,i+2d,...,i+kd]的“特殊”子表, 分别进行直接插入排序, 当整个表中元素“基本有序” , 再对全体记录进行一次直接插入排序。
    ![[Pasted image 20251003060751.png]]

排序过程:

  1. 先取小于n的步长d1d_{1}d1,把表的元素分成d1组, 所有距离为d1d_{1}d1的元素放在一组
  2. 同一组的元素, 在各组中进行直接插入排序
  3. 然后取第二个步长d2>d1d_{2}>d_{1}d2>d1,重复过程
  4. 直到所取d=1, 即所有元素在一组中, 再直接进行插入排序。
  • 性能分析
    (希尔排序的性能分析比较复杂不需要掌握, 只需要知道是不稳定算法即可)
    空间效率: 空间复杂度为 O(1)
    时间复杂度: 复杂度最好为 O(n^1.3), 最坏情况下为 O(n^2)
    稳定性: 不稳定。 同样大小的元素可能在不同组, 相对位置就可能会变化。

选择排序

定义

基本思想是每趟在后面的 n-i+1(i=1,2,n-1)个待排序元素中选取关键字最小的元素, 作为有序子序列的第 i 个元素, 直到 n-1 趟做完, 完成 n 个元素的排序。
![[Pasted image 20251003062201.png]]

简单选择排序
  • 基本思路
    假设排序表为 L[0...n−1]L[0...n-1]L[0...n1],第 i 趟排序即从 L[i−1…n−1]L[i-1\dots n-1]L[i1n1]中选择最小的元素与 L[i]L[i]L[i]交换, 每一趟排序可以确定一个元素的最终位置
    ![[Pasted image 20251003062757.png]]
void SelectSort(ElemTypeA[],int n) {
	int i, j, min, temp;
	for(i = 0; i < n-1; i++){ 
		min=i;
		for(j = i+1; j < n; j++){//寻找最值
			if(A[j] < A[min]) min = j;
		}
		if(min != i){//交换当前的最值到最终位置
			temp = A[i]; 
			A[i] = A[min]; 
			A[min] = temp; 
		}
	}
}
  • 性能分析
    空间效率: 仅使用了常数辅助单元, 空间复杂度为 O(1) 。
    时间复杂度: 选择排序移动次数很少, 不会超过 3(n-1),但是比较次数和初始 状态无关, 为 n(n-1)/2, 因此时间复杂度为 O(n^2) 。
    稳定性: 不稳定, 找到第 i 大的元素, 和 i 位置元素相换, 可能导致相等元素的相对位置变化。
堆排序
  • 定义
    n 个关键字序列 L[1...n]L[1...n]L[1...n]称为堆, 当且仅当该序列满足:
    L[i]<=L[2i]L[i]<=L[2i]L[i]<=L[2i]L[i]<=L[2i+1]L[i]<=L[2i+1]L[i]<=L[2i+1],则称该堆为小根堆
    L[i]>=L[2i]L[i]>=L[2i]L[i]>=L[2i]L[i]>=L[2i+1]L[i]>=L[2i+1]L[i]>=L[2i+1], 则称该堆为大根堆(1≤i≤[n2]1\le i\le\left[ \frac{n}{2} \right]1i[2n]
    ![[Pasted image 20251003083903.png]]

排序过程中将L[1...n]L[1...n]L[1...n]当作完全二叉树的层序遍历序列
小根堆构建的完全二叉树, 根节点比左右子树小。
大根堆构建的完全二叉树, 根节点比左右子树大。

  • 基本思路
    首先将存放在 L[1…n]L[1…n]L[1n]中的 n 个元素建成初始堆, 由于堆本身的特点, 堆顶元素就是最大值。 然后将堆的最后一个元素放到根结点, 再对根结点做向下调整, 直到完全二叉树再次满足堆的性质, 再输出新的根结点, 也就是剩下元素形成的堆的最大结点…重复上述操作, 就输出了从大到小的排序序列。
    [[Pasted image 20251003063711.png]]

  • 堆排序的思路(以大根堆为例):

  1. 将 n 个元素建成初始堆(堆的初始化);
  2. 将堆顶元素(当前大根堆的最大值)拿下来, 与大根堆的最后一个元素进行交换, 此时大根堆的性质一定遭到了破坏, 则需要我们重新调整为大根堆。

堆的初始化过程:

  1. 从当前堆的最后一个非叶子结点(floor(n/2))开始, 对其子树进行调整,
  2. 依次向上调整每一棵子树, 直至根结点;

如何调整子树?
若当前子树的根结点的值不是当前子树所有结点的最大值, 则交换值, 使当前子树符合大根堆定义。

堆排序算法

void HeapSort(ElemType A[], int n)
	BuildMaxHeap(A,n); 
	for(i = n; i > 1; i--){
		swap(A[],A[1]);//堆顶元素与堆尾元素交换
		HeapAdjust(A, 1, i-1); 
	}
}

void BuildMaxHeap(ElemType A[],int n){ 
	int i;
	for(i = n/2; i > 0; i--){
		HeapAdjust(A,i,n); //调整堆
	}
}

void HeapAdjust(ElemTypeA[],int k,int n){ 
	int i;
	A[0] = A[k];
	for(i = 2*k; i <= n; i *= 2){//检查当前子树 
		if(i < n && A[j] < A[i+1]) i++;
		if(A[0] >= A[i]) break; 
		else{
			A[k] = A[i];
			k = i;
		}
	}
	A[k] = A[0];
}
  • 性能分析
    空间效率: 空间复杂度为 O(1)
    时间复杂度: 建堆时间 O(n), 之后排序, 有 n-1 次向下调整操作,每次调整时间复杂度为 O(h), h 是堆的二叉树的树高, 因此堆的时间复杂度为 O(nlogn)
    稳定性: 不稳定
    注: 升序序列建大根堆, 降序序列建小根堆

  • 堆排序总结:

  1. 求升序序列初始要构建大根堆,求降序序列初始要构建小根堆;
  2. 构建初始堆时,从n/2处开始向上调整堆;
  3. 堆排序时不断将堆顶元素与当前堆底元素交换,更新并调整当前堆;
  4. 插入元素时,将待插入元素置于堆底然后沿单侧树高向上调整,直至整个堆合法;
  5. 删除元素时,将堆底元素置于堆顶然后沿单侧树高向下调整,直至整个堆合法;
  6. 堆排序的时间复杂度是O(nlogn),空间复杂度是O(1); 插入操作的时间复杂度是O(logn),删除操作的时间复杂度是O(logn);

交换排序

定义

所谓的交换, 是指根据序列中的两个元素关键字的比较结果来对换这两个记录在序列中的位置。
![[Pasted image 20251003084902.png]]

冒泡排序
  • 基本思路
    基本思想从后往前(或者从前往后) 两两比较相邻元素的值, 若为逆序(即 A[i−1]>A[i]A[i-1]>A[i]A[i1]>A[i]), 则交换他们, 直到序列比较完, 成为第一趟冒泡。 每一趟冒泡将序列的最小(或最大)元素放到了序列最终位置 …
    这样最多做 n-1 趟冒泡就能把所有元素排好序。
    ![[Pasted image 20251003065129.png]]
void BubbleSort(ElemTypeA[],intn){ 
	int i,j;
	bool flag;
	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;//所有元素已经有序
	}
}
  • 性能分析
    空间效率: 仅使用了常数辅助单元, 空间复杂度为 O(1)
    时间复杂度: 最好情况下, 初始序列有序, 一趟冒泡之后结束, 跳出循环, 比较次数为 n-1,移动次数为0, 从而最好情况下复杂度为O(n), 当序列为逆序时, 需要 n-1 趟排序, 第 i 趟进行 n-i 次关键字比较, 因此最差复杂度为 O(n^2), 因此算法时间复杂度为 O(n^2)
    稳定性: 稳定
快速排序
  • 基本思路
  1. 在有 n 个元素的待排序序列 L 中选取一个元素作为枢轴(pivot), 一般会随机选取或者取当前序列的首元素作为枢轴元素, 通过一趟排序将整个待排序列分为三部分,L[1,...,k−1],L[k],L[k+1,..,n]L[1, ..., k-1], L[k], L[k+1, .., n]L[1,...,k1],L[k],L[k+1,..,n],其中左半部分所有的元素值都小于 L[k]L[k]L[k], 右半部分所有的元素值都大于 L[k]L[k]L[k], 而此时元素 L[k]L[k]L[k]即在它的最终位置上;
  2. 对新的待排序序列 L[1,...,k−1]L[1, ..., k-1]L[1,...,k1]L[k+1,..,n]L[k+1, .., n]L[k+1,..,n]分别进行第一步的排序过程;
  3. 直至每个新的待排序列只剩下一个元素或为空时, 算法结束。
    ![[Pasted image 20251003070114.png]]
void QuickSort(ElemTypeA[],int low,int high){ 
	int pos;
	if(low < high){
		pos = Partition(A, low, high);//选取枢轴位置
		QuickSort(A, low, pos-1); 
		QuickSort(A, pos+1, high);
	}
}

int Partition(ElemTypeA[], 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; 
}
  • 性能分析
    当初始待排序列为有序序列时, 是最坏的情况, 此时空间复杂度是O(n), 时间复杂度是 O(n^2)
    当初始待排序列为乱序序列时, 是最好的情况, 此时空间复杂度是O(logn), 时间复杂度是 O(nlogn)
    稳定性: 不稳定。

注: 快速排序最坏情况分析
如果待排序序列已经是升序的, 而要把它排成升序的序列, 就是待排序列为有序的情况, 每次选取第一个元素作为枢轴, 那么每一次划分结束都是枢轴右半边的元素没有发生变化,那么这种情况就是最坏的情况, 时间复杂度就 O(n^2);
如果初始待排序序列是升序的, 而我要把它排成降序的序列, 选择第一个元素也就是当前最小元素作为枢轴, 第一次划分结束枢轴一定是在最右边的, 而它的左边是剩余的全部元素, 每一趟都一样。
所以对于有序序列不管升序还是降序, 不管你是要排成升序还是降序, 都是最坏情况。
示例如下: (初始序列为升序, 将它排成升序的, 枢轴元素每次都取当前子表的首元素, 此时每次划分都是最坏的情况)
![[Pasted image 20251003070907.png]]

其他排序

归并排序
  • 基本思路
    假定待排序表含有 n 个记录, 则可将其视为 n 个一个记录的有序表, 然后两两归并, 得到[n/2][n/2][n/2]个长度为 2 或 1 的有序表; 继续两两归并,直到合并成为一个长度为 n 的有序表为止, 这种排序方法称为 2 路归并排序。
    ![[Pasted image 20251003071039.png]]

![[Pasted image 20251003085100.png]]

void Merge(int* arr, int left,int mid, int right){
	int *temp = new int[right·left +1];//申请一个空间来存放合并好的数组(后面需要销毁)
	int st1 = left;//这里是数组1的开始位置 
	int st2 = mid+1;//这里是数组2的开始位置 intt=0;//合并数组的开始位置
	while(st1 <= mid && st2 <= right){//这里结束的条件是一个数组已经放进去完了
		//从开始位置比较,如果数组1的元素大于数组2,则将数组2的元素存进去一个,然后位置+1,否则相反 
		temp[t++] = arr[st1] < arr[st2] ? arr[st1++] : arr[st2++];
	}
	while(st1 <= mid){//如果是st1没有放完就直接放在最后面
		temp[t++] =arr[st1++];
	}
	while(st2 <= right){//如果是st2没有放完就直接放在最后面
	}
		temp[t++] = arr[st2++];
	}
	for(int j = 0; j < t; ++j){//这里把临时创建的数组拷贝到原来的数组中
		arr[left + j] = temp[j]; 
	}
	delete[] temp;//销毁临时变量
}

Merge()是用来将前后相邻的两个有序表合并成为一个有序表。

void MergeSort(int* arr, int start, int end){
	if(arr == NULL || start >= end)
		return;
	if(start < end)
	{
		int mid =(start + end)/2;//找到每个分块的中间值 
		MergeSort(arr, start, mid);//左边递归进行分离和合并
		MergeSort(arr, mid+1, end);//右边递归进行分离和合并 
		Merge(arr,start,mid,end);//左右合并
	}
}
  • 性能分析
    空间效率: 归并需要辅助数组, 因此为 O(n)
    时间复杂度: 每趟归并所有元素都需要遍历一遍, 因此每趟时间复杂度为 O(n), 共需要进行[log2n][log_{2}n][log2n]趟归并, 因此时间复杂度为O(nlog2nnlog_{2}nnlog2n)
    稳定性: 稳定
基数排序
  • 基本思路
    假设长度为n的线性表中每个结点aja_{j}aj的关键字由d元组(kjd−1,kjd−2,…,kj1,kj0)(k_{j}^{d-1},k_{j}^{d-2},\dots,k_{j}^{1},k_{j}^{0})(kjd1,kjd2,,kj1,kj0)组成,满足0≤kji≤r−1(0≤j<n,0≤i≤d−1)0≤k_{j}^{i}≤r-1(0≤j<n,0≤i≤d-1)0kjir10j<n,0id1)。其中kjd−1k_{j}^{d-1}kjd1为最主位关键字,kj0k_{j}^{0}kj0为最次位关键字。
    为实现多关键字排序,通常有两种方法:第一种是最高位优先(MSD)法,按关键字位权重递减依次逐层划分成若干更小的子序列,最后将所有子序列依次连接成一个有序序列。第二种是最低位优先(LSD)法,按关键字权重递增依次进行排序,最后形成一个有序序列。
  • 基数排序过程(以 r=10 为基数的 LSD):
  1. 首先声明 r 个队列 Q0, Q1, …, Qr-1 ;
  2. 对 i=0, 1, …, d-1, 依次做一次分配和收集;
    分配: 开始时, 将所有的队列置空, 然后依次考察线性表中的每个结点 aj(j=0, 1, …, n-1), 将关键词放到对应的队列中。
    收集: 将 Q0-Qr-1 各队列中的结点依次首尾相接, 得到新的结点序列,组成新的线性表。
  3. 重复上述过程 d 次。

基数排序:采用最低位优先基数排序,对如下十个记录进行排序
![[Pasted image 20251003085203.png]]

第一趟
![[Pasted image 20251003085220.png]]

第二趟
![[Pasted image 20251003085233.png]]

第三趟
![[Pasted image 20251003085246.png]]

  • 基数排序性能分析
    空间效率: 一趟排序需要辅助存储空间为 r, 后续也需要队列, 但是可以重用, 所以空间复杂度为 O® 时间复杂度: 基数排序需要进行 d 趟分配收集, d 是位数, 每一趟分配需要 O(n), 所以元素都需要遍历一次, 一趟收集需要 O®,和队列的多少有关, 所以基数排序的时间复杂度为 O(d(n+r)), 它与序列的初始状态无关。
    稳定性: 稳定

内部排序的比较和应用

排序算法比较

算法的比较一般通过三个因素: 时空复杂度、 稳定性、 算法过程的特点
![[Pasted image 20251003081907.png]]

排序算法应用

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

  1. 待排序的元素数目 n;
  2. 元素本身信息量的大小;
  3. 关键字的结构及其分布情况;
  4. 稳定性的要求;
  5. 语言工具的条件, 存储结构及辅助空间的大小;

外部排序

归并排序方法
  • 基本思路
  1. 将外存上的待排序信息分为 k 个初始序列, 进行 k 次内部排序得到 k 个初始归并段;
  2. 内存工作区分为三个区域, 输入缓冲区 in1, in2, 输出缓冲区 out1,从 0, 1 初始归并段中各取出一块数据读入 in1, in2, 对其进行 2 路归并,排序结果存入 out1;若 out1 此时满了, 将 out1 的所有结果输出到归并段 R1, 然后继续进行归并;若 in1, in2 其中一个已空, 那么再从初始归并段中取出一块数据,继续进行归并;
  3. 直至 0, 1 初始归并段的全部内容完成归并排序;
  4. 2-k 的初始归并段重复上述过程
    ![[Pasted image 20251003085454.png]]
败树者
  • 定义
    败者树: 也称失败树, 树形选择排序的一种变体, 视为一颗完全二叉树。 可以帮助归并排序。
  • 基本思路
    每一个叶节点存放各归并段在归并过程中当前参加比较的记录, 内部结点用来记忆左右子树的失败者, 胜利者向上继续进行比较, 直到根节点。
    ![[Pasted image 20251003085541.png]]
置换—选择排序
  • 目的
    为了加长初始归并段的长度, 减小初始归并段的个数
  • 方法
    设初始待排文件为FI,初始归并段输出文件为FO,内存工作区为WA,FO 和WA的初始状态为空,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为空,由此得到全部归并段

待排文件 FI={3, 24, 48, 46, 9, 14, 91, 12, 29}, WA 容量为 3
![[Pasted image 20251003082702.png]]

最佳归并树
  • 定义
    归并树: 用来描述 m 路归并, 只有度为 0 和 m 的结点的严格 m 叉树
  • 意义
    由于前面的选择置换算法, 归并段长度不等, 所以哪些段先归并哪些后归并, 将影响 I/O 次数。 此时应用前面的哈夫曼树的方法, 推广到 m 叉树的情景。 让记录数少的归并段先归并, 记录数多的初始归并段最后归并, 就可以建立总 I/O 次数最少的最佳归并树。
    注: 求 k 路归并的最小归并次数本质上就是构造 k 叉哈夫曼树

假设初始 9 个归并段, 其长度为 4, 12, 5, 28, 22, 9, 15, 7, 33 现状做 3 路平衡归并。
![[Pasted image 20251003083009.png]]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值