内部排序算法

外部排序:待排序的记录数量很大,以至内存不能一次全部容纳,在排序过程中还需对外存进行访问。

内部排序:待排序记录存放在计算机随机存储器中进行的排序过程。内部排序大致分为五类:插入排序,交换排序,选择排序,归并排序和计数排序。

常用基于比较的排序算法:冒泡排序、插入排序、选择排序、希尔排序、归并排序、快速排序

非比较的排序算法:计数排序、桶排序、基数排序

 

插入排序


1.直接插入排序

基本思想:对于未排序的数据,在已排序序列中从后向前扫描,找到相应位置并插入,直至完成排序。选择第一个数据作为已经有序的序列。

void InsertSort(int a[],int n){
    for(int i=1;i<n;i++){
        int tmp=a[i];
        int j;
        for(j=i-1;j>=0&&a[j]>tmp;j--){
            a[j+1]=a[j];                //将大的元素后移
        }
        a[j+1]=tmp;                     //放置到正确有序的位置
    }
}

时间复杂度为O(N^2)

2.折半插入排序

基本思想:在直接插入的基础上将关键字之间的比较改进为二分查找的比较,减少比较次数,但移动记录次数不变。

void BInsertSort(int a[],int n){
    for(int i=1;i<n;i++){
        int tmp=a[i];
        int left=0,right=i-1;
        while(left<=right){            //二分查找
            int mid=(left+right)/2;
            if(a[mid]>tmp)
                right=mid-1;
            else
                left=mid+1;
        }
        for(int j=i-1;j>=right+1;j--)  //将a[right+1,i-1]的元素全部后移
            a[j+1]=a[j];
        a[right+1]=tmp;                //插入到相应正确的位置
    }
}

时间复杂度仍为O(N^2)

3.二路插入排序

上面折半插入减少了元素间比较的次数,二路插入在此基础上继续改进,使用辅助空间一个循环数组来达到减少元素移动的次数。

构建一个相同大小的循环数组b,该数组分为两路,一路是有序序列b[first,last],另一路是空的,根据情况选择向哪一路插入:

若a[i]<b[first],变化first=(first-1+n)%n,b[first]=a[i],插入到第二路中。

若a[i]>=b[last],变化last=last+1,b[last]=a[i],插入到第二路中。

若b[first]<=a[i]<b[last],则通过二分插入将a[i]插入到第一路中。

可以看出,在插入到第二路的情况下是不需要移动元素的,提高了效率。

void tworoad_InsertSort(int a[],int n){
	int first=0,last=0;
	int *b=(int*)malloc(sizeof(int)*n);    //申请辅助空间
	b[0]=a[0];                             //选择第一个作为有序的基准数
	for(int i=1;i<n;i++){
		int tmp=a[i]; 
		if(tmp<b[first]){                  //插入到第二路的情况,不需要元素的移动
			first=(first-1+n)%n;
			b[first]=tmp;
		}
		else if(tmp>=b[last]){
			last++;
			b[last]=tmp;
		}
		else{                //否则二分插入到第一路(选取区间左闭右开),这个时候需要进行元素的移动
			int left=first,right=last,mid,d;
			while(left!=right){
				d=(right-left+n)%n;        
				mid=(left+d/2)%n;
				if(tmp<b[mid])
					right=mid;
				else
					left=(mid+1)%n;
			}
			for(int k=last+1;k!=left;k=(k-1+n)%n)
				b[k]=b[(k-1+n)%n];
			b[left]=tmp;
			last++;                        //注意插入到第一路,第一路长度扩增
		}
	}
	for(int i=0;i<n;i++){                  //将空间b中有序的序列返回到a中
		a[i]=b[(i+first)%n];
	}
}

以上三个插入排序在序列的直观上来看都是每次迭代会使序列前面有序区的序列长度加1,后面无序区长度减1外不发生变化。

4.希尔排序

基本思想:希尔排序又称为缩小增量排序,它的关键是从待排序的n个元素中选取一个增量值p,将序列划分为p个子序列,每个序列中存放的是间隔为p的元素,对每个子序列进行直接插入排序使其有序。然后p缩小,继续按此进行划分和排序,直至p==1时对整个序列进行最后一次排序,得到最终的有序序列。

