数据结构排序算法总结

一、排序相关概念

  • 排序:
  • 内部排序:
  • 外部排序:
  • 排序的稳定性:

二、八大排序算法关系

数据结构排序算法总结_第1张图片

三、八大排序分析

(1)直接插入排序
数据结构排序算法总结_第2张图片

  • 基本思想:每次将一个待排序的记录按其关键字大小插入到前面已排好的子序列中。
  • 代码:
    step1——查找出插入位置
    step2——前面的元素全部后移
    step3——复制到插入位上
void InsertSort(int A[],int n){
	int i,j;
	for (i=2;i<=n;i++)     //数组从A[1]开始计数,依次将A[2]-A[n]插入到前面已排序序列
		if(A[i]<A[i-1]){        //若A[i]的关键字小于其前驱,需将A[i]插入到i-1前面的有序表
			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];        //复制到插入位置(之所以为j+1是因为之前的j--)
		}
}
  • 性能分析:
    空间复杂度:O(1)
    时间复杂度:O(n²) ——总比较次数:最好(n-1),最差(n(n-1)/2),平均(1/2(n+2)(n-1)) 总移动次数:

(2)二分插入排序

  • 基本思想:先二分法找出元素的待插入位置,然后统一地移动待插入位置后的所有元素。
    前提是左半边为有序序列。
  • 代码:与直插法相比,只是多了个low, high, mid查找而已。
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[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(1)
    时间复杂度:O(n²) ——只是总比较次数减少了,至多:O(n㏒n) ,但移动才占主导。

(3)希尔排序(缩小增量排序)
数据结构排序算法总结_第3张图片
数据结构排序算法总结_第4张图片

  • 基本思想:先取一个小于n的步长d1,把表中全部记录分成d1组,所有距离为d1的倍数的记录放在同一组中,在各组中进行直接插入排序;然后取第二个步长d2=d1/2,重复上述步骤;,直到dn=1。
  • 代码:
    与直插法相比:1. 前后记录位置的增量是dk,不是1; 2. A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到。
void ShellSort(int A[],int n){
	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[i-dk],A[i-2*dk]...使用直插法,只不过把1换成了dk
				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];
			}
}
  • 性能分析:
    空间复杂度:O(1)
    时间复杂度:最差O(n²)
    不稳定

(3)冒泡排序
数据结构排序算法总结_第5张图片

  • 基本思想:两两比较,逆序对交换。最多执行n-1趟。
  • 代码:
void BubbleSort(int A[],int n){
	for(i=1;i<n;i++){
		int swap=0;       //交换标志
		for(j=1;j<=n-i;j++){
			if(A[j]]>A[j+1]){          //逆序,交换
				A[0]=A[j+1];
				A[j+1]=A[j];
				A[j]=A[0];
				swap=1;
			}
		if(swap==0)break;           //此趟冒泡没有发生交换,排序结束
		}
	}
}
  • 性能分析:
    空间复杂度:O(1)
    时间复杂度:O(n²)
    冒泡排序的子序列一定全局有序(有序子序列的所有元素的关键字一定小于or大于无序子序列中所有元素的关键字)

改进的冒泡排序

  • 基本思想:若后面的若干数据已按关键字有序,则后面不再参与排序。
    里面一层循环在某次扫描中没有执行交换,则说明此时数组已经全部有序列,无需再扫描了。
  • 代码:核心在于设置一个记录这一趟最后交换位置的变量。
void Bubble_Modified_Sort(int A[],int n){
	i=n;
	while(i>1){
		LastExchangeIndex=1;
		for(j=1;j<i;j++){
			if(A[j]>A[j+1]){          //逆序,交换
				A[0]=A[j+1];
				A[j+1]=A[j];
				A[j]=A[0];
				LastExchangeIndex=j;           //记录交换位置
			}
		}
		i=LastExchangeIndex;
	}
}

(4)快速排序
数据结构排序算法总结_第6张图片

  • 基本思想:快速排序算法是基于分治算法和递归算法的。通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。一般选取第一个记录的关键字为基准数。
  • 代码:
    step1——i =L; j = R; 将基准数挖出形成第一个坑A[i]。
    step2——j–由后向前找比它小的数,找到后挖出此数填前一个坑A[i]中。
    step3——i++由前向后找比它大的数,找到后也挖出此数填到前一个坑A[j]中。
    step4——再重复执行2,3二步,直到i==j,将基准数填入A[i]中
int Quick_Partition(int A[],int i
,int j){           //划分操作
	//一次划分,左端位置为i,右端位置为j,划分元为A[i]
	A[0]=A[i];          //暂存A[0]
	while(i<j){
		while(i<j&&A[j]>=A[0])   j--;   //右边所指比基准数大,不用交换,j往左移继续找
		if(i<j){             //把右边所指比基准数小的填到坑里
			A[i]=A[j];
			i++;             //i右移,换左边找
		}
		while(i<j&&A[i]<=A[0])   i++;   //左边所指比基准数小,不用交换,i往右移继续找
		if(i<j){             //把左边所指比基准数大的填到坑里
			A[j]=A[i];
			j--;             //j左移,换右边找
		}
	}
	A[i]=A[0];           //将基准数放入调整后的位置
	return i;            //返回基准数位置
}

