排序算法是一种比较简单的算法,从咱们一开始接触计算机编程开始接触的可能就是排序或者搜索一类的算法,可是由于排序在其余的一些算法中应用较多,因此为了提升性能已经研究了多种排序算法。目前区别排序算法主要仍是以时间复杂度,空间复杂度,稳定性等来排序,接下来咱们分别分析。算法
区别一个排序算法是不是稳定算法只需看相同的关键字在排序完成后是否保持原来二者的先后关系便可,好比对于[1,2,3,4,1],a[0]=a[4],a[0]在a[4]以前,稳定的排序在排序完成后a[0]依旧在a[4]的前面,反之则是不稳定排序算法。shell
冒泡排序(Bubble Sort)是一种比较简单的排序算法。基本原理为选定一个数做为比较标准,遍历整个数组比较两个数的大小,若是顺序不对则进行交换,知道没有再须要交换的数为止。冒泡排序是稳定的排序算法
冒泡排序算法的运做以下:编程
public static void bubbleSort(int[] arr){ for(int i=0;i<arr.length;i++){ for(int j=0;j<arr.length-i-1;j++){ if(arr[j] > arr[j+1]){ swap(arr, j, j+1); } } } }
若是序列的初始状态是正序的,一趟扫描便可完成排序,不须要交换操做。通过n次的循环后排序完成,因此时间复杂度为O(n),整个过程没有使用辅助空间,空间复杂度为O(1)。数组
选择排序(Selection sort)是一种很简单排序算法。它要求是每一次从待排序的元素中选出最小(最大)的一个元素,存放在起始位置,而后再从剩余未排序元素中继续寻找最小(大)元素,而后放到上一位已经排好序的后面。以此类推,直到所有待排序的数据元素排完。 选择排序是不稳定的排序方法。多线程
选择排序算法的运做以下:工具
public static void insertSort(int[] arr){ for(int i=0;i<arr.length;i++){ //一趟以后最小的数到了下标为i的位置 for(int j=i+1;j<arr.length;j++){ if(arr[i] > arr[j]){ swap(arr, i, j); } } } }
若是数据自己就是有序的,0次交换;最坏的状况下须要进行n-1次交换;比较操做次数固定为N^2/2,时间复杂度为O(n^2),空间复杂度为O(1)。性能
插入排序是比较简单的排序方法,插入排序将待排序数组分为两部分,一部分是已排序部分,另外一部分则是待排序部分。最开始仅第一个数字为已排序部分。而后每次从待排序部分取出一个数,同已排序部分的数据进行比较,选出恰好前一个数比该数小,后一个数比该数大(第一位除外),将该数放在这个位置。进过遍历后整个数组有序。大数据
选择排序算法的运做以下:ui
public static void insertSort(int[] nums){ int i,j; for(i=1;i<nums.length;i++){ int temp = nums[i]; //将元素后移 for(j=i-1;j>=0&&temp<nums[j];j--){ nums[j+1] = nums[j]; } nums[j+1] = temp; } }
在将n个元素的序列进行升序或者降序排列,采用插入排序最好状况就是序列已是有序的了,在这种状况下,须要进行的比较操做需n-1次便可。最坏状况就是序列是反序的,那么此时须要进行的比较共有n(n-1)/2次。平均来讲插入排序算法复杂度为 O(n^2)。因此插入排序不适合对于数据量比较大的排序应用。可是在须要排序的数据量很小或者若已知输入元素大体上按照顺序排列,插入排序的效率仍是不错。线程
在插入排序的时候,咱们看到每一次进行比较都有两次比较操做j>=0&&temp<nums[j]
,即既要保证不越界又要判断数据是否符合条件,假设在反序的状况下就几乎多出一倍的比较次数。这里咱们使用一个哨兵来消除掉多的比较操做。
public static void insertWithSentinelSort(int[] nums){ int i,j; for(i=1;i<nums.length;i++){ //将第一个元素指定为哨兵 //要求传入的数组比原数组长度大1 nums[0] = nums[i]; //将元素后移 //这里只需比较数据是否符合条件 for(j=i-1;nums[j]>nums[0];j--){ nums[j+1] = nums[j]; } nums[j+1] = nums[0]; } }
添加哨兵的好处就是将本来的比较次数减小,提升了算法效率。
希尔排序是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是把记录按下标的必定的步长进行分组,对每组数据使用直接插入排序算法排序;随着步长逐渐减小,每组包含的关键词愈来愈多,当步长为1时,恰好就是一个插入排序。而在此时整个数据序列已经基本有序,插入排序在对几乎已经排好序的数据操做时,效率高,能够达到线性排序的效率。因此希尔排序的总体效率较高。
希尔排序的步骤:
public static void shellSort(int[] nums){ int size = nums.length/2; int i,j; while(size>=1){ for(i=0;i<nums.length;i++){ for(j=i;j+size<nums.length;j+=size){ if(nums[j]>nums[j+size]){ swap(nums, j, j+size); } } } size/=2; } }
希尔排序的时间复杂度分析比较复杂,由于它和所选取的步长有着直接的关系。步长的选取没有一个统一的定论,只须要使得步长最后为1便可。希尔排序的时间复杂度根据所选取的步长不一样时间复杂度范围在o(n^1.3)~o(n^2)。
快速排序是对冒泡排序的改进。
快排的基本步骤:
public static void quickSort(int[] nums, int low, int high){ if(low<high){ int partation = partition(nums, low, high); //这里返回的low值的位置已经肯定了 因此不用参与排序 quickSort(nums, 0, low-1); quickSort(nums, low+1, high); } } //进行一次排序 将待排序列分为两个部分 public static int partition(int[] nums, int low, int high){ //选取第一个值为枢纽值 int pivo = nums[low]; while(low<high){ while(low<high&&nums[high]>=pivo){ high--; } nums[low] = nums[high]; while(low<high&&nums[low]<=pivo){ low++; } nums[high]=nums[low]; } nums[low] = pivo; return low; }
时间复杂度
在最优状况下,Partition每次都划分得很均匀,若是排序n个关键字,其递归的深度就为log2n+1,即仅需递归log2n 次。时间复杂度为O(nlogn)。
最糟糕的状况就是待排序列为须要排序方向的逆序。每次划分只获得一个比上一次划分少一个记录的子序列。这时快排退化为冒泡排序。时间复杂度为O(n^2)。
快排的平均复杂度为O(nlogn),证实过程较长,直接贴个连接吧。
空间复杂度
被快速排序所使用的空间,根据上面咱们实现的代码来看,在任何递归调用前,仅会使用固定的額外空間。然而,若是须要产生 o(logn)嵌套递归调用,它须要在他们每个存储一个固定数量的信息。由于最好的状况最多须要O(logn)次的嵌套递归调用,因此它须要O(logn)的空间。最坏状况下须要 O(n)次嵌套递归调用,所以须要O(n)的空间。
归并是指将两个及以上的有序序列合并成一个有序序列。
归并排序步骤:
public static void mergeSort(int[] nums, int[] temp, int left, int right){ if(left<right){ int mid = (left+right)/2; mergeSort(nums, temp,left,mid); mergeSort(nums, temp,mid+1,right); merge(nums,temp, mid, left, right); } } public static void merge(int[] nums, int[] temp, int mid, int left, int right){ int i=left,j=mid+1,k=0; while(i<=mid&&j<=right){ if(nums[i]<nums[j]){ temp[k++] = nums[i++]; }else { temp[k++] = nums[j++]; } } while(i<=mid){ temp[k++] = nums[i++]; } while(j<=right){ temp[k++] = nums[j++]; } //将temp中的元素所有拷贝到原数组中 //这里必须将原来排好序的数组值复制回去 //不然后续的对比前面排序长的数组排序时会出错 //好比4 1 2 3 讲过排序后分为1 4 和2 3两组 //若是没有将值复制回去那么合并后将是2 3 4 1 k=0; while(left<=right){ nums[left++] = temp[k++]; } }
归并排序是一种效率高且稳定的算法。可是却须要额外的空间。
归并排序的比较是分层次来归并的。第一次将序列分为两部分,第二次将第一次获得的两部分各自分为两部分。最后分割合并就相似一课二叉树。其平均时间复杂度为O(nlogn)。空间复杂度由于其须要额外长度为n的辅助空间,其空间复杂度为O(n)。
上面演示的代码也被成为2-路归并排序,其核心思想是将觉得数组中先后响铃的两个有序序列合并为一个有序序列。可是实际上平时咱们不会使用这种排序方式。
可是归并排序使用场景仍是不少的,特别是在对数量较大的序列进行排序是,好比目前咱们有大量的数据存储在文本中,如今须要对其进行排序。因为内存的限制没有办法一次性加载全部的数据,这时候咱们就可使用归并排序,将大的文件分割为若干份小文件,分别对这些小文件的数据进行排序后使用归并排序再将其进行排序。而且排序过程当中可使用多线程等手段提升算法效率。
在JDK中,Arrays工具类为咱们提升了各类类型的排序方法,Arrays.sort在JDK1.6及以前使用的是归并排序,在1.7开始使用的是TimSort排序。
TimSort算法是一种起源于归并排序和插入排序的混合排序算法,设计初衷是为了在真实世界中的各类数据中能够有较好的性能。基本工做过程是: