算法 - Java实现八大内排序算法(图解)

排序分类

1、内部排序
指将需要处理的所有数据都加载到内部存储器中进行排序。
2、外部排序
数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
算法 - Java实现八大内排序算法(图解)_第1张图片

一、冒泡排序

冒泡排序也属于内部排序法,属于交换排序

基本思想

通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。

图解

int[] nums = new int[]{79,56,90,4,32,27,8};

①第一轮,把最大的数沉到最下面。
79与56相比,大于56,交换
79与90相比,相等,不变
90与4相比,大于4,交换
90与32相比,大于32,交换
90与27相比,大于27,交换
90与8相比,大于8,交换
算法 - Java实现八大内排序算法(图解)_第2张图片
②第二轮,把第二大的数沉到倒数第二。
56与79相比,小于,不变
79与4相比,大于,交换
79与32相比,大于,交换
79与27相比,大于,交换
90与8相比,大于,交换
算法 - Java实现八大内排序算法(图解)_第3张图片
③第n轮,以此类推…

代码实现

public class BubbleSortDemo {
    public static void main(String[] args) {
        int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        bubbleSort(nums);
        System.out.println("\n排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 冒泡排序算法
     * @param nums 要排序的数组
     */
    public static void bubbleSort(int[] nums){
        boolean isSort = false; // 在一次流程中是否排过序
        int temp = 0;           // 临时变量
        for (int i = 0; i < nums.length; i++){
            for (int j = 0; j < nums.length - 1 - i; j++){
                if (nums[j] > nums[j + 1]){  // 如果前面的数大于后面的数,交换
                    temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                    // 因为排序发生过变动,所以需要继续排序
                    isSort = true;
                }
            }
            if (isSort){
                // 发生了变动,重置,继续排序
                isSort = false;
            }else{
                // 没有发生变动,说明已经排好序了
                break;
            }
        }
    }
}

二、快速排序

快速排序(Quicksort)是对冒泡排序的一种改进,属于交换排序

基本思想

通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

图解

int[] nums = new int[]{11,21,28,12,5,8,30};

①第一轮
算法 - Java实现八大内排序算法(图解)_第4张图片
②第二轮、第三轮
算法 - Java实现八大内排序算法(图解)_第5张图片
③分成两组之后,递归开始
算法 - Java实现八大内排序算法(图解)_第6张图片

代码实现

public class quickSortDemo {

    public static void main(String[] args) {
        int a = (int) Math.ceil(9 / 2);
        System.out.println(a);

        int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        quickSort(nums,0,nums.length - 1);
        System.out.println("\n排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 快速排序算法
     * @param nums 要排序的数组
     * @param left 数组最左端记录的下标
     * @param right 数组最右端记录的下标
     */
    public static void quickSort(int[] nums, int left, int right){
        if (left > right){
            // 不合理
            return;
        }
        // 定义两个变量(哨兵)来接管left和right,left和right后面会用得到
        int l = left;
        int r = right;
        // 定义一个参考值,这里以最右端的作为参考值
        int reference = nums[l];
        // 定义一个临时变量,用于交换使用
        int temp = 0;
        while (l != r){  // 只要两个哨兵没有碰头就继续找
            // 参考值取的是左边的,则让右边的先找比参考值小的数
            while(nums[r] >= reference && l < r){
                // 没找到,继续往前找
                r--;
            }
            // 右边已经找到了,让左边的去找比参考值大的数
            while(nums[l] <= reference && l < r){
                // 没找到,继续往后找
                l++;
            }
            // 两个都找到了,就互相交换值之后,再继续找,直到碰面
            temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
        }
        // 循环结束,表明l和r已经碰面(找到同一个数字上去了)
        // 这时就要将碰面的这个值与参考值交换
        temp = nums[l];
        nums[l] = nums[left];
        nums[left] = temp;
        // l和r碰面的地方将原本数组分割成两个,再对这两个进行上诉操作,直到无法操作就完成排序了
        // 也就是说碰面的地方已经放到了属于他应该在的位置,不用管了
        quickSort(nums,left,l - 1);
        quickSort(nums,r + 1,right);
    }
}

三、直接插入排序

直接插入排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的,属于插入排序

基本思想

把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

图解

int[] nums = new int[]{4,56,79,32,27,8,90};

初始状态,就默认第一个数已经插入好了(即在有序表里)。
第1次插入,第二个数和有序表(即图中红色框起来的数据)里的做对比,有序表里找不到比这个数大的,所以不变。第2次也是如此。
第3次插入,32比79小,79后移,32又比56小,56后移。32比4大,找到了位置了。即下标为1的地方。
第n-1次插入,依次类推…
总的来说,就是把一个数组分成两个,一个有序表,一个无序表。初始阶段,有序表就一个元素,无序表n-1个元素。把无序表里的每个元素拿出来,插入到有序表里去。
算法 - Java实现八大内排序算法(图解)_第7张图片

代码实现

public class InsertSortDemo {
    public static void main(String[] args) {
        int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        insertSort(nums);
        System.out.println("\n排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 插入排序算法
     * @param nums 要排序的数组
     */
    public static void insertSort(int[] nums){
        int insertVal = 0;      // 要插入的数据
        int insertIndex;        // 要插入的位置
        for (int i = 1; i < nums.length; i++){
            insertVal = nums[i];   // 当前要插入的数据
            insertIndex = i - 1;    // 要插入的位置,肯定在这个要插入数据的前面找
            /**
             * 1、要保证不越界,如果insertIndex==0,表示插入到最前端
             * 2、其次保证前面没有数比这个更大
             */
            while(insertIndex >= 0 && insertVal < nums[insertIndex]){   // 开始找插入的位置
                // 至少找到一个比这个数要大的,那么就把比这个数大的数往后移动
                nums[insertIndex + 1] = nums[insertIndex];
                insertIndex--;   // 继续往前找
            }
            // 退出循环,表明再也找不到比这个数大的了,即找到了要插入的位置
            if((insertIndex + 1) != insertIndex){  // 如果要插入的位置就是自己,则表明顺序是对的,无需赋值
                nums[insertIndex + 1] = insertVal;
            }
        }
    }
}

四、希尔排序

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序

基本思想

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

图解

int[] nums = new int[]{4,56,79,32,27,8,90,65,30,63};

算法 - Java实现八大内排序算法(图解)_第8张图片
算法 - Java实现八大内排序算法(图解)_第9张图片

代码实现

public class ShellSortDemo {

    public static void main(String[] args) {
        int a = (int) Math.ceil(9 / 2);
        System.out.println(a);

        int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        shellSortByExchange(nums);
        System.out.println("\n排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 希尔排序算法
     *
     * @param nums 要排序的数组
     */
    public static void shellSortByExchange(int[] nums) {
        // 第一轮循环,确定间隔(增量)/分了几组
        for (int gap = nums.length / 2; gap > 0; gap /= 2){
            // 分了几组,就对几组进行操作
            for(int i = 0; i < gap; i++){  // 这里的i,其实就是每一个组的第一个元素
                for (int j = i + gap; j < nums.length; j++){  // 直接插入法
                    int insertIndex = j - gap;   // 要插入的位置
                    int insertValue = nums[j];   // 要插入的位置
                    while(insertIndex >= 0 && insertValue < nums[insertIndex]){
                        nums[insertIndex + gap] = nums[insertIndex];  // 后移
                        insertIndex -= gap;  // 继续往前找
                    }
                    // 退出循环,表示已经找到
                    if((insertIndex + gap) != j){    // 如果要插入的位置就是自己,则表明顺序是对的,无需赋值
                        // 这里为什么要insertIndex + gap呢?
                        // 因为在前面的循环里,找到了插入的下标,但是它认为不是最合适的,所以继续往前找
                        // 结果不符合循环条件了,但是下标却往前移动了,要移动回来才对
                        nums[insertIndex + gap] = insertValue;
                    }
                }
            }
        }
    }
}

五、简单选择排序

简单选择排序(select sorting)也是一种简单的排序方法,属于选择排序

基本思想

第一次从nums[0] ~ nums[n-1] 中选取最小值,与 nums[0] 交换,第二次从 nums[1]~nums[n-1] 中选取最小值,与 nums[1] 交换,第三次从 nums[2] ~ nums[n-1] 中选取最小值,与nums[2]交换,…,第i次从 nums[i-1] ~ nums[n-1] 中选取最小值,与nums[i-1]交换,…, 第n-1次从 nums[n-2] ~ nums[n-1] 中选取最小值,与nums[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

图解

int[] nums = new int[]{79,56,4,32,27,8,90};

①第一轮,把最小值找出来,放到最上面。
79与56相比,新的最小值56
56与4相比,新的最小值4
4与32相比,最小值不变
4与27相比,最小值不变
4与8相比,最小值不变
4与90相比,最小值不变
完成一轮比较,最小值是4,与第一个数字进行交换。
算法 - Java实现八大内排序算法(图解)_第10张图片
②第二轮,从后面的数中找出最小值,放在后面的数的最上面(全局第二)。
56与79相比,最小值不变,为56
56与32相比,新的最小值32
32与27相比,新的最小值27
27与8相比,新的最小值8
8与90相比,最小值不变
算法 - Java实现八大内排序算法(图解)_第11张图片
③第n-1轮,依次类推…

代码实现

public class SelectSortDemo {
    public static void main(String[] args) {
        int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        selectSort(nums);
        System.out.println("\n排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 选择排序算法
     * @param nums 要排序的数组
     */
    public static void selectSort(int[] nums){
        for (int i = 0; i < nums.length - 1; i++){
            int minIndex= i;        // 最小值的下标
            int minNum = nums[i];   // 最小值
            for (int j = i + 1; j < nums.length; j++){
                if(minNum > nums[j]){   // 找到比最小值还要小的
                    // 重置最小值及其下标
                    minNum = nums[j];
                    minIndex = j;
                }
            }
            // 一轮比较完毕,找到真正的最小值
            if (minIndex != i){  // 最小值下标不是当前这个数的下标,即做交换
                nums[minIndex] = nums[i];
                nums[i] = minNum;
            }
        }
    }
}

六、归并排序

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

图解

算法 - Java实现八大内排序算法(图解)_第12张图片
以上图为例,演示治的阶段:
算法 - Java实现八大内排序算法(图解)_第13张图片

代码实现

public class MergeSortDemo {
    public static void main(String[] args) {
        int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        int[] temp = new int[nums.length];
        mergeSort(nums,0,nums.length - 1,temp);
        System.out.println("\n排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 归并排序算法 - 分+治
     * @param nums 要排序的数组
     * @param left 数组最左端
     * @param right 数组最右端
     * @param temp 临时数组 - 需要额外开辟一个空间作为辅助
     */
    public static void mergeSort(int[] nums, int left, int right, int[] temp){
        if (left < right){
            // 求出中间下标
            int mid = (left + right) / 2;
            mergeSort(nums, left, mid, temp);
            mergeSort(nums,mid + 1, right, temp);
            merge(nums, left, mid, right, temp);
        }
    }

    /**
     * 合并算法
     * @param nums 原数组
     * @param left 数组最左端
     * @param mid 数组中间下标
     * @param right 数组最右端
     * @param temp 临时数组 - 需要额外开辟一个空间作为辅助
     */
    public static void merge(int[] nums, int left, int mid, int right, int[] temp){
        // 定义两个变量接管left和right
        int l = left;   // 即左边有序序列的当前索引
        int r = mid + 1;  // 即右边有序序列的当前索引
        int t = 0;      // 指向temp数组的当前索引

        // 1、先把左右有序序列按照规则填充到temp数组,直到有一个有序序列处理完毕
        while(l <= mid && r <= right){
            if (nums[l] <= nums[r]){ // 左边有序序列的当前元素小于等于右边有序序列当前元素的情况
                temp[t] = nums[l];
                t++;   // t下标移动
                l++;   // l下标移动
            }else{  // 右边有序序列的当前元素小于左边有序序列当前元素的情况
                temp[t] = nums[r];
                t++;
                r++;
            }
        }

        // 2、上面循环完毕,表示有一个序列里的数据已经完全被填充到temp里了,这时只需把没有填充完的那个序列填充到temp即可

        // 左边数组还有剩余元素,将左边元素全部填充到temp里
        while(l <= mid){
            temp[t] = nums[l];
            t++;
            l++;
        }
        // 右边数组还有剩余元素,将左边元素全部填充到temp里
        while(r <= right){
            temp[t] = nums[r];
            t++;
            r++;
        }

        // 3、将辅助数组temp里的元素拷贝回nums数组
        // 注意:这里不能直接nums = temp,这是分治,并不是最后一次
        // 如果nums = temp,就会导致后续的递归出问题
        t = 0;
        int tempLeft = left;
        while (tempLeft <= right){
            nums[tempLeft] = temp[t];
            t++;
            tempLeft++;
        }
    }
}

七、基数排序

基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
1、基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法。
2、基数排序(Radix Sort)是桶排序的扩展
3、基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基本思想

将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

图解

int[] nums = new int[]{79,56,4,32,27,16,88,35,32};

算法 - Java实现八大内排序算法(图解)_第14张图片

代码实现

public class RadixSortDemo {
    public static void main(String[] args) {
        int[] nums = new int[]{79,56,90,4,32,27,16,88,35,32};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        radixSort(nums);
        System.out.println("\n排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 基数排序算法
     * @param nums 要排序的数组
     */
    public static void radixSort(int[] nums){
        // 获取数组中最大数的位数
        int max = nums[0];   // 先默认第一个就是最大的
        for (int i = 1; i < nums.length; i++){
            if (max < nums[i]){
                max = nums[i];
            }
        }
        // 最大数的位数
        int maxLength = (max + "").length();
        // 十个桶,防止溢出的情况(个/十....位全是同一个数字的情况),二维数组大小为原数组大小
        int[][] bucket = new int[10][nums.length];

        /**
         * 用于记录每个桶中放了多少个数据
         * 比如:bucketElementCounts[0]就是记录的bucket[0]这个桶中放入的数据个数
         */
        int[] bucketElementCounts = new int[10];

        for (int i = 0, n = 1; i < maxLength; i++, n *= 10){
            // n表示的就是要取个十百千万位...等用到的除数
            for (int j = 0; j < nums.length; j++){
                // 取出原数组中的每个值对应位的值
                int digitOfElement = nums[j] / n % 10;
                // 放到对应的桶里去
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = nums[j];
                bucketElementCounts[digitOfElement]++;  // 这个桶里的数据个数+1
            }
            // 从桶里取出来,放回原数组
            int index = 0;
            for (int k = 0; k < bucketElementCounts.length; k++){
                if (bucketElementCounts[k] != 0){  // 表明这个桶里有数据
                    // 循环遍历这个桶,取出数据放入原数组
                    for (int l = 0; l < bucketElementCounts[k]; l++){
                        nums[index++] = bucket[k][l];
                    }
                }
                // 桶的计数置0
                bucketElementCounts[k] = 0;
            }
        }

    }
}

基数排序的说明

1、基数排序是对传统桶排序的扩展,速度很快.
2、基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
3、基数排序时稳定的。[注:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的]
4、有负数的数组,我们不用基数排序来进行排序, 如果要支持负数,参考: 基数排序为负整数

八、堆排序

大堆顶和小堆顶

1、堆排序是利用这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
2、堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆
注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。
3、每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
4、大顶堆举例说明
算法 - Java实现八大内排序算法(图解)_第15张图片
我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子:
在这里插入图片描述
大顶堆特点:

arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2]  // i 对应第几个节点,i从0开始编号

5、小顶堆举例说明
算法 - Java实现八大内排序算法(图解)_第16张图片
小顶堆特点:

arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2] // i 对应第几个节点,i从0开始编号

基本思想

1、将待排序序列构造成一个大顶堆
2、此时,整个序列的最大值就是堆顶的根节点。
3、将其与末尾元素进行交换,此时末尾就为最大值。
3、然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.

图解

int[] nums = new int[]{20,10,45,15,35,25,50,40,30};
①调整大堆顶

算法 - Java实现八大内排序算法(图解)_第17张图片
算法 - Java实现八大内排序算法(图解)_第18张图片
算法 - Java实现八大内排序算法(图解)_第19张图片
算法 - Java实现八大内排序算法(图解)_第20张图片
算法 - Java实现八大内排序算法(图解)_第21张图片
算法 - Java实现八大内排序算法(图解)_第22张图片

②排序+重构大堆顶

算法 - Java实现八大内排序算法(图解)_第23张图片
算法 - Java实现八大内排序算法(图解)_第24张图片
算法 - Java实现八大内排序算法(图解)_第25张图片
按照上诉规则,一直进行下去即可。

代码实现

public class HeapSortDemo {

    public static void main(String[] args) {
        int[] nums = new int[]{20,10,45,15,35,25,50,40,30,65,7,3,1,85,46,96,32,20,48,73};
        System.out.println("排序前:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
        heapSort(nums);
        System.out.println();
        System.out.println("排序后:");
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }

    /**
     * 堆排序算法
     * @param nums 要排序的数组
     */
    public static void heapSort(int[] nums){
        for (int i = nums.length / 2 - 1; i >= 0; i--){
            // 构建大顶堆,从第一个非叶子节点开始,从下往上 + 从右往左
            adjustHeap(nums,i,nums.length);
        }
        // 调整完毕之后,交换堆顶元素和末尾元素
        int temp = 0;
        for (int i = nums.length - 1; i > 0; i--){
            temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;
            // 交换完毕之后,重新对堆进行调整
            adjustHeap(nums,0,i);
        }
    }

    /**
     * 调整大顶堆
     * @param nums 要调整的数组
     * @param i 非叶子节点在数组中的索引
     * @param length 对多少个元素继续调整
     */
    public static void adjustHeap(int[] nums, int i, int length){
        int temp = nums[i];    // 当前调整的非叶子节点
        // 从左子节点开始,依次往下调整
        for (int k = i * 2 + 1; k < length; k = k * 2 + 1){
            if (k + 1 < length && nums[k] < nums[k + 1]){
                // 如果左子节点比右子节点小,k指向右子节点
                k++;
            }
            if (nums[k] > temp){
                // 如果子节点大于父节点,则将子节点赋值给父节点
                nums[i] = nums[k];
                // 其实这两步可以省略,加上这个配合图更好理解
                nums[k] = temp;
                temp = nums[k];

                i = k;   // 父节点更新
            }else{   // 表明已经是调整好了的
                break;
            }
            // 一轮结束,没有退出循环,以新的父节点开始,继续调整
        }

    }
}

排序算法之间的比较

算法 - Java实现八大内排序算法(图解)_第26张图片
此外,我还做了一个测试,80000条数据,各个排序各用时间(在同一环境下,电脑比较渣,所以时间都比较长):
冒泡排序:13037毫秒
快速排序:57毫秒
直接插入排序:696毫秒
希尔排序:4694毫秒
选择排序:2712毫秒
归并排序:17毫秒
基数排序:20毫秒
堆排序:10毫秒
当然,排序不能仅仅根据时间来判定好坏。要根据场景,选择合适的排序方式。

你可能感兴趣的:(数据结构与算法)