《王道》数据结构之排序(八)


概述

排序(Sort),就是重新排列表中的元素,使表中的元素满⾜按关键字有序的过程

排序的分类

  • 内部排序:数据都在内存中(关注时间、空间复杂度)
  • 外部排序:数据太多无法全部放入内存(还要关注如何使读/写磁盘次数更少)

排序的评价指标

  • 算法的稳定性(关键字相同的元素经过排序后,相对顺序不会改变则是稳定的)
  • 时间复杂度(主要来⾃对⽐关键字、移动元素的操作)
  • 空间复杂度

请添加图片描述


一、插入排序

1.1 算法思想

每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中, 直到全部记录插入完成。
在这里插入图片描述

1.2 算法实现的两种方式与性能分析

在这里插入图片描述

1.2.1 直接插入排序

顺序查找找到插入的位置, 适用于顺序表、链表

代码实现:

//直接插入排序:
//法一:(无哨兵) 
void InsertSort(int A[],int n)
{		
	int temp;//临时存放待插入的数据,用于比较(因为若小于前面一个数,则要后移覆盖待插入数据的位置)
	
	//将各元素插入已排好序的序列中,A[i]之前的已排好序
	for(int i = 1; i < n; i++)		
	{
		if(A[i] < A[i-1])
		{
			temp = A[i];		//temp暂存A[i],用于比较和比较后的插入
			//1.检查所有前面已排好序的元素,下标为0~i-1,所有大于temp的元素都要依次往后挪一位
			for(int j = i-1; j >= 0 && A[j] > temp; j--)
			{
				A[j+1] = A[j];	
			}
			//2.找到下标为j的元素小于等于temp时,插入到下标为j+1的位置(若没找到,退出循环时j=-1,则插入到下标为0的位置)
			A[j+1] = temp;
		}
	}
}

//法二:(带哨兵,数组下标为0的位置存放哨兵,数据存放在下标为1到n的地方,数组大小为n+1)可以每次少比较j>=0 
void InsertSort(int A[],int n)
{		
	//将各元素插入已排好序的序列中,A[i]之前的已排好序
	for(int i = 2; i < n+1; i++)		
	{
		if(A[i] < A[i-1])
		{
			A[0] = A[i];		//复制为哨兵存放在A[0]
			//1.检查所有前面已排好序的元素,所有大于哨兵的元素都要依次往后挪一位
			for(int j = i-1; A[0] < A[j]; j--)
			{
				A[j+1] = A[j];	
			}
			//2.找到下标为j的元素小于等于哨兵时,插入到下标为j+1的位置(若没找到,找到哨兵即退出循环j=0,则插入到下标为1的位置)
			A[j+1] = A[0];
		}
	}
}

算法效率分析:

  • 空间复杂度:O(1)
  • 时间复杂度:平均时间复杂度O(n2)
    主要来⾃对⽐关键字、移动元素若有 n 个元素,则需要 n-1 趟处理
    最好情况复杂度:O(n)——原本就有序,共n-1趟处理,每⼀趟只需要对⽐关键字1次,不⽤移动元素
    最坏情况复杂度:O(n^2^)——原本为逆序,第i趟需对⽐关键字 i+1次,移动元素 i+2 次
  • 算法稳定性:稳定

1.2.2 折半插入排序

先用折半查找找到应插入的位置, 仅适用于顺序表
在这里插入图片描述
过程:
一直到low > high 时才停止折半查找,应将 [low, i-1] 内的元素全部右移,并将 A[0] 复制到 low 所指位置。
注意: 当mid所指元素等于当前元素时,应继续令low = mid+1 , 以保证“ 稳定性”

代码实现:

void InsertSort(int A[],int n)
{		
	int temp;//临时存放待插入的数据,用于比较(因为若小于前面一个数,则要后移覆盖待插入数据的位置)
	int low,high,mid;	//折半所用到的指针
	
	//将各元素插入已排好序的序列中,A[i]之前的已排好序
	for(int i = 1; i < n; i++)		
	{
		//每次插入都要初始化
		low = 0; high = i-1;temp = A[i];
		
		//1.找到要插入的位置low
		while(low <= high)
		{
			mid = (low+high)/2;
			if (temp < A[mid])
			{
				high = mid - 1;
			}
			else 
			{
				low = mid + 1;
			}
		}

		//2.右移[low, i-1] 内的元素
		for(int j = i-1; j >= low; j--)
		{
			A[j+1] = A[j];
		}
		
		//3.插入temp
		A[low] = temp;
	}
}

算法效率分析:

  • 时间复杂度:⽐起“直接插⼊排序”,⽐较关键字的次数减少了,但是移动元素的次数没变,整体来看时间复杂度依然是O(n2)

二、希尔排序

仅适⽤于顺序表,不适⽤于链表

2.1 算法思想

希尔排序:先将待排序表分割成若⼲形如 L[i, i + d, i + 2d,…, i + kd] 的“特殊”子表,对各个子表分别进行直接插入排序。然后缩小增量d重复上述过程,直到d=1为⽌。(先追求表中元素部分有序,再逐渐逼近全局有序)
在这里插入图片描述
增量d也等于分组的数量
增量d的取值可以任意

2.2 算法实现

代码实现:

//希尔排序,哨兵实现(即0存放哨兵,n个数据存到下标为1~n的数组中,数组大小为n+1)
void ShellSort(int A[],int n)
{
	int d;
	//步长/增量d变化,进行分组
	for(d = n/2; d >= 1; d = d/2)
	{
		//将每组各元素插入已排好序的序列中,A[i]之前的已排好序
		for(int i = 1+d; i <= n; i++)		//从第二个元素(下标为1+d)开始往前比较,有点像并发,每一次循环会跳转到下一分组进行排序
		{
			//和同一分组的上一个进行比较
			if(A[i] < A[i-d])
			{
				A[0] = A[i];		//复制为哨兵存放在A[0]
				//1.检查所有前面已排好序的元素,所有大于哨兵的元素都要依次往后挪一位
				for(int j = i-d; j > 0 && A[0] < A[j]; j = j-d )//注意j>0条件
					{
						A[j+d] = A[j];	
					}
				//2.找到下标为j的元素小于等于哨兵时,插入到下标为j+d的位置(若没找到,找到哨兵即退出循环j=0,则插入到下标为1的位置)
				A[j+d] = A[0];
			}	
		}
	}
}

2.3 算法性能分析

  • 空间复杂度O(1)
  • 时间复杂度:和增量序列 d1, d2, d3… 的选择有关(⽬前⽆法⽤数学⼿段证明确切的时间复杂度)
    最坏时间复杂度为 O(n2),当n在某个范围内时,可达O(n1.3)
  • 稳定性:不稳定

三、交换排序

交换排序:根据序列中两个元素关键字的比较结果来交换这两个记录在序列中的位置,分为冒泡排序快速排序

3.1 冒泡排序

适⽤于顺序表、链表

3.1.1 算法思想

从后往前(或从前往后)两两⽐较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列⽐较完。称这样过程为⼀趟冒泡排序。

顺序表思想:
在这里插入图片描述
链表思想:
在这里插入图片描述

3.1.2 算法实现

代码实现:

//顺序表从后往前冒泡的实现
void BubbleSort(int A[],int n)
{
	int temp;
	//一、i表示已排好i个元素,当排好n-1个即可退出循环(总共轮数为i-1躺)
	for(int i = 0; i < n-1; i++)
	{
		bool flag = false;				//表示本躺冒泡是否发生交换的标志位
		//二、表示每一趟需要比较的次数(i=0时,j取值范围为1~n-1)
		for(int j = n-1; j > i; j--)
		{
			if(A[j-1] > A[j])			//若为逆序则交换,算法是稳定的
			{
				temp = A[j];
				A[j] = A[j-1];
				A[j-1] = temp;
				flag = true;
			}
		}
		if(flag==false)			//若某一趟没有发生交换,说明表已经有序
		{
			return;
		}
	}
}

