排序算法总结

Posted 不倒翁*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法总结相关的知识,希望对你有一定的参考价值。

文章目录

下面来详细介绍以下以上几种排序算法的具体实现

1.插入排序

插入排序分为直接插入排序希尔排序.
首先我们来看看直接插入排序.

1.1直接插入排序

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
下面举个例子具体讲讲上面那句话是什么意思.
相信大家都打过扑克牌吧.如下图所示,当我们抓扑克牌时,一般都是按照一定的顺序来放置的(假设我们这里是升序),当我们新抓到一张7时,我们会去找一个合适的位置来存放7这张牌,**这个合适的位置就是该位置前的牌的数值都比7小,该位置后的牌的值都比7大,然后我们就在该位置上插入7,**插入排序就是用了这种思想.

下面我们来具体看看代码是怎么实现的.

void InsertSort(int* a, int n)

	for(int i = 0; i<n-1; i++)
	
		//将x插入到【0,end】的有序空间
		int end = i;
		int x = a[end+1];
		while(end>=0)
		
		 //如果当前点的位置值比x大就把当前点的值后移
			if(a[end]>x)
			
				a[end+1] = a[end];
				end--;
			else
			
				break;
			
		
		a[end+1] = x;
	

对于一个算法而言我们最关注的问题就是它的效率问题,下面我给出直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1),它是一种稳定的排序算法
  4. 稳定性:稳定

1.2希尔排序

我们可以知道一个排序算法他的时间复杂度最不好的情况就是O(N^2),那么有没有什么方法可以优化插入排序,可以使得插入排序的时间复杂度减小呢.这时希尔提出了一种方法,他的方法思想大致如下:
他首先将一组数据按一定间距分成若干组,先对每组的数据先进行一个预排,然后逐渐缩小间距,知道间距缩小称为1,排序过程就算完成了.
假如给定一串数组为: 9 1 2 5 7 4 8 6 3 5 假设我们第一次选取间据 gap = 5,然后进行排序,则第一趟排序的结果为: (下图中颜色相同的为同一组)

接着我们缩小gap,让gap = 2,可以得到:

最后我们让gap = 1

当gap = 1时,就可以将数组中的元素全部排列完. 下面看代码具体实现过程:

void ShellSort(int* a, int n)

	int gap = n ;
	while(gap>1)
	
		// 后面加1 是为了让gap最后能取到 1
		gap = n/3+1;
		for(int i = 0; i<n-gap; i++)
		
			int end = i;
			int x = a[end+gap];
			while(end>=0)
			
				if(a[end]>x);
				
					a[end+gap] = a[end];
					end -= gap;
				else
				
					break;
				
			
			a[end+gap] = x;
		
	

希尔排序的特性总结:

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
  3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的希尔排序的时间复杂度都不固定
  4. 稳定性:不稳定

2.选择排序

选择排序的基本思想为:从一组数据中选出最小(或者最大)的数据,放在序列的起始端,直到待排数组元素达到全部有序就算完成.

2.1直接选择排序

直接选择排序的步骤基本如下:
1.在元素集合array[i]–array[n-1]中选择关键码最小(大)的数据元素.
2.若它不是这组元素中的第一个(或最后一个)元素,则将它与这组元素中第一个(或最后一个)的元素交换.
3.在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素.

void Swap(int* p1, int* p2)

	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;


void select_Sort(int* a, int n)

	int begin = 0, end = n - 1;
	int min,max;
	min = max = begin;
	while(begin < end)
	
		for(int i = 0; i <= end; i++)
		
			 if(a[i] > a[max])
			 
			 	max = i;
			 
			 if(a[i] < a[min])
			 
			 	min = i;
			 
		
		Swap(&a[begin],&a[min]);
		//begin = maxi时, 最大的数被换走了 需要调整一下maxi的值
		if(max == begin)
		
			max = min;
		
		Swap(&a[max],&a[end]);
		begin++;
		end--;
	
	

直接选择排序的特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

2.2 堆排序

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
对于堆排序,我们首先要了解什么是堆.以及怎么建堆.
大堆简单理解就是所有的父亲节点都比子节点大,小堆则相反.
对于排升序,我们这里主要是建大堆,建堆有两种方法,一种是向上建堆,一种是向下建堆(这里不做详细解释建堆的过程).下面我们主要来看看向下建堆的具体代码与解释.

//向下调整建大堆
void AdjustDown(int* a, int n, int parent)

	int child = parent * 2 + 1;
	whild(child < n)
	
		//保证右孩子存在,并且选出两个孩子中较小的一个
		if((child+1) < n && a[child+1] > a[child])
		
			child++;
		
		
		if(a[child]>a[parent])
		
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent*2 + 1;
		else
		
			break;
		
	


建完大堆后就可以找到一个数组中的最大的元素了,然后将做大的元素跟最后的元素进行交换,然后再进行建堆,找出第二大的元素,以此类推,就可以完成排序了.

void HeapSort(int* a, int n)

	for(int i = n-1-1; i >= 0; i--)
	
		AdjustDown(a,n,i);
	
	int end  = n -1;
	while(end > 0)
	
		Swap(&a[0],&a[end]);
		AdjustDown(a,end,0);
		end--;
	

直接选择排序的特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定

3.交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

3.1冒泡排序

冒泡排序的原理:相邻两个数据进行两两比较,然后将偏大或偏小的数据向一方移动。
这个排序算法比较简单,这里就不做过多讲述,下面直接看具体实现过程:

void BullSort(int* a, int n)

	int end = n;
	while(n>0)
	
		int exchange = 0;
		for(int i = 1; i < end; i++)
		
			if(a[i-1]>a[i])
			
				Swap(&a[i-1],&a[i]);
				exchange  = 1;
			
		
		if(exchange == 0)
		
			break;
		
		end--;
	

冒泡排序的特性总结:

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

3.2 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止.
将区间按照基准值划分为两半部分的常见方法有三种:分别是 1.hoare版本 2.挖坑法 3.前后指针法
首先我们来看看第一种方法

//单趟排序 将比keyi位置的小值 排在a[keyi]前面 将比keyi位置的大值 排在a[keyi]后面
//O(N)
int 	Partion(int* a, int left, int right)

	int keyi = left;
	while(left<right)
	
		//左边做key时,右边先走
		//右边先走, 找小于a[keyi]的值
		while( left < right && a[right] >= a[keyi])
		
			right--;
		
		//左边后走 找大于a[keyi]的值
		while(left<rigjt && a[left]<= a[keyi])
		
			left++;
		
		Swap(&a[left], &a[right]);
	
	Swap(&a[left],&a[keyi]);
	//返回相遇时的下标
	return left;

经过上面的单趟排序后,我们可以将区间按照基准值划分为两半部分,但是我们不得不考虑一些特殊情况,就是加入这个数组一开始就是有序的呢,那么我们还继续进行上面的操作不是会浪费时间吗? 所以,我们可以在进行上面操作之前,要对key值进行适当的选取就能够避免上述问题了.那么我们怎么选取key值呢.我们可以在数组中 选出左边,右边和中间中,值在中间的值作为key值,然后在进行划分处理,下面看具体代码

//得到中间的值作为key值
int GetMid(int* a, int left, int right)

	int mid = left + (right - left)/2;
	if(a[left]>a[mid])
	
		if(a[right]>a[mid])
		
			 if(a[left]>a[right])
			 	return right;
			 else
			 	return left;
		
		else
			return mid;
	
	else
	
		if (a[right] > a[mid])
		
			return mid;
		
		else
		
			if (a[right] > a[left])
				return right;
			else
				return left;
		
	
	


int Partion(int* a, int left, int right)

	// 三数取中 -- 面对有序最坏情况,变成选中位数做key,变成最好情况
	int mid = GetMid(a, left, right);
	//将key值放到左边
	Swap(&a[left], &a[mid]);

	int keyi = left;
	while (left < right)
	
		//左边做key 右边先走
		//右边先走 找小于a[keyi]的值
		while (left < right && a[right] >= a[keyi])
		
			right--;
		

		//左边后走 找大于a[keyi]的值
		while (left < right && a[left] <= a[keyi])
		
			left++;
		

		Swap(&a[left], &a[right]);
	
	Swap(&a[left], &a[keyi]);

	//返回相遇时的下标
	return left;


接下来介绍第二种划分的方法: 挖坑法
挖坑法首先是将第一个数据放在临时变量key中,形成一个坑位,这种方法和第一种放法差不多.

int Partion2(int* a, int left, int right)

	int mid = GetMid(a, left, right)
	Swap(&a[mid],&a[left]);
	int key = a[left];
	//假设坑位在左边
	int pivot = left;
	while(left<right)
	
		//右边找小, 将坑位放在里面
		while(left<right && a[right] >= key)
		
			right--;
		
		Swap(&a[right],&a[piovt]);
		//更新坑位
		piovt  = right;
		while(left<right && a[left]<= key)
		
			left++;
		
		Swap(&a[left],&a[povit]);
		povit = left;
	
	a[povit] = key;
	return povit;

第三中方法为:前后指针法

int Partion3(int* a, int left, int right)

	int mid = GetMid(a,left.right);
	Swap(&a[left],&a[mid]);

	int keyi = left;
	int prve = left;
	int cur = prve + 1;
	while(cur <= right)
	
		if(a[cur] < a[keyi] && 	++prve != cur)
		
			Swap(&a[cur],&a[prve]);
		
		cur++;
	
	Swap(&a[prve],&a[keyi]);
	return prve;

准备工作做好后,就可以开始进行快速排序了,首先我们来看看快速排序的递归实现过程

//快速排序
//时间复杂度为O(N*logN)
void QuickSort(int* a, int left, int right)

	//结束递归的条件
	if (left >= right)
		return;
	
	int keyi = Partion3(a, left, right);
	
	//[left, keyi-1] keyi [keyi+1, right]
	//递归 先把keyi左边的排好  然后排keyi右边的
	QuickSort(a, left, keyi - 1);
	QuickSort(a, keyi + 1, right);

使用递归的话,有一个缺点就是当递归深度太深时,会容易使得栈溢出,所有我们就只能考虑使用非递归来实现快速排序了.快速排序的非递归是通过栈来实现的.

void QuickSortNonR(int* a, int leftl, int right)

		Stack st;
		stackInit(&st);
		stackPush(&st, left);
		stackPush(&st, right);

		while(!stackEmpty(&st))
		
			int end = stackTop(&st);
			stackPop(&st);
			int begin = stackTop(&st);
			stackPop(&st);

			int keyi = Partion(a, begin, end);
			if(keyi+1 < end)
			
				stackPusn(&st, keyi+1);
				stackPush(&st,end);
			
			if(begin < keyi - 1)
			
				stackPush(&st, begin);
				stackPush(&st, keyi - 1);
			
			
		

以上是关于排序算法总结的主要内容,如果未能解决你的问题,请参考以下文章

经典排序算法--快速排序

快速排序算法总结

排序算法总结

Arrays排序算法

基础排序算法总结(代码+图片分析)

c# 实现直接插入排序算法-升序