数据结构和算法-7.排序算法

1. 排序算法的介绍

  • 排序也叫排序算法(Sort Algorithm),排序是将一组数据,按照指定的顺序进行排列的过程
  • 分类:内部排序,将需要处理的所有数据都加载到内部存储器中进行排序
  • 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序

排序

2. 算法的复杂度

2.1 算法的时间复杂度

度量一个算法执行时间的两种方法
1.事后统计
这个方法可行,但又两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机硬件等环境因素,这种方式要在同一台计算机的相同状态下运行,才能比较
2.事前估算
通过分析某个算法的时间复杂度来判断

2.2 时间频度

  • 一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,他花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)

2.3 时间复杂度

2.3.1 常见的时间复杂度

  1. 常数阶O(1)
  2. 对数阶O(log2 n)
  3. 线性阶O(n)
  4. 线性对数阶O(nlog2 n)
  5. 平方阶O(n^2)
  6. 立方阶O(n^3)
  7. k次方阶O(n^k)
  8. 指数阶O(2^n)

时间复杂度

2.3.2 常数阶

  • 无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
  • 代码在执行的时候,它消耗的时间并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。

常熟阶

2.3.3 对数阶

对数阶
对数阶

2.3.4 线性阶

  • for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度

线性阶

2.3.5 线性对数阶

  • 线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)

线性对数阶

2.3.6 平方阶

  • 如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(nn),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(mn)

平方阶

时间复杂度

3. 冒泡排序

3.1 基本介绍

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

3.2 图解和思想

冒泡排序

3.3 代码实现

public class BubbleSort {
    public static void main(String[] args) {
        int arr[] = {3, 9, -1, 10, 20};
        //int arr[] = {1, 2, 3, 4, 5, 6};
        int temp = 0;//临时变量
        boolean flag = false;//标识变量,表示是否进行过交换
        for (int i = 0; i < arr.length - 1; i++) {
            //第一趟排序,将最大的数排在最后
            for (int j = 0; j < arr.length - 1 - i; j++) {
                //如果前面的数比后面的数大,则交换
                if (arr[j] > arr[j + 1]) {
                    flag = true;
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            System.out.println("第" + (i + 1) + "趟排序后的数组");
            System.out.println(Arrays.toString(arr));
            if (!flag) {//在一趟排序中,没有发生过交换
                break;
            } else {
                flag = false;//重置flag,进行下次判断
            }
        }
    }

//        //第二趟排序,将第二大的数排在倒二
//        for (int j = 0; j < arr.length - 1 - 1; j++) {
//            //如果前面的数比二后面的数大,则交换
//            if (arr[j] > arr[j + 1]) {
//                temp = arr[j];
//                arr[j] = arr[j + 1];
//                arr[j + 1] = temp;
//            }
//        }
//        System.out.println("第二趟排序后的数组");
//        System.out.println(Arrays.toString(arr));
//        //第3趟排序,将第3大的数排在倒3
//        for (int j = 0; j < arr.length - 1 - 2; j++) {
//            //如果前面的数比二后面的数大,则交换
//            if (arr[j] > arr[j + 1]) {
//                temp = arr[j];
//                arr[j] = arr[j + 1];
//                arr[j + 1] = temp;
//            }
//        }
//        System.out.println("第3趟排序后的数组");
//        System.out.println(Arrays.toString(arr));
//
//        //第4趟排序,将第二大的数排在倒4
//        for (int j = 0; j < arr.length - 1 - 3; j++) {
//            //如果前面的数比二后面的数大,则交换
//            if (arr[j] > arr[j + 1]) {
//                temp = arr[j];
//                arr[j] = arr[j + 1];
//                arr[j + 1] = temp;
//            }
//        }
//        System.out.println("第4趟排序后的数组");
//        System.out.println(Arrays.toString(arr));

}

4. 选择排序

4.1 基本介绍

  • 是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的

4.2 图解和思想

选择排序(select sorting)基本思想是

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

选择排序
选择排序

4.3 代码实现

public class SelectSort {
    public static void main(String[] args) {
        //int[] arr = {101, 34, 119, 1};
        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] = (int) (Math.random() * 80000);
        }
        Date start = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
        String date1 = simpleDateFormat.format(start);
        System.out.println(date1);

        selectSort(arr);
        Date end = new Date();
        String date2 = simpleDateFormat.format(end);
        System.out.println(date2);
        //System.out.println(Arrays.toString(arr));
    }

    //选择排序
    public static void selectSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            //第一轮排序
            int minIndex = i;
            int min = arr[i];
            for (int j = i + 1; j < arr.length; j++) {
                if (min > arr[j]) {//说明假定的最小值不是最小的
                    min = arr[j];
                    minIndex = j;
                }
            }
            //将最小值和arr[0]交换
            if (minIndex != i) {
                arr[minIndex] = arr[i];
                arr[i] = min;
            }
            //System.out.println("第" + (i + 1) + "轮后");
            //System.out.println(Arrays.toString(arr));
        }
//        //第2轮排序
//        minIndex = 1;
//        min = arr[1];
//        for (int j = 1 + 1; j < arr.length; j++) {
//            if (min > arr[j]) {//说明假定的最小值不是最小的
//                min = arr[j];
//                minIndex = j;
//            }
//        }
//        //将最小值和arr[0]交换
//        if (minIndex != 1) {
//            arr[minIndex] = arr[1];
//            arr[1] = min;
//        }
//        System.out.println("第2轮后");
//        System.out.println(Arrays.toString(arr));
//        //第2轮排序
//        minIndex = 2;
//        min = arr[2];
//        for (int j = 2 + 1; j < arr.length; j++) {
//            if (min > arr[j]) {//说明假定的最小值不是最小的
//                min = arr[j];
//                minIndex = j;
//            }
//        }
//        //将最小值和arr[0]交换
//        if (minIndex != 2) {
//            arr[minIndex] = arr[2];
//            arr[2] = min;
//        }
//        System.out.println("第3轮后");
//        System.out.println(Arrays.toString(arr));
    }
}

5. 插入排序

5.1 基本介绍

-是对欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的

5.2 图解和思想

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

插入排序

5.3 代码实现

public class InsertSort {

    public static void main(String[] args) {
        int[] arr = new int[80000];
        for (int i = 0; i < 80000; i++) {
            arr[i] = (int) (Math.random() * 80000);
        }
        Date start = new Date();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
        String date1 = simpleDateFormat.format(start);
        System.out.println(date1);

        insertSort(arr);
        Date end = new Date();
        String date2 = simpleDateFormat.format(end);
        System.out.println(date2);
        //int[] arr = {101, 34, 119, 1, -1, 89};
    }

    //插入排序
    public static void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {

            //第1轮
            //定义待插入的数
            int insertVal = arr[i];
            int insertIndex = i - 1;//arr[1]前面这个数的下标

            //给insertVal找插入位置时,不越界
            while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
                //给insertVal找到插入的位置
                //将arr[insertIndex]后移
                arr[insertIndex + 1] = arr[insertIndex];
                insertIndex--;
            }
            //退出while时,说明插入的位置找到,insertIndex+1
            arr[insertIndex + 1] = insertVal;
            //System.out.println("第" + i + "轮后");
            //System.out.println(Arrays.toString(arr));
        }

//        //第二轮
//        insertVal = arr[2];
//        insertIndex = 2 - 1;//arr[1]前面这个数的下标
//
//        //给insertVal找插入位置时,不越界
//        while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
//            //给insertVal找到插入的位置
//            //将arr[insertIndex]后移
//            arr[insertIndex + 1] = arr[insertIndex];
//            insertIndex--;
//        }
//        //退出while时,说明插入的位置找到,insertIndex+1
//        arr[insertIndex + 1] = insertVal;
//        System.out.println("第2轮后");
//        System.out.println(Arrays.toString(arr));
//
//        //第3轮
//        insertVal = arr[3];
//        insertIndex = 3 - 1;//arr[1]前面这个数的下标
//
//        //给insertVal找插入位置时,不越界
//        while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
//            //给insertVal找到插入的位置
//            //将arr[insertIndex]后移
//            arr[insertIndex + 1] = arr[insertIndex];
//            insertIndex--;
//        }
//        //退出while时,说明插入的位置找到,insertIndex+1
//        arr[insertIndex + 1] = insertVal;
//        System.out.println("第3轮后");
//        System.out.println(Arrays.toString(arr));
    }
}

6. 希尔排序

6.1 基本介绍

  • 简单插入排序存在的问题,数组arr={1,2,3,4,5,6,1}时,过程是
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
  • 当需要插入的数是较少的数时,后移的次数很多,效率有影响
  • 希尔排序(Donald Shell)也是一种插入排序,是简单插入排序经过改进后的一个更高效的版本,成为缩小增量排序

6.2 图解和思想

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

希尔排序

6.3 代码实现

public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        shellSort2(arr);
        System.out.println(Arrays.toString(arr));

    }

    public static void shellSort(int[] arr) {
        int temp;
        int count = 0;
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            //第一轮,将10个数据分成5组
            for (int i = gap; i < arr.length; i++) {
                //遍历各组中所有元素(共gap组,每组有2个元素),步长gap
                for (int j = i - gap; j >= 0; j -= gap) {
                    //如果当前元素大于加上步长后的那个元素,说明要交换
                    if (arr[j] > arr[j + gap]) {
                        temp = arr[j];
                        arr[j] = arr[j + gap];
                        arr[j + gap] = temp;
                    }
                }
            }
            System.out.println((++count) + "轮后" + Arrays.toString(arr));
        }


