常见数据排序算法

常见的数据排序算法可以大致分为两类:
1.比较类排序
2.非比较类排序
 比较类排序算法主要是通过数据间的比较实现,其时间复杂度是无法突破O(nlogn)的
 非比较类排序算法不是通过数据间的排序实现,其时间复杂度可以以线性完成,时间和速度会比非比较类排序算法更优
现在简单看一下各算法时空复杂度比较
在这里插入图片描述
从表格中可以看出来前七种排序方式时间复杂度很难突破nlogn
其实还不如用STL库自带的sort 同样是nlogn而且写起来很方便

说一下其中的几个概念问题:
1.稳定性:如果有两个数a,b并且a=b,且a在b前面。如果排序后a仍在b之前则称算法稳定。如果经过排序后可能变成了b在a之前,则算法不稳定
2.时间复杂度:估测n变化时操作预估次数
3.空间复杂度:执行操作时需要存储空间的大小
时空复杂度都是关于n的函数

冒泡排序
所有学编程的第一个掌握的排序算法
顾名思义,冒泡排序就像冒泡一样,气泡大的向上走即可
怎么来实现算法呢
假如我们需要将数列进行从小到大排序
最简单的方法就是首先先找出当前数列中最大的数字排到数列的最后,之后再将第二大的的元素排到倒数第二个位置,以此类推就能完成对数组的排序
比如说对于数列4 3 2 1我们要将数字从小到大排序
具体过程实现
在这里插入图片描述
代码实现

#include<stdio.h>

int main(){
    int num[1100];
    int N,i,j;
    scanf("%d",&N);
    for(i=1;i<=N;++i){
        scanf("%d",&num[i]);
    }
    for(i=1;i<=N;++i){
        for(j=1;j<=N-i;++j){
            if(num[j]>num[j+1]){
                int temp=num[j+1];
                num[j+1]=num[j],num[j]=temp;
            }
        }
    }
    for(i=1;i<=N;++i){
        printf("%d ",num[i]);
    }
    return 0;
}

这样就实现了排序,由于需要对所有数据进行比较,所以时间复杂度是很高的,大概在n²的数量级,这样对于小一点的数据是轻松并且简单的能实现的,但是数据一旦过多,那么时间是无法承受的。

将想扩充一下就可以想到插入排序

选择排序
选择排序的基本思想就是:如果要将数据从小到大排列,那么就可以将数列中最小的数字标记出来提到最前面,之后下一轮的时候将剩余数据里第二小的数字提到第二位,以此类推就可以得到从大到小的排序。这就是选择排序的基本思路
同样假如要处理数据4 3 2 1,要将该组数据从小到大排序
具体过程实现
在这里插入图片描述
经过这样的操作,实现了选择排序

代码实现

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

int main(){
	int N;
	scanf("%d",&N);
	int num[100],flag,minn;
	for(register int i=0;i<N;++i){
		scanf("%d",&num[i]);
		if(i==0){
			minn=num[i];
		} 
	}
	for(register int i=0;i<N-1;++i){
		for(register int j=i;j<N;++j){
			if(num[j]<minn){
				minn=num[j],flag=j;
			}
			if(j==N-1){
				num[flag]=num[i],num[i]=minn,minn=num[i+1];
			}
		}
	}
	for(register int i=0;i<N;++i){
		printf("%d ",num[i]);
	}
	return 0;
} 

通过这段代码就实现了数据的选择排序,很容易看出来选择排序的时间复杂度是n²的。并不比冒泡排序快,而且这种排序方式并不稳定同样的数据位置可能会变化

插入排序
插排顾名思义就是将选择好的数字插入到已知数列中,同样的例子,对于数列4 3 2 1 如果要将数据从小到大排序可以将数字中最小的提前选出来排到第一个,将后面的元素一次往后排
我们来模拟一下具体的实现过程
对于数据4 3 2 1 ,如果要将数据从小到大排序
在这里插入图片描述
经过一系列操作后,完成了插入排序
代码实现

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

int main(){
	int N,minn,flag;
	scanf("%d",&N);
	int num[100];
	for(register int i=0;i<N;++i){
		scanf("%d",&num[i]);
	}
	minn=num[0],flag=0;//注意这里初值的使用 
	for(register int i=0;i<N-1;++i){
		for(register int j=i;j<N;++j){
			if(num[j]<minn){
				flag=j,minn=num[j];
			}
		}
		if(flag!=i){
			for(register int j=flag;j>i;--j){//注意这里循环的起始和终止条件 
				num[j]=num[j-1];
			}
			num[i]=minn; 
		}
		minn=num[i+1];//最后更新一下初始的最小值 
	}
	for(register int i=0;i<N;++i){
		printf("%d ",num[i]);
	}
	return 0;
} 

插排有许多需要关注的细节,他的思路很简单,但是又许多细节需要考虑。我写的时候也调试了很多次。
对于这样的排序方式,他的时间复杂度并不稳定,如果数列又特别强的有序性的话,其时间复杂度将会趋近于线性,当数列完全是顺序则时间复杂度为n,如果数列完全逆序,那么时间复杂度将会将会变成n²。
由于插入排序时间复杂度并不稳定并且细节较多,个人更推荐冒泡和选择排序
但是插入排序很像priority_queue(优先队列)的思想,priority_queue在之后会提到

这几个排序方式时间复杂度几乎相同,我们接下来看看时间复杂度较低的几个非线性时间的排序算法

希尔排序(Shell Sort)
由于插排时间复杂度很高并且不稳定,提出了Shell Sort,希尔排序本质上是一种分组排序,将增量逐步取半,进行插排,当增量为1时,在进行一次插排,就能得到排序结果
增量逐步取半主要由于习惯和使时间复杂度尽量降低的原因
对于数列9 1 5 8 3 7 4 6 2进行从小到大的排序
演示一下过程
在这里插入图片描述
这里暂时没有写希尔排序的代码,因为希尔排序的代码细节比较多,不推荐书写,这里介绍是为了了解一下这样的思想可以简化运算数量,降低时间复杂度,让插排变得更容易执行。
希尔排序的时间复杂度很难证明,和他选择的增量有关,总之希尔排序是插入排序的改进版,绝大多数情况下希尔排序的时间复杂度要低于插入排序。

归并排序
归并排序是建立在归并操作上的一种算法,我们要使用到一种分治的思想。
何为分治,就像字面上写的,就是“分而治之”,就是将一个大的问题分解成多个小的容易解决的问题。
比如对于我们要在一串有序的数列中查找一个已知数的位置,最容易想到的就是整个数组全部比对一遍,当找到数据的位置时,停止循环,输出结果,但是如果数据数量很大,并且需要查找的个数很多,都是极限数据那么这样的算法时间效率就很低。
我们想一下使用分治的思想,将问题简化,我们将问题简化成将这一个数组分成许多段,在很多段数列中分别查找数字是否存在于这一段数列中,这样就引出了一个小一点但是很常用的算法——二分查找,二分就是很经典的具有分治思想的算法。二分的基本思想就是类似于查字典,我们要查找一个指定字母开头的单词, 如果从前往后直接翻显然并不现实,那么这样的话我们可以首先将字典翻到中间,看看这里的字母,如果小于那么就再去查找后半段,如果大于就去查找前半段,之后再去查找前/后半段的中间部分,知道查到了字母的位置,这样就是二分的基本方法,这里对二分法不做过多介绍,之后的内容中会专门腾出一块专区取介绍二分法,因为二分法在算法竞赛中时非常常见的
同样的应用二分法的还有最常见的求全排列,例如求1 2 3 4的全排列,我们是将问题分解为求以1,2,3,4开头的全排列,之后再将问题进一步分解,分解成更小的问题 ,这样的问题就是应用了分治的思想

介绍了这么多就是为了引入分治的思想,那么归并排序就是主要在分治的思想上进行,对于一组数据的排序怎样进行分治呢,我们可以将数组分半,逐步将数据分到两个一组,在进行归并排序也就是最简单的排序方式,唯一的缺点就是归并排序需要开辟额外的存储空间,但是其时间复杂度是所有非线性时间排序中最低的时间复杂度可以降低到O(nlogn)

比如对于数据1 2 4 5 3 9 7 8,我们用树的形式来演示一下运行过程
在这里插入图片描述
归并排序的时间复杂度是O(nlogn)的,空间复杂度为O(n)在运行的时候需要一个额外的与原数组长度相同的辅助数组来协助算法运行,但是归并排序是最稳定的排序算法

快速排序(Quick Sort)
快速排序在C++的STL库中有库函数sort,但是快排是一种很重要的排序方式,其平均时间复杂度是O(nlogn)在非线性时间排序算法中时间复杂度很优秀。
快排和冒泡排序一样同样属于交换排序,同样是通过元素之间的比较和交换位置来达到排序的目的,但是冒泡排序在每一轮只是将一个元素移动到数列的一端,而快速排序则是每一轮挑选一个基准元素根据大小关系将元素移动到数列的两端,比它大的元素移动到数列的一端,比它小的移动到另一端,从而将数列拆分成两部分。
这样就采用了分治的基本思想。
通过这样的方法,可以大大降低时间复杂度,在这样的算法下快排的平均时间复杂度为O(nlogn)。
如何选择基准元素:最简单的方式是选择数列的第一个元素,但是如果我们选择一个极端的情况如果数列原本是逆序,希望能将数列顺序输出,在这样的极端情况下时间复杂度退化到了O(n²)。其实这种情况可以随机选择一个数字,即使这样也会有机率选到数列的最大值或者最小值,从而影响分治的结果,因此快排的平均时间复杂度是O(nlogn),其最坏情况下的时间复杂度将会到达冒泡排序的时间复杂度O(n²);
概括的来讲算法的基本思路
首先从数组中挑出一个基本元素,称为基准元素pivot,通过排序将所有比基准小的元素放到基准前面,比基准大的元素放到基准后面,之后逐渐的将小于基准的子数列和大于基准的子数列进行排序。
如何移动元素
有两种方法分别是挖坑法和指针交换法
先看一段动图简单理解下算法
在这里插入图片描述
这里以对数列4 7 6 5 3 2 8 1进行排序为例演示过程:
首先最简单的选取基准元素pivot,并且标记这个元素的位置index,定义left,right分别指向元素的开始和结束

首先初始基准元素的位置位于left处,我们从指针right开始,将指针指向的元素和基准元素进行比较,如果比基准元素大,则将right指针向左移动(因为我们需要由小到大排序)如果比pivot小则将right所指向的元素填入left所指向的元素中(之前已用pivot将index所指向的元素取出,这样已经形成了填坑法所说的坑,之后将数字填入即可)
之后将left右移,right不动,此时相当于将1取出填入了原先4所占的坑中,这样原来1所占的坑已经空出,用index进行标记
现在数列变为了
在这里插入图片描述
此时left与index重合,我们用index和left所指向的数进行比较,这里left=7,index=1,这里的left大于index,这时进行填坑,并将right左移,index移动到left处
在这里插入图片描述
同样进行上述操作,
之后所有步骤按上述方式进行
基本过程如下在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
经过如上的各个步骤,之后再分段处理各段分好的数组,就可以得到按顺序排序好的数组

代码实现

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

int num[100];

inline int partition(int p,int r){//快排主体 
	int key=num[r],i=p-1;
	for(int j=p;j<r;++j){
		if(num[j]<=key){
			++i;
			int temp=num[i];
			num[i]=num[j],num[j]=temp;
		}
	}
	int temp=num[i+1];
	num[i+1]=num[r],num[r]=temp;
	return i+1;
}
inline void Quick_Sort(int p,int r){
	int position=0;
	if(p<r){
		position=partition(p,r);//基准点的最终位置 
		Quick_Sort(p,position-1);//划分左半段 
		Quick_Sort(position+1,r);//划分右半段 
	}
}

int main(){
	int n;
	printf("请输入数组长度:");
	scanf("%d",&n);
	printf("请输入数组:");
	for(int i=0;i<n;++i){
		scanf("%d",&num[i]);
	}
	Quick_Sort(0,n-1);
	for(int i=0;i<n;++i){
		printf("%d ",num[i]);
	}
	return 0; 
} 

一点点的总结
在这里插入图片描述
来看一张图,对于快速排序,我们每次都是将基准元素排序到位,就像上图所示,每一轮排序都是讲一个基准元素排序到位,相比于冒泡排序,快速排序每一次的交换都是跳跃式的,这样数的交换就不会像冒泡排序一样再相邻的数字之间进行交换,因此,交换的次数减少,交换速度自然就提高了,但是在最坏的情况下,快速排序的时间复杂度仍然会无线接近于O(n²),但是其平均时间复杂度为O(nlogn)

堆排序

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值