//链表从前往后冒泡的实现
void BubbleSort(int A[],int n)

3.1.3 算法性能分析

  • 空间复杂度:O(1)
  • 时间复杂度:平均时间复杂度=O(n2)
    最好情况(有序)——⽐较次数=n-1;交换次数=0,最好时间复杂度=O(n)
    最坏情况(逆序)——⽐较次数=(n-1)+(n-2)+…+1 = n*(n-1)/2;交换次数=⽐较次数,最坏时间复杂度=O(n2)
    注意:每次交换都需要移动元素3次
  • 稳定性:稳定的

3.2 快速排序

仅适⽤于顺序表,不适⽤于链表

3.2.1 算法思想

①在待排序表L[1…n]中任取⼀个元素pivot作为枢轴(即基准),通过⼀次划分将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素小于pivot,L[k+1…n]中的所有元素大于等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为⼀次“划分”。
②然后分别递归地对两个子表重复上述过程,直⾄每部分内只有⼀个元素或空为⽌,则所有元素放在了其最终位置上。
注意:⼀次“划分”≠⼀趟排序。⼀次划分可以确定⼀个元素的最终位置,⽽⼀趟排序也许可以确定多个元素的最终位置。(对所有尚未确定最终位置的所有元素进⾏⼀遍处理称为⼀趟排序,下图一层QuickSort对应一趟排序)
在这里插入图片描述
算法优化思路:
尽量选择可以把数据中分的枢轴元素(枢轴默认取首元素):
①选头、中、尾三个位置的元素,取中间值作为枢轴元素;
②随机选⼀个元素作为枢轴元素

3.2.2 算法实现

代码实现:

//快速排序(递归调用)
void QuickSort(int A[], int low, int high)
{
	if(low < high)	//递归跳出的条件
	{
		int pivotpos = Partition(A , low, high);	//确定枢轴位置并划分
		QuickSort(A, low, pivot-1);				//划分为左子表
		QuickSort(A, pivot+1, high);				//划分为右子表
	}
}

//辅助函数:用数组首元素将序列划分为左右两部分,并返回枢轴位置
int Partition(int A[], int low, int high)
{
	//1.确定基准/枢轴
	int pivot = A[low];
	//2,用low、high搜索枢轴的最终位置,并将数组划分为比枢轴小的左半部分和比枢轴大的右半部分
	while(low<high)		
	{
		//用high找到比枢轴小的元素,放到此时A[low]的位置
		while(pivot <= A[high] && low<high)		//如果没有low<high条件,low = high时仍满足pivot <= A[high],不会跳出循环,high继续减小移至low左侧。因此要加上low<high条件
		{
			high--;
		}
		A[low] = A[high];
		//用low找到比枢轴大的元素,放到此时A[high]的位置
		while(pivot >= A[low] && low<high)
		{
			low++;
		}
		A[high] = A[low];
	}	
	//当low=high时,表示已经⽤第⼀个元素(枢轴)把待排序序列“划分”为左边更⼩,右边更⼤的两个部分。
	//3.此时将基准放入此时low/high所指位置
	A[low] = pivot;		
	return low;			//返回枢轴位置
}

3.2.3 算法性能分析

在这里插入图片描述
把n个元素组织成⼆叉树,⼆叉树的层数就是递归调⽤的层数
(n个结点的⼆叉树最⼩⾼度 =[log2n]向下取整+1,最⼤⾼度 = n)

  • 空间复杂度=O(递归层数)
    最好空间复杂度=O(log2n)
    最坏空间复杂度=O(n)
  • 时间复杂度=O(n*递归层数)
    平均时间复杂度=O(nlog2n)
    最好时间复杂度=O(nlog2n)
    最坏时间复杂度=O(n2)
    每⼀层的QuickSort 只需要处理剩余的待排序元素,时间复杂度不超过O(n)
  • 稳定性:不稳定

算法优化思路:
若每⼀次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低。故若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)

尽量选择可以把数据中分的枢轴元素:
①选头、中、尾三个位置的元素,取中间值作为枢轴元素;
②随机选⼀个元素作为枢轴元素

四、选择排序

选择排序:每⼀趟在待排序元素中选取关键字最⼩(或最⼤)的元素加⼊有序⼦序列

4.1 简单选择排序

适⽤性:既可以⽤于顺序表,也可⽤于链表

4.1.1 算法思想

每⼀趟在待排序元素中选取关键字最⼩的元素加⼊有序⼦序列,n个元素的简单选择排序需要 n-1 趟处理(⽆论有序、逆序、还是乱序)
在这里插入图片描述

4.1.2 算法实现

//选择排序:从待排序元素中比较全部,选择最小的加入有序子序列,而不是每次找到更小的值就交换(频繁交换增加复杂度)
void SelectSort(int A[], int n)
{
	//共排序n-1躺
	for(int i = 0; i < n-1; i++)
	{
		int min = i;				//记录最小元素下标
		for(int j = i+1; j < n; j++)
		{	
			if(A[min] > A[j])
			{
				min = j;			//更新最小元素下标
			}
		}
		
		//若更新了最小元素下标,则需交换(需移动元素三次)
		if(min != i)
		{
			swap(A[i],A[min]);
		}
	}
}

//辅助函数
void swap(int &a, int &b)
{
	int temp = a;
	a = b;
	b = temp;
}

4.1.3 算法性能分析

  • 空间复杂度:O(1)
  • 时间复杂度=O(n2)
    总共需要对比关键字 (n-1)+(n-2)+…+1 = n*(n-1)/2次;元素交换次数<n-1
  • 稳定性:不稳定

4.2 堆排序

4.2.1 算法思想

4.2.2 算法实现

4.2.3 算法性能分析

五、归并排序

归并:把两个或多个已经有序的序列合并成⼀个
(m路归并,每选出⼀个元素需要对⽐关键字 m-1 次)
在这里插入图片描述

5.1 算法思想

归并排序:利用归并的思想,把数组内的两个最小的有序序列(在内部排序中⼀般采⽤2路归并,则最小的有序序列只有一个元素)归并为⼀个大的有序序列,再将归并好的有序序列与其他归并好的有序序列依次归并,直至数组有序。(核⼼操作:把数组内的两个有序序列归并为⼀个,即归并)
在这里插入图片描述

5.2 算法实现

辅助函数:归并
在这里插入图片描述
归并算法:
在这里插入图片描述

步骤与代码实现:
在这里插入图片描述

//创建一个临时数组B[]存放所有A[]的元素(为避免每一次调用都创建一个数组,在函数体外面创建)
int *B = new int[sizeof(A)/sizeof(A[0])];	//数组非空

//归并排序
void MergeSort(int A[], int low, int high)
{
	//递归划分到最小,先归并最小的两个有序数组,将左右两个⼦序列分别进⾏归并排序(最小时每个⼦序列只含有1个元素)
	if(low < high)
	{
		int mid = (low+high)/2;	//从中间划分
		MergeSort(A, low, mid);
		MergeSort(A, mid+1, high);
		Merge(A,low,mid,high);
	}
}

//辅助函数:A[low...mid]和A[mid+1...high]各自有序,将两个部分归并
void Merge(int A[], int low, int mid, int high)
{
	//1.将所有A[low]~A[high]的元素复制到数组B[]中
	for(int k = low; k <= high; k++)
	{
		B[i] = A[i];
	}
	
	//2.i指向B[]中左边有序数组的待归并元素,j指向B[]中右边有序数组的待归并元素,k指向A[]中归并插入的位置
	int i = low;
	int j = mid + 1;
	for(int k = i; i <= mid && j <= high; k++)
	{
		//归并,将较小值复制到A中
		if(B[i]<=B[j])		//保证稳定
		{
			A[k] = B[i++];
		}
		else
		{
			A[k] = B[j++];
		}
	}
	while(i <= mid)		//若B[]中某一个有序数组已插入完,将另一个的有序数组剩下的插入到A[]
	{
		A[k++] = B[i++];
	}
	while(j <= high)
	{
		A[k++] = B[j++];
	}
}