关于增量值p的取法有很多种,最初shell提出的是p=n/2向下取整,p=p/2向下取整直至p==1,还有其他许多高效的增量p的取法。希尔排序是一种不稳定的排序算法。

void ShellSort(int a[],int n){
	int p=n/2;                  //初始化增量值
	while(p>=1){                //循环,最后一次排序时增量值为1,就是对全体序列进行插入排序,但是由于之前的工作,使得此次的排序快了很多
		for(int i=p;i<n;i++){
			int tmp=a[i];
			int j;
			for(j=i-p;j>=0&&a[j]>tmp;j-=p)
				a[j+p]=a[j];
			a[j+p]=tmp;
		}
		p=p/2;                  //增量缩小
	}
}

平均时间复杂度为O(N^1.3),最好为O(N),最坏为O(N^2)。

 

交换排序 

1.冒泡排序

最基本的交换排序

void bubbleSort(int arr[], int n){
	for(int i=0;i<n-1;i++){
		int flag=1;                   //设置哨兵
		for(int j=0;j<n-1-i;j++){
			if(a[j]>a[j+1]){
				int tmp=a[j];
				a[j]=a[j+1];
				a[j+1]=tmp;
				flag=0;               
			}
		}
		if(flag) break;               //如果flag==1则说明没有发生交换,序列已经有序了
	} 
}

平均情况O(N^2),最好O(N),最坏O(N^2),是一种稳定的排序。从序列上直观的看就是每次迭代都会将最大的元素移到后面的有序区。

2.快速排序(三种算法实现)

我们知道在最好的情况下,基准元素将待排序列区域一分为二,左边的元素全部小于基准数,右边的全部大于,对左右子序列递归调用快速排序。但是如果序列基本有序,那么快排就会大大退化。因此选取最好的基准元素都是尽可能的将序列平均一分为二,通常使用三数取中法(在序列首尾中间三个位置选取中值数作为基准元素)

int getmid(int a[],int l,int r){     //快排优化————三数取中法
  int mid=l+(r-l)/2;
  if(a[l]<=a[r]){
    if(a[l]>=a[mid])
      return l;
    else if(a[r]<=a[mid])
      return r;
    else
      return mid;
  }
  else{
    if(a[l]<=a[mid])
      return l;
    else if(a[r]>=a[mid])
      return r;
    else
      return mid;
  }
}

①挖坑法

基本思想:先选择一个基准数(一般选择第一个元素)拿出来,序列中空出它的位置;对该序列从后向前寻找找到小于基准数的元素,将这个元素搬出填入空出的位置,该元素的位置成为新的空位置;然后在序列从前向后寻找大于基准数的元素,将其搬出填入到空位置中,继续从后向前寻找......;直至前后两头相遇,将基准数填入该位置中。

void quick_sort(int s[],int l,int r){
	if(l<r){              //只有区间数大于1时才需要排序
        int mid=getmid(a,l,r),tmp=a[l];
        a[l]=a[mid];a[mid]=tmp;
        tmp=a[l];         //快排优化
		int i=l,j=r;      //选择第一个元素为基准数 
		while(i<j){                //直到i==j相遇跳出循环 
			while(i<j&&s[j]>=tmp)  //从后向前找小于tmp的数 
				j--;
			if(i<j)
				s[i++]=s[j];
			while(i<j&&s[i]<=tmp)  //从前向后找大于tmp的数 
				i++;
			if(i<j)
				s[j--]=s[i];
		}
		s[i]=tmp;                 //填入基准数
		quick_sort(s,l,i-1);    //二分后左右递归 
		quick_sort(s,i+1,r);
	}
}

②交换法

基本思想:与挖坑填充的思路不同在于使用的是交换方法,当数组元素很多时使用挖坑法比较高效,不需要开临时交换变量。

 

如果使用非递归的形式,可以将递归改为栈操作!将l,i-1和i+1,r压栈,直至栈为空!

void QiockSort(vector<int> &v, int left, int right)
{
    if (left >= right)
        return;
    stack<int> s;
    s.push(left);
    s.push(right);
    while (!s.empty())
    {
        int right = s.top();   //注意压进去的时候为先左后右,取出时相反!先右后左!
        s.pop();
        int left = s.top();
        s.pop();
        if (left < right)
        {
            int boundary = partition(v, left, right);
            // 左区间
            s.push(left);
            s.push(boundary-1);
            // 右区间
            s.push(boundary + 1);
            s.push(right);
        }

    }
}

使用递归的快排会因为递归产生空间消耗,最好情况下空间复杂度为O(logN),最坏情况空间复杂度为O(N)

平均复杂度O(NlogN),最好情况O(NlogN),最坏情况在序列正序或者逆序时为O(N^2),是一种不稳定的排序算法。

 

选择排序

1.简单选择排序

基本思想:从第1个元素开始,在其后选择最小的元素与它交换位置,经过n-1次选择交换后序列有序,时间复杂度O(N^2)。从序列上直观看是每次迭代使序列前面的有序区加1,后面无序区长度减1但是因为交换会发生变化!

void SelectSort(int a[],int n){
	int i,j,k;
	for(i=0;i<n-1;i++){
		k=i;
		for(j=i+1;j<n;j++){
			if(a[k]>a[j])
				k=j;
		}
		if(k!=i){
			int tmp=a[k];
			a[k]=a[i];
			a[i]=tmp;
		}
	}
}

2.堆排序 

基本思想:利用堆的性质:i为父节点则下标2*i+1,2*i+2分别为其左右子节点下标。利用大根堆的性质,每次选取堆顶节点(即堆的最大元素)与堆的末尾元素交换,然后重新调整堆成为大根堆,大根堆元素个数减1,末尾有序区元素个数加1。继续将堆顶节点与堆末尾交换重复进行操作,直至堆的元素只剩下一个(剩下的这一个一定是整个序列的最小值),此时完成排序。

void HeapDown(int a[],int p,int n){   //堆的调整函数,如果不符就将其调整为大根堆
	int tmp=a[p];
	int c=p*2+1;       //获取左孩子下标(如果数组下标从1开始则为c=2*p) 
	while(c<n){
		if(c+1<n&&a[c+1]>a[c])
			c++;       //如果右孩子大,则就用右孩子比较
		if(tmp>=a[c])  //不需要调整
		    break;
		a[p]=a[c];     //进行调整,同时再看孩子节点是否要调
		p=c;
		c=2*p+1;
	}
	a[p]=tmp;          //将不合适的堆顶元素调到合适的位置
}
void HeapSort(int a[],int n){
	for(int i=n/2;i>=0;i--){     //自下而上初始化构建大根堆
		HeapDown(a,i,n);
	}
	for(int i=n-1;i>=1;i--){     //操作n-1次,每次操作都要重新调整一下堆
		int tmp=a[i];
		a[i]=a[0];
		a[0]=tmp;
		HeapDown(a,0,i);
	}
}

建堆的时间复杂度为O(N),N-1次取顶元素调整为O(N*logN)

故堆排序的最好最坏的时间复杂度都是O(NlogN),是一种不稳定的排序算法。 

特点是不额外使用辅助空间,适合排列相对有序的序列。

当想得到一个序列中第k个最小的元素之前的部分排序序列,最好采用堆排序。

 

归并排序

1.递归的归并排序

基本思想:将一个无序序列不断二分,直至二分到每一部分只有一个元素时,将这两个部分通过合并函数进行合并为一个有序的序列。随着递归的不断执行,最后一次合并就是将原序列左右两部分有序的序列合并为一个,得到有序序列。

将序列分为小数列需要logN次,每次都需要进行一次合并操作为O(N),故归并排序的时间复杂度为O(NlogN),在最好最坏的情况下均是如此。并且注意到归并排序的特点是每次都在相邻的数据之间进行操作。因此从序列上直观的看就是每次迭代使得在某一步长下的相邻元素有序。

数组下标从1开始:

void merge(int a[], int first, int last, int tmp[])  
{  
    int mid=first+(last-first)/2;
    int i=first,m=mid; //借助辅助空间tmp进行合并,在这里统一使用一个辅助空间,如果是每次调用都要现new一个临时空间的话会比较费时
    int j=mid+1,n =last;  
    int k = 1;  
    while (i <= m && j <= n)  
    {  
        if (a[i] <= a[j])  
            tmp[k++] = a[i++];  
        else  
            tmp[k++] = a[j++];  
    }  
    while (i <= m)  
        tmp[k++] = a[i++];  
    while (j <= n)  
        tmp[k++] = a[j++];  
    for (i = 1; i < k; i++)        //将合并好的序列传回a中 
        a[first + i-1] = tmp[i];  
}
void MergeSort(int a[],int first,int last,int tmp[]){  //first、last是首尾元素的位置
	if(first<last){
		int mid=(first+last)/2;
		MergeSort(a,first,mid,tmp);    //二分递归 
		MergeSort(a,mid+1,last,tmp);
		merge(a,first,last,tmp);   //最后将两部分有序的合并为一个有序序列 
	}
}

2.非递归的归并排序

基本思想是将相邻的元素通过合并函数合并为一个有序序列,从相邻步长为1开始,步长以2*k逐步增加1、2、4、8......直至最后步长增至序列长度的一半,那么整个序列可看作被划分为两个相邻的“元素”,对其合并得到有序序列。

void merge(int a[], int first, int mid, int last, int tmp[])  
{  
    int i=first,m=mid;             //合并函数与递归相同,都是借助辅助空间tmp进行合并 
    int j=mid+1,n =last;  
    int k = 1;  
    while (i <= m && j <= n)  
    {  
        if (a[i] <= a[j])  
            tmp[k++] = a[i++];  
        else  
            tmp[k++] = a[j++];  
    }  
    while (i <= m)  
        tmp[k++] = a[i++];  
    while (j <= n)  
        tmp[k++] = a[j++];  
    for (i = 1; i < k; i++)        
        a[first+i-1] = tmp[i];  
}
void MergeSort_interate(int a[],int n){                 
	int tmp[100005];
	for(int k=1;k<n;k=2*k){    //步长k以2倍增加
		int i=1;
		while(i<=n-2*k+1){
			merge(a,i,i+k-1,i+2*k-1,tmp);  //依据步长划分的相邻序列合并
			i+=2*k;
		}
		if(i<n-k+1){    //当长度为奇数时,需要考虑最后单着的那个元素,将其和之前的序列合并一次
			merge(a,i,i+k-1,n,tmp);
		}
	}
}

在N值很大的情况下,使用归并排序比堆排序要快一些,但是归并排序的辅助空间是最大的。 

 

桶排序

基本思想:假设待排序的数组a中共有N个整数,并且已知数组a中数据的范围[0, MAX)。在桶排序时,创建容量为MAX的桶数组r,并将桶数组元素都初始化为0;将容量为MAX的桶数组中的每一个单元都看作一个"桶"。在排序时,逐个遍历数组a,将数组a的值,作为"桶数组"的下标。当a中数据被读取时,就将桶的值加1。牺牲空间换取时间。

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void bucketSort(int a[],int n,int max){
	int bucket[max];
	memset(bucket,0,max*sizeof(int));    
	for(int i=0;i<n;i++)
		bucket[a[i]]++;
	for(int i=0,j=0;i<max;i++){
		while(bucket[i]>0){
			a[j++]=i;
			bucket[i]--;
		}
	}
} 
int main(){
	int a[10001];
	int n;scanf("%d",&n);
	int max=-1;
	for(int i=0;i<n;i++){
		scanf("%d",&a[i]);
		if(a[i]>max)
		  max=a[i];
	}
	bucketSort(a,n,max+1);   //注意开的空间数目要比最大值大1
	for(int i=0;i<n;i++)
		printf("%d ",a[i]);
		return 0;
}

基数排序

基本思想:RadixSort是桶排序的扩展。将整数按位数切割成不同的数字,然后按每个位数分别比较(这是一种多关键字排序的思想)。从最低位开始依次进行排序,这样从低到高按位数排序完成后,数列就变为一个有序数列。

因此选取关键字排序的次序也有不同的方法,如果从最高位开始排则为最高位优先(MSD),如果从最低为开始则为最低位优先(LSD),是一种稳定的排序算法。其时间复杂度为:O(d(n+k)),n是待排序序列的规模,d是待排序列最大的位数,k是每位数的范围,这也是一种空间换时间的算法。因此,它适用于n值很大但是关键字较小的排序。

 

总结表 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值