十大排序算法总结 内部排序

版权声明:此文章为许诗宇所写,如需转载,请写下转载文章的地址 https://blog.csdn.net/xushiyu1996818/article/details/84762032

目录

排序测试模板

一、冒泡排序

总体思想

编程思想

复杂度及优缺点

优化

二、选择排序

总体思想

​编程思想

复杂度及优缺点

三、插入排序

总体思想

编程思想

复杂度及优缺点

四、希尔排序

总体思想

编程思想

复杂度及优缺点

五、归并排序

总体思想

编程思想

复杂度及优缺点

六、快速排序

总体思想

编程思想

复杂度及优缺点

七、堆排序

总体思想

编程思想

复杂度及优缺点

八、计数排序

总体思想

编程思想

复杂度及优缺点

九、桶排序

总体思想

编程思想

复杂度及优缺点

十、基数排序

总体思想

编程思想

复杂度及优缺点

总体复杂度



排序测试模板

基本上所有算法都是根据这个模板测试的,只需修改createRandomIntArray的参数和

                long begin = System.currentTimeMillis();
		SelectSort.selectSort(ito);//执行程序
		long end = System.currentTimeMillis();	

这段中执行的方法即可

package algorithm.sort.selectSort;
import algorithm.sort.selectSort.SelectSort;




public class Main {
	
	public static void main(String[] args) {
		//int[][] testTable = {{1,2,3,0},{1,2,3,4},{1,2567678,3,4,6767,45,12,345,3435,34,66666},
		//{1,1,1,3,3,4,3,2,4,2},{1,6,2,7,1}};
		int[][] testTable={createRandomIntArray(-100,100,50),
				createRandomIntArray(-100,100,400),createRandomIntArray(-1000,1000,500)};
			
		for (int[] ito : testTable) {
			test(ito);
		}
	}
		 
	private static void test(int[] ito) {
		boolean rtn;
		
		for (int i = 0; i < ito.length; i++) {
		    System.out.print(ito[i]+" ");		    
		}
		System.out.println();
		System.out.println("length="+ito.length);
		//开始时打印数组
		long begin = System.currentTimeMillis();
		SelectSort.selectSort(ito);//执行程序
		long end = System.currentTimeMillis();	
		
		//System.out.println(ito + ": rtn=" +rtn);
		for (int i = 0; i < ito.length; i++) {
		    System.out.print(ito[i]+" ");
		}//打印结果几数组
		
		System.out.println();
		System.out.println("耗时:" + (end - begin) + "ms");
		System.out.println("-------------------");
		System.out.println("-------------------");
	}
	public static int[] createRandomIntArray(int min,int max,int length){
		int[] result=new int[length];
		for(int i=0;i<length;i++){
			double rand=Math.random();
			result[i]=(int)(min+(max-min)*rand);
		}
		
		return result;
	}

}

一、冒泡排序

总体思想

其大体思想就是通过与相邻元素的比较和交换来把大的数交换到最后面。这个过程类似于水泡向上升一样,因此而得名

1 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3 针对所有的元素重复以上的步骤,除了最后一个。
4 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

编程思想

从i=0到length-2进行循环a
在循环a内,从j=0到length-2-i进行循环b
在循环b内,如果j位比j+1位大,则二者互换,
循环b结束,循环b导致从0到length-2-i内最大的那个数在length-1-i位上
循环a结束。
每次最大的数在length-1-0(最后一位),length-1-1,...length-1-(length-2)=1位上

public class BubbleSort {
	public static void bubbleSort(int[] nums){
		int length=nums.length;
		if(length==0){
			return;
		}
		for(int i=0;i<length-1;i++){
			for(int j=0;j<length-1-i;j++){
				if(nums[j]>nums[j+1]){
					swap(nums, j, j+1);
				}
			}
		}
	}
	public static void swap(int[] nums,int i,int j){
		int temp=nums[i];
		nums[i]=nums[j];
		nums[j]=temp;
	}
}

复杂度及优缺点

泡排序总比较次数=(n-1)+(n-2)+(n-3)+...+3+2+1=n(n-1)/2

n(n-1)/2是O(n^2)阶的级,所以

冒泡排序速度是阶O(n^2)的算法。

最差情况是O(n^2),最好情况,普通算法是O(n^2),优化过是O(n)

空间是o(1)的算法

速度很慢,因为还要不断交换

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。

优化

设置一个变量,初始值为false,当发生一次交换就将变量设置为true,如果循环完后还是false,说明数组已经有序,结束循环。

所以最优情况下速度为o(n)

如果原始有序,那么一次扫描完一次交换也不会有,即变量=false,此时return,终止排序。此时时间复杂度是O(n).

note:原始数据只要有一对需要改顺序,复杂度就又会变成O(n^2)的。

二、选择排序

总体思想


将整个数组遍历一遍,将最小的数和首个元素互换
然后将第二个到最后的数组遍历,其中最小的和第二个互换
以此类推。

举个例子,对5,3,8,6,4这个无序序列进行简单选择排序,首先要选择5以外的最小数来和5交换,也就是选择3和5交换,一次排序后就变成了3,5,8,6,4.对剩下的序列一次进行选择和交换,最终就会得到一个有序序列。


编程思想


1 计算数组长度length
2 从i=0到length-1循环
3 每个循环内,先设置min=num[i],minIndex=i,初始化
4 再从j=i到length-1循环
5 在这个循环内,找到最小的min和对应的minIndex
6 这个循环结束,将i与minIndex位的数组元素颠倒,另第i位为从i到length-1的最小数
7 这个循环结束,排序成功

public class SelectSort {
	public static void selectSort(int[] nums){
		int length=nums.length;
		if(length==0){
			return;
		}
		int min=0;
		int minIndex=0;
		for(int i=0;i<length;i++){
			min=nums[i];
			minIndex=i;
			for(int j=i+1;j<length;j++){
				if(nums[j]<min){
					min=nums[j];
					minIndex=j;
				}
			}
			if(minIndex!=i){
				swap(nums,i,minIndex);
			}
		}
	}
	public static void swap(int[] nums,int i,int j){
		int temp=nums[i];
		nums[i]=nums[j];
		nums[j]=temp;
	}
}

复杂度及优缺点

选择排序总比较次数=(n-1)+(n-2)+(n-3)+...+3+2+1=n(n-1)/2

n(n-1)/2是O(n^2)阶的级,所以

选择排序的速度是阶O(n^2)的算法。

空间是o(1)的算法

适用于小列表排序。

其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。

选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

三、插入排序

总体思想

1 将数组划分为已排序的和未排序的,已排序的在前,初始为第一个
2 每次循环,从未排序的取出第一个,按照大小,插入已排序的数组的对应位置,比它大的依次向后移动一位

或者

1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。

2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

举个栗子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。

编程思想

1 循环a从i=1到length-1结束
2 在循环a内,now为num[i],hasINsert初始为false
3 在循环a内,开始从j=i到j>0的循环b
4 在b内,如果nums[j]>now,那么nums[j]=nums[j-1],如果不是,那么nums[j]=now,hasInsert=true
     比如已排序的 0 2 3  现在now=1,i=3,j=3
     3>1,所以 nums[3]=3(nums[2]),j=2
     2>1,所以nums[2]=2,j=1
     0<1,所以nums[1]=1,
5 循环b结束,如果hasInsert=false,说明b是通过i>0的条件退出,而不是break的,这种情况已排序的数组都比now大
  所以,nums[0]=now
6 循环a结束      

package algorithm.sort.insertSort;

import java.util.Arrays;
/* 插入排序
1 将数组划分为已排序的和未排序的,已排序的在前,初始为第一个
2 每次循环,从未排序的取出第一个,按照大小,插入已排序的数组的对应位置,比它大的依次向后移动一位

1 循环a从i=1到length-1结束
2 在循环a内,now为num[i],hasINsert初始为false
3 在循环a内,开始从j=i到j>0的循环b
4 在b内,如果nums[j]>now,那么nums[j]=nums[j-1],如果不是,那么nums[j]=now,hasInsert=true
     比如已排序的 0 2 3  现在now=1,i=3,j=3
     3>1,所以 nums[3]=3(nums[2]),j=2
     2>1,所以nums[2]=2,j=1
     0<1,所以nums[1]=1,
5 循环b结束,如果hasInsert=false,说明b是通过i>0的条件退出,而不是break的,这种情况已排序的数组都比now大
  所以,nums[0]=now
6 循环a结束       


*/
public class InsertSort {
	public static void insertSort(int[] nums){
		int length=nums.length;
		if(length<=1){
			return;
		}
		for(int i=1;i<length;i++){
			int now=nums[i];
			boolean hasInsert=false;
			for(int j=i;j>0;j--){
				if(nums[j-1]>now){
					nums[j]=nums[j-1];
				}
				else{
					nums[j]=now;
					hasInsert=true;
					break;
				}
			}
			if(!hasInsert){
				nums[0]=now;
			}
		}
		
		
	}
	
}

也可以

for (int i = 0; i < arr.length; i++) {//假设第一个元素放到了正确的位置上,这样仅需遍历1~n-1
            int j=i;
            int target=arr[i];
            while(j>0&&target<arr[j-1]){
                arr[j]=arr[j-1];
                j--;
            }
            arr[j]=target;
        }

复杂度及优缺点

简单插入排序的时间复杂度也是O(n^2)。

最佳用例效率:O(n),当列表已经被排序时,产生最佳用例。

最差用例效率:O(n^2),当列表反向顺序排列时,产生最差用例。

插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳 定的。

四、希尔排序

总体思想

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性的效率

但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位

希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

子序列的构成不是简单的逐段分割,而是将某个相隔某个增量的记录组成一个子序列。


 先建立一个递减间隔的数组,比如数组长度为100,递减的数组为50,20,10,5,2,1
 然后对逐个的间隔50到1逐个进行排序
 比如对于50,将第1和51个进行直接插入排序,2和52个等等
 然后对于20,将1,21,41,61,81,进行 直接插入排序,2,22,42,62,82等等
 数组的最后一定是1,进行最后的插入排序

编程思想

insertSort(int[] nums,int interval)
确定一个数组,根据一个interval进行排序
首先对i=0到interval-1进行循环a
循环a内,i此时为排序的分数组的第一个
如果i+interval>length-1,说明分数组只有一个,不排序,break
然后对j=i+interval到length-1进行循环b,其中j每次加interval
循环b内,now=nums[j],k=j,
然后进行循环c,k从j到k>interval-1并且nums[k-interval]>nums[k]
循环c内,nums[k]=nums[k-interval],将比now大的数,逐个向后移动一位
结束循环c,nums[k]=now,成功插入新增的数
结束循环b
结束循环a


递减数组1
length/2  length/4  length/8 ...1
 比如
length=400
200 100 50 25 12 6 3 1
    
递减数组2
max2^k -1   2^(k-1)-1 ... 1
length=500
255 127 63 31 15 7 3 1

递减数组3
length/2  length/2-interval*1  length/2-interval*2  ...  1
length=50
25 22 19 16 13 10 7 4 1

public class ShellSort {
	public static void insertSort(int[] nums){
		int length=nums.length;
		if(length<=1){
			return;
		}
		List<Integer> decrementArray=createDecrementArray1(length);
		for(int i=0;i<decrementArray.size();i++){
			/*System.out.println("decre="+decrementArray.get(i)+" ");*/
			insertSort(nums,decrementArray.get(i));
			/*for(int j=0;j<length;j++){
				System.out.print(nums[j]+" ");
			}
			System.out.println();*/
		}
		System.out.println();
		
	}
	public static List<Integer> createDecrementArray1(int length){
		List<Integer> list=new ArrayList<>();
		Integer now=length/2;
		while(now>1){
			list.add(now);
			now=now/2;
		}
		list.add(1);		
		return list;
	}
	public static List<Integer> createDecrementArray2(int length){
		List<Integer> list=new ArrayList<>();
		int max=(int)(Math.log10(length)/Math.log10(2));
		for(int i=max;i>0;i--){
			int now=(int)Math.pow(2, i)-1;
			list.add(now);
		}
		return list;
	}
	public static List<Integer> createDecrementArray3(int length,int interval){
		List<Integer> list=new ArrayList<>();
		int now=length/2;
		while(now>1){
			list.add(now);
			now=now-interval;
		}
		list.add(1);
		return list;
	}
	
	public static void insertSort(int[] nums,int interval){
		int length=nums.length;
		for(int i=0;i<interval;i++){
			//i为begin的地方
			if(i+interval>length-1){
				break;
			}
			for(int j=i+interval;j<length;j=j+interval){
				int now=nums[j];
				int k=j;
				 while(k>interval-1&&now<nums[k-interval]){
					 nums[k]=nums[k-interval];
		             k=k-interval;
		         }
				nums[k]=now;
			}
		}
	}
	
}

复杂度及优缺点

希尔排序的速度与它选择的递减数组密切相关,速度快的数组,最好是互质的

如果选择不好,可能o(n^2)

 希尔排序的分析是复杂的,时间复杂度是所取增量的函数,这涉及一些数学上的难题。但是在大量实验的基础上推出当n在某个范围内时,时间复杂度可以达到O(n^1.3)。

好的话,如下

 希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。

五、归并排序

总体思想

多次将若干个已经排序好的有序表合并成一个有序表。直接将两个表合并的归并成为二路归并。

其基本思想是,先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列,然后合并两个子序列,然后把子序列看成由两个有序序列。。。。。倒着来看,其实就是先两两合并,然后四四合并。。。最终形成有序序列。空间复杂度为O(n),时间复杂度为O(nlogn)。

首先mergeSort(nums,0,length-1)
进行递归
首先mergeSort数组begin与end的左侧,让左侧有序
再mergerSort右侧
左侧和右侧都有序后,对左侧和右侧进行merge,一起排序
由于此次排序左右都有序,建立一个临时数组,对左右从头到尾进行扫描,谁小就进入数组,一遍扫描即可

编程思想