void QuickSort(int A[],int low,int high){      //快速排序(递归)
	while(low<high){         //递归到每部分只有一个空元素为止
		int i=Quick_Partition(A,low,high);      //将表一分为二
		Quick_Partition(A,low,i-1);
		Quick_Partition(A,i+1,high);
	}
}
  • 性能分析:
    空间复杂度:平均O(㏒n),最坏O(n)

    为什么空间复杂度是O(㏒n)?
    因为快排是递归的,需要借助一个递归工作栈来存放每层递归的数据,即应等于二叉树的最大深度。最好:㏒(n+1)向上取整,最差:O(n),平均:O(㏒n)

    时间复杂度:平均O(n㏒n),最坏O(n²)
    不稳定

(5)简单选择排序
数据结构排序算法总结_第7张图片

  • 基本思想:第一趟时,从第一个记录开始,通过n – 1次关键字的比较,从n个记录中选出关键字最小的记录,并和第一个记录进行交换。第二趟从第二个记录开始,选择最小的和第二个记录交换。以此类推,直至全部排序完毕。
  • 代码:两层循环实现:
    第一层循环:依次遍历序列当中的每一个元素
    第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。
void SelectSort(int A[],int n){ 
	for(i=1;i<n;i++){           //n-1趟选择
		int min=i;                 //记录最小元素位置
		for(j=i+1;j<=n;j++)         //从A[i...n-1]中选择最小的元素
			if(A[j]<A[min])     min=j;        //更新最小元素位置
			if(min!=i){        //若最小不在第i个元素,则将他两交换
				A[0]=A[min];
				A[min]=A[i];
				A[i]=A[0];
			}
	}
}
  • 性能分析:
    空间复杂度:O(1)
    时间复杂度:O(n²)
    不稳定

(6)堆排序

  • 堆的一些性质
    本质是一种数组对象
    任意叶子结点小于(或大于)其所有父节点,对左右孩子的大小关系不做任何要求。
    堆排序,就是基于大顶堆或者小顶堆的一种排序方法。
    若想得到升序,则建立大顶堆;若想得到降序,则建立小顶堆。

  • 基本思想:
    step1——构建大(小)顶堆:从最后一个非叶子结点(n/2向下取整)从后往前执行下沉操作
    step2——取顶,与最后元素交换
    step3——对交换后的n-1个序列元素进行调整,使其满足大顶堆的性质;
    step4——重复至堆中只有1个元素为止

void Sift(int A[],int low,int high){         //大顶堆调整算法
	int i=low,j=2*i;          //A[j]是A[i]的左孩子结点
	int temp=A[i];           //temp暂存根数据
	while(j<=high){
		if(j<high&&A[j]<A[j+1])   ++j;        //若存在右子树且右子树根的关键字大,则沿右分支调整
		if(temp<A[j]){           //若比存的根节点还大,就交换
			A[i]=A[j];         //A[j]换到父节点
			i=j;               //修改i,j值,以便继续向下调整
			j=2*i;
		}
		else break;           //调整结束
	}
	A[i]=temp;               //当前待调整的结点放到比其大的孩子结点位置上
}

void HeapSort(int A[],int n){           //堆排序
	int i,temp;
	for(i=n/2;i>=1;--i)            //建立初始堆,i为最后一个非叶子结点
		Sift(A,i,n);
	for(i=n;i>=2;--i){            //进行n-1次循环,完后堆排序(注:从最后一个元素开始调整)
		//交换堆顶元素A[1]和堆中最后一个元素
		temp=A[1];
		A[1]=A[i];
		A[i]=temp;
		Sift(A,1,i-1)              //每次交换堆顶元素和堆中最后一个元素之后,都要对堆进行调整
	}
}			
  • 性能分析:
    空间复杂度:O(1)
    时间复杂度:全是:O(nlogn)
    不稳定

(7)二路归并排序

2路归并一般用在内部排序中,多路归并一般用在外部磁盘数据排序中。

数据结构排序算法总结_第8张图片

  • 基本思想:分治法,就是将一个数组一刀切两半,递归切,直到切成单个元素,然后重新组装合并,单个元素合并成小数组,两个小数组合并成大数组,直到最终合并完成,排序完毕。

    归并排序其实要做两件事:
    分解----将序列每次折半拆分
    合并----将划分后的序列段两两排序合并
    因此,归并排序实际上就是两个操作,拆分+合并

  • 与快速排序的区别与联系:

    1.联系:
    ➀两者均采用分治法
    ➁均采用递归调用实现
    2.区别:
    ➀快速排序:“先治后分”——当两个子数组有序时,整体也就有序,不需要归并这一步。
    ➁归并排序:“先分后治”——每一轮排序后需要归并,以达到局部的上一层有序。
    ➂递归调用发生点不同:快速排序递归发生在处理整个数组之后;而归并排序发生在之前。

  • 代码:

    如何合并?
    L[first…mid]为第一段,L[mid+1…last]为第二段,并且两端已经有序,现在我们要将两端合成达到L[first…last]并且也有序。

    step1——首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[]
    step2——重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[]
    step3——此时将temp[]中的元素复制给L[],则得到的L[first…last]有序

    如何分解?
    采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列分组;直到分组后组内只有一个元素,此时可认为组内所有元素有序,则分组结束。

