算法篇——几种排序算法的比较(侏儒排序、插入、归并、快排)

498 阅读12分钟

内容一 几种排序的比较

  1. 分别针对随机生成的若干组整数序列(比 如规模为1000个数,10000个数,100000 个数)进行排序,排序算法使用四种方法, 至少包括以下三种经典方法:插入排序算 法、合并排序算法和快速排序算法、近些年提出的比较新颖的排序算法
  2. 统计各种情况下排序所耗费的时间
  3. 改为降序
  4. 10个数,在使用快速排序进行排序时,尝试给出数组的演化情况
  5. 稳定性与空间性能

选择的第四个排序

侏儒排序

侏儒排序(英语:Gnome Sort)或愚人排序(英语:Stupid Sort)最初在2000年由伊朗计算机工程师Hamid Sarbazi-Azad提出,他称之为“愚人排序”。此后Dick_Grund也描述了这一算法,称其为“侏儒排序”。此算法类似于插入排序,但是移动元素到它该去的位置是通过一系列类似冒泡排序的移动实现的。从概念上讲侏儒排序非常简单,甚至不需要嵌套循环。

解释

下面是侏儒排序的伪代码

procedure gnomeSort(a[]):
    pos := 0
    while pos < length(a):
        if (pos == 0 or a[pos] >= a[pos-1]):
            pos := pos + 1
        else:
            swap a[pos] and a[pos-1]
            pos := pos - 1

样例

给定一个未排序的数组a = [5, 3, 2, 4],侏儒排序在while循环中执行以下步骤。粗体表示pos变量当前所指的元素。

当前数组下一步操作
[5, 3, 2, 4]a[pos] < a[pos-1],交换
[3, 5, 2, 4]a[pos] >= a[pos-1],pos自增
[3, 5, 2, 4]a[pos] < a[pos-1],交换;pos > 1,pos自减
[3, 2, 5, 4]a[pos] < a[pos-1],交换;pos <= 1,pos自增
[2, 3, 5, 4]a[pos] >= a[pos-1],pos自增
[2, 3, 5, 4]a[pos] < a[pos-1],交换;pos > 1,pos自减
[2, 3, 4, 5]a[pos] >= a[pos-1],pos自增
[2, 3, 4, 5]a[pos] >= a[pos-1],pos自增
[2, 3, 4, 5]pos == length(a),完成

随机数可能用到的拓展知识:

Arrays.stream(ints) 将基本类型数组转换为基本类型流。 int[ ] => IntStream .boxed() 将基本类型流转换为对象流。 => Stream< Integer > .collect(Collectors.toList()) 将对象流收集为集合。 => List< Integer >

.toArray(Integer[ ]::new) 将对象流转换为对象数组。=> Integer[ ]

.mapToInt(Integer::valueOf) 将对象流转换成基本类型流。=> IntStream .toArray() 将基本类型流转换为基本类型数组。 => int[ ]

list.stream() 将列表装换为对象流。List< Integer > => Stream< Integer >

实验过程:

一. 测试性能

1.随机数类,里面有两个方法用于生成随机数

第一个是真随机:int类型里所有整数随机,用于真正测试排序性能

第二个是100以内的随机数:(因为用第一个测试的时候发现生成的数字的位数都太长了,看着费劲)用于简单验证排序的正确性

import java.util.Random;
import java.util.stream.IntStream;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class RandomUtil {
    //真正的int类型的全范围random
    public static int[] generateRandomNum(int length){
        Random random = new Random();
        IntStream ints = random.ints(length);
        //int流转为 int数组
        return  ints.toArray();


    }

    //int范围内随机生成的数字都太长了,写一个100以内的随机数
    public static int[] randomBetween100(int length){
        int[] nums=new int[length];
        for (int i = 0; i < nums.length; i++) {
            nums[i]=(int)(Math.random()*100);
        }
        return nums;
    }
}
  1. 把四种排序算法封装成了一个类