mergeSort
1 如果end<=begin 说明此时排序的数组仅有一个或没有,不排序
2 mid(begin+end)/2
3 mergeSort(nums, begin, mid);
4 mergeSort(nums, mid+1, end);
5 merge(nums, begin,mid, end);

merge
1 如果mid>end 说明begin到mid中已经排序完,不用排序
2 如果mid=end,说明在merge的上一步mergeSort(nums, begin, mid);已经排序完mergeSort(nums, begin, end);
不用再排序
3 以end-begin+1为长度建立temp数组,数组index为k
4 begin到mid的index为i=begin   mid+1到end的index为mid+1
5 以i<=mid&&j<=end 为循环条件进行循环
6 在循环中,nums[i],nums[j] 谁小,就i或j++,temp[k]=nums[i或j],然后k++
7 循环结束后将其中没有塞满的数组加入temp
8 将temp的数组逐个替换begin到end的nums数组的东西

或者merge理解

1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列

2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置

3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置

4. 重复步骤3直到某一指针达到序列尾

5. 将另一序列剩下的所有元素直接复制到合并序列尾

public class MergeSort {
	public static void mergeSort(int[] nums,int begin,int end){
		int length=nums.length;
		if(length<=1){
			return;
		}
		if(end<=begin){
			return;
		}		
		int mid=(begin+end)/2;
		mergeSort(nums, begin, mid);
		mergeSort(nums, mid+1, end);
		merge(nums, begin,mid, end);
		
	}
	public static void merge(int[] nums,int begin,int mid,int end){
		if(mid>=end){
			return;
		}
		int length=end-begin+1;
		int[] temp=new int[length];
		int k=0;
		int i=begin;
		int j=mid+1;
		while(i<=mid&&j<=end){
			if(nums[i]<nums[j]){
				temp[k]=nums[i];
				i++;
			}
			else{
				temp[k]=nums[j];
				j++;
			}
			k++;
		}
		if(i>mid){
			while(j<=end){
				temp[k]=nums[j];
				j++;
				k++;
			}
		}
		if(j>end){
			while(i<=mid){
				temp[k]=nums[i];
				i++;
				k++;
			}
		}
		for(int m=0;m<length;m++){
			nums[begin+m]=temp[m];
		}
		
	}
	
}

复杂度及优缺点

时间复杂度为O(nlogn)。

空间复杂度为O(n)。

速度较快,但是需要额外的空间

归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。

六、快速排序

总体思想

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

1 从数列中挑出一个元素,称为 “基准”(pivot),

2 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

3 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

1、先从数列中取出一个数作为基准数
2、分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边
3、再对左右区间重复第二步,直到各区间只有一个数

编程思想

快速排序编程的难点就是如何把数组按照基准点排序,共有两种方法,填坑法,交换法

填坑法

1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j–由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。

public class QuickSort {
	
	
	public static void quickSort(int[] nums,int begin,int last) {
		int length=nums.length;
		if(length==0||length==1){
	    	return;
	    }
		
		int base=nums[begin];
		int i=begin;
		int j=last;
		int mid=0;
		//i为第一个,j为最后一个,base为第一个数字,此时i处为base
		while(true){
			//j从后往前找到比base小的数字,与i作颠倒,颠倒后j处为base,i处为之前j处比base小的数
			while(j!=i){
				if(nums[j]<base ){
					nums[i]=nums[j];
					nums[j]=base;
					break;
				}
				else{
					j--;
				}
			}
			//如果上一个j过程结束,如果i与j相同,中间为i,跳出大过程
			if(i==j){
				mid=i;
				break;
			}
			//i从后往前找到比base大的数字,与j作颠倒,颠倒后i处为base,j处为之前i处比base大的数,跳出这个小循环
			while(i!=j){
				if(nums[i]>base){
					nums[j]=nums[i];
					nums[i]=base;
					break;
				}
				else{
					i++;
				}
			}
			//如果上一个i过程结束,如果i与j相同,中间为i,跳出大过程
			if(i==j){
				mid=i;
				break;
			}
			
		}
		//System.out.println(Arrays.toString(nums));
		//从此处开始mid处为base,比mid处大的,则比base大,反之亦然
		//对两次进行递归快速排序
		if((mid-begin)>1){
			quickSort(nums, begin, mid-1);
		}
		if((last-mid)>1){
			quickSort(nums, mid+1, last);			
		}		
		
		return;
		
	}

}

交换法

举个栗子:对5,3,8,6,4这个无序序列进行快速排序,思路是右指针找比基准数小的,左指针找比基准数大的,交换之。

5,3,8,6,4 用5作为比较的基准,最终会把5小的移动到5的左边,比5大的移动到5的右边。

5,3,8,6,4 首先设置i,j两个指针分别指向两端,j指针先扫描(思考一下为什么?)4比5小停止。然后i扫描,8比5大停止。交换i,j位置。

5,3,4,6,8 然后j指针再扫描,这时j扫描4时两指针相遇。停止。然后交换4和基准数。

4,3,5,6,8 一次划分后达到了左边比5小,右边比5大的目的。之后对左右子序列递归排序,最终得到有序序列。

上面留下来了一个问题为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就是在左边,所以最后相遇的数要和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找到比基准数小的数。

/**
 *@Description:<p>实现快速排序算法</p>
 *@author 王旭
 *@time 2016-3-3 下午5:07:29
 */
public class QuickSort {
    //一次划分
    public static int partition(int[] arr, int left, int right) {
        int pivotKey = arr[left];
        int pivotPointer = left;
        
        while(left < right) {
            while(left < right && arr[right] >= pivotKey)
                right --;
            while(left < right && arr[left] <= pivotKey)
                left ++;
            swap(arr, left, right); //把大的交换到右边,把小的交换到左边。
        }
        swap(arr, pivotPointer, left); //最后把pivot交换到中间
        return left;
    }
    
    public static void quickSort(int[] arr, int left, int right) {
        if(left >= right)
            return ;
        int pivotPos = partition(arr, left, right);
        quickSort(arr, left, pivotPos-1);
        quickSort(arr, pivotPos+1, right);
    }
    
    public static void sort(int[] arr) {
        if(arr == null || arr.length == 0)
            return ;
        quickSort(arr, 0, arr.length-1);
    }
    
    public static void swap(int[] arr, int left, int right) {
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
    }
    
}

复杂度及优缺点

快速排序是不稳定的,它的速度与它的基准点有关,基准点的好坏大大影响速度

在最差情况下,划分由 n 个元素构成的数组需要进行 n 次比较和 n 次移动。因此划分所需时间为 O(n) 。最差情况下,每次主元会将数组划分为一个大的子数组和一个空数组。这个大的子数组的规模是在上次划分的子数组的规模减 1 。该算法需要 (n-1)+(n-2)+…+2+1= O(n^2) 时间。 
在最佳情况下,每次主元将数组划分为规模大致相等的两部分。设 T(n) 表示使用快速排序算法对包含 n 个元素的数组排序所需的时间,因此,和归并排序的分析相似,快速排序的 T(n)= O(nlogn)。

空间复杂度

        其实这个空间复杂度不太好计算,因为有的人使用的是非就地排序,那样就不好计算了(因为有的人用到了辅助数组,所以这就要计算到你的元素个数了);我就分析下就地快速排序的空间复杂度吧;

        首先就地快速排序使用的空间是O(1)的,也就是个常数级;而真正消耗空间的就是递归调用了,因为每次递归就要保持一些数据;

     最优的情况下空间复杂度为:O(logn)  ;每一次都平分数组的情况

     最差的情况下空间复杂度为:O( n )      ;退化为冒泡排序的情况

 快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。

七、堆排序

总体思想

利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

 堆排序是借助堆来实现的选择排序,思想同简单的选择排序,以下以大顶堆为例。注意:如果想升序排序就使用大顶堆,反之使用小顶堆。原因是堆顶元素需要交换到序列尾部。

  首先,实现堆排序需要解决两个问题:

  1. 如何由一个无序序列键成一个堆?

  2. 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

  第一个问题,可以直接使用线性数组来表示一个堆,由初始的无序序列建成一个堆就需要自底向上从第一个非叶元素开始挨个调整成一个堆。

  第二个问题,怎么调整成堆?首先是将堆顶元素和最后一个元素交换。然后比较当前堆顶元素的左右孩子节点,因为除了当前的堆顶元素,左右孩子堆均满足条件,这时需要选择当前堆顶元素与左右孩子节点的较大者(大顶堆)交换,直至叶子节点。我们称这个自堆顶自叶子的调整成为筛选。

  从一个无序序列建堆的过程就是一个反复筛选的过程。若将此序列看成是一个完全二叉树,则最后一个非终端节点是n/2取底个元素,由此筛选即可。举个栗子:

49,38,65,97,76,13,27,49序列的堆排序建初始堆和调整的过程如下:

编程思想

 1 数组先初始化成最大堆
 2 将最大的与最后的互换,然后最后的开始下沉,让第一个为原来次大的
 3 依次循环,最后最大,第一个最小
 
 数组第i位的两个子节点
 左2*(i+1)-1=2*i+1     右 2*i+2
 i位的父节点
 (i+1)/2-1=(i-1)/2 向下取整
 
 建立初始最大堆
 1 last为完全二叉树(除了最底一层其他每层都是满的1,2,4,8个。。。)倒数第二层的最后一个
 2 从last到第一个逐个上浮,upjust
 3 upadjust中如果它<左子树,则二者互换,如果它<右子树,则二者互换。
 4 然后检查它的左右子节点,是否大于左右子节点的左右子节点。如果小于,则upadjust它

