以下排序算法模版都会用Comparable接口数据类型,只要实现了Comarable接口的数据类型比如Integer、Double、String和其他许多高级数据类型(如File和URL),这些数据类型的数组可以作为参数调用排序方法。
这里的输入不是Scanner cin = new Scanner(System.in),因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快,所以采用BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
但是也有弊端,比如存在文件中的数据有几万条或者更多,那么必定是有很多行数据,但是这么读取只能读取一行,就需要如下改进:
while ((line = cin.readLine()) != null) {
......
}
直接看关键排序代码即可, 写的是通用类型的排序,整型浮点型字符串都可以,只需修改输入定义的数组引用类型即可。
以下算法参考《算法(第四版)》
目录
冒泡排序
最好情况(加了标记辅助的)时间复杂度O(n)
平均情况O(n*n)
最坏情况O(n*n)
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; publicclassBubble_Sort { publicstaticbooleanisLess(Comparablea, Comparableb) { returna.compareTo(b) <0; } publicstaticvoidbubbleSort(Comparable[] a) { if (a==null||a.length<2) { return; } for (intend=a.length-1; end>0; --end) { booleanflag=true; // 如果不要标记符,那么冒泡的最好情况也只能是O(n*n)了for (inti=0; i<end; ++i) { if (isLess(a[i+1], a[i])) { // 升序swap(a, i+1, i); flag=false; } } if (flag) break; // 已经有序了,最好情况时间复杂度O(n) } } publicstaticvoidswap(Object[] a, inti, intj) { Objecttemp=a[i]; a[i] =a[j]; a[j] =temp; } publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } bubbleSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } }
选择排序
时间复杂度的最好最坏和平均情况都是O(n*n)
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; publicclassSelection_Sort { publicstaticbooleanisLess(Comparablea, Comparableb) { returna.compareTo(b) <0; } publicstaticvoidselectionSort(Comparable[] a) { if (a==null||a.length<2) { return; } for (inti=0; i<a.length-1; ++i) { intmin=i; for (intj=i+1; j<a.length; ++j) { min=isLess(a[j], a[min]) ?j : min; // 升序 } if (min!=i) { swap(a, i, min); } } } publicstaticvoidswap(Object[] a, inti, intj) { Objecttemp=a[i]; a[i] =a[j]; a[j] =temp; } publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } selectionSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } }
冒泡排序和选择排序实际一般用不到了,只是一个教学版本,但至少得掌握吧
插入排序
时间复杂度最好情况是O(n)
最坏和平均情况是O(n*n)
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; publicclassInsertion_Sort { publicstaticbooleanisLess(Comparablea, Comparableb) { returna.compareTo(b) <0; } publicstaticvoidInsertionSort(Comparable[] a) { if (a==null||a.length<2) { return; } for (inti=1; i<a.length; ++i) { // 从前往后排序for (intj=i; j>0&&isLess(a[j], a[j-1]); --j) { // 前面数据一定已经排好序,刚刚开始比较发现a[j]>a[j+1],// 不满足就进行下一轮循环swap(a, j, j-1); // 因此最好情况是O(n),本身是有序数组,外层O(n),内层O(1)忽略 } } } publicstaticvoidswap(Object[] a, inti, intj) { Objecttemp=a[i]; a[i] =a[j]; a[j] =temp; } publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } InsertionSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } }
希尔排序
最好情况时间复杂度O(n^1.25)
最坏情况是O(n^5/3)而不是O(n^2),要优于插入排序
平均情况O(nlogn)~O(n^5/3)
透彻理解希尔排序的性能至今仍然是一项挑战,实际上希尔排序是至今唯一无法准确描述其对于乱序的数组的性能的排序方法
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; publicclassShell_Sort { publicstaticbooleanisLess(Comparablea, Comparableb) { returna.compareTo(b) <0; } publicstaticvoidshellSort(Comparable[] a) { intn=a.length; inth=1; while (h<n/3) h=3*h+1; // 递增序列1,4,13,40,121,364,1093......while (h>=1) { for (inti=h; i<n; i++) { for (intj=i; j>=h&&less(a[j], a[j-h]); j-=h) { swap(a, j, j-h); } // 可以打印交换过程,方便理解/** for (int k = 0; k < n; ++k) { System.out.print(a[k] + " "); }* System.out.println();*/ } h/=3; } } privatestaticbooleanless(Comparablev, Comparablew) { returnv.compareTo(w) <0; } privatestaticvoidswap(Object[] a, inti, intj) { Objectswap=a[i]; a[i] =a[j]; a[j] =swap; } publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } shellSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } }
希尔排序为了加快速度简单的改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。希尔排序的思想是使数组中任意间隔为h的元素都是有序的,这样的数组被称为h有序数组。实现希尔排序的一中方法是对于每个h,用插入排序将h个子数组独立的排序。但因为子数组是相互独立的,一个更简单的方法是h子数组中将每个元素交换到比它还大的元素之前去(将比它大的元素向右移动了一格)。只需要在插入排序的代码中将移动元素的距离由1改为h即可。这样,希尔排序的实现就转化为了一个类似于插入排序但使用不同增量的过程。
如何选择递增序列h呢?算法的性能不进取决于h,还取决于h之间的数学性质,比如它们的公因子等。有很多论文研究了各种不同的递增序列,但都无法证明某个递增序列是“最好的”。
归并排序
最好最坏平均的时间复杂度都是O(nlogn)
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; publicclassMergeSort { // 不把help[]声明为merge()方法的局部变量,是为了避免每次归并时,即使是归并很小的数组,都创建一个新的数组。// 如果这么做,那么创建新数组将成为归并排序运行时间的主要部分。更好的解决方案是把help[]变为sort方法的局部变量,// 并将它作为参数传递给merge()方法publicstaticComparable[] help; publicstaticvoidmergeSort(Comparable[] a) { if (a==null||a.length<2) { return; } help=newComparable[a.length]; sort(a, 0, a.length-1); } publicstaticvoidsort(Comparable[] a, intlo, inthi) { if (lo>=hi) { return; } // 为什么不直接(lo+hi)>>1呢,因为lo+hi可能溢出,而hi-lo不溢出,lo+((hi-lo)>>1)是小于hi的,也不溢出,更安全intmid=lo+ ((hi-lo) >>1); // 对于java最好(lo + hi) >>> 1sort(a, lo, mid); sort(a, mid+1, hi); merge(a, lo, mid, hi); } publicstaticbooleanisLess(Comparablea, Comparableb) { returna.compareTo(b) <0; } publicstaticvoidmerge(Comparable[] a, intlo, intmid, inthi) { intj=lo, k=mid+1; for (inti=lo; i<=hi; ++i) { help[i] =a[i]; } for (inti=lo; i<=hi; ++i) { if (j>mid) { a[i] =help[k++]; } elseif (k>hi) { a[i] =help[j++]; } elseif (isLess(help[k], help[j])) { a[i] =help[k++]; } else { a[i] =help[j++]; } } } publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } mergeSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } }
归并排序比希尔排序快吗??在实际的应用中,它们的运行时间之间的差距在常数级别之内(希尔排序使用的是经过验证的递增序列),因此相对性能取决于具体的实现。
理论上来说,还没有人能够证明希尔排序对于随机数据的运行时间是线性对数级别的,因此存在平均情况下希尔排序的性能的渐进增长率更高的可能性,在最坏的情况下,这种差距的存在已经被证实了,但这对实际应用没有影响。
快速排序
原地排序:空间复杂度为O(1),相对于归并排序来说,占用非常小的内存便可以实现很高效的排序的效果
平均状态下的时间复杂度为O(nlogn), 最好O(nlogn),最坏O(n*n)
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; importjava.util.Random; publicclassQuickSort { publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } quickSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } privatestaticRandomrandom=newRandom(System.currentTimeMillis()); privatestaticvoidquickSort(Comparable[] a) { shuffle(a); // 随机任意打乱顺序,消除对输入的依赖,之后会讲为什么quickSort(a, 0, a.length-1); } privatestaticvoidquickSort(Comparable[] a, intlo, inthi) { if (hi<=lo) return; intj=partition(a, lo, hi); quickSort(a, lo, j-1); quickSort(a, j+1, hi); } privatestaticintpartition(Comparable[] a, intlo, inthi) { inti=lo, j=hi+1; Comparablev=a[i]; while (true) { while (less(a[++i], v)) if (i>=hi) break; while (less(v, a[--j])) if (j<=lo) break; if (i>=j) break; swap(a, i, j); // 将第一个大于v的数--a[i]和第一个小于v的数--a[j]交换 } swap(a, lo, j); returnj; } privatestaticvoidswap(Comparable[] a, inti, intj) { Comparabletemp=a[i]; a[i] =a[j]; a[j] =temp; } privatestaticbooleanless(Comparablecom, Comparablev) { returncom.compareTo(v) <0; } privatestaticvoidshuffle(Object[] a) { validateNotNull(a); // 判断数组是否为空intn=a.length; for (inti=0; i<n; i++) { intr=i+uniform(n-i); Objecttemp=a[i]; a[i] =a[r]; a[r] =temp; } } publicstaticintuniform(intn) { if (n<=0) thrownewIllegalArgumentException("argument must be positive: "+n); returnrandom.nextInt(n); } privatestaticvoidvalidateNotNull(Objectx) { if (x==null) { thrownewIllegalArgumentException("argument is null"); } } }
有一个能被证明的命题就是:快速排序最多需要约(N*N)/ 2次比较,但随机打乱能够预防这种情况。
问:随机将数组打乱似乎占用了排序用时的一大部分时间,这么做值得吗?
值得。这能够防止出现最怀情况并使运行时间可以预计。Hoare在1960年提出这个算法的时候就推荐了这种方法——它是一种(也是第一批)偏爱随机性的算法。
下面给出三向切分的快速排序作为参考:
对于大量重复元素, 时间复杂度可降到O(N)
privatestaticvoidquickSort3Way(Comparable[] a, intlo, inthi) { if (hi>=lo) return; intlt=lo, i=lo+1, gt=hi; Comparablev=a[lo]; while (i<=gt) { intcmp=a[i].compareTo(v); if (cmp<0) swap(a, lt++, i++); elseif (cmp>0) swap(a, i, gt--); else++i; } // 现在a[lo...lt-1] < v = a[lt..gt] < a[gt + 1...hi]成立quickSort3Way(a, lo, lt-1); quickSort3Way(a, gt+1, hi); }
对于大量重复元素的数组,它将排序时间从 线性对数级别 降低到了线性级别。这种对于重复元素的适应性使得三向切分的快速排序成为排序库函数的最佳算法选择——需要将包含大量重复元素的数组排序的用例很常见。
堆排序:
下沉排序(数值大的下沉,这样就是升序,如果数值小的下沉,那么就是降序)
最好最坏平均时间复杂度都为O(nlogn),空间复杂度为O(1)
升序代码:
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; publicclassHeapSort { publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } heapSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } privatestaticvoidheapSort(Comparable[] a) { intN=a.length; for (intk=N>>1; k>=1; --k) { // 堆的构造,从树底端开始往上恢复堆有序sink(a, k, N); } while (N>1) { // 从上往下沉,让数组变成升序(下沉排序)swap(a, 1, N--); // 第一个一定是最大的,换到数组最后sink(a, 1, N); // 剩下的元素继续恢复堆有序 } } privatestaticvoidsink(Comparable[] a, intk, intn) { while ((k<<1) <=n) { // (k << 1) <= N说明一定有左孩子intj=k<<1; // 左孩子下标if (j<n&&less(a, j, j+1)) // j < N说明一定有右孩子,如果左孩子小于右孩子的值++j; // 下标转移到右孩子if (less(a, j, k)) // 如果k结点的孩子中的最大值还是比k结点的值小,不用下沉了,已经堆有序break; swap(a, k, j); // 和孩子中的最大值交换k=j; // 现在这个元素已经在刚刚的j位置上,继续记录这个元素的位置,继续判断看能否继续下沉 } } privatestaticvoidswap(Object[] a, intk, intj) { // 第k和第j个元素交换,因为是下标所以-1Objecttemp=a[k-1]; a[k-1] =a[j-1]; a[j-1] =temp; } privatestaticbooleanless(Comparable[] a, intj, inti) { returna[j-1].compareTo(a[i-1]) <0; } }
测试结果:
-2 5 34 768 12 678 32 675 0 345 -1 12
-2 -1 0 5 12 12 32 34 345 675 678 768
降序代码:
(仅仅将less换成了greater)
importjava.io.BufferedReader; importjava.io.IOException; importjava.io.InputStreamReader; publicclassHeapSort { publicstaticvoidmain(String[] args) throwsIOException { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); Stringline=br.readLine().trim(); br.close(); String[] s=line.split(" +"); // 输入全部转成字符串, 分割成数组元素intlen=s.length; /*** 为什么不直接Scanner按照类型读取? 因为读取花费的时间太大,主要时间都在读取上面了,不如直接读入然后在进行转换操作来得快* 如果直接字符串排序就不需要接下来的转换操作了。*/Integer[] a=newInteger[len]; // 如果要排字符串用String[],排double用Double[],以此类推,就成了通用排序数组for (inti=0; i<len; ++i) { a[i] =Integer.valueOf(s[i]); } heapSort(a); if (len!=0) { System.out.print(a[0]); } for (inti=1; i<len; ++i) { System.out.print(" "+a[i]); } } privatestaticvoidheapSort(Comparable[] a) { intN=a.length; for (intk=N>>1; k>=1; --k) { // 堆的构造,从树底端开始往上恢复堆有序sink(a, k, N); } while (N>1) { // 从上往下沉,让数组变成降序(下沉排序)swap(a, 1, N--); // 第一个一定是最小的,换到数组最后sink(a, 1, N); // 剩下的元素继续恢复堆有序 } } privatestaticvoidsink(Comparable[] a, intk, intn) { while ((k<<1) <=n) { // (k << 1) <= N说明一定有左孩子intj=k<<1; // 左孩子下标if (j<n&&greater(a, j, j+1)) // j < N说明一定有右孩子,如果左孩子大于右孩子的值++j; // 下标转移到右孩子if (greater(a, j, k)) // 如果k结点的孩子中的最小值还是比k结点的值大,不用下沉了,已经堆有序break; swap(a, k, j); // 和孩子中的最小值交换k=j; // 现在这个元素已经在刚刚的j位置上,继续记录这个元素的位置,继续判断看能否继续下沉 } } privatestaticvoidswap(Object[] a, intk, intj) { // 第k和第j个元素交换,因为是下标所以-1Objecttemp=a[k-1]; a[k-1] =a[j-1]; a[j-1] =temp; } privatestaticbooleangreater(Comparable[] a, intj, inti) { returna[j-1].compareTo(a[i-1]) >0; } }
测试结果:
-2 5 34 768 12 678 32 675 0 345 -1 12
768 678 675 345 34 32 12 12 5 0 -1 -2
命题1:用下沉操作由N个元素构造堆只需要少于2N次比较以及少于N次交换
命题2:将N个元素排序,堆排序只需要少于(2NlgN+2N)次比较(以及一半次数的交换),其中2N项来自堆的构造,2NlgN项来自于每次下沉操作最大可能需要2NlgN次比较。
============================Talk is cheap, show me the code============================