import java.util.Arrays;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class SortCompare {
    //插入排序

    /**
     * 假设第一个有序,从前往后遍历依次将得到的数字插入前面的有序序列中
     * 不需要辅助空间
     */
    public static void insertSort(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        for (int i = 1; i < nums.length; i++) {
            int current = nums[i];//记录待插入数据
            int preIndex = i - 1;//记录有序数组最后一个数字的位置
            //挪位
            while (preIndex >= 0 && current < nums[preIndex]) {
                nums[preIndex + 1] = nums[preIndex];
                preIndex--;
            }
            nums[preIndex + 1] = current;
        }
        System.out.println(Arrays.toString(nums));

    }

    //归并排序
    /**
     * 二路归并排序
     * 包含1.两个有序数组合并成一个有序数组
     * 2.递归
     */
    //设定辅助数组长度
    private static int[] temp;

    public SortCompare(int length) {
        temp = new int[length];
    }

    //不要在merge函数里构造新数组,因为merge函数会被多次调用,影响性能
    private static void merge(int[] nums, int left, int right) {
        ;

        for (int i = left; i <= right; i++) {
            temp[i] = nums[i];
        }
        int middle = (left + right) / 2;
        int l = left;
        int r = middle + 1;

        for (int i = left; i <= right; i++) {
            //左边数组里的数字已经合并完,右边还有剩余
            if (l == middle + 1 && r < right + 1)
                nums[i] = temp[r++];
                //右边数组里的数字已经合并完,左边还有剩余
            else if (r == right + 1 && l < middle + 1)
                nums[i] = temp[l++];
            else if (temp[l] <= temp[r])
                nums[i] = temp[l++];
            else {
                nums[i] = temp[r++];

            }

        }

    }

    private static void Sort(int[] nums, int left, int right) {


        if (left < right) {
            Sort(nums, left, (left + right) / 2);
            Sort(nums, (left + right) / 2 + 1, right);
            merge(nums, left, right);

        }

    }

    public static void mergeSort(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        Sort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(nums));

    }


    //快速排序

    /**
     * 快速排序
     * 以数列第一个基准数,记录下基准数的值,并挖坑,从后往前找比基准数小的转移到坑中,
     * 从前往后找比基准数大的填入新坑
     * 最后low与high相遇时将基准数填入该处
     * 即完成一趟快排。
     * 一趟快排的结果是:基准数前面的数字都比它小,基准数后面的数字都比它大
     * 然后递归即可
     */
    private static void quicksort(int[] nums, int low, int high) {

        int base = nums[low];
        int l = low;
        int h = high;
        while (l < h) {
            while (l < h && nums[h] >= base)
                h--;
            if (nums[h] < base)
                nums[l++] = nums[h];
            while (l < h && nums[l] <= base)
                l++;
            if (nums[l] > base)
                nums[h--] = nums[l];
        }
        nums[l] = base;

        //一开始没加退出条件,陷入死循环了 栈溢出
        if (low < l)
            quicksort(nums, low, h - 1);
        if (high > h)
            quicksort(nums, l + 1, high);


    }

    public static void quickSortUtil(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        quicksort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(nums));

    }

    //侏儒排序

    /**
     * 在把大的往后挪的同时
     * 指针所指的数字的前面那些总是是有序的,中途有交换所以会打乱前面的排序,所以指针会向前挪作调整
     */
    public static void stupidSort(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        int pos = 0;
        while (pos < nums.length) {
            //如果指针所指的数字大于或者等于前一个,证明有序,指针往后挪
            if (pos == 0 || nums[pos] >= nums[pos - 1])
                pos++;
                //如果前面的数字比自己大,证明无序,就与前面的交换,然后调整
            else {
                int temp = nums[pos];//辅助空间就要这一个temp
                nums[pos] = nums[pos - 1];
                nums[pos - 1] = temp;
                pos--;
            }
        }
        System.out.println(Arrays.toString(nums));

    }


}

  1. 测试

首先用简单数据测试一下写得各个方法有没有问题

import java.util.Arrays;
import java.util.Scanner;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class TestValidity {
    public static void main(String[] args) {
        //如果想在同一个main方法里测试,测试的时候需要注意,要用同一个数组测试不同的排序方法,需要在每个方法里新建副本,用副本测试
        //因为java数组是传引用,在每个方法里排序,会直接作用到原数组,再用这个排好序的数组测试下面的排序方法就没有意义了
        //也可以选择在junit里测
        System.out.println("用小数据测试一下排序代码的正确性:");
        System.out.println("输入数组要测试的数组长度;");
        Scanner scanner=new Scanner(System.in);
        int i = scanner.nextInt();
        int[] nums=RandomUtil.randomBetween100(i);
        System.out.println("随机生成的数组为:");
        System.out.println(Arrays.toString(nums));

        System.out.println("输入数组为:"+Arrays.toString(nums));
        System.out.println("插入排序结果:");
        SortCompare.insertSort(nums);

        System.out.println("输入数组为:"+Arrays.toString(nums));
        System.out.println("合并排序结果:");
        //把合并排序的辅助数组new在外面了,可以提高性能
        SortCompare sortCompare = new SortCompare(i);
        SortCompare.mergeSort(nums);

        System.out.println("输入数组为:"+Arrays.toString(nums));
        System.out.println("快速排序结果:");
        SortCompare.quickSortUtil(nums);

        System.out.println("输入数组为:"+Arrays.toString(nums));
        System.out.println("侏儒排序结果:");
        SortCompare.stupidSort(nums);


    }
}

