[数据结构]各种排序算法的java实现

一.算法分类

第一类 原始

最简单又是最复杂:插入排序,冒泡排序,选择排序,思路最简单,但是时间复杂度是最大的o(n2),空间复杂度倒是还好,是O(1)

第二类 进阶

借助了空间或者数据结构,降低了时间复杂度的算法:快速排序,归并排序,堆排序,希尔排序,时间复杂度都是O(nlogn),空间复杂度不一定

第三类 特殊

针对于特殊的数据使用的;基数排序和桶排序

二.Java实现

2.1.1原始-直接插入排序

思路:

将第i个元素插入0-i-1个元素中的合适位置,并将该位置以后的每一个元素都向后挪动一个位置,使得前i个元素是有序的。用这种方式使元素从第2个开始到第n个都变成有序的
代码:

public void insertSort(int[] a ){
	int n= a.length();
	int k = 0;
	int temp;
	for(int i =1;i<n;i++){//从第2个元素a[1]开始到第n个元素a[n-1]
		for(int j = 0;j<i;j++){//在前i-1个元素中找位置
			if(a[j]>a[i])
			{//比较当前元素a[i]与前面的元素的大小
				k=j;//记录下第一个比a[i]大的下标,这是a[i]要插入的位置
				temp = a[i];//记录原来的a[i]
				//将a[k]后面的元素都后移一格
				for(k=i;k>j;k--)
				{
					a[k]=a[k-1];
				}
				a[j]=temp;//该放的位置放上原来的a[i]
				break;//退出此次循环,前i个元素已经有序
			}
		}
	}
}

时间复杂度:两层循环,必然是O(n2),第一层循环代表进行n次插入排序,第二次循环代表每插入排序需要进行移动的次数
空间复杂度: 没用到啥,就是O(1)
稳定性分析:不稳定,比如两个值都是k,第二个的k会插到第一个k的前面

2.1.2 原始-冒泡排序

思路:

通过左右元素比较的方式,将大的元素放到右边,慢慢的大的元素就会浮到右边,第一轮将第一大的元素放到最右边,第二轮将第二大的元素放到右边倒数第二个…这样一共进行n-1轮之后就会使前n-1大的元素都就位了,n个元素自然就都有序了
代码:

public void bubbleSort(int[] a ){
	int n = a.length();
	int temp;
	for(int i =0;i<n-1;i++)
	{//进行n-1趟冒泡
		//第i趟的时候,后面的i个元素位置都定好了,只需要对前面的n-i个元素冒泡
		for(int j = 0;j<n-i;j++)
		{
			if(a[j]>a[j+1])//与右边的元素比大小,如果当前的元素比右边的大,就交换
			{
				temp = a[j];
				a[j] = a[j+1];
				a[j=1] = temp;
			}
		}
	}
}

