排序算法是程序员必备技能之一,可能大家平时用到的排序算法种类比较少,完全忽视了运行时的时间空间效率问题,不过,作为一个进阶的程序员,了解各排序算法的优缺点以及实现思路很有必要,先列出常用的十大算法的概况:
算法的优劣性取决于具体需要排序的数组的大小,混乱程度,数组内容,业务需求等等,我们不能离开应用场景去评判一个算法的优劣性,没有最好,只有最适合。
以下是在Java平台下实现的算法的测试结果,所有数据都是随机生成的,所有的算法都是计算同一套数据进行测试,当然,这些结果说明不了问题,只是一个参考:
1. 20个数据的结果(为了验证算法的正确性及数据的统一性)
2. 1000个数据的结果(数据量太大,就不打印数组了, 下同)
3. 10000个数据的结果
4. 100000个数据的结果
5.500000个数据的结果
可以看到,同样的数据采用不同的算法所耗费的时间差距特别大,所以我们平时在使用的时候一定要选择合适的算法。
再次说明,这个结果只是一个参考,并不能定义算法的优劣,算法耗费的时间不仅跟计算次数有关,还跟赋值等操作息息相关,跟算法实现方式也有一定的关系,我们要在最合适的场景采用最合适的算法。
java代码如下,只是提供一个思路,代码注释已经很清楚了,就不再解释了:
Main:算法的主流程
package com.jaden.sort;
public class Main {
public static void main(String[] args) {
int count = 500000;
int[] src = createRandomAry(count); //生成随机数目的数组
System.out.println("Array count = " + count );
//printAry(src);
System.out.println("-------------------------------------------------");
for(int i=Sort.BUBBLE_SORT; i<=Sort.RADIX_SORT; i++) {
int[] tmp = copyAry(src); //每次拷贝,便于比较不同算法计算统一数据的时间
Sort sort = SortFactory.createSort(i); //工厂类创建具体算法
long begin = System.currentTimeMillis(); //开始时间
sort.sort(tmp); //计算
long after = System.currentTimeMillis(); //结束时间
//printAry(tmp);
formatPrint(sort, after-begin); //打印算法名称及耗费时间
}
}
/*
* 创建乱序数组
*/
private static int[] createRandomAry(int count) {
int []ary = new int[count];
for(int i=0; i<count; i++) {
ary[i] = (int)(Math.random() * count * 100);
}
return ary;
}
/*
* 打印数组
*/
private static void printAry(int []ary) {
for(int i : ary) {
System.out.print(i);
System.out.print(" ");
}
System.out.println("");
}
private static String getClassName(Class classex) {
return classex.getName();
}
private static void formatPrint(Sort sort, long spend) {
String name = getClassName(sort.getClass());
System.out.println(name + " | spend = " + spend + "ms");
}
/*
* 复制数组,确保各种排序用到的数组是同一个
*/
private static int[] copyAry(int[] src) {
int size = src.length;
int[] dest = new int[size];
for(int i=0; i<size; i++) {
dest[i] = src[i];
}
return dest;
}
}
SortFactory:
package com.jaden.sort;
/*
* 工厂类,用来创建各种排序算法
*
*/
public class SortFactory {
public static Sort createSort(int type) {
switch (type) {
case Sort.BUBBLE_SORT:
return new BubbleSort();
case Sort.SELECTION_SORT:
return new SelectionSort();
case Sort.INSERTION_SORT:
return new InsertionSort();
case Sort.SHELL_SORT:
return new ShellSort();
case Sort.MERGE_SORT:
return new MergeSort();
case Sort.QUICK_SORT:
return new QuickSort();
case Sort.HEAP_SORT:
return new HeapSort();
case Sort.COUNTING_SORT:
return new CountingSort();
case Sort.BUCKET_SORT:
return new BucketSort();
case Sort.RADIX_SORT:
return new RadixSort();
default:
return new BubbleSort();
}
}
}
下面是各种算法:
1. 冒泡排序:
package com.jaden.sort;
/*
* 冒泡排序
* 思想:每次比较相邻的两个元素,如果相邻元素顺序错误,那么则将其交换,第一次排序完成,最大的值位于数组的最后
* 时间复杂度:O(n²)
* 空间复杂度:O(1)
*/
public class BubbleSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
int tmp;
for(int i=0; i<size-1; i++) {
for(int j=0; j<size-i-1; j++) {
//交换值,将最大值放到后面
if(src[j] > src[j+1]) {
tmp = src[j];
src[j] = src[j+1];
src[j+1] = tmp;
}
}
}
return src;
}
}
2. 选择排序:
package com.jaden.sort;
/*
* 选择排序
* 思想:从index=0遍历数组,选择最小的元素,与第一个元素(index=0)交换,那么第一个元素即排好序的,
* 然后从index=1遍历数组,再选择剩余数组最小元素,与第二个元素(index=1)交换,那么前两个也排好序了。
* 然后从index=2开始。。。。
* 不停遍历,最终能够得到一个排好序的数组
* 时间复杂度:O(n²)
* 空间复杂度:O(1)
*/
public class SelectionSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
int tmp, min;
for(int i=0; i<size-1; i++) {
min = i;
for(int j=i+1; j<size; j++) {
if(src[min] > src[j]) {
min = j;
}
}
//最小值,跟i交换
tmp = src[i];
src[i] = src[min];
src[min] = tmp;
}
return src;
}
}
3. 插入排序:
package com.jaden.sort;
/*
* 插入排序
* 思想:原数组第一个元素可以认为是已经排好序的新数组,第二个元素与新数组的元素(第一个元素)进行比较,如果比第一个元素小,那么放到该元素前面,如果比第一个元素大,那么不用改变位置,加入到新数组后面。新数组是从小到大排序好的数组,元素个数为2
* 然后第三个元素同新数组的元素分别比较,假如第三个元素是最小的,插入到最前,假如第三个元素是第二小的,插入到新数组的中间,假如最大,那么直接放到新数组的后面,新数组元素个数为3
* 依次遍历整个老数组,将值插入到合适的位置,遍历完成,数组就排序完成。。
* 虽然用新的数组理解起来方便,但是实际上是没有新的数组的,是用索引值去区分的,index以前,是有序,后面是无序
* 插入排序涉及到数组的整体后移,比如说,前面5个是有序,但是第6个值大小正好比第3个大,比第4个小,那么将第6个插入到第4个的时候,原先的4要到第5,原先的5要到第6位,注意处理这个逻辑就好
* 时间复杂度:O(n²)
* 空间复杂度:O(1)
*/
public class InsertionSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
int tmp;
int p; //指针插入位置
for(int i=0; i<size-1; i++) {
tmp = src[i+1];
p = i;
while (p >=0 && tmp < src[p]) {
src[p+1] = src[p];
p--;
}
src[p+1] = tmp;
}
return src;
}
}
4. Shell排序
package com.jaden.sort;
/*
* 希尔排序
* 思想:按照某个步长对元素进行排序,假如步长为4,那么index=0,index=4,index=8.。。。的元素进行比较排序,index=1,index=5,index=9.。。。的元素进行比较排序,
* index=2,index=6,index=10.。。。的元素进行比较排序,index=3,index=7,index=11.。。。的元素进行比较排序
* 第一次排序完成,减小步长,同理按照上面的规则进行排序
* 确保最后一次的步长为1
*
* 假如只进行一次一个步长为1的排序,那么就是插入排序了
* 时间复杂度:O(n^(1.3—2))
* 空间复杂度:O(1)
*/
public class ShellSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
int gap = 1;
int interval = 3; //动态设置间隔
int tmp, p;
while (gap < size / interval) {
gap = gap * interval + 1;
}
for(; gap>0; gap=gap/interval) {
for(int i=gap; i<size; i++) {
tmp = src[i];
p = i - gap;
while (p >= 0 && tmp < src[p] ) {
src[p+gap] = src[p];
p -= gap;
}
src[p+gap] = tmp;
}
}
return src;
}
}
5. 归并排序
package com.jaden.sort;
/*
* 归并排序
* 思想:将数组由中间拆分成2个数组,然后再分别将2个数组拆分为4个数组,依次递归,那么最后会将原数组拆分为一个个元素
* 1个元素的数组可以看成是有序数组,然后分别将两个相邻有序数组按顺序合并,合并完的数组仍然是有序的,然后依次递归,每次都可以看成是两个有序数组的合并
* 有序数组的合并,所用的时间复复杂度是O(n)
* 递归到最终和并完成,得到的新的数组就是有序的了
* 归并排序,可以看成是分割和合并的递归,注意处理好各边界逻辑
* 时间复杂度:O(nlgn)
* 空间复杂度:O(n)
*/
public class MergeSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
if(size < 2) {
return src;
}
slice(src, 0, size-1);
return src;
}
/*
* 分割成最小单位
*/
private void slice(int[] src, int start, int end) {
int middle = (end + start) / 2;
if(end - start > 0) {
slice(src, start, middle);
slice(src, middle+1, end);
merge(src, start, middle, end);
}
}
/*
* 合并有序数组
*/
private void merge(int[] src, int start, int middle, int end) {
int[] tmp = new int[end - start + 1];
int p1 = start; //指向第一段顺序数组的低位
int p2 = middle + 1; //指向第二段顺序数组的低位
for(int i=0; i<end-start+1; i++) {
if(p1 <= middle && p2 <= end) {
if(src[p1] < src[p2]) {
tmp[i] = src[p1];
p1++;
}else {
tmp[i] = src[p2];
p2++;
}
}else if(p1 > middle) { //符合此条件,代表第一段数组全部合并完,直接将剩余的第二段数组合并
tmp[i] = src[p2];
p2++;
}else if(p2 > end) { //符合此条件,代表第二段数组全部合并完,直接将剩余的第一段数组合并
tmp[i] = src[p1];
p1++;
}
}
//将临时数组的值存回到src中
for(int i=start; i<end+1; i++) {
src[i] = tmp[i-start];
}
}
}
6. 快速排序
package com.jaden.sort;
/*
* 快速排序
* 思想:取第一个元素做为基准值,把比他大的放到右边,把比他小的放到左边,第一次循环完成,那么左边都是比他小的,右边都是比他大的,那么,这个值在最终排序完成的数组中的位置将不会改变
* 那么新的数据可以分成三个部分:所有比他小的左边数组(未排序)上一个基准值,所有比他大的右边数组(未排序),用递归的思想,左边的数组可以按上面的算法来排序基准值,右边的数组同理也可以得到基准值
* 这一次循环完毕,那么就得到了:未排序-基准左边-未排序-基准1-未排序-基准右边-未排序。然后再次递归,直至把所有的基准值的位置选对,那么整个数组就排序好了。。
* 注意各个边界值的处理
* 时间复杂度:O(nlgn)
* 空间复杂度:O(nlgn)
*/
public class QuickSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
if(size < 2) {
return src;
}
int left = 0;
int right = size - 1;
quickSort(src, left, right);
return src;
}
private void quickSort(int[] src, int left, int right) {
if(right - left > 0) {
int p = left + 1;
int level = src[left];
for(int i=left+1; i<=right; i++) {
if(src[i] < level) {
swip(src, p, i);
p++;
}
}
swip(src, p-1, left);
//左边递归
quickSort(src, left, p-2);
//右边递归
quickSort(src, p, right);
}
}
private void swip(int[] src, int x, int y) {
int tmp = src[x];
src[x] = src[y];
src[y] = tmp;
}
}
7. 堆排序:
package com.jaden.sort;
/*
* 堆排序
* 思想:堆,是一个完全二叉树结构。与数组不同,假设下标是从1开始,那么最后一个根节点的索引是个数index = n/2,它的左右叶子是index*2和index*2+1(这个是完全二叉树的特性)
* 排序的原理是,从最后一个根节点开始,比较该根节点与它的左右节点值的大小,如果谁大,那么将该节点值交换为最大的值。
* 从最后一个根节点往第一个节点依次遍历,第一个遍历完成,那么最大的值肯定位于第一个节点也就是array[0],将该值与数组最后一个值交换,那么最大的值放到最后了,
* 第一次排序完成,已经排序的元素是最后1个。为了方便理解,将最后一个值从树中移除,那么新的树,就比之前少了一个元素。
* 第二次,依然按照上面的规律来遍历,遍历完成,找到了新树的最大值,也就是原数组的第二大值,放到第一次的最大值前面,同理从树移除该元素,新的树又少了一个元素
* 如此下去,每次遍历都能找到当前树中的最大值,遍历完成后,顺序也就排序好了
* 时间复杂度:O(nlgn)
* 空间复杂度:O(1)
*/
public class HeapSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
int unsortSize = size;
for(int i=unsortSize; i>0; i--) {
for(int j=i/2; j>0; j--) {
heapUp(src, j, i);
}
swip(src, 0, i-1);
}
return src;
}
/*
* 堆上浮,从一个根节点与他的左右叶子(如果都存在)对比,将最大的值与根进行交换
*/
private void heapUp(int[] src, int index, int unsortSize) {
int max = index - 1;
int right = index*2;
int left = index*2 - 1;
if(right < unsortSize && src[max] < src[right]) {
max = right;
}
if(left < unsortSize && src[max] < src[left]) {
max = left;
}
swip(src, index - 1, max);
}
private void swip(int[] src, int x, int y) {
int tmp = src[x];
src[x] = src[y];
src[y] = tmp;
}
}
8. 计数排序
package com.jaden.sort;
/*
* 计数排序
* 思想:适用于数组全部是正整数的情况,假如数据最大值是100,那么可以新建一个数组newAry,数组个数是101个,分别对应对应0-100的索引,将所有索引对应的值初始化为0,假如一个数出现一次,那么对应索引的值加1,
* 如:出现了一次50,那么新数组newAry[50]=0+1=1,如果再次出现50,那么newAry[50]=1+1=2, 遍历原数组,将所有的值映射到新的数组中。
* 那么最后,newAry中值大于0的下标,对应的是原数组的值,newAry的值,对应的是原数组中该值出现的次数。
* 最后,遍历newAry,以上面的规则,将数组排序
* 时间复杂度:O(n+k)
* 空间复杂度:O(n+k)
*/
public class CountingSort implements Sort {
@Override
public int[] sort(int[] src) {
int size = src.length;
int max = findMax(src);
int []countAry = new int[max + 1];
//统计每个值出现的次数
for(int i=0; i<size; i++) {
countAry[src[i]]++;
}
//将每个值从小到大赋予给src数组
int size2 = countAry.length;
int index = 0;
for(int i=0; i<size2; i++) {
for(int j=0; j<countAry[i]; j++) {
src[index++] = i;
}
}
return src;
}
/*
* 找出数组中的最大值
*/
private int findMax(int[] src) {
int size = src.length;
int maxIndex = 0;
for(int i=0; i<size; i++) {
if(src[maxIndex] < src[i]) {
maxIndex = i;
}
}
return src[maxIndex];
}
}
9. 桶排序
package com.jaden.sort;
import java.util.ArrayList;
import java.util.Collections;
/*
* 桶排序
* 思想:将数组分成若干个数据范围(桶),比如,一个数组最小值为0,最大值为100,假定有10个桶,那么数组0-10的为一个桶,10-20为第二个桶, 依次类推。根据范围将数据放入对应的桶。
* 那么,从前到后,前面桶的值肯定都小于后面桶的值,只要把每个桶内部的数据排序完成,那么再从第一个桶到最后一个桶,依次取数据,那么新的数据顺序的数组即排列好的数组
* 桶内的排序可以自选,考虑到桶内的元素个数不是定长,直接采用ArrayList进行存储,利用Collections进行排序,这里只是为了说明桶排序的思想
* 时间复杂度:O(n+k)
* 空间复杂度:O(n+k)
*/
public class BucketSort implements Sort {
int BucketCount = 10; //定义十个桶
int maxIndex = 0;
int minIndex = 0;
@Override
public int[] sort(int[] src) {
int size = src.length;
findMaxAndMin(src);
int min = src[minIndex];
int max = src[maxIndex];
//计算每个桶的范围
int perCount = (int)Math.ceil(1.0f * (max - min + 1) / BucketCount);
//初始化ArrayList,因为其可以动态增加,目前并不知道每个桶的大小
ArrayList<ArrayList<Integer>> bucket = new ArrayList<>(BucketCount);
for(int i=0; i<BucketCount; i++) {
bucket.add(new ArrayList<Integer>());
}
//通过每个值的范围,将值加入到对应的桶里面
for(int i=0; i<size; i++) {
bucket.get((src[i] - min) / perCount).add(src[i]);
}
//对每个桶进行排序
for(int i = 0; i < BucketCount; i++){
Collections.sort(bucket.get(i));
}
//排序完成,按顺序取出每个桶的值
int index = 0;
for(int i=0; i<BucketCount; i++) {
for(int x : bucket.get(i)) {
src[index++] = x;
}
}
return src;
}
/*
* 找出数组中的最大值,最小值
*/
private void findMaxAndMin(int[] src) {
int size = src.length;
for(int i=0; i<size; i++) {
if(src[maxIndex] < src[i]) {
maxIndex = i;
}
if(src[minIndex] > src[i]) {
minIndex = i;
}
}
}
}
10. 基数排序:
package com.jaden.sort;
import java.util.ArrayList;
/*
* 基数排序
* 思想:适用于正整数的排序,先找出整个数组中的最大值,然后再判断该值一共有多少位,假如是5位数,那么从个位开始,遍历整个数组,将整个数组分成10个部分,个位数为0的存到第一个部分,个位数为1的存在第二个部分,以此类推
* 第一次循环完成,然后从第1部分到第10部分(也就是从个位数为0到个位数为9)依次给数组赋值,新的数组就是按个位数排序的数组了。然后开始第二次循环,从十位开始,还是一样,将十位数的数值分成十个部分,然后再生成新的数组
* 然后是百位,千位,万位依次重复,那么最后得到的数组就排好序了
* 仔细思考,假如我们要比较两个数的大小,那么先看最高位,如果最高位一样,那么看次高位,。。。依次向下,直到个位,那么就能区分两个值的大小了,这也是基数排序的思想
* 时间复杂度:O(n*k)
* 空间复杂度:O(n+k)
*/
public class RadixSort implements Sort {
@Override
public int[] sort(int[] src) {
//初始化list,用来存储每次比较后的临时变量,因为list可以动态增加
ArrayList<ArrayList<Integer>> tmpList = new ArrayList<>(10);
for(int i=0; i<10; i++) {
tmpList.add(new ArrayList<>());
}
int size = src.length;
int max = findMax(src);
int maxDigit = getDigit(max); //最大位数
int index;
for(int digit=1; digit <=maxDigit; digit++) {
for(int i=0; i<size; i++) {
tmpList.get(src[i] % (int)Math.pow(10, digit) / (int)Math.pow(10, digit-1)).add(src[i]); //分别存储在0-9的list中
}
index = 0;
for(int i=0; i<10; i++) {
for(int x : tmpList.get(i)) {
src[index++] = x; //每次排序完成后把数组改序
}
tmpList.get(i).clear(); //清除ArrayList
}
}
return src;
}
/*
* 找出数组中的最大值
*/
private int findMax(int[] src) {
int size = src.length;
int maxIndex = 0;
for(int i=0; i<size; i++) {
if(src[maxIndex] < src[i]) {
maxIndex = i;
}
}
return src[maxIndex];
}
/*
* 获取一个整数的位数
*/
private int getDigit(int a) {
if(a < 0) {
return 0;
}
int digit = 0;
int c = a;
do {
c = c / 10;
digit++;
}while(c > 0);
return digit;
}
}
代码差不多已经贴完了,网上有一些动图或者视频能够快速理解各个排序的思路,如果有条件的话,那么可以在当前链接上查看各个排序的视频:
https://www.youtube.com/watch?v=kPRA0W1kECg
Demo github地址: 点此获取