结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
用小数据测试一下排序代码的正确性:
输入数组要测试的数组长度;
20
随机生成的数组为:
[53, 82, 88, 47, 55, 4, 86, 38, 88, 0, 38, 51, 60, 70, 42, 32, 77, 40, 73, 38]
输入数组为:[53, 82, 88, 47, 55, 4, 86, 38, 88, 0, 38, 51, 60, 70, 42, 32, 77, 40, 73, 38]
插入排序结果:
[0, 4, 32, 38, 38, 38, 40, 42, 47, 51, 53, 55, 60, 70, 73, 77, 82, 86, 88, 88]
输入数组为:[53, 82, 88, 47, 55, 4, 86, 38, 88, 0, 38, 51, 60, 70, 42, 32, 77, 40, 73, 38]
合并排序结果:
[0, 4, 32, 38, 38, 38, 40, 42, 47, 51, 53, 55, 60, 70, 73, 77, 82, 86, 88, 88]
输入数组为:[53, 82, 88, 47, 55, 4, 86, 38, 88, 0, 38, 51, 60, 70, 42, 32, 77, 40, 73, 38]
快速排序结果:
[0, 4, 32, 38, 38, 38, 40, 42, 47, 51, 53, 55, 60, 70, 73, 77, 82, 86, 88, 88]
输入数组为:[53, 82, 88, 47, 55, 4, 86, 38, 88, 0, 38, 51, 60, 70, 42, 32, 77, 40, 73, 38]
侏儒排序结果:
[0, 4, 32, 38, 38, 38, 40, 42, 47, 51, 53, 55, 60, 70, 73, 77, 82, 86, 88, 88]

Process finished with exit code 0

测试通过,输入的数组都是一样的,排序结果也一样,没啥问题。

接下来分别生成1000、10000、100000个随机数来比较各个排序方法所需要的运行时间。

(把排序里面的输出部分的代码注释掉了)

import java.util.Scanner;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class TestPerformance {
    public static void main(String[] args) {

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("请输入随机生成的数组长度:");
            int i = scanner.nextInt();
            int[] nums = RandomUtil.generateRandomNum(i);
            long start = System.currentTimeMillis();
            SortCompare.insertSort(nums);
            long end = System.currentTimeMillis();
            System.out.println("插入排序用时:" + (end - start) + "毫秒");

            SortCompare sortCompare = new SortCompare(i);
            long start2 = System.currentTimeMillis();
            SortCompare.mergeSort(nums);
            long end2 = System.currentTimeMillis();
            System.out.println("合并排序用时:" + (end2 - start2) + "毫秒");

            long start3 = System.currentTimeMillis();
            SortCompare.quickSortUtil(nums);
            long end3 = System.currentTimeMillis();
            System.out.println("快速排序用时:" + (end3 - start3) + "毫秒");

            long start4 = System.currentTimeMillis();
            SortCompare.stupidSort(nums);
            long end4 = System.currentTimeMillis();
            System.out.println("侏儒排序用时:" + (end4 - start4) + "毫秒");


        }

    }
}

结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
请输入随机生成的数组长度:
1000
插入排序用时:5毫秒
合并排序用时:0毫秒
快速排序用时:1毫秒
侏儒排序用时:7毫秒
请输入随机生成的数组长度:
10000
插入排序用时:26毫秒
合并排序用时:5毫秒
快速排序用时:2毫秒
侏儒排序用时:94毫秒
请输入随机生成的数组长度:
100000
插入排序用时:1423毫秒
合并排序用时:12毫秒
快速排序用时:9毫秒
侏儒排序用时:6918毫秒
请输入随机生成的数组长度:
1000000

当数据量级达到100万的时候,因为先跑的是插入排序,所以我的电脑没反应了.....一直在计算(我不想等就直接退出了),可见插入排序面对大量数据效率很低。