时间复杂度: 两个for循环,立即推是O(n2
空间复杂度:没用到啥,就是O(1)
稳定性分析:绝对稳定,每一轮都有一个元素的位置定得死死的

2.1.3 原始-选择排序

思路:

第一轮选择最小的元素,与第一个元素交换,第一轮选择最小的元素,与第一个元素交换,这样一共进行n-1轮之后就会使前n-1小的元素都就位了,n个元素自然就都有序了(也可以先选大的元素和右边的交换)
代码:

public void chooseSort(int[] a ){
	int n = a.length();
	int temp ;
	int min;
	int minIndex ;
	//进行n-1次选择与交换,也可以理解为为a[0]-a[n-1]的元素找位置
	for(int i =0;i<n-1;i++)
	{
		min = 256;
		minIndex = i;
		//位置i,放的是位置i到位置n里面最小的元素(a[i]--a[n]中最小的)
		for(int j = i;j<n;j++)
		{	
			if(a[j]<min)//找更小的,一直比较,找到头就能找到最小的
			{
				//找到了更小的就记录
				min= a[j];
				minIndex = j;
			}
		}
		//找到了最小的元素之后,将a[i]和a[minIndex]交换
		if(i!=minIndex){//下标不相同才交换,否则就自己换自己了
			temp = a[i];
			a[i] = min;
			a[minIndex] = temp;
		}
		
	}
}

时间复杂度: 两个for循环,立即推是O(n2
空间复杂度:也没用到啥,就是O(1)
稳定性分析:先举例子再说答案,比如两个值都是k,如果第二个的k选位置的时候,第一个k位置早就选好了,那就稳了(比如 1,7,2,8,2); 但是如果在第一个k选位置之前,被最小的数替换到了第二个k的后面就再也不动了,这样就不稳(比如 2,2,1,3),结论是不稳的

2.2.1 进阶-快速排序

思路:

随机选一个元素为参考元素(简单起见可以是第一个),然后把所有小于它的元素放到它的左边 ,大于它的元素放到他的右边,,这个移动可以通过交换的方式进行,然后再递归,分别对两边的元素快速排序。
代码:

public void fastSort(int[] a,int h,int t){//h,t分别是头,尾元素的下标
	int m = a[h];//取第一个元素为参考元素,m是参考元素的值
	int i = h;int j = t;//设置i,j两个指针
	int temp;
	int mid ;//一轮之后参考元素的位置
	while(i<j)//指针不重合之前
	{
		//每一轮都有一个指针指向的元素等于m,所以只有一个指针在走
		while(a[j]>m)//a[j]比参考元素大,把它放在右边不管,继续向左找下去
		{
			j--;
		}
		a[j]=m;a[i]=a[j]
		while(a[i]>m)//a[i]比参考元素小则把它放在左边不管,继续向右找下去
		{
			i++;
		}
	
		//都定下来之后交换
		temp = a[i];
		a[i] = a[j];
		a[j] = temp;
		
	}
	//结束之后,i,j相等,得到min
	min = i;
	//递归对两边使用
	fastSort(a,h,min)
	fastSort(a,min+1,t)
	
	
	
	
}
public void useFastSort(int[]a)
{
	fastSort(a,0,a.length-1);

}

这是我写的第一个思路,后来发现是有破绽的,当遇到一个与m相等的元素时,i,j都不走了,出现死锁。
所以来看第二种实现(只是while循环不同):

while(i<j)//指针不重合之前
	{
		while(a[j]>=m)//a[j]比参考元素大或者相等,把它放在右边不管,继续向左找下去
		{
			j--;
		}
		a[i]=a[j];//找到之后把a[i]替换成a[j],现在j位置上的元素已经在i那里有一份了,j位置可以看成没有用的了,接下来可以装别的元素,而第一轮的a[i]是m,是已经被记录下来的,也可以看做没有用了,就装其他的元素
		i++;
		while(a[i]>=m)//a[i]比参考元素小则把它放在左边不管,继续向右找下去
		{
			i++;
		}
		a[j]=a[i];
		j--;
	}
	//一轮结束了之后,i=j,这个位置上的元素自然也是重复过的,但少了参考元素,现在把参考元素放进来
	a[i]=m;

时间复杂度: 每一轮的时间复杂度是0(n),要进行几轮就看有几次递归。要进行几轮递归呢?我们可以换个角度看:如果把这个数组看成是树,递归是对左右子数的递归的话,这棵树的深度就是递归的次数,深度最小的情况是这棵树是完全二叉树,深度是log2n,深度最大的情况是这棵树每层只有一个节点(所有有子数的节点只有右节点或只有左节点,一字排开),所以时间复杂度综合来看是O(nlogn);
空间复杂度:递归用到了栈,深度就是上面分析的树的高度,就是O(logn)
稳定性分析:肯定不稳定,一轮下来都稳定不了,后面的小的元素很容易就可以到前面的小的元素的左边,比如3,4,1,2,1,一轮之后1,2,1, 3,4

2.2.2 进阶-归并排序

思路:

一组一组地排,先一组两个(可能有落单的),组内排好序,然后在两个相邻的组合并变成一组四个元素(有一组可能不够四个),组内排好序,然后再合并…这样下去直到最大的组是整个数组,这个其实也是通过递归来做的,通过由大变小最后处理小模块的递归,有两个过程,一个是合并后排序,还有一个过程是递归过程。
代码:

public void mergeSort(int[] a,int h,int t)
{
	if(h<t){//先分别递归排好序再合并(如果只有两个元素,到后面会直接执行merge())
	int mid = h+(t-h)/2;
	mergeSort(a,h,mid);
	mergeSort(a,mid+1,t);
	merge(a,h,mid,t);//合并
	}

}
public void merge(int[] a,int low,int mid,int high)//合并加排序
{	
	//一个临时数组来存
	int [] temp = new int[high-low+1];
	//两个指针
	int p1 = low;
	int p2 =mid+1;
	int k = 0;//temp数组的下标
	while(p1<=mid&&p2<=high)
	{
		if(a[p1]>=a[p2])
		{
			temp[k++] = a[p2++]//小的进来
		}else
		{
			temp[k++] = a[p1++]//小的进来
		}
	}
	//把提前结束的半边剩下的有序部分接在后面
	while(p1<min)
	{
		temp[k++] = a[p1++];
	}
	while(p2<high)
	{
		temp[k++] = a[p2++];
	}
	//最后把temp数组的东西装回去,注意a是从下标为low的地方开始装
	for(int i = 0;i<temp.length,i++)
	{
		a[low+i] = temp[i];
	}
}

时间复杂度: 合并的过程是O(n),递归的过程是O(logn)
空间复杂度:递归过程用了栈,复杂度是O(logn)
稳定性分析:稳定的,按上面的写法,元素相等时,总是先取前面组的元素放入temp,这样可以实现相邻两组的相等的元素的相对位置不变。在这样的排序是稳定的。

2.2.3进阶-堆排序

思路:

利用大根堆的思想,不断地取出根元素,递归到最后可以实现有序。具体做法是:
step1建堆: 先建立一个元素个数为n的大根堆,此时堆顶元素是最大的,
step2交换: 把堆顶与堆尾元素交换,那么此时获得了最大的元素,放在了最后,而前n-1个元素组成的堆又不是大根堆了,要进行调整
step3调整: 把这n-1个元素的堆调整成大根堆,此时堆顶是这n-1个元素里面最大的,同时也是所有元素里第二大的,再次把这个第二大与堆尾元素(倒数第二个)交换,此时最后两个元素都找到了自己的位置
重复以上步骤,直到堆的元素只剩下1个。
需要的函数是:一个包括了所有步骤的元素,一个建立堆的元素,一个调整的元素,还有三个寻找父节点,子节点的计算函数;

代码:

	// 堆排序,总指挥部,调用其他函数完成了所有的步骤
	public void heapSort(double[] a) {
		heapSize = a.length;
		// 建立大根堆
		buildMaxHeap(a);
		//交换,缩小无序区,调整
		for (int i = a.length - 1; i > 0; i--) {
			double temp = a[i];
			a[i] = a[0];
			a[0] = temp;
			heapSize--;//缩小无序区
			maxHeapity(0);//通过调整的方式获得大根堆而不是调用buildMaxHeap()
		}

	}
	
}

先写找父子节点的辅助函数:

// 找出函数的的根节点,左孩子,右孩子
	protected int parent(int i) {
		return (i - 1) / 2;
	}

	protected int left(int i) {
		return 2 * i + 1;
	}

	protected int right(int i) {
		return 2 * i + 2;
	}
}