//        //第2轮,将10个数据分成5/2=2组
//        for (int i = 2; i < arr.length; i++) {
//            for (int j = i - 2; j >= 0; j -= 2) {
//                //如果当前元素大于加上步长后的那个元素,说明要交换
//                if (arr[j] > arr[j + 2]) {
//                    temp = arr[j];
//                    arr[j] = arr[j + 2];
//                    arr[j + 2] = temp;
//                }
//            }
//        }
//        System.out.println("2轮后" + Arrays.toString(arr));
//
//        //第3轮,将10个数据分成2/2=1组
//        for (int i = 1; i < arr.length; i++) {
//            //遍历各组中所有元素(共5组,每组有2个元素),步长5
//            for (int j = i - 1; j >= 0; j -= 1) {
//                //如果当前元素大于加上步长后的那个元素,说明要交换
//                if (arr[j] > arr[j + 1]) {
//                    temp = arr[j];
//                    arr[j] = arr[j + 1];
//                    arr[j + 1] = temp;
//                }
//            }
//        }
//        System.out.println("3轮后" + Arrays.toString(arr));
    }

    //对交换式的希尔排序进行优化->移位法
    public static void shellSort2(int[] arr) {
        for (int gap = arr.length / 2; gap > 0; gap /= 2) {
            //从第gap个元素,逐个对其所在的组进行直接插入排序
            for (int i = gap; i < arr.length; i++) {
                int j = i;
                int temp = arr[j];
                if (arr[j] < arr[j - gap]) {
                    while (j - gap >= 0 && temp < arr[j - gap]) {
                        //移动
                        arr[j] = arr[j - gap];
                        j -= gap;
                    }
                    //退出while后,就给temp找到了插入的位置
                    arr[j] = temp;
                }
            }
        }
    }
}

7. 快速排序

7.1 基本介绍

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

7.2 图解和思想

快速排序

7.3 代码实现

public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {-9, 78, 0, 23, -567, 70};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    public static void quickSort(int[] arr, int left, int right) {
        int l = left;//左下标
        int r = right;//右下标
        int pivot = arr[(left + right) / 2];//中间值
        int temp = 0;//临时变量,交换时使用
        //while循环的目的,让比pivot小的值放到左边,比pivot大的值放右边
        while (l < r) {
            //在pivot左边一直找,找到大于等于pivot值才退出
            while (arr[l] < pivot) {
                l += 1;
            }
            //在pivot右边一直找,找到小于等于pivot值才退出
            while (arr[r] > pivot) {
                r -= 1;
            }
            //如果l>=r说明pivot左右两边的值,已经按照左边都是小,右边都是大的
            if (l >= r) {
                break;
            }
            //交换
            temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;

            //交换完后,发现这个arr[l]==pivot值,r--前移
            if (arr[l] == pivot) {
                r -= 1;
            }
            //交换完后,发现这个arr[r]==pivot值,l++后移
            if (arr[r] == pivot) {
                l += 1;
            }
        }
        //如果l==r,必须l++,r--否则栈溢出
        if (l == r) {
            l += 1;
            r -= 1;
        }
        //左递归
        if (left < r) {
            quickSort(arr, left, r);
        }
        //右递归
        if (right > l) {
            quickSort(arr, l, right);
        }
    }
}

8. 归并排序

8.1 基本介绍

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

8.2 图解和思想

归并排序

归并排序

8.3 代码实现

public class MergeSort {
    public static void main(String[] args) {
        int arr[] = {8, 4, 5, 7, 1, 3, 6, 2};
        int temp[] = new int[arr.length];//归并排序需要一个额外空间
        mergeSort(arr, 0, arr.length - 1, temp);
        System.out.println(Arrays.toString(arr));
    }

    //分+合方法
    public static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left < right) {
            int mid = (left + right) / 2;//中间索引
            //向左递归进行分解
            mergeSort(arr, left, mid, temp);
            //向右递归分解
            mergeSort(arr, mid + 1, right, temp);
            //合并
            merge(arr, left, mid, right, temp);
        }
    }

    /*合并的方法
     * arr 排序的原始数组
     * left左边有序序列的初始索引
     * mid中间索引
     * right右边索引
     * temp临时中转数组
     * */
    public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;//初始化i,左边有序序列的初始索引
        int j = mid + 1;//初始化j,右边有序序列的初始索引
        int t = 0;//指向temp数组的当前索引

        //1.先把所有两边的数据按照规则填充到temp数组,直到左右两边的有序序列有一边处理完毕为止
        while (i <= mid && j <= right) {
            //如果左边有序序列的当前元素小于等于右边有序序列的当前元素
            if (arr[i] <= arr[j]) {
                temp[t] = arr[i];
                t += 1;
                i += 1;
            } else {
                temp[t] = arr[j];
                t += 1;
                j += 1;
            }
        }
        //2.把有剩余数据的一边的数据依次全部填充到temp
        while (i <= mid) {//左边的有序序列还有剩余元素,就全部填充到temp
            temp[t] = arr[i];
            t += 1;
            i += 1;
        }
        while (j <= right) {//右边的有序序列还有剩余元素,就全部填充到temp
            temp[t] = arr[j];
            t += 1;
            j += 1;
        }
        //3.将temp数组的元素拷贝到arr
        t = 0;
        int tempLeft = left;
        while (tempLeft <= right) {
            arr[tempLeft] = temp[t];
            t += 1;
            tempLeft += 1;
        }
    }
}