再测一组:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
请输入随机生成的数组长度:
1000
插入排序用时:4毫秒
合并排序用时:1毫秒
快速排序用时:0毫秒
侏儒排序用时:6毫秒
请输入随机生成的数组长度:
10000
插入排序用时:21毫秒
合并排序用时:3毫秒
快速排序用时:2毫秒
侏儒排序用时:93毫秒
请输入随机生成的数组长度:
100000
插入排序用时:1418毫秒
合并排序用时:11毫秒
快速排序用时:9毫秒
侏儒排序用时:6395毫秒
请输入随机生成的数组长度:

二. 快排的实现步骤

 private static int count=0; 
private static void quicksort(int[] nums, int low, int high){ 
 ...
 //在递归开始之前添了一句话
 System.out.println("第"+(++count)+"趟基准数为:"+base+",排序结果为:"+Arrays.toString(nums));
 ...
 }

测试:

import java.util.Arrays;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class TestQuickSort {
    public static void main(String[] args) {
        int[] nums=RandomUtil.randomBetween100(10);
        System.out.println("生成的随机数组为:"+ Arrays.toString(nums));
        SortCompare.quickSortUtil(nums);

    }

}

结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"
生成的随机数组为:[24, 95, 7, 96, 33, 62, 34, 11, 72, 92]
第1趟基准数为:24,排序结果为:[11, 7, 24, 96, 33, 62, 34, 95, 72, 92]
第2趟基准数为:11,排序结果为:[7, 11, 24, 96, 33, 62, 34, 95, 72, 92]
第3趟基准数为:7,排序结果为:[7, 11, 24, 96, 33, 62, 34, 95, 72, 92]
第4趟基准数为:96,排序结果为:[7, 11, 24, 92, 33, 62, 34, 95, 72, 96]
第5趟基准数为:92,排序结果为:[7, 11, 24, 72, 33, 62, 34, 92, 95, 96]
第6趟基准数为:72,排序结果为:[7, 11, 24, 34, 33, 62, 72, 92, 95, 96]
第7趟基准数为:34,排序结果为:[7, 11, 24, 33, 34, 62, 72, 92, 95, 96]
第8趟基准数为:33,排序结果为:[7, 11, 24, 33, 34, 62, 72, 92, 95, 96]
第9趟基准数为:62,排序结果为:[7, 11, 24, 33, 34, 62, 72, 92, 95, 96]
第10趟基准数为:95,排序结果为:[7, 11, 24, 33, 34, 62, 72, 92, 95, 96]
[7, 11, 24, 33, 34, 62, 72, 92, 95, 96]

Process finished with exit code 0

三.改为逆序

所有改的地方都做了标注

import java.util.Arrays;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class ReverseSort {
    //插入排序
    public static void insertSort(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        for (int i = 1; i < nums.length; i++) {
            int current = nums[i];
            int preIndex = i - 1;
           //current < nums[preIndex]改成大于
            while (preIndex >= 0 && current > nums[preIndex]) {
                nums[preIndex + 1] = nums[preIndex];
                preIndex--;
            }
            nums[preIndex + 1] = current;
        }
        System.out.println(Arrays.toString(nums));

    }

    //归并排序

    private static int[] temp;

    public ReverseSort(int length) {
        temp = new int[length];
    }

    private static void merge(int[] nums, int left, int right) {
        ;

        for (int i = left; i <= right; i++) {
            temp[i] = nums[i];
        }
        int middle = (left + right) / 2;
        int l = left;
        int r = middle + 1;

        for (int i = left; i <= right; i++) {
            if (l == middle + 1 && r < right + 1)
                nums[i] = temp[r++];
            else if (r == right + 1 && l < middle + 1)
                nums[i] = temp[l++];
            //temp[l] <= temp[r]
            else if (temp[l] >= temp[r])
                nums[i] = temp[l++];
            else {
                nums[i] = temp[r++];

            }

        }

    }

    private static void Sort(int[] nums, int left, int right) {


        if (left < right) {
            Sort(nums, left, (left + right) / 2);
            Sort(nums, (left + right) / 2 + 1, right);
            merge(nums, left, right);

        }

    }

    public static void mergeSort(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        Sort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(nums));

    }


    //快速排序

    private static int count=0;
    private static void quicksort(int[] nums, int low, int high) {

        int base = nums[low];
        int l = low;
        int h = high;
        while (l < h) {
            //nums[h] >= base 改成<=
            while (l < h && nums[h] <= base)
                h--;
            //nums[h]<base 改成>
            if (nums[h]>base)
                nums[l++] = nums[h];
            //nums[l] <= base 改成>=
            while (l < h && nums[l] >= base)
                l++;
            //nums[l]>base 改成<
            if (nums[l] < base)
                nums[h--] = nums[l];
        }
        nums[l] = base;

        if (low<l)
            quicksort(nums, low, l - 1);
        if (high>h)
            quicksort(nums, l + 1, high);




    }

    public static void quickSortUtil(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        quicksort(nums, 0, nums.length - 1);
        System.out.println(Arrays.toString(nums));

    }

    //侏儒排序

    public static void stupidSort(int[] test) {
        int[] nums = Arrays.copyOf(test, test.length);
        int pos = 0;
        while (pos < nums.length) {
            //nums[pos] >= nums[pos - 1] 改成<=
            if (pos == 0 || nums[pos] <= nums[pos - 1])
                pos++;
            else {
                int temp = nums[pos];//辅助空间就要这一个temp
                nums[pos] = nums[pos - 1];
                nums[pos - 1] = temp;
                pos--;
            }
        }
         System.out.println(Arrays.toString(nums));

    }

}

测试:

import java.util.Arrays;

/**
 * @author SJ
 * @date 2020/10/14
 */
public class TestReverse {
    public static void main(String[] args) {
        int[] nums=RandomUtil.randomBetween100(10);
        System.out.println("随机生成的数组为:"+ Arrays.toString(nums));

        System.out.println("插入:");
        ReverseSort.insertSort(nums);
        System.out.println("归并:");
        ReverseSort reverseSort = new ReverseSort(10);
        ReverseSort.mergeSort(nums);
        System.out.println("快速:");
        ReverseSort.quickSortUtil(nums);
        System.out.println("侏儒:");
        ReverseSort.stupidSort(nums);
    }
}

测试结果:

"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
随机生成的数组为:[69, 57, 89, 83, 29, 17, 66, 91, 21, 76]
插入:
[91, 89, 83, 76, 69, 66, 57, 29, 21, 17]
归并:
[91, 89, 83, 76, 69, 66, 57, 29, 21, 17]
快速:
[91, 89, 83, 76, 69, 66, 57, 29, 21, 17]
侏儒:
[91, 89, 83, 76, 69, 66, 57, 29, 21, 17]

Process finished with exit code 0

四.稳定性

排序算法的稳定性是指在待排序的序列中,存在多个相同的元素,若经过排序后这些元素的相对词序保持不变,即xm=xnx_m=x_n,排序前m在n前,排序后m依然在n前,则称此时的排序算法是稳定的。

插入排序:

image-20201014164820371

从插入排序可以看出,其原理是在一个已经排好序的序列中依次插入一个新的元素。如果碰到相等的元素,就把新元素插入相等元素的后面,即他们原来的顺序没有变化,因此插入排序是稳定的。

如果line20改成

 while (preIndex >= 0 && current <= nums[preIndex])

则不稳定。

合并排序:

image-20201014165222779

从归并算法可以看出,其原理是将待排序列递归地划分为短序列,直到每部分都只包含一个元素,然后再合并,合并时如果两个元素相等也会按照元素之前的顺序,把下标小的元素先放入结果列表中,依然没有破环相同元素之间原本的顺序,因此归并算法也是稳定的。

如果line61改成

       else if (temp[l] < temp[r])

则不稳定

快速排序:

image-20201014165506870

快速排序是不稳定的,如“ 5 3 3 4 3 8 9 10 11”第一次切分,主元5要和元素3交换,即改变了3和另两个相等元素之间的顺序。

侏儒排序:

image-20201014170057469

前面的数字比自己大才交换位置,相等的话不交换。

如果line147改成

  if (pos == 0 || nums[pos] > nums[pos - 1])

则不稳定。

五.空间性能

插入:O(1)

归并:O(n)

快速:在递归调用前,仅会使用固定的额外空间。然而,如果需要产生O(nlgn)O(nlgn)嵌套递归调用,它需要在他们每一个存储一个固定数量的信息。因为最好的情况最多需要O(lgn)O(lgn)次的嵌套递归调用,所以它需要O(lgn)O(lgn)的空间。最坏情况下需要O(n)O(n)次嵌套递归调用,因此需要O(n)O(n)的空间。

侏儒:O(1)