下沉
1 将第一个与最后一个互换,然后length-1,相当于不去动最后一个
2 然后downAdjust(nums,0,length-i-1);
3 在downAdjust,比价i的左右子树,与他的最大的并且比i大的互换,然后downAdjust(left/right)

public class HeapSort {
	public static void heapSort(int[] nums){
		int length=nums.length;
		if(length<=1){
			return;
		}
		int last=(int)Math.pow(2, (int)(Math.log10(length+1)/Math.log10(2)))-2;
		for(int i=last;i>=0;i--){
			upAdjust(nums,i,length);
		}
		for(int i=0;i<length;i++){
			swap(nums, 0, length-i-1);
			downAdjust(nums,0,length-i-1);
		}
		
	}
	//调整i位和它的子节点的大小,使大的上浮
	public static void upAdjust(int[] nums,int i,int length){
		int left=2*i+1;
		int right=2*i+2;
		if(left<length&&nums[left]>nums[i]){
			swap(nums, i, left);
		}
		if(right<length&&nums[right]>nums[i]){
			swap(nums, i, right);
		}
		if(left<length){
			upCheck(nums, left, length);
		}
		if(right<length){
			upCheck(nums, right, length);
		}
		
		
	}
	
	public static void upCheck(int[] nums,int i,int length){
		int left=2*i+1;
		int right=2*i+2;
		if((left<length&&nums[left]>nums[i])||(right<length&&nums[right]>nums[i])){
			upAdjust(nums, i, length);
		}
			
	}
	
	public static void downAdjust(int[] nums,int i,int length){
		int left=2*i+1;
		int right=2*i+2;
		if(left<length&&nums[left]>nums[i]&&(right>=length||(right<length&&nums[left]>=nums[right]))){
			swap(nums, i, left);
			downAdjust(nums, left, length);
		}
		if(right<length&&nums[right]>nums[i]&&nums[left]<nums[right]){
			swap(nums, i, right);
			downAdjust(nums, right, length);
		}
	}
	
	public static void swap(int[] nums,int i,int j){
		int temp=nums[i];
		nums[i]=nums[j];
		nums[j]=temp;
	}
	
}

复杂度及优缺点

堆排序的平均时间复杂度为Ο(nlogn) 。

我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。

八、计数排序

总体思想

基本思想如下

首先找到数组中的最大值,然后新建一个数组,bucket 此数组的长度是数组最大值+1,其实新建的这个数组中的下标值就是原数组的数据值,这里为什么长度是数组最大值加一呢
注意*:是因为比如数组最大值是9,然后如果你设置bucket数组的长度为9,那么他的下标最大值就是8,那么原数组的9就没有桶存了
好了继续,找到最大值后,开始遍历原数组,把原数组的数据加入bucket的下表中,bucket[i],每当有1个i bucket[i]的值就加一, 然后已经装入桶后, 遍历桶,如果bucket[j]位置-->0就说明此下标有数据,也就是说,此下标在原数组里有这个值, 然后排序 就是从大到小了 arr[i++]=j;

然后将原数组的按个=新的数组的元素(有几个的话,=多次,比如nums[2]=5,nums[3]=5)

编程思想

因为排序的数组可能不是从0开始,所以从min开始,到max,建立max-min+1 的数组

如果min=-1 max=3 size=5 now=1 index=2(now-min)
min=0 max=3 now=1 index=1
min=1 max=3 now=2 index=1(now-min)

可以得到数字的值now与新数组的index的关系
index=now-min  now=index+min

public class CountSort {
	public static void countSort(int[] nums){
		int length=nums.length;
		if(length<=1){
			return;
		}
		int max=nums[0];
		int min=nums[0];
		for(int i=0;i<length;i++){
			int now=nums[i];
			if(now>max){
				max=now;
			}
			if(now<min){
				min=now;
			}
		}
		int size=max-min+1;
		int[] map=new int[size];
		for(int i=0;i<length;i++){
			int now=nums[i];
			map[now-min]++;
		}
		int index=0;
		for(int i=0;i<length;i++){
			while(map[index]==0){
				index++;
			}
			map[index]--;
			int now=index+min;
			nums[i]=now;
			
		}
		
	}
	
}

复杂度及优缺点

计数排序是一种非常快捷的稳定性强的排序方法,时间复杂度O(n+k),其中n为要排序的数的个数,k为要排序的数的组max-min+1。

计数排序对一定量的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序。计数排序是消耗空间发杂度来获取快捷的排序方法,其空间发展度为O(K)同理K为要排序的max-min+1。

九、桶排序

总体思想