void Merge(int A[],int B[],int low,int mid,int high){
	//表A[low...mid],A[mid+1...high]各自有序,将其合并为有序表B[low...high]
	i=low;j=mid+1;k=i;
	while(i<=mid&&j<=high){
		if(A[i]<A[j])
			B[k++]=A[i++];
		else
			B[k++]=A[j++];
	}
	while(i<=mid)   B[k++]=A[i++];         //若第一个表没检测完,直接复制
	while(i<=high)   B[k++]=A[j++];         //第二个....

	for(int k=low;k<=high;k++)
		A[k]=B[k];
}

void MergeSort(int A[],int B[],int low,int high){
	if(low==high)	B[high]=A[high];
	else{
		int mid=(low+high)/2;          //从中间划分两个子序列
		MergeSort(A,B,low,mid);          //从左侧子序列进行递归排序
		MergeSort(A,B,mid+1,high);       //从右侧子序列进行递归排序
		Merge(A,B,low,mid,high);       //将两个有序表合成一个有序表
	}
}

void Binary_MergeSort(int A[],int n){
	MergeSort(int A[],int B[],1,n);
}
  • 性能分析:
    空间复杂度:O(n)——需要一个与表等长的存储单元数组空间
    时间复杂度:O(nlogn)

(9)基数排序

  • 基本思想:不基于比较进行排序,而采用多关键字思想(每个位数),基于分配—收集。
    分为最高位优先排序(MSD)和最低位优先排序(LSD)
  • 性能分析:
    空间复杂度:O®——一趟排序需要r个辅存空间(r个队列),会重复使用
    时间复杂度:O(d(n+r))——共需d趟分配和收集,一趟分配O(n),一趟收集O®

四、性能分析

  • 空间复杂度即额外使用的空间容量
  • 当序列有序或基本有序时,直接插入排序和冒泡排序的时间复杂度为O(n),其中,直插排是因为每趟只用比较一次,但是要比较n趟;而冒泡排是因为只用比较一趟,但是一趟比较n次。而快速排序时间复杂度退化为O(n²),因为此时树不平衡,为单边树。
  • 除了归并排序外,在最坏情况下,所有稳定的排序算法的时间复杂度都是O(n²),但是归并排序牺牲了空间复杂度,因为他用了一个和原树一样大的数来暂存数据,所以空间复杂度O(n)。
    但其中,快速排序,虽然时间复杂度最坏是O(n²),但平均是O(nlogn) ,空间复杂度为O(logn) ,因为用线程栈辅存——树的深度。

五、答题技巧

  1. 每趟排序后均会使一个记录放在最终位置的排序:简单选择排序、冒泡排序、堆排序。

  2. 经过i趟排序后,插入排序前i+1个元素局部有序;2路归并排序每2i个元素有序。

  3. 折半插入排序的比较次数与序列初态无关,都是O(nlogn),而直接插入排序的比较次数与序列初态有关,为O(n)~O(n²)。

  4. 冒泡排序的交换次数为排序中逆序的个数。

  5. 绝大部分内部排序只适用于顺序存储结构。

  6. 虽然众多排序算法的平均时间复杂度都为O(nlogn) ,但快排的常数因子是最小的,平均性能是最好的。

  7. 当快排每次的枢纽都能将表对半分时,速度最快;表本身已经有序或逆序,速度最慢。

  8. 快排阶段性排序结果特点:第i趟完成时,会有i个以上的数出现在它的最终位置,即他左边的数都比他小, 右边的数都比他大。

  9. 快排的过程构成一棵树,递归深度即递归树的高度。枢纽值每次都将子表等分时,递归树高度为logn;枢纽值每次都是字表的最大或最小值时,递归树退化为单链表,树高为n。

  10. 取一大堆数据中的k个最值时,优先采用堆排序。

  11. 如何判断一组序列是不是堆?将其表示为完全二叉树,再看父子结点关系是否满足堆的定义。

  12. 堆的几种操作分析:

    (1)筛选堆的过程——自底向上调整
    ➀先把数据全部按完全二叉树的顺序堆起来。
    ➁从最后一棵子树,从下往上,从右往左,调整堆顺序。

    (2)删除堆顶的最大元素——自顶向下调整
    注:删除比较是将左右孩子中最大的那个与父节点交换,所以要先左右孩子比较,再将最大的孩子与父节点比较,需要2次。

    (3)往一个完整的堆中插入元素——自底向上调整

  13. 选择排序算法的比较次数始终为n(n-1)/2,与序列状态无关。

  14. 调整堆和建初始堆的时间复杂度不同,因为建初始堆的时候每一步都要多次调整堆。调整堆:O(logn),建初始堆:O(n)。

你可能感兴趣的:(考研数据结构)