排序算法总结

排序算法总结


一.交换排序

1.冒泡排序
原理: 比较相邻的元素,如果前者比后者大,就交换他们两个,对从前往后对每一对相邻元素作同样的工作,这样一趟冒泡后,最后的元素就是最大的数。这样每一趟冒泡确定一个元素的位置,n趟后数组即有序。同样也可以从后往前冒泡,依次确定最小值。
原始版的冒泡(如果出现全部数已经排序好,但循环还没结束会出现不必要的时间浪费)

public static void bubble(int []a)
	{
		if(a.length==0) return ;
		int t=a.length;
		for(int i=0;i<t-1;i++) //在每一趟中确定最大的数,每一趟(如果有数的位置发生改变)都能最终确定一个数的位置
		{
			for(int j=0;j<t-i-1;j++) //逐一遍历整个数组,除了已经确定好位置的数
			{
				if(a[j]>a[j+1])  //石沉法,前面的数大于后面的数就交换两个数的位置
				{
					int temp=a[j];
					a[j]=a[j+1];
					a[j+1]=temp;
				}
			}
		}
	}

改进版的冒泡法:(标志法)

public static void bubble(int []a)
	{
		if(a.length==0) return ;
		int t=a.length;
		boolean flag;
		for(int i=0;i<t-1;i++) //在每一趟中确定最大的数,每一趟(如果有数的位置发生改变)都能最终确定一个数的位置
		{
			flag=false;
			for(int j=0;j<t-i-1;j++) //逐一遍历整个数组,除了已经确定好位置的数
			{
				if(a[j]>a[j+1])  //石沉法,前面的数大于后面的数就交换两个数的位置
				{
					flag=true;
					int temp=a[j];
					a[j]=a[j+1];
					a[j+1]=temp;
				}
			}
		   if(!flag)  break; //这一趟中的数没有发生位置交换说明所有的数已经排序成功了.
		}
	}

特征分析:

1、空间复杂度O(1),最好时间复杂度O(n),最坏时间复杂度O(n2),平均时间复杂度O(n2)。
2、冒泡排序是稳定的(因为交换的数都是相邻的):同一类型的数相对位置不会改变.

应用:
1.利用冒泡的每一趟排序都能确定一个数的最终位置,可以适合在规模巨大的数据中找到前n大(小)的数,即进行n次外循环即可.
2.利用冒泡的稳定性,可以在对一组数据进行操作时不改变同一类型数据的相对位置. 比如在一个数组中把所有奇数放在偶数前且不改变相对位置关系.

2.快速排序
原理: 快速排序原理是首先要找到一个中枢,把小于中枢的值放到他前面,大于中枢的值放到他的右边,然后再以此方法对这两部分数据分别进行快速排序,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

基本思路:

快排算法是基于分治策略的排序算法,其基本思想是,对于输入的数组 a[low, high],按以下三个步骤进行排序。

(1)分解:以 a[p] 为基准将 a[low: high] 划分为三段 a[low: p-1],a[p] 和 a[p+1: high],使得 a[low: p-1] 中任何一个元素小于等于 a[p], 而 a[p+1: high] 中任何一个元素大于等于 a[p]。

(2)递归求解:通过递归调用快速排序算法分别对 a[low: p-1] 和 a[p+1: high] 进行排序。

(3)合并:由于对 a[low: p-1] 和 a[p+1: high] 的排序是就地进行的,所以在 a[low: p-1] 和 a[p+1: high] 都已排好序后,不需要执行任何计算,a[low: high] 就已经排好序了。

public static void quicksort(int []a,int left,int right) //固定基准,以中间数为例
	{
		if(a.length==0||left>=right) return ;
		int mid=a[(left+right)/2];
		int i=left;
		int j=right;
		while(i<=j)
		{
			while(i<=j&&a[i]<mid) i++;
			while(i<=j&&a[j]>mid) j--;
			if(i<=j)
			{
				int temp=a[i];
				a[i]=a[j];
				a[j]=temp;
				i++;
				j--;
			}
		} //跳出循环后,i的左边是小于mid的数,j的右边是大于mid的数
		quicksort(a,left,j);
		quicksort(a,i,right);
	}