简单来说就是把数组 arr 划分为n个大小相同子区间(桶),每个子区间各自排序,最后合并。这样说是不是和分治法有点像了 啊!因为分治法就是分解 —— 解决 ——合并这样的套路。我认为这样想没毛病。可以理解为桶排序是一种特殊的分治法,特殊的地方主要体现在前两部分(这样说不知道对不对~)。
分组排序的算法可以用快速排序,因为内部的元素比较随机,速度快

假设有一组长度为N的待排关键字序列K[1….n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。 

编程思想

1 桶的数目buckets为length的开方,保证有效率
2 每个数对应的桶的index=(now-min)/(max-min+1)*buckets
此时(min-min)/(max-min+1)*buckets=0
(max-min)/(max-min+1)*buckets=buckets-1
很合适,不然,用max-min作为除数,max时,为buckets,超出桶数
3 对每个桶做排序quickSortFirst(totalList.get(i));
这个函数对arraylist和数组进行转换,让我原来的快速排序复用
4 将桶排过序的元素逐个取代nums的东西

public class BucketSort {
	public static void bucketSort(int[] nums){
		int length=nums.length;
		if(length<=1){
			return;
		}
		int max=nums[0];
		int min=nums[0];
		for(int i=0;i<length;i++){
			int now=nums[i];
			if(now>max){
				max=now;
			}
			if(now<min){
				min=now;
			}
		}
		int buckets=(int)Math.sqrt(length);
		List<ArrayList<Integer>> totalList=new ArrayList<>();
		for(int i=0;i<buckets;i++){
			totalList.add(new ArrayList<>());
		}
		for(int i=0;i<length;i++){
			int now=nums[i];
			int index=(now-min)/(max-min+1)*buckets;
			totalList.get(index).add(now);
		}
		for(int i=0;i<buckets;i++){
			quickSortFirst(totalList.get(i));
		}
		int index=0;
		for(int i=0;i<buckets;i++){
			List<Integer> list=totalList.get(i);
			int size=list.size();
			if(size==0){
				continue;
			}
			for(int j=0;j<size;j++){
				nums[index]=list.get(j);
				index++;
			}
		}
		
	}
	
	public static void quickSortFirst(ArrayList<Integer> list){
		int length=list.size();
		if(length==0||length==1){
			return;
		}
		int[] nums=new int[length];
		for(int i=0;i<length;i++){
			nums[i]=list.get(i);
		}
		quickSort(nums, 0, length-1);
		for(int i=0;i<length;i++){
			list.set(i, nums[i]);
		}
	}
	
	public static void quickSort(int[] nums,int begin,int last) {
		int length=nums.length;
		if(length==0||length==1){
	    	return;
	    }
		
		int base=nums[begin];
		int i=begin;
		int j=last;
		int mid=0;
		//i为第一个,j为最后一个,base为第一个数字,此时i处为base
		while(true){
			//j从后往前找到比base小的数字,与i作颠倒,颠倒后j处为base,i处为之前j处比base小的数
			while(j!=i){
				if(nums[j]<base ){
					nums[i]=nums[j];
					nums[j]=base;
					break;
				}
				else{
					j--;
				}
			}
			//如果上一个j过程结束,如果i与j相同,中间为i,跳出大过程
			if(i==j){
				mid=i;
				break;
			}
			//i从后往前找到比base大的数字,与j作颠倒,颠倒后i处为base,j处为之前i处比base大的数,跳出这个小循环
			while(i!=j){
				if(nums[i]>base){
					nums[j]=nums[i];
					nums[i]=base;
					break;
				}
				else{
					i++;
				}
			}
			//如果上一个i过程结束,如果i与j相同,中间为i,跳出大过程
			if(i==j){
				mid=i;
				break;
			}
			
		}
		//System.out.println(Arrays.toString(nums));
		//从此处开始mid处为base,比mid处大的,则比base大,反之亦然
		//对两次进行递归快速排序
		if((mid-begin)>1){
			quickSort(nums, begin, mid-1);
		}
		if((last-mid)>1){
			quickSort(nums, mid+1, last);			
		}		
		
		return;
		
	}
	
}

复杂度及优缺点

桶排序是计数排序的改进,当桶每次只装一个的时候,就是计数排序

桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
对N个关键字进行桶排序的时间复杂度分为两个部分:
(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。
(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(Ni*logNi) 。其中Ni 为第i个桶的数据量。
很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(N*logN)了)。因此,我们需要尽量做到下面两点:
(1) 映射函数f(k)能够将N个数据平均的分配到M个桶中,这样每个桶就有[N/M]个数据量。
(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。
对于N个待排数据,M个桶,平均每个桶[N/M]个数据的桶排序平均时间复杂度为:
O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)
当N=M时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(N)。
总结:桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)。当然桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

十、基数排序

总体思想

基数排序又称为“桶子法”,从低位开始将待排序的数按照这一位的值放到相应的编号为0~9的桶中。等到低位排完得到一个子序列,再将这个序列按照次低位的大小进入相应的桶中,一直排到最高位为止,数组排序完成。

基数排序不同于其他的排序算法,它不是基于比较的算法。基数排序是一种借助多关键字排序的思想对单逻辑关键字进行排序的方法。它是一种稳定的排序算法。多关键字排序中有两种方法:最高位优先法(MSD)和最低位优先法(LSD)。通常用于对数的排序选择的是最低位优先法,即先对最次位关键字进行排序,再对高一位的关键字进行排序,以此类推。

算法的思想:类似于桶式排序,我们需要给待排序记录准备10个桶,为什么是10个??因为一个数的任何一位上,其数字大小都位于0~9之间,因此采用10个桶,桶的编号分别为0,1,2,3,4...9,对应待排序记录中每个数相应位的数值,基数排序也是因此而得名。我们先根据待排序记录的每个数的个位来决定让其加入哪个桶中。例如:待排序数组为

278     109    63    930   589   184   505    269   8    83

求取每个数的个位数,依次为:8   9   3  0  9   4  5  9  8  3

依照其个位数决定将其加入哪个桶中

[0] 930      
[1]        
[2]        
[3] 63 83    
[4] 184      
[5] 505      
[6]        
[7]        
[8] 278 8    
[9] 109 589 269  

此步骤即为教材中所说的分配,接下来就是要进行收集,依照桶的编号,将含有数据的桶中的数据依次取出,形成的新的数据记录为:

930   63  83  184  505  278  8  109  589  269

再对这个数组按照十分位进行分配进桶,收集,最后再按照百位进行分配进桶,收集。就可得到最终的排序结果。

编程思想

1 先得到nums数组的max,确定最大位数
2 radixSortDigit(nums,i) i从1到最大位数,从个位排序到最大位
3 radixSortDigit(int[] nums,int digit) 排第i位(从后往前数,个位为digit=1)
4 List<ArrayList<Integer>> buckets 中有10个arraylist,
5 对nums进行循环,对每个num求第digit位,加入对应的arraylist
6 将buckets中从0-9的arraylist逐个取出,将arraylsit中的东西逐个覆盖nums

public class RadixSort {
	
	
	public static void radixSort(int[] nums) {
		int length=nums.length;
		if(length==0||length==1){
	    	return;
	    }
		int max=nums[0];
		for(int i=0;i<length;i++){
			if(nums[i]>max){
				max=nums[i];
			}
		}
		int digits=((int)Math.log10(max))+1;
		for(int i=1;i<=digits;i++){
			radixSortDigit(nums,i);
		}
		
	}
	public static void radixSortDigit(int[] nums,int digit){
		List<ArrayList<Integer>> buckets=new ArrayList<>();
		int length=nums.length;
		for(int i=0;i<10;i++){
			buckets.add(new ArrayList<Integer>());
		}
		for(int i=0;i<length;i++){
			int now=nums[i];
			int dig=getDigit(now, digit);
			buckets.get(dig).add(now);
		}
		int index=0;
		for(int i=0;i<10;i++){
			List<Integer> list=buckets.get(i);
			int size=list.size();
			if(size==0){
				continue;
			}
			for(int j=0;j<size;j++){
				nums[index]=list.get(j);
				index++;
			}
		}
		
		
	}
	public static int getDigit(int num,int digit){
		if(num<Math.pow(10, digit-1)){
			return 0;
		}
		//1301mod1000=301  301/100=3
		int result=(int)num%((int)Math.pow(10, digit));
		result=result/((int)Math.pow(10, digit-1));
		return result;
	}

}

复杂度及优缺点

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。

总体复杂度

排序方法 平均时间 最坏情况 最好情况 辅助空间 稳定性
冒泡排序 O(n^2) O(n^2) 普通O(n^2)优化O(n) O(1)
选择排序 O(n^2) O(n^2) O(n^2) O(1)
插入排序 O(n^2) O(n^2) O(n) O(1)
希尔排序 视递减数组 O(n^5/4) O(n^3/2) O(n) O(1)
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n)
快速排序 O(nlogn) O(n^2) O(nlogn) O(nlogn)-最差O(n^2)
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1)
计数排序 O(n+k)    k为要排序的数的组max-min+1 O(n+k) O(n+k) O(k)
桶排序 O(N+C),其中C=N*(logN-logM),M为桶的数量 M=1,就是快速排序 M=max-min+1,变成计数排序 O(N+M) 与桶内部排序方法的稳定性相同
基数排序 O(N*digit) digit为最大数字的位数 O(N*digit) O(N*digit) O(n)

猜你喜欢

转载自blog.csdn.net/xushiyu1996818/article/details/84762032