5.3 算法性能分析

2路归并的“归并树”——形态上就是⼀棵倒⽴的⼆叉树
高度为h的二叉树至多有 (2h-1)个结点,n个元素进⾏2路归并排序,应满足n ≤ 2h-1,归并趟数为h-1 = [log2n]向上取整
结论:n个元素进⾏2路归并排序,归并趟数= [log2n]向上取整
在这里插入图片描述

  • 空间复杂度:O(n)
    来⾃于辅助数组B
  • 时间复杂度:O(nlog2n)
    归并趟数为h-1 = [log2n]向上取整,每趟归并时间复杂度为O(n)
  • 稳定性:稳定的

六、基数排序

基数排序不是基于“比较”的排序算法
基数排序通常基于链式存储实现

6.1 算法思想

假设长度为n的线性表中每个结点aj的关键字由d元组 (kjd−1,kjd−2,…kj0 )组成。其中,0 ≤ kji ≤ r - 1(0 ≤ j<n, 0 ≤ i ≤d - 1,r 称为“基数”)————kjd−1称为最⾼位关键字(最主位关键字),kj0 称为最低位关键字(最次位关键字)
如358结点,由三元组(百、十、个)组成,元组中每个取值范围为0~9

  1. 基数排序得到递减序列的过程如下
    ①初始化: 设置 r 个空队列(先入先出),Qr-1, Qr-2,…, Q0
    ②按照各个关键字位权重递增的次序(如:个、十、百),对d个关键字位分别做“分配”和“收集”
    (分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插⼊Qx队尾)
    (收集:把 Qr-1, Qr-2,…, Q0各个队列中的结点依次出队并链接(使当前处理的关键字位大的优先出队,得到按当前关键字位递减排序的序列))
    按照各个关键字位权重递增的次序做“分配”和“收集”,可使队列内部有序(如要获得递减的序列,则当以百位进行分配时,十位已经从大到小排好序,进入队列时是十位大的优先入队)
    在这里插入图片描述
    举例:
    在这里插入图片描述
  2. 基数排序得到递增序列的过程如下
    ①初始化: 设置 r 个空队列,Q0, Q1,…, Qr−1
    ②按照各个关键字位权重递增的次序(个、十、百),对d个关键字位分别做“分配”和“收集”
    (分配:顺序扫描各个元素,若当前处理的关键字位=x,则将元素插⼊Qx队尾)
    (收集:把 Q0, Q1,…, Qr−1 各个队列中的结点依次出队并链接(使当前处理的关键字位小的优先出队,得到按当前关键字位递增排序的序列))

6.2 算法实现

代码实现:

//单链表结点和链表结构
typedef struct LinkNode{
	ElemType data;
	struct LinkNode*next;
}LinkNode, *LinkList;

//链式队列结构
typedef struct{
	LinkNode *front,*rear;//队列的队头和队尾指针
}LinkQueue;

//收集队列内元素步骤:(故收集⼀个队列只需O(1)时间)
p->next = Q[6].front;
Q[6].front = NULL ;
Q[6].rear = NULL ;
p = ...;

6.3 算法性能分析

  • 稳定性:稳定的
  • 空间复杂度:O( r )
    需要 r 个辅助队列
  • 时间复杂度:O(d(n+r))
    ⼀趟分配O(n),以链表存储时⼀趟收集O( r ),总共d趟分配
    在这里插入图片描述
    在这里插入图片描述

6.4 算法应用场景

(把关键字拆为d个部分,每个部分可能取得 r 个值)

基数排序擅长解决的问题:
①数据元素的关键字可以⽅便地拆分为 d 组,且 d 较⼩
②每组关键字的取值范围不⼤,即 r 较⼩
③数据元素个数 n 较⼤

在这里插入图片描述

总结

代码附录

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值