更新中。
排序
java
排序算法说明
排序的定义
对一序列对象根据某个关键字进行排序。
术语说明
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
有些读者可能好奇,为什么要区分稳定与不稳定,两个值相等谁前谁后无所谓啊。这块我们要考虑多一点,我们脑中的排序可能把它局限在整数的排序,其实在编程里不只int排序还有其他各种对象的排序,通过继承obj的compareTo方法来实现比较大小。比较的规则可以由我们自己来定义。现在回过头来思考稳定与不稳定,假如比较某个对象,要求碰到相等的对象不改变原来的默认前后顺序,遇到这种需求的时候,我们就不能用不稳定排序了,因为不稳定排序不能确定两个相等的对象谁在前谁在后。
说到这,我们也应该有这个意识,就是排序不是针对数字的,在编程中,所有对象都可以来排序。现实中的需求也不只是对数字排序,比如按字母顺序排序等,本文为了代码简洁,只针对int整数排序,我们主要学的是排序思想,思想有了只需将int换成你想排序的对象,然后重写compareTo方法就可以了。
算法总结
图片名词释义
- n: 数据规模
- k: “桶”的个数
- In-place: 占用常数内存,不占用额外内存
- Out-place: 占用额外内存
参考:https://www.cnblogs.com/onepixel/articles/7674659.html
冒泡排序
冒泡排序:对长度为n的数组,两个挨着的元素进行比较,比较一次,向后移动一次,每次比较将较大的元素放在后边,这样遍历一次数组,会把最大的放到最后边,然后再从0到n-1遍历,0到n-2遍历。。。最后遍历到0到1,
代码实现
/**
* 传统的冒泡排序
* @param arr
* @return
*/
public static int[] bubbleSort(int [] arr){
if(arr==null){
System.out.println("输入不能为null");
throw new NullPointerException();
}
for(int i=0;i<arr.length-1;i++){
for(int j=0;j<arr.length-i-1;j++){
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
return arr;
}
改进的冒泡排序:鸡尾酒排序,鸡尾酒排序是先从左到右遍历0-n两两比较,将最大的数放到最右边,然后再从n-1到0向左遍历,两两比较,将最小的数放在最左边,然后再从1到n-1将最大的数放在n-1位置,然后在从n-2到1遍历,把最小的放在1的位置,这样来来回回直到将整个数组遍历完。
代码实现
/**
* 改进冒泡排序,
* @param arr
* @return
*/
public static int[] newBubbleSort(int [] arr){
if(arr==null){
System.out.println("输入不能为null");
throw new NullPointerException();
}
int left=0;
int right=arr.length-1;
while(left<right){
for(int i=left;i<right;i++){
if(arr[i]>arr[i+1]){
int temp=arr[i];
arr[i]=arr[i+1];
arr[i+1]=temp;
}
}
right--;
for(int j=right;j>left+1;j--){
if(arr[j]<arr[j-1]){
int temp=arr[j];
arr[j]=arr[j-1];
arr[j-1]=temp;
}
}
left++;
}
return arr;
}
对以上两种排序方法进行了循环次数计数,换位次数计数,结果是一样的。而且鸡尾酒排序还需要多创建right/left两个临时变量,但是,当arr={2,3,4,5,1}这种排序,鸡尾酒排序效率会略高于冒泡排序。。
综上所述
如果要求使用冒泡排序实现排序的话,两个排序时间复杂度一样,而鸡尾酒排序略显复杂,针对局部有序情况,可能鸡尾酒排序会省去几次调换位置,所以如果使用冒泡排序,尽量使用鸡尾酒排序。
选择排序
选择排序应该是大多数人都会想到的一种排序方法,首先比较前n个元素,选出最小的放在1,再比较2到n的元素,选出最小的放在2,一次类推,最后比较大n-1完成排序。
代码实现
public static int[] selectionSort(int[] arr){
if(arr==null){
System.out.println("数组不能为null");
throw new NullPointerException();
}
for(int i=0;i<arr.length-1;i++){
int min=i;
for(int j=i+1;j<arr.length;j++){
if(arr[min]>arr[j]){
min=j;
}
}
int temp=arr[i];
arr[i]=arr[min];
arr[min]=temp;
}
return arr;
}
二元选择排序
二元选择排序是对传统选择排序的一种优化,减少了循环次数,传统选择排序遍历一次只找出最小值,而二元排序是遍历一次找出最大值和最小值,并且每次的遍历距离都在往中间缩短,当遍历到i=n/2时结束。
代码实现
public static int[] newSelectionSort(int[] arr,int n){
if(arr==null){
System.out.println("数组不能为null");
throw new NullPointerException();
}
int min,max,temp,i,j;
for(i=0;i<n/2;i++){
min=i;max=i;
for(j=i+1;j<n-i;j++){
if(arr[max]<arr[j]){
max=j;
continue;
}
if(arr[min]>arr[j]){
min=j;
}
}
temp=arr[i];
arr[i]=arr[min];
arr[min]=temp;
if(max==i)
{
temp=arr[n-i-1];
arr[n-i-1]=arr[min];
arr[min]=temp;
}
else
{
temp=arr[n-i-1];
arr[n-i-1]=arr[max];
arr[max]=temp;
}
}
return arr;
}
这里要注意,在循环一次之后,调换最大值和最小值的位置时,需要增加一个判断,因为当最大值为i时(也就是起始位置),先调换的最小值位置(最小值放在最左边),所以会把最大值调换走,然后你再调换最大值的时候其实调换的不是最大值。
综上所述
建议使用二元选择排序。
插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
思路
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
方法一
初步代码实现
public static int[] insertSort(int[] arr,int n){
if(arr==null){
System.out.println("输入的数组不能为null");
throw new NullPointerException();
}
int temp;
for(int i=1;i<n;i++){
while((i-1)>=0&&arr[i-1]>arr[i]){
temp=arr[i-1];
arr[i-1]=arr[i];
arr[i]=temp;
i--;
}
}
return arr;
}
代码中主要实现是while循环部分,通过与前一个元素作对比,如果后者小于前者,则互换位置,但是这个循环部分代码可以进行优化,可以换一种思路交换位置,举个例子{2,3,1},上边代码的步骤是:
1. 先将1与3交换位置,{2,1,3}
2. 再将1与2交换位置,{1,2,3}
我们可以改进下代码
1. 先将1放到临时变量里int get=arr[2]=1;
2. 将1的位置arr[2]赋值arr[1]即arr[2]=arr[1];结果{2,3,3}
3. 然后j--,将arr[1]=arr[0];结果{2,2,3}
4. 最后将arr[0]=get=arr[2]=1;结果{1,2,3}
这样就减少了while循环中利用临时变量交换位置的代码。
初步代码改进实现
public static int[] insertSort(int[] arr,int n){
if(arr==null){
System.out.println("输入的数组不能为null");
throw new NullPointerException();
}
for (int i = 1; i < n; i++)
{
int get = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > get)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = get;
}
return arr;
}
这样看着就更优雅一些了。
方法二
在方法一的基础上我们做一些思考,我们在方法一中,遍历到i时,是先拿前i-1个已排好序的数列跟arr[i]做比较,然后依次交换位置,将arr[i]的值替换到合适的位置,从而满足前i个元素顺序排列。这种想法没问题,但是仔细想想,前i-1个元素是顺序排列的,我们这样一个个比再插入到合适的位置是不是太浪费时间了?有没有更好的办法?有啊!二分插入啊!对于一个已排好顺序的数组,新插入一个元素,怎么样插入元素最快以满足插入后也是有序的数组,当然是二分插入。想一想:假如有16元素的数组{2,3,4....17},已排好序,现在要新插入一个元素1,如果按交换排序,就从17开始一个个比较吧,先与17比较,再与16比较。。。。最后比较到2,比较了16次。。才找到要插入的位置,如果用二分插入呢?先与数组的中间元素(16+1)/2=8比较,比arr[8]=10小,然后在与arr[8]左边中间比较,比arr[4]=6小,再与arr[4]左边中间比较arr[2]小,再与arr[2]中间arr[1]比较,再与arr[1]中间左边arr[0]比较,一共用了5次,相差是不是很多。那我们就用代码实现一下。
代码实现
public static int[] insertSort2(int[]arr,int n){
if(arr==null){
System.out.println("输入的数组不能为null");
throw new NullPointerException();
}
int left,right,mid,temp;
for(int i=1;i<n;i++){
left=0;
right=i-1;
temp=arr[i];
//通过比较找到要插入的位置
while(left<=right){
mid=(left+right)/2;
if(arr[i]>arr[mid]){
left=mid+1;
}else{
right=mid-1;
}
}
//位置找到了,把元素插入到该位置
for(int j=i-1;j>=left;j--){
arr[j+1]=arr[j];
}
arr[left]=temp;
}
return arr;
}
当数组里大部分是已排好序的话,使用二分排序是最优的选择。
希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。
思路
参考(https://www.cnblogs.com/chengxiao/p/6104371.html)
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
代码实现
public static int[] shellSort(int[] arr,int n){
if(arr==null){
System.out.println("输入数组不能为null");
throw new NullPointerException();
}
int temp,j;
//按照增量将数组分成若干组
for(int gap=n/2;gap>0;gap=gap/2){
//按照当前增量,遍历数组,按照基本插入排序思想排序
for(int i=gap;i<n;i++){
temp=arr[i];
for(j=i;j-gap>=0&&(arr[j]<arr[j-gap]);j-=gap)
arr[j]=arr[j-gap];
arr[j]=temp;
}
}
return arr;
}
第一次接触希尔排序的同学看了可能一头雾水,其实结合到代码里他只在插入排序的基础上多了一步,就是按照增量进行分组, 增量递减,所以不要被他的概念吓到,其实理解起来不难,增加的这一步是为了优化插入排序的速度。
希尔排序的性能在实践中是完全可以接受的,即使是对于数以万计的N仍是如此。并且由于代码简介,成为编程中的常用算法。
快速排序
快速排序是实践中的一种快速的排序算法,在C++或对Java基本类型的排序中特别有用。它的平均运行时间是O(NlogN)。该算法之所以特别快,主要是由于非常精炼和高度优化的内部循环。
思路
- 取数组arr中任一元素,称之为枢纽元(pivot);
- 然后遍历数组,将小于等于枢纽元(pivot)的元素分为一组,将大于枢纽元(pivot)的元素分为一组;
- 递归完成2的操作,完成排序;
方法一
方法一中没有随机选取一元素而是选择的最右边的元素作为枢纽元。
代码实现
/**
* 快速排序简单写法
* @param arr
* @param left
* @param right
* @return
*/
public static int[] quickSort1(int[] arr,int left,int right){
if(left>=right){
return arr;
}
int index=partiton(arr, left, right);
quickSort1(arr,left,index-1);
quickSort1(arr,index+1,right);
return arr;
}
/**
* 简单写法的分组方法
* @param arr
* @param left
* @param right
* @return
*/
private static int partiton(int[] arr,int left,int right){
int pivot=arr[right];
int tail=left-1;
for(int i=left;i<right;i++){
if(arr[i]<=pivot){
swap(arr,i,++tail);
}
swap(arr,tail+1,right);
}
return tail+1;
}
方法二
方法一是用递归实现的,我们知道递归的缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。但是,对于某些问题,如果不使用递归,那将是极端难看的代码。那么我们就用非递归实现以下。
非递归快排
因为递归实质就是用栈来实现的,所以我们用出栈进栈来模拟递归,因为这里对栈的功能要求不多,我们可以自己写一个栈,不知道怎么写的,可以参考我写的另外一篇文章:《剑指Offer面试题7(Java版):用两个栈实现队列与用两个队列实现栈》。由于篇幅原因我就直接用系统jdk自带的Stack类。栈的基本操作这就不写了,不知道的同学自己去百度下。
代码实现
/**
* 非递归实现的快速排序
* @param arr
* @param left
* @param right
* @return
*/
public static int[] quickSort3(int[] arr,int left,int right){
if(arr==null){
System.out.println("输入的数组不能为null");
throw new NullPointerException();
}
if(left<right){
Stack<Integer> stack=new Stack<Integer>();
//首先选择基数(pivot枢纽元),然后将数组分两组,一组小于枢纽元,一组大于等于枢纽元
int pivot=partition(arr, left, right);
//将左侧数组按照顺序放入栈
if(left<pivot-1){
stack.push(left);
stack.push(pivot-1);
}
//将右侧数组按照顺序放入栈
if(right>pivot){
stack.push(pivot);
stack.push(right);
}
//只要栈不为空,一直循环下去,一直取出来分区,然后再把分好区的放进去,直到区不能再分,就一直往外取,最后将栈取空。结束
while(!stack.empty()){
int rightTemp=stack.pop();
int leftTemp=stack.pop();
int pivotTemp=partition(arr,leftTemp,rightTemp);
if(leftTemp<pivotTemp-1){
stack.push(leftTemp);
stack.push(pivotTemp-1);
}
if(pivotTemp+1<rightTemp){
stack.push(pivotTemp+1);
stack.push(rightTemp);
}
}
}
return arr;
}
方法三
我们回看方法一,会发现有些时候会出现一个问题,就是当数组元素是有序排列的,如果选取最右边的元素作为枢纽元,分成的两组:组1包含0到n-1的元素,组2没有元素,然后继续递归还是跟前边一样,这样就失去了快排的效率。所以为了防止出现选取的元素是数组中的最大值或者最小值,我们采用三数中值分割法。
三数中值分割法
取数组中的最左边left元素、中间元素center(left+right)/2、最右边元素right元素,将三者按照大小顺序排列,中间的center我们当做枢纽元。既然已经将三个元素排序了,那我们就不要浪费这段排序,将最小值放在数组arr的最左边,将最大值放在数组的最右边,将枢纽元放在right-1的位置(放在这块方便代码实现)。
代码实现
/**
* 快速排序标准算法
* @param arr
* @param left
* @param right
* @return
*/
public static int[] quickSort(int[] arr,int left,int right){
if(left+10<=right){
int pivot=pivotFactory(arr, left, right);
int i=left,j=right;
for(;;){
while(arr[++i]<pivot){}
while(arr[--j]>pivot){}
if(i<j){
swap(arr,i,j);
}else{
break;
}
}
swap(arr,i,right-1);
quickSort(arr, left, i-1);
quickSort(arr,i+1,right);
return arr;
}else{
return InsertSort.insertSort(arr, arr.length);
}
}
/**
* 三数中值分割法
* @param arr
* @param left
* @param right
* @return
*/
private static int pivotFactory(int[] arr,int left,int right){
int center=(left+right)/2;
if(arr[left]>arr[center]){
swap(arr,left,center);
}
if(arr[right]<arr[left]){
swap(arr,right,center);
}
if(arr[right]<arr[center]){
swap(arr,right,center);
}
swap(arr,center,right-1);
return arr[right-1];
}
/**
* 交换位置
* @param arr
* @param a
* @param b
*/
private static void swap(int[] arr,int a,int b){
int temp=arr[a];
arr[a]=arr[b];
arr[b]=temp;
}
在上边代码中,细心同学可能会发现我增加了如下判断
if(left+10<=right){}
为什么加这个?因为插入排序对基本排好序的数组来做排序的速度很快,而快速排序能将无序数组快速的变化为基本有序,那大家可能就问,就使用快排就行了嘛,的确,使用快排也是很快速的,但是在接近排序完成的时刻,换成插入排序算法更加能提高排序速度,(插入排序的调用代码,在上边的插入排序代码中有写)。
综上所述
快速排序在处理基本类型是最常用的,效率最好的,为什么是基本类型,因为基本类型对排序稳定性没有需求。而非基本类型,有写是对排血稳定性有要求的。对于对象的排序,稳定性很重要。比如成绩单,一开始可能是按人员的学号顺序排好了的,现在让我们用成绩排,那么你应该保证,本来张三在李四前面,即使他们成绩相同,张三不能跑到李四的后面去。
归并排序
思路
代码实现
综上所述
堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的)。
思路
堆可以由一维数组表示,如下图的堆,可以表示成数组,数组顺组如下。
我们设树的某个节点node为数组的第i位,则有如下性质,
- node的左子节点为2*i+1;
- node的右子节点为2*i+2;
什么是大顶堆?就是当前节点大于等于它的左子节点和右子节点,表示如下:
- 大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
- 小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
以上几点很重要,便于我们写代码。
我们可以利用大顶堆做升序排列,利用小顶堆做降序排列。我们下文以大顶堆升序排列为例,
以上名词不懂的可以参考堆的一些基础知识。由于篇幅原因就不细致介绍。
算法步骤:
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
代码实现
/**
* 堆排序(升序排列)
* @param arr
* @param n
* @return
*/
public static int[] heapSort(int[] arr,int n){
if(arr==null){
System.out.println("输入的数组不能为null");
throw new NullPointerException();
}
//重建大顶堆
for(int i=n/2-1;i>=0;i--){
adjustHeap(arr,i,n);
}
//循环执行交换堆顶与最后一个元素,再调整堆
for(int j=n-1;j>=0;j--){
swap(arr,j,0);
adjustHeap(arr, 0, j);
}
return arr;
}
/**
* 校正大顶堆
* @param arr
* @param i
* @param j
*/
private static void adjustHeap(int[] arr,int i,int j){
int temp=arr[i];
for(int k=2*i+1;k<j;k=2*k+1){
//判断当前节点的左子节点和右子节点大小,让k的指针指向较大的子节点
if(k+1<j&&arr[k]<arr[k+1]){
k++;
}
//较大子节点与当前节点比较,如果较大子节点大于当前节点,则互换位置,
if(arr[k]>temp){
swap(arr,k,i);
//因为较大子节点与当前节点互换了位置,就需要考虑较大节点与其子节点
//是否还满足大顶堆的条件,就将i指向较大子节点处,下次循环会从这个较大子节点处开始执行
i=k;
}
}
}
/**
* 交换位置
* @param arr
* @param i
* @param j
*/
private static void swap(int[] arr,int i,int j){
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
综上所述
堆的一个典型的应用是优先队列(Priority Queue)
优先队列:出队顺序与入队顺序无关,只与队列中元素的优先级有关,优先级最高的元素最先出队。
优先队列在生活中的例子:
- 医院看病
- 操作系统:选择优先级最高的任务执行。特别注意:理解“动态执行”这个概念。
- 上网:服务端依次回应客户端的请求:通常也是使用优先队列。
- 人工智能(游戏)领域的典型应用。入队、出队的操作是很频繁的。
问题:从 1000000 元素中选出前 100 名。
问题抽象:在 N 个元素中选出前 M 个元素。
使用排序的时间复杂度为:O(NlogN),使用优先队列的时间复杂度为:O(NlogM)
这个就是TOP(K)问题了,可以建立100个元素的小顶堆,后面的数和根部进行比较,如果大于根部,则入堆并删除堆顶。直到遍历完所有元素,留在堆里的就是top(100)。我们可以用现成的优先队列PriorityQueue来实现这个功能。PriorityQueue内部是一个小顶堆。
Github上的demo地址
https://github.com/OptimusMX/JavaSorting
参考文献
https://www.cnblogs.com/onepixel/articles/7674659.html
http://www.cnblogs.com/eniac12/p/5329396.html
https://blog.youkuaiyun.com/happy_wu/article/details/51841244