9. 基数排序

9.1 基本介绍

  • 基数排序(RadixSort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort),顾名思义,是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用
  • 基数排序是属于稳定性的排序,是桶排序的扩展
  • 速度很快,空间换时间的方式,占用内存很大,容易造成OOM

9.2 思想和图解

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

基数排序

9.3 代码实现

public class RadixSort {
    public static void main(String[] args) {
        int[] arr = {53, 3, 542, 748, 14, 214};
        sortRadix(arr);
    }

    //基数排序方法
    public static void sortRadix(int[] arr) {

        //得到数组中最大的数
        int max = arr[0];//假设第一个数是最大数
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        //最大数是几位数
        int maxLength = (max + "").length();

        //定义一个二维数组,表示10个桶,每个桶就是一个一维数组
        int[][] bucket = new int[10][arr.length];
        //记录每个桶中实际存放多少个数据,定义一个一维数组记录各个桶每次放入的数据个数
        int[] bucketElementCounts = new int[10];

        for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
            //第一轮(针对每个元素的个位进行排序)
            for (int j = 0; j < arr.length; j++) {
                //取出元素的个位
                int digitOfElement = arr[j] / n % 10;
                //放入到对应的桶中
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
                bucketElementCounts[digitOfElement]++;
            }
            //按照桶的顺序,依次取出数据放入原来数组
            int index = 0;
            //遍历每一个桶,并将桶中的数据放入到原数组
            for (int k = 0; k < bucketElementCounts.length; k++) {
                //如果桶中有数据放到原数组
                if (bucketElementCounts[k] != 0) {
                    //循环该桶放入数据
                    for (int l = 0; l < bucketElementCounts[k]; l++) {
                        //取出元素放入到arr
                        arr[index++] = bucket[k][l];
                    }
                }
                //第一轮处理后需要将每个bucketElementCounts[k]=0
                bucketElementCounts[k] = 0;
            }
            System.out.println("第" + (i + 1) + "轮" + Arrays.toString(arr));
        }


//        //第2轮(针对每个元素的十位进行排序)
//        for (int j = 0; j < arr.length; j++) {
//            //取出元素的十位
//            int digitOfElement = arr[j] / 10 % 10;
//            //放入到对应的桶中
//            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
//            bucketElementCounts[digitOfElement]++;
//        }
//        //按照桶的顺序,依次取出数据放入原来数组
//        index = 0;
//        //遍历每一个桶,并将桶中的数据放入到原数组
//        for (int k = 0; k < bucketElementCounts.length; k++) {
//            //如果桶中有数据放到原数组
//            if (bucketElementCounts[k] != 0) {
//                //循环该桶放入数据
//                for (int l = 0; l < bucketElementCounts[k]; l++) {
//                    //取出元素放入到arr
//                    arr[index++] = bucket[k][l];
//                }
//            }
//            bucketElementCounts[k] = 0;
//        }
//        System.out.println("第2轮" + Arrays.toString(arr));
//
//        //第3轮(针对每个元素的百位进行排序)
//        for (int j = 0; j < arr.length; j++) {
//            //取出元素的百位
//            int digitOfElement = arr[j] / 100 % 10;
//            //放入到对应的桶中
//            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
//            bucketElementCounts[digitOfElement]++;
//        }
//        //按照桶的顺序,依次取出数据放入原来数组
//        index = 0;
//        //遍历每一个桶,并将桶中的数据放入到原数组
//        for (int k = 0; k < bucketElementCounts.length; k++) {
//            //如果桶中有数据放到原数组
//            if (bucketElementCounts[k] != 0) {
//                //循环该桶放入数据
//                for (int l = 0; l < bucketElementCounts[k]; l++) {
//                    //取出元素放入到arr
//                    arr[index++] = bucket[k][l];
//                }
//            }
//            bucketElementCounts[k] = 0;
//        }
//        System.out.println("第3轮" + Arrays.toString(arr));
    }
}

10. 排序算法对比

算法对比

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值