建立大根堆:

	// 建立大根堆
	public void buildMaxHeap(double[] a) {
		heapSize = a.length;
		for (int i = parent(heapSize - 1); i >= 0; i--) {
			maxHeapity(i);// 从下而上的调整
		}
	}

调整,调整自己与孩子的

	// 进行调整
	public void maxHeapity(int i) {
		int l=left(i);
		int r=right(i);
		int largest=i;
		if(l<=heapSize-1&&a[l]>a[i]) {//有左孩子且比根节点大
			largest=l;
			
		}
		if(r<=heapSize-1&&a[r]>a[largest]) {//有右孩子来挑战最大元素
			largest=r;
		}
		if(largest!=i) {//如果最大的是孩子不是自己,则需要调整这个结构并对调整后的大孩子进行调整
			double temp=a[i];
			a[i]=a[largest];
			a[largest]=temp;
			maxHeapity(largest);//对调整后的大孩子进行调整
		}
	}

时间复杂度: 调整n次,每一次调整的次数是树的高度logn,所有时间复杂度是O(nlogn)
空间复杂度:调整的时候用到了递归,利用了栈,递归次数是栈的高度,所以是O(logn)
稳定性分析:不稳定,最简单的例子:1,5,5,第一轮之后:1,5,5,右孩子与根相等时不调整,那么堆顶先输出的被放在了后面,右孩子与根的相对位置就乱了。

2.2.4 进阶-希尔排序

思路:

可以理解成步长更大的插入排序,这与直接插入排序比优势在于,直接插入排序的步长是1,每一次需要移动的元素都很多,总的次数大,而步长越大,每次移动的距离越大,次数越少,尽管希尔排序的步长会慢慢减小,但数据越来越趋于基本有序,后面需要移动的情况就会减少很多,数据量越多,希尔排序的优势就越明显。

这个过程中,步长也很重要,会直接影响效率。那么步长怎么定呢?研究表明,步长h值最初等于1,然后用公式h=3*h+1,得到1,4,13,40,121,364…这个魔法序列能让效率到达最高。但是这样不好写惹。

在希尔的原稿中,他建议初始的间距为N/2,简单地把每一趟排序分成了两半是步长的最好选择(引用来自https://www.cnblogs.com/jsgnadsj/p/3458054.html)

那我们就定得简单粗暴一点,一半一半地选吧

代码:

public void shellSort(int[] a){
	int length = a.length;
	for(int gap = length/2;gap>0;gap/=2)//可以看成一共有gap组
	{
		for(int i = gap;i<length;i++)//取小组内的第二个元素a[i]
		{	
			int temp =a[i];//记录这个元素
			//从后往前找,没办法从前往后,因为不知道c偏移量
			int k= i-gap
			//由于是从后往前,所以只能边比较边移动,不能找到了位置再移动
			while(k>=0&&a[k]>temp)
			{	//比它大的元素都要移动
				a[k+gap] = a[k];
				k-= gap;
			}
			//找到下一个比a[i]小的元素a[k]就停下
			//而a[k+gap]是一个已经移动过的值,也是temp应该放入的位置
			a[k+gap] = temp;
			
			
		}
	}
	
}

时间复杂度: 执行次数依赖于增量序列,上面的这个方法一共进行了logn次直接插入排序,每次插入排序的时间复杂度来源于

for(int i = gap;i<length;i++)

所以插入排序的时间复杂度是O(n),所以希尔排序的复杂度是O(nlogn)。
空间复杂度:也没用到啥,就是O(1)
稳定性分析:不稳定,直接插入都不稳定这个更不可能稳定了。

2.3 特殊-桶式基数排序

思路:

每个值都建立一个桶,根据数值将不同的数放入桶中然后依次倒出,一般来说是建立10个桶,分别是0-9,然后先对个位数进行排序,然后对十位数进行排序…慢慢升高。有多少位数就进行多少次桶排,用一个bucket数组来记录每个桶里面有多少数值,最后倒出的时候可以知道桶里面的元素在哪个位置
radix是基数,也是桶的个数,最常见的是基数是10,代表0-9,d是数组里面的最高位数字,最高位是几,就进行几轮桶排,比如最大的数是999,是三位数,就进行三轮排序
代码:

public  void radixSort(int[] data, int radix, int d) {
		// 缓存数组
		int[] tmp = new int[data.length];
		// buckets用于记录待排序元素的信息
		// buckets数组定义了max-min个桶
		int[] buckets = new int[radix];
 
		for (int i = 0, rate = 1; i < d; i++) {
			// 重置count数组,开始统计下一个关键字
			Arrays.fill(buckets, 0);
			// 将data中的元素完全复制到tmp数组中
			System.arraycopy(data, 0, tmp, 0, data.length);
			// 计算每个待排序数据的子关键字
			for (int j = 0; j < data.length; j++) {
				int subKey = (tmp[j] / rate) % radix;
				buckets[subKey]++;
			}
			//记录这个桶在数组的第几位结束,这样我们可以知道第j个桶在数组中下标是
			//从bucket[j-1]到bucket[j]+buckets[j - 1];
			for (int j = 1; j < radix; j++) {
			//从1开始,因为第一个桶的元素在数组中
			//是从0到bucket[1];
				buckets[j] = buckets[j] + buckets[j - 1];
			}
			// 按子关键字对指定的数据进行排序
			//从后往前放,先放大的再放小的,因为这样就比较好放了
			for (int m = data.length - 1; m >= 0; m--) {
				int subKey = (tmp[m] / rate) % radix;
				data[--buckets[subKey]] = tmp[m];
			}
			rate *= radix;//这个是模,可以实现第一次处理个位,第二次处理十位
		}
 
	}

时间复杂度: 执行d*(n+radix+n)次,所以时间复杂度是O(n)
空间复杂度:用到桶了,就是O(M),M是桶的个数
稳定性分析:稳定的,都是从头开始,如果遇到相等的值也不会打乱次序

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值