排序算法(上)

排序算法 (O(n^2)时间复杂度)

Posted by CDz on November 3, 2017

排序算法(上)

1.排序算法 (O(n^2)时间复杂度)


1.1选择排序

核心思想

从第一个元素开始,扫描后面所有元素,当比其小时就进行交换。

    //外循环 扫描所有的元素
    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);
            }
        }
    }

1.2插入排序

核心思想

默认将整个数组,分成了 一个是有序的,一个是无序的。关键点在于,将一个元素放入一个有序数组,且保持其数组有序。

    //数组的第二个元素开始,因为此时默认第一个元素为有序的
    for(int i = 1 ; i < arr.length ; i++){
        //遍历这个默认有序的数组,找到 *第一个* 小于这个无序元素的位子 停止
        for(int j = i ; j > 0 && arr[j] < arr[j - 1]; j --){
            swap(arr,j,j-1);
        }
    }


    //优化,思路减少交互次数。
    //可以将 无序的元素,复制一份,作为对比,大于其的就向前挪一位。最后找到小于其的跳出循环,(减少赋值次数)
    for(int i = 1 ; i < arr.length ; i++){
        int e = arr[i];
        int j;
        //遍历这个默认有序的数组,找到 *第一个* 小于这个无序元素的位子 停止
        for(j = i ; j > 0 && e < arr[j - 1]; j --){
            arr[j] = arr[j - 1];
        }
        arr[j] = e;
    }

优点

当遇到一个近乎有序的数组时,可以非常快的排序完成,时间复杂度为O(n)级别的。在对于其他算法的优化时也非常的有用,因为在小于一定数量的数组,其本身就有序性很强,使用插入排序效率很高。

1.3冒泡排序

核心思想

两两交互,最大的通过两两交互到最右边。其过程每一步都是将最大的哪一个往后移,很像水中的泡泡慢慢上升一样,所以顾名思义。

    for(int i = 0 ; i < arr.length ; i++){
        for(int j = 1 ; j < arr.length - i ; j++){
            if(arr[j] < arr[j - 1]){
                swap(arr,j - 1,j);
            }
        }
    }

    //优化
    // 优化核心: 减少循环次数
    // 减少 外循环次数:设置一个标记,当内循环没有一次交换时就跳出
    // 减少 内循环次数:控制循环的上限,将上限设置为 内循环最后一次交换的位置。
    // 也有缺点 就是 标记为太多,有时可能会 影响性能。
    int i,j,pos;
    int k = arr.length;
    boolean flag = false;
    for(i = 0 ; i < arr.length ; i++){
        for(j = 0 ; j < k ; j++){
            flag = true;
            if(arr[j] < arr[j - 1]){
                flag = fasle;
                swap(arr,j,j-1);
                pos = j;
            }
        }
        k = pos;
        if(flag){
            break;
        }
    }

1.4希尔排序

核心思想

是插入排序的一种优化,数组全部个数的一半为长度size,取出数组中,从第一个开始,此元素相隔size个的元素,进行插入排序,后面的元素依次内推。外部,每次size减少一半,直到最后size为1停止。

排序分解图:

希尔排序分解图

思想理解:

这样的思想,最大利用了插入排序对于有序数组的快捷性。当size非常大时,其内循环的组数就比较多,且需要排序元素却少。当内循环的组数少时。元素比较多,但是其元素在之前已经被粗密度的排过一次序了。

一句话就是:开始 粗密度,组数多,元素少,排序快。后面 细密度,组数少,元素多,但是近乎有序。

    int d = arr.length;
    while(true){
        d = d/2;
        for(int i = 0; i < d; i++ ){
            for(int j = 0; j < arr.length;j+=d){
                int e = arr[j];
                int l ;
                for(l = j - d ; l > 0 && arr[l] > e ; l = l - d){
                    //这里加d的原因是,此时的元素,后移一位。
                    arr[l+d] = arr[l]; 
                }
                //这里+d 是因为 第一次初始 l 减了一个d。
                arr[l + d] = e;
            }
            if(d==1){
                break;
            }
        }
    }

1.5总结测试

测试

测试用例 长度 50_000,元素随机范围为 [0,50_000]
第一次:
selectSort-->5.35 s
insertSort-->5.119 s
bubbleSortInt-->3.366 s
betterBubbleSortInt-->3.534 s
shellSort-->0.008 s
第二次:
selectSort-->5.224 s
insertSort-->5.169 s
bubbleSortInt-->3.484 s
betterBubbleSortInt-->3.561 s
shellSort-->0.009 s


测试用例 长度 10_000,元素随机范围为 [0,10_000]
第一次:
selectSort-->0.173 s
insertSort-->0.167 s
bubbleSortInt-->0.191 s
betterBubbleSortInt-->0.151 s
shellSort-->0.003 s
第二次:
selectSort-->0.161 s
insertSort-->0.159 s
bubbleSortInt-->0.137 s
betterBubbleSortInt-->0.152 s
shellSort-->0.005 s

测试用例 长度 10_000,元素随机范围为 [0,20]
第一次:
selectSort-->0.084 s
insertSort-->0.098 s
bubbleSortInt-->0.142 s
betterBubbleSortInt-->0.153 s
shellSort-->0.004 s
第二次:
selectSort-->0.074 s
insertSort-->0.088 s
bubbleSortInt-->0.161 s
betterBubbleSortInt-->0.159 s
shellSort-->0.003 s

测试用例 长度 1_000_000,元素随机范围为 [0,1_000_000]
shellSort-->0.213 s

测试用例 长度 1_000_000,元素随机范围为 [0,20]
shellSort-->0.1 s

测试用例 长度 10_000_000,元素随机范围为 [0,10_000_000]
shellSort-->3.529 s

测试用例 长度 10_000_000,元素随机范围为 [0,20]
shellSort-->1.922 s

总结

  • 可以看出 冒泡排序 数组 数量多且,重复度低的情况下要比 插入与选择排序的效率好。
  • 当 数组元素重复度高时,选择排序与插入排序的效率比冒泡排序要好。
  • 而冒泡排序的优化,其实并不理想,因为中间存在了大量的多余赋值操作。
  • 希尔排序 无论哪一种数据都很快
    • 希尔排序,可以最大接受范围 百万级别的排序 0.2s
    • 排序效率与元素的重复程度有关
    • 进而在证实 其中是插入排序进化版