数据结构——排序

简介: 数据结构——排序

引言



排序的稳定性


对于一组待排序的数据,数据中有两个或两个以上的相同的元素,在经过排序后,这两个或两个以上的相同元素与之前未排序的顺序相同,则说明此排序操作是稳定的。


举例说明:


在下图中,待排序数据 ① 经过排序成为 ② ,我们发现绿色数字5和红色数字5与排序前的顺序并未发生改变,那么排序 ② 就是稳定的。

反之,排序 ③ 就是不稳定的。


15d4b7ba6b774ed684c493d567198d11.png


三个稳定的排序


本篇博客所提到的稳定的排序:直接插入、冒泡、归并。


一、直接插入排序


思想:


从数组的第二个元素开始,将当前元素存为临时变量,然后与当前下标之前的位置进行比较元素,大的放后面,小的放前面。


/**
 * 直接插入排序
 * 时间复杂度(最坏情况下):O(n^2)
 * 时间复杂度最好情况下:O(n),即一组数据已经有序了
 * 所以,对于插入排序来说,待排序的一组数据趋于有序,那么排序的速度越快
 * 空间复杂度:O(1)
 * 是否稳定:稳定
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {8,5,9,4,7};
        insertSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void insertSort(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int temp = arr[i];
            int j = i-1;
            for (; j >= 0 ; j--) {
                if(arr[j] > temp){ //前面的元素比后面的元素大,就交换
                    arr[j+1] = arr[j];
                }else {
                    break;
                }
            }
            arr[j+1] = temp;
        }
    }
  //写成这样更简洁
  public static void insertSort2(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int temp = arr[i];
            int j = i-1;
            for (; j >= 0 && arr[j] > temp; j--) {
                arr[j+1] = arr[j];//前面的元素比后面的元素大,就交换
            }
            arr[j+1] = temp;
        }
    }


输出结果:


163187ec1951457aa0912d3aeced1f2c.png

c264fec1a3f248e7a9bfee78ad451694.png


二 、希尔排序



希尔排序本质上就是直接插入排序,只不过对其进行了分组。当我们给定了一组数据,直接插入排序直接对这组数据进行了排序,而希尔可以将其先分成某个组别,之后对这些组别分别进行排序操作。


让我们来看下面的一个例子,可以发现每当我们进行一次排序后,小的数字都比较靠前,而大的数字都比较靠后,这就会致使下一次的排序速度的更快。

关于时间复杂度这里我还没弄懂,不过大话数据结构这本书上说明:不同的增量序列所对应的时间复杂度不同,增量序列实际上对应的就是我们进行排序时分的组别元素的个数,这和算法很有关系,我暂且不作考虑了。。。


但是有一点很重要,就是:不管我们分了多少组,每一组有多少元素,我们都必须在最后的时候,分为一组。也就是说:我们最后要对整组数据进行排序。这很好理解,因为不论我们怎么分组排序,最后很大可能都不会把一组数据排序好我们想要的结果,但是我们最后一次排序的时候一定会快很多,因为最后一组数据非常趋近于我们想要的排序序列!


bea9690849f547fd99a16f4388d78ad8.png


/**
 * 希尔排序
 * 时间复杂度[ 和增量是有关系的 ]:O(n^1.3) - O(n^1.5)
 * 空间复杂度:O(1)
 * 稳定性:不稳定
 * 希尔排序在排序的过程中,发生了跳跃性的交换元素,
 * 在最坏的情况下,就很有可能发生了排序后的相同元素与排序前的顺序不匹配
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {4,12,5,8,9,14,11,6,15,2,13,1,3,10,7};
        shellSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    //希尔排序,传入 gap 进行分组
    public static void shellSort(int[] arr){
        int gap = arr.length;
        while(gap > 1){
            shell(arr, gap);
            gap = gap / 2; //[7组,3组,1组]
        }
        shell(arr, 1);
    }
    //开始排序
    public static void shell(int[] arr, int gap){
        for (int i = gap; i < arr.length; i++) {
            int temp = arr[i];
            int j = i - gap;
            for (; j >= 0; j = j-gap) {
                if(arr[j] > temp){
                    arr[j+gap] = arr[j];
                }else {
                    break;
                }
            }
            arr[j+gap] = temp;
        }
    }
}


输出结果:


99260768e69945be918b37022987ef90.png


三、选择排序


思想:


每一趟排序后,第一个元素一定是最小的。


/**
 * 时间复杂度:O(n^2)
 * 时间复杂度在最坏情况下或者是最好情况下都是 O(n^2)
 * 空间复杂度:O(1)
 * 稳定性:不稳定
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {8,5,9,4,7};
        selectSort2(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void selectSort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            for(int j = i+1; j < arr.length; j++){
                if(arr[i] > arr[j]){
                    int temp = arr[j];
                    arr[j] = arr[i];
                    arr[i] = temp;
                }
            }
        }
    }
  //选择排序优化
    public static void selectSort2(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            int minIndex = i;
            for(int j = i+1; j < arr.length; j++){
                if(arr[j] < arr[minIndex]){
                    minIndex = j;
                }
            }
            if(minIndex != i){ //如果两个数相同,就不进行交换
                int temp = arr[minIndex];
                arr[minIndex] = arr[i];
                arr[i] = temp;
            }
        }
    }
}


输出结果:


f7e304786fd249d68050627006243911.png


四、堆排序


堆排序在我之前的博客上有介绍,这里就不再赘述,主要阐明一下思想。


思想:


① 如果我们将待排序的一组数据进行升序,那么我们就先创建一个大顶堆,目的是始终将堆顶的元素置为最大值。

② 堆排序的实际操作就是将堆顶元素与堆尾元素进行交换。

③ 每次堆排序过后,就立即调整一次,调整为大顶堆,以便下一次排序。


/**
 * 堆排序
 * createMaxHeap 时间复杂度:O(n)
 * shiftDown 时间复杂度:O(log n)
 * heapSort 时间复杂度:O(n * log n) [ heapSort 函数里面嵌套了 shiftDown 函数 ]
 * =>总时间复杂度:O(n * log n) [ O(n) + O(n * log n) ]
 *
 * 空间复杂度:O(1)
 * 稳定性:跳跃式排序,不稳定(试想极端条件下,待排序的每个数据都为1)
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {8,5,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        createMaxHeap(arr);
        heapSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    //堆排序
    public static void heapSort(int[] arr){
        int end = arr.length - 1;
        while (end > 0){
            //1. 先交换
            int temp = arr[0];
            arr[0] = arr[end];
            arr[end] = temp;
            //2. 后调整
            //每次调整的其实都是树的根节点及根节点向下
            shiftDown(arr,0, end);
            end--;
        }
    }
    //创建一个大顶堆,目的是始终将堆顶的元素置为最大值
    public static void createMaxHeap(int[] arr){
        int child = arr.length - 1;
        //处理每棵树的顺序
        for (int parent = (child-1)/2; parent >= 0 ; parent--) {
            shiftDown(arr,parent,arr.length);
        }
    }
  //向下调整
    public static void shiftDown(int[] arr, int parent, int len){
        int child = parent*2 + 1;
        while(child < len){
            if(child+1 < len && arr[child] < arr[child+1]){
                child++;
            }
            if(arr[child] > arr[parent]){
                int temp = arr[parent];
                arr[parent] = arr[child];
                arr[child] = temp;
            }
            parent = child;
            child = parent*2 + 1;
        }
    }
}


输出结果:


933cb0b34aa7468f9ca10bb6b69db19e.png


五、冒泡排序


思想:


将紧挨着的数据进行比较,一趟代表遍历完数组一次,而每一趟冒泡排序之后,最大的元素一定放在最后一位。


5da418610fed46cc8bafb985c0a47724.png868fcdef295e46f191c23e679db90bc6.png


优化分析:


在内层 for 循环中的 if 语句中,判断布尔类型 isSorted ,如果进入了 if 语句,说明进行了交换,isSorted 置为 false;反之,如果某一趟的 if 语句一次也没进去,isSorted 就为初始的 true,说明从这一趟往后,数据已经有序。


cc3dad7026c846ee86c133851e12b1c0.png


/**
 * 冒泡排序
 * 时间复杂度:O(n^2)
 * 空间复杂度:O(1)
 * 稳定性:稳定
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {8,5,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        bubbleSort2(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length-1; i++) { //假设待排序有5个数据,只比较了4趟
            for (int j = 0; j < arr.length-1-i; j++) { //每一趟比较了多少次
                if(arr[j] > arr[j+1]){
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
        }
    }
  //冒泡排序优化
    public static void bubbleSort2(int[] arr){
        //举个例子 arr = {4,3,5,6,7}, 第一趟冒泡排序结束以后,实际上已经有序
        for (int i = 0; i < arr.length-1; i++) { 
            boolean isSorted = true;
            for (int j = 0; j < arr.length-1-i; j++) {
                if(arr[j] > arr[j+1]){
                //若某一趟一次也没有交换,那么isSorted 还是为 true,即数组已经有序
                    int temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    isSorted = false;
                }
            }
            if(isSorted){
                break;
            }
        }
    }
}


输出结果:


9870ba805fa84612b31753ff36f26d6f.png


六、快速排序


思想:


通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录进行排序,以达到整个序列有序的目的。


2985b8489181434ab30e710f0dc80733.png


步骤总结:


(1)选择枢轴(基准、pivot)

(2)分割数据

① 从后面序列找比枢轴小的放到枢轴前面

② 从前面序列找枢比轴大的放到枢轴后面

通过枢轴分割数据,使得枢轴左边的值比枢轴小,枢轴右边的值比枢轴大

(3)重复递归被枢轴分割的数据,可以先处理左边的数据,再处理右边的数据


1. 快速排序递归写法


/**
 * 快速排序
 * 递归写法
 * 最坏时间复杂度:O(n^2), 最好时间复杂度:O(n*logn)[刚好平均分割整个数据]
 * 最坏空间复杂度:O(n), 最好空间复杂的:O(logn)
 * 稳定性:不稳定
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {5,2,3,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        int start = 0;
        int end = arr.length-1;
        quickSort(arr,start,end);
        System.out.println(Arrays.toString(arr));
    }
    public static void quickSort (int[] arr, int start, int end){
        if(start >= end){ //边界条件
            return;
        }
        int i = start;
        int j = end;
        int temp = arr[i];
        while (i < j){
            //1. 从后面序列找比枢轴小的放到枢轴前面
            //whille 循环中的"="防止死循环重复赋值, i<j 防止此次循环数组越界
            while (i < j && arr[j] >= temp){
                j--;
            }
            arr[i] = arr[j];
            //2. 从前面序列找比枢轴大的放到枢轴后面
            while (i < j && arr[i] <= temp){
                i++;
            }
            arr[j] = arr[i];
        }
        arr[i] = temp;//填上缺口
        int pivot = i; //拿到每次新的枢轴
        //3. 分别递归求左右两边的序列
        quickSort(arr,start,pivot-1);
        quickSort(arr,pivot+1,end);
    }
}


输出结果:


391b87ff45a04820bcaa68f1fca3e19c.png


图解分析:


736abbc00e33464f9c99774f1e7895c3.png0feb04b4561f4a98b2002bd2694c5813.png


2. 优化快速排序


(1)随机选取基准


随机选取基准的时候,会带来不确定的因素,很有可能优化不成功。


(2)使用三数取中法来确定分割的基准,一般取左端、右端及中间的三个数,而这在这三个数中,中间大小的数被确定为基准。比方说:[ 3 1 5 ],我们取 3,因为最小数为 1,最大数为 5。这样可以避免两种极端的情况:


① 当最大或最小的数分布在数据的两边

② 当数据整体有序的情况


(3) 将和基准相同的元素,从分散的位置聚集到一起,这样会使递归的次数减少,从而使程序运行速度更快。


f416c08fde8242ba978305b5c334a808.png


(4)当分割一定的长度后,使用直接插入排序。


3. 三数取中法优化


/**
 * 快速排序优化
 * 三数取中法
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {6,1,2,7,9,3,4,5,10,8};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        //int[] arr = {5,2,3,9,4,7};
        quickSort(arr,0,arr.length-1);
        System.out.println(Arrays.toString(arr));
    }
    public static void quickSort(int[] arr, int start, int end){
        if(start >= end){
            return;
        }
        //三数取中法,防止取到顺序/逆序的情况
        int midValIndex = findMidValIndex(arr, start, end); 
        //交换基准
        int temp = arr[start];
        arr[start] = arr[midValIndex];
        arr[midValIndex] = temp;
        int base = partition(arr,start,end); //接收基准
        quickSort(arr,start,base-1);
        quickSort(arr,base+1,end);
    }
  //找中间值
    public static int findMidValIndex(int[] arr, int start, int end){
        int mid = (start+end) / 2;
        if(arr[start] < arr[end]){
            if(arr[mid] < arr[start]){
                return start;
            }else if(arr[mid] > arr[end]){
                return end;
            }else {
                return mid;
            }
        }else {
            if(arr[mid] < arr[end]){
                return end;
            }else if(arr[mid] > arr[start]){
                return start;
            }else {
                return mid;
            }
        }
    }
    //寻找基准
    public static int partition(int[] arr, int start, int end){
        int temp = arr[start];
        while(start < end){
            //在数据的后半部分寻找比基准小的
            while(start < end && arr[end] >= temp){ // “=” 防止死循环,防止重复赋值
                end--;
            }
            arr[start] = arr[end];
            //在数据的前半部分寻找比基准大的
            while(start < end && arr[start] <= temp){
                start++;
            }
            arr[end] = arr[start];
        }
        arr[start] = temp;
        return start;
    }
}


4. 快速排序的非递归写法


/**
 * 快速排序
 * 非递归写法
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {5,2,3,9,4,7};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        int start = 0;
        int end = arr.length-1;
        quickSort(arr,start,end);
        System.out.println(Arrays.toString(arr));
    }
    public static void quickSort(int[] arr, int start, int end){
        Stack<Integer> stack = new Stack<>();
        int pivot = partition(arr,start,end);
        if(start+1 < pivot){
            stack.push(start);
            stack.push(pivot-1);
        }
        if(pivot < end-1){
            stack.push(pivot+1);
            stack.push(end);
        }
        while (!stack.isEmpty()){
            end = stack.pop();
            start = stack.pop();
            pivot = partition(arr,start,end);
            if(start+1 < pivot){
                stack.push(start);
                stack.push(pivot-1);
            }
            if(pivot < end-1){
                stack.push(pivot+1);
                stack.push(end);
            }
        }
    }
    public static int partition (int[] arr, int i, int j){
        int temp = arr[i];
        while (i < j){
            //1. 从后面序列找比枢轴小的放到枢轴前面
            //"="防止死循环重复赋值, i<j 防止此次循环数组越界
            while (i < j && arr[j] >= temp){
                j--;
            }
            arr[i] = arr[j];
            //2. 从前面序列找枢比轴大的放到枢轴后面
            while (i < j && arr[i] <= temp){
                i++;
            }
            arr[j] = arr[i];
        }
        arr[i] = temp;
        return i;//拿到每次的枢轴
    }
}


七、归并排序


1. 合并两个有序数组


理解归并排序之前,一定要知道两个已经有序的数组是怎么合并的,并理解它们的思想。


思想:


① 定义 s1 为 arr1 数组的初始下标,e1 为 arr1 数组的末尾下标。

② 定义 s2 为 arr2 数组的初始下标,e2 为 arr2 数组的末尾下标。

③ 将 s1 和 s2 进行比较,较小者放入新的数组中,紧着,让 s1 / s2 进行遍历,之后循环重复实现以上的操作,直到有一个数组遍历完,就可以将另一个未遍历完的数组中的剩下元素放入新数组的末尾。


9c2813ab2d134fe695bdb5d36eb3eb48.png


代码较为简单,不再赘述。


/**
 * 合并两个有序的数组
 */
public class Test {
    public static void main(String[] args) {
        int[] arr1 = {1,6,7,9};
        int[] arr2 = {2,3,5,8};
        int[] arr = merge(arr1,arr2);
        System.out.println(Arrays.toString(arr));
    }
    public static int[] merge(int[] arr1, int[] arr2){
        int[] arr = new int[arr1.length+arr2.length];
        int s1 = 0;
        int e1 = arr1.length-1;
        int s2 = 0;
        int e2 = arr2.length-1;
        int i = 0;
        while (s1 <= e1 && s2 <= e2){
            if(arr1[s1] < arr2[s2]){
                arr[i++] = arr1[s1++];
            }else {
                arr[i++] = arr2[s2++];
            }
        }
        while (s1 <= e1){
            arr[i++] = arr1[s1++];
        }
        while (s2 <= e2){
            arr[i++] = arr2[s2++];
        }
        return arr;
    }
}


输出结果:


ade6243f1b7d4c11af6059e5ea985623.png


现在我们来看一下归并排序的思想:


归并排序就是先在一个待排序数组中选择一个中间下标对应元素(并不是大小为中间元素),通过这个元素分割成两个逻辑上的数组,在这两个数组中,再寻找中间对应元素,之后不断分割,直至某个数组无法再分。这样一来,就可以对某一个数组进行有序排序,之后再进行整合,而这里每次整合过后,都要回归原始数组中。


在下图分析中,拆就是递的过程,合就是归的过程,所以这体现了递归的思想。


a4c06c34bae2435abaa24e203c268a5d.png


2. 归并排序递归写法


/**
 * 归并排序
 * 时间复杂度:O(n*logn)
 * 空间复杂度:O(n)
 * 稳定性:稳定
 */
public class Test {
    public static void main(String[] args) {
        //int[] arr = {9,6,7,1,3,8,5,2};
        int[] arr = {8,5,9,2,6,11,26,56,35,49,99,70,21,2,4,7,2};
        int low = 0;
        int high = arr.length-1;
        mergeSort(arr,low,high);
        System.out.println(Arrays.toString(arr));
    }
    public static void mergeSort(int[] arr, int low, int high){
        if(low == high){
            return;
        }
        int mid = (low+high) / 2;
        mergeSort(arr,low,mid); //先拆左边
        mergeSort(arr,mid+1,high); //再拆右边
        merge(arr,low,mid,high); //拆完过后,开始合并
    }
    //合并
    public static void merge(int[] arr, int low, int mid, int high){
        int[] temp = new int[high-low+1];
        int i = 0;
        int s1 = low;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = high;
        while (s1 <= e1 && s2 <= e2){
            if(arr[s1] < arr[s2]){
                temp[i++] = arr[s1++];
            }else {
                temp[i++] = arr[s2++];
            }
        }
        while (s1 <= e1){
            temp[i++] = arr[s1++];
        }
        while (s2 <= e2){
            temp[i++] = arr[s2++];
        }
        //将 temp 中的数组拷贝至原数组中
        for (int j = 0; j < i; j++) {
            arr[j+low] = temp[j]; //”j+low“ 以防对原数组进行覆盖
        }
    }
}