public static int partion(int []a,int left,int right) //挖坑法
	{
		if(a.length==0||left>=right) return -1;
		int i=left;
		int j=right;
		int pet=a[left];
		while(i<j)
		{
			while(i<j&&a[j]>pet) j--;
			if(j<=i) break;
			a[i]=a[j];
			while(i<j&&a[i]<=pet) i++;  //跳出循环的时候,i的左边的数都<=pet
			if(j<=i) break; //越界,所有的数都搜索完
			a[j]=a[i];
		}  //当i==j的时候,就是基数的位置
		a[i]=pet;
		return i;
	}
	public static void quicksort_(int []a,int left,int right) //挖坑法
	{
		if(a.length==0||left>=right) return ;
		int pos= partion(a,left,right);
		quicksort_(a,left,pos-1);
		quicksort_(a,pos+1,right);
	}

这个做法有点类似双指针思想,每一趟都划分出两个不同范围,再进行进一步的排序,体现了分治的思想
应用: 获取第k小的数

public static int partion(int []a,int left,int right) //挖坑法,进行一轮划分,每一轮确定一个数的最终位置
	{
		if(a.length==0||left>=right) return -1;
		int i=left;
		int j=right;
		int pet=a[left]; //选定最左边的数为基数
		while(i<j)
		{
			while(i<j&&a[j]>pet) j--; //跳出循环的时候,j的右边都是大于基数的数
			if(j<=i) break;
			a[i]=a[j];
			while(i<j&&a[i]<=pet) i++;  //跳出循环的时候,i的左边的数都<=pet
			if(j<=i) break; //越界,所有的数都搜索完
			a[j]=a[i];
		}  //当i==j的时候,就是基数的位置
		a[i]=pet;
		return i;
	}
	public static int quicksort_(int []a,int left,int right,int k) //挖坑法,获取第k小的数
	{
		if(left==right&&k==1) return a[left];//递归结束条件:数组只有一个数了,k=1的情况
		int pos= partion(a,left,right);
		int tem=pos-left+1;//小于或等于每一趟选定的基数的数的数量
		if(k<=tem) return quicksort_(a,left,pos,k);
		else return quicksort_(a,pos+1,right,k-tem);
	}

荷兰国旗问题(划分过程思想比较像)

public static int[] sortThreeColor(int[] A, int n) {
        int i0 = -1;//指向0区域的指针,0区域初始大小为0
        int i2 = n;//指向2区域的指针,2区域初始大小为0
        int temp;
        for(int i = 0; i < i2; i++){//注意!!!,这里是i<i2,即小于2区域的位置处
            if(A[i] == 1)
                continue;
            if(A[i] == 0){
                //交换0区域的后一位和i指向的元素
                temp = A[i];
                A[i] = A[i0 + 1];
                A[i0 + 1] = temp;
                //0区域向后扩大一位
                i0++;
            }else if(A[i] == 2){
                //交换2区域的前一位和i指向的元素
                temp = A[i];
                A[i] = A[i2 - 1];
                A[i2 - 1] = temp;
                //2区域向前扩大一位
                i2--;
                //注意!!!由于2区域前面的数据元素是没有经过搜索的,因此当把它交换过来的时候,指针i应该停留在原处,这里先-1在for中+1,相当于i没有变化
                i--;
            }
        }
        return A;
    }

特征分析:
(1) 时间复杂度最好为O(nlog2n),最差为n^2(退化成单支树,根本上是因为基数的选择问题) 空间复杂度为O(log2n)
(2)该排序算法不稳定,不能保证排序后同一类型的数相对位置不会改变.
(3) 该算法在数据规模巨大,完全无序的情况下的效率最高.在处理小规模数据时的表现不好.这个时候可以改用插入排序。
(4) 快速排序采用了一种分治的策略,通常称其为分治法,现在各种语言中自带的排序库很多使用的都是快速排序。这种分治可以应用于各种不同情景,比如获取第k小的数.

改进方法:(针对枢轴的选择)
1、选取随机数作为枢轴。
2、使用左端,右端和中心的中值做为枢轴元。
3、每次选取数据集中的中位数做枢轴。

插入排序

1.直接插入排序
基本思想:每步将一个待排序的纪录,按其关键字的大小插入到已经排好序的有序数据中,直到全部插入完为止。

public static void insertsort(int []a)
    {
    	if(a.length==0) return ;
    	for(int i=0;i<a.length;i++) //每趟将a[i]插入到前面的排序子序列中
    	{
    		int temp=a[i];
    		int j=i-1;
    		while(j>=0&&temp<a[j])  //如果前面的数比a[i]大则交换位置
    		{
    			a[j+1]=a[j];
    			j--;
    		}
    		a[j+1]=temp;
    	}
    }

2.折半插入排序
在直接插入排序的基础上,如果数据量比较大,为了减少关键码的比较次数,可以使用折半插入来寻找要插入的位置。但是仍然不改变此算法的时间复杂度O(n^2),空间复杂度为O(1). 该算法具有稳定性

public static int Binary(int []a,int index,int x) //在数组中找到大于x的第一个数,即插入的位置
	{
		 int left=0;
		 int right=index;
		 while(left<=right)
		 {
			 int mid=(left+right)/2;
			 if(a[mid]>x)
			 {
				 right=mid-1;
			 }
			 else left=mid+1;
		 }
		 return left;
	}
    public static void insertsort(int []a)
    {
    	if(a.length==0) return ;
    	for(int i=0;i<a.length;i++)
    	{
    		int temp=a[i];
    		int j=i-1;
    		if(j>=0)
    		{
    			int ans=Binary(a,j,temp);
    			//System.out.println(ans);
    			for(int k=j;k>=ans;k--) //把ans以及后面的数全都挪后一位
    			{
    				a[k+1]=a[k];
    			}
    			a[ans]=temp;  //填数
    		}
    	}
    }

插入排序适用于已有部分数据有序的情况,有序部分越大越好。

选择排序

简单选择排序

它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

public static void simpleSelectSort(int []a)
	{
		if(a.length==0) return ;
		int min;
		for(int i=0;i<a.length-1;i++)
		{
			min=i;
			for(int j=i+1;j<a.length;j++)
			{
				if(a[j]<a[min])
				{
					min=j;//记录最小值的下标
				}
			}
			if(min!=i)  //在后面的数找到最小值,就交换
			{
				int temp=a[i];
				a[i]=a[min];
				a[min]=temp;			
			}
		}
	}

特征分析:
1、空间复杂度O(1),最好/最坏/平均时间复杂度都是O(n2),比较次数O(n2),移动次数O(n)。
2、选择排序是不稳定的排序方法

归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

在这里插入图片描述

public static void Mergesort(int []a,int []b,int start,int end)
	{
		if(a.length==0) return ;
		if(start==end)
		{
			b[start]=a[start];
			return ;
		}
		int mid=(start+end)/2; //取中间点
		Mergesort(a,b,start,mid); //将数组左部分排好序
		Mergesort(a,b,mid+1,end);//将数组右部分排好序
		int i=start;
		int j=mid+1;   //双指针
		int t=start;
		while(i<=mid&&j<=end)
		{
		if(i<=mid&&a[i]<=a[j])
		{
			b[t++]=a[i++];
		}
		if(j<=end&&a[j]<=a[i])
		{
			b[t++]=a[j++];
		}
		}
		while(i<=mid) b[t++]=a[i++];
		while(j<=end) b[t++]=a[j++];
		for(int k=start;k<=end;k++)
		{
			a[k]=b[k];
		}
	}

特征分析:
1.归并排序的最好、最坏和平均的时间复杂度都为O ( n l o g n ) ,空间复杂度为O(n)
2. 因为它需要进行元素的两两比较所以不存在跳跃,所以归并排序是稳定的排序算法,但递归实现的归并排序需要的额外空间比较大,而迭代实现则相对较小,运行时间开销也较小,因此应尽量使用迭代方法。
3.该算法体现了分治思想,但和快速排序有所不同.该算法可以类比树的后序遍历.

应用场景:
求一个数组中逆序对的个数.
为什么逆序对要用归并排序呢?
该算法是从底向上计算的,每次归并的前提是两组数组都是有序的,在合并前就计算好两个数组分别初始的逆序对,再进行合并.在两部分有序的数组里找逆序对其实有点贪心思维在里面.本质上也是分治(大问题分解成一个个小问题)

static int ans=0;
	public static void Mergesort(int []a,int []b,int start,int end)
	{
		if(a.length==0) return ;
		if(start==end)
		{
			b[start]=a[start];
			return ;
		}
		int mid=(start+end)/2; //取中间点
		Mergesort(a,b,start,mid); //将数组左部分排好序
		Mergesort(a,b,mid+1,end);//将数组右部分排好序
		int i=start;
		int j=mid+1;   //双指针
		int t=start;
		while(i<=mid&&j<=end) //左右指针各自移动一次算一趟
		{
		 if(a[i]<=a[j])//左指针的移动,'='是因为两个相同的数不算逆序
		{
			b[t++]=a[i++];
		}
		 else if(a[j]<a[i]) //右指针的移动,当右指针移动时才需要记录逆序对的个数
		{
			b[t++]=a[j++];
			 ans+=(mid-i+1); //数组左部分大于a[j]的数的个数,每一个右指针的移动都要计算
		}
		
		}
		while(i<=mid) b[t++]=a[i++];
		while(j<=end) b[t++]=a[j++];
		for(int k=start;k<=end;k++)
		{
			a[k]=b[k];
		}

堆排序

堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序
堆的定义性质:

大根堆:arr(i)>arr(2i+1) && arr(i)>arr(2i+2)

小根堆:arr(i)<arr(2i+1) && arr(i)<arr(2i+2)

基本思想:

1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,每一趟都能确定一个数(在n-1中最大的)的位置,如此反复执行,便能得到有序数组

在这里插入图片描述

public static void headsort(int []a)
	{
		if(a.length==0) return ;
		for(int t=a.length/2-1;t>=0;t--) //从第一个非叶子节点开始调整
		{
			adjust(a,t,a.length);
		}
		for(int t=a.length-1;t>0;t--) //每一趟都确定最大的数,放在数组的末尾
		{
			swap(a,0,t); 
			adjust(a,0,t);
		}
	}
	public static void adjust(int []a,int i,int length)//在[i,length)内调整为大根堆
	{
		int t=length;
		int ans=a[i];
		int j=i; //j表示a[i]应该插入的位置
		for(int k=2*i+1;k<t;k=2*k+1) //遍历左孩子
		{
			if(k+1<t&&a[k]<a[k+1]) //确定左右孩子最大的点
			{
				k++;
			}
			if(a[k]>ans)
			{
				a[j]=a[k];
				j=k;
			}
			else break;  //找到最终a[i]应该插入的位置
		}
		a[j]=ans;
	}
	public static void swap(int []a,int i,int j)
	{
		int temp=a[i];
		a[i]=a[j];
		a[j]=temp;
	}

特征分析:

1、空间复杂度O(1),平均时间复杂度O(n log n),依次堆调整O(log n)——即堆的高度。由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数量较少的情况。

2、堆排序时不稳定的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值