输出结果:




3. 归并排序非递归写法


/**
 * 归并排序
 * 非递归写法
 */
public class Test {
    public static void main(String[] args) {
        int[] arr = {9,6,7,1,3,8,5,2};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void mergeSort(int[] arr){
        int size = 1; // 第一次归并两个元素,第二次归并四个元素...
        while (size < arr.length){
            for (int i = 0; i < arr.length; i += size*2) {
                int low = i;
                int mid = low+size-1;
                if(mid >= arr.length){
                    mid = arr.length-1;
                }
                int high = mid+size;
                if(high >= arr.length){
                    high = arr.length-1;
                }
                merge(arr,low,mid,high);
            }
            size *= 2;
        }
    }
    public static void merge(int[] arr, int low, int mid, int high){
        int[] ret = new int[high-low+1];
        int s1 = low;
        int e1 = mid;
        int s2 = mid+1;
        int e2 = high;
        int i = 0;
        while (s1 <= e1 && s2 <= e2){
            if(arr[s1] < arr[s2]){
                ret[i++] = arr[s1++];
            }else {
                ret[i++] = arr[s2++];
            }
        }
        while (s1 <= e1){
            ret[i++] = arr[s1++];
        }
        while (s2 <= e2){
            ret[i++] = arr[s2++];
        }
        for (int j = 0; j < i; j++) {
            arr[j+low] = ret[j];
        }
    }
}


输出结果:

684f33b639084b7cb76f69241fa4c9f9.png


八、了解三个不需要比较数据就能进行排序的方法


1. 基数排序


说明:可以将下面装数据的容器想象成队列的结构,而不是栈。

下图只是为了方便演示,所以才会画成这个容器哦。


e930a6df2d3c4f10bb1e2e11f18e1503.png


2. 计数排序


636c437b5dee48ad863fcc185804793f.png


3. 桶排序


f5b609ce9bf642b9af59f44e8d2dadba.png



目录
相关文章
|
21天前
|
C语言
数据结构——排序【上】
数据结构——排序【上】
16 2
|
1月前
|
搜索推荐 算法 测试技术
【数据结构】排序
【数据结构】排序
|
2月前
|
搜索推荐
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
【数据结构常见七大排序(三)上】—交换排序篇【冒泡排序】And【快速排序】
|
2月前
|
搜索推荐
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
|
2月前
|
搜索推荐 测试技术
【数据结构常见七大排序(一)】—插入排序篇【直接插入排序】And【希尔排序】
【数据结构常见七大排序(一)】—插入排序篇【直接插入排序】And【希尔排序】
|
3月前
|
搜索推荐 算法
【排序】数据结构——排序算法概念及代码详解(插入、冒泡、快速、希尔)
【排序】数据结构——排序算法概念及代码详解(插入、冒泡、快速、希尔)
|
4月前
|
存储 搜索推荐 算法
C语言数据结构算法,常用10种排序实战
插入排序(Insertion Sort) 希尔排序(Shell Sort) 选择排序(Selection Sort) 冒泡排序(Bubble Sort) 归并排序(Merge Sort) 快速排序(Quick Sort) 堆排序(Heap Sort) 基数排序(Radix Sort)
43 1
C语言数据结构算法,常用10种排序实战
TU^
|
3月前
|
搜索推荐 算法 测试技术
数据结构~~排序
数据结构~~排序
TU^
25 1
|
3月前
|
搜索推荐 算法 测试技术
数据结构——排序
数据结构——排序
26 1
|
3月前
|
搜索推荐
深入理解数据结构第六弹——排序(3)——归并排序
深入理解数据结构第六弹——排序(3)——归并排序
20 1