算法学习----各种排序算法的实现和对比2


算法学习----各种排序算法的实现和对比2



1 简单排序算法

简单排序算法主要有:冒泡排序法、选择排序法、插入排序法。详细内容见:算法学习----各种排序算法的实现和对比1

2  中级的排序算法:归并排序

这里所谓的中级排序算法是指在算法的时间复杂度上比较优秀的算法,那就是基于递归思想的的归并排序。归并排序在速度上比简单排序快的多,时间复杂度为O(N*logN),但空间上需要一个大小等于原来数组的工作空间。
       归并算法的中心是归并两个已经有序的数组。归并两个有序数组A和B,就生第三个数组C。
归并排序思想的核心是把一个数组划分成两半,排序每一半,然后用归并方法把数组的两半归并成一个有序的数组。主要还要用到递归来完成。
算法的执行过程如图所示:


程序实现如下:
package intdata.InsertionSort;

public class Meger {
	
	static int[] a = {2,1,5,34,12,9,64,123,32,432,13};
	static int[] workSpace = new int[11];
	
	public static void main(String[] args){
		regMegerSort(a,0,10);
		
		
		for(int i = 0;i< a.length; i++){
			System.out.println(a[i]);
		}
		
	}
	
	private static void meger(int[] workSpace,int lower,int mid,int upper) {
		
		int j=0;
		int lowerBound = lower;
		int mider = mid -1;
		int n = upper-lower+1;
		
		while(lower <= mider && mid <= upper){
			if(a[lower]<a[mid]) {
				workSpace[j++]=a[lower++];
			}
			else {
				workSpace[j++]=a[mid++];
			}
			
			while(lower <= mid){
				workSpace[j++] = a[lower++];
			}
			
			while(mid <= upper){
				workSpace[j++] = a[mid++];
			}
			
			for(j=0;j<n;j++){
				a[lowerBound+j] = workSpace[j];
			}
			
		}	
		
	}
	
	private static void regMegerSort(int[] workSpace, int lowerBound,int upperBound){
		
		if(lowerBound == upperBound) 
			return;
		else {
			int mid = (lowerBound + upperBound)/2;
			regMegerSort(workSpace,lowerBound,mid);
			
			regMegerSort(workSpace,mid+1,upperBound);
			
			meger(workSpace, lowerBound,mid+1, upperBound);
		}
	}
}
程序还是比较简单,易于实现的。

小结一下:
1 归井两个有序数组意思是创建第三个数组,这个数组按顺序存储从这两个有序数组中取到的所有数据项。
2 在归并排序中,一个大数组的单个数据项的子数组归井为两个数据项的子数组,然后两个数据项的子数组归并为4 个数据项的子数组,
如此下去直到所有的数组数据项有序。
3 归井排序需要O(N*logN) 时间。
4 归并排序需要一个大小等于原来数组的工作空间。



3 高级排序


这里主要的两个高级的排序算法:希尔排序和快速排序。最后还有基数排序,虽然不常用但很有趣的排序算法。
这两种排序算法都比简单排序算法快得多:希尔排序大约需要O(N* (logN)2)时间,快速排序需要O(N*logN)时间。这两种排序算法都和归并排序不同,不需要大量的辅助存储空间。希尔排序几乎和归并排序一样容易实现,而快速排序是所有通用排序算法中最快的一种排序算法。

希尔排序:

  1. 希尔排序是基于插入排序,但是增加了一个新的特性,大大地提高了插入排序的执行效率。
  2. 希尔排序对于多达几千个数据项的,中等大小规模的数组排序表现良好。
  3. 希尔排序不像快速排序和其他时间复杂度为O(N*logN)的排序算法那么快,因此对非常大的文件排序,它不是最优选择。
  4. 但是,希尔排序比选择排序和插入排序这种时间复杂度为O(N2 ) 的排序算法还是要快得多,并且它非常容易实现。
  5. 希尔排序算法的代码既短又简单。

这里值得说的是:希尔排序在最坏情况下的执行效率和在平均情况下的执行效率相比没有差很多。(和快速排序相比,除非采取了预防措施,否则快速排序在最坏情况下的执行效率会非常差。〉一些专家,提倡差不多任何排序工作在开始时都可以使用希尔排序算法,若在实际中证明它不够快,再改换成诸如快速排序这样更高级的排序算法。



希尔排序是基于插入排序的,但插入排序的复制次数太多了。
下面先看看简单插入排序的代码:
<span style="font-size:18px;">public static void insertionSort(int[] a){ //a 为数字对象的一个引用。引用=地址
		int i,j,key,n=a.length; //i为数组下标;j为循环次数,与i有联系;key为容器。
		for(j=1;j<n;j++){
			key=a[j];//将新的数据(牌)给容器
			i=j-1;//i为手中原有的牌数,a[i]为手中原有的(牌)
			while(i>=0 && a[i] > key ){//从小到大
				a[i+1]=a[i]; //将原数往后放一位
				i--; //循环,直到减为0。			
			}
			a[i+1]=key;//把新的数据插入正确的标号。	
		}
	}</span>
插入排序的思想是将新的要插入的值和原来数组中的值一个一个做比较,然后在一个一个复制移动,直到插入合适的位置。最坏的情况下,要从头到尾都复制移动一次。时间复制度要O(N*N),最好的情况下,就是数组基本有序的情况下,只需要移动2个左右的数就可以排行序,这时的时间复杂度为O(N)。这就是为什么说,插入排序对基本有序的数组排序,效率很高的原因。
那么假设,如果能以某种方式不必一个一个地移动所有中间的数据项(也就是每次移动间隔多个值),就能把较小的数据快速的移动到左边。,那么这个算法的效率就大大的提高了。
基于此,希尔排序采用了N增量排序的方式进行。希尔排序通过加入插入排序中元素之间的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能大跨度地移动。当这些数据项排过一趟序后,希尔排序算法减小数据项的间隔再进行排序,依此进行下去。进行这些排序时数据项之间的间隔被称为增量,并且习惯上用字母h 来表示。

看下图是一个完整的以4为增量的排序过程:




这样是不是已经基本有序了。剩下就看插入算法的了。
直接看下面希尔排序算法的代码:

<span style="font-size:18px;">package intdata.InsertionSort;

public class SheelSortApp {
	
	public static void main(String[] args){
		long[] a = {5,1,9,3,46,90,19,100,11,65};

		shellSort(a);
		
		for(int i = 0; i<a.length; i++){
			System.out.print(a[i]+"  ");
		}
	}

	private static void shellSort(long[] a) {
		int inner,outer;
		long temp;
		
		int h=1;
		while(h<a.length /3)
			h = 3*h+1;
		
		while(h>0){
			
			for(outer = h;outer < a.length; outer++){
				temp = a[outer];
				inner = outer;
				
				while(inner > h-1 && a[inner -h] > temp){
					a[inner] = a[inner-h];
					inner -= h;
				}
				a[inner] = temp;
			}//end for
			
			h = (h-1)/3;
		}//end while
	}//end shellSort
} //end class
</span>

程序结果:1  3  5  9  11  19  46  65  90  100  
这就是传说中的希尔排序算法,是不是也很简单。注意看程序中的变量h,他就是增量排序中的增量,它的取值不唯一。这里的取值是算法书中推荐的值的取法。


快速排序:

  • 1. 快速排序是基于划分思想的。
  • 2.快速排序算法是对自身的递归调用。
  • 3.要理解快速排序算法必须先理解划分算法。

由于快速排序是基于划分思想的,那先来看看划分思想。
划分数据就是把数据分为两组,使所有关键字大于特定值的数据项在一组,使所有关键字小于特定值的数据项在另组。这个特殊值就是叫枢纽或中枢。它和希尔排序中的增量h一样,其值的选取对算法的运行效率有影响。

1  划分算法:

划分算法由两个指针开始工作,两个指针分别指向数组的两头。(这里使用"指针"这个词是指示数组数据项的,而不是C++ 中所说的指针。)在左边的指针: leftPtr 向右移动,而在右边的指针: rightPtr 向左移动。


算法思想: 当leftPtr 遇到比枢纽小的数据项时,它继续右移,因为这个数据项的位置已经处在数组的正确 一边了。
但是,当遇到比枢纽大的数据项时,它就停下来。类似的,当rightPtr 遇到大于枢纽的数据项时,它继续左移,
但是当发现比枢纽小的数据项时,它也停下来。当第一个循环在发现比枢纽大的数据项时退出:第二个循环在发现比枢纽小的数据项时退出。当这两个循环都退出之后, leftPtr 和rightPtr 都指着在数组的错误一方位置上的数据项,所以交换这两个数据项。交换之后,两个指针继续移动,继续以上操作。当两个指针最终相遇时,结束划分,完成划分了。
下面看看程序代码;
package intdata.InsertionSort;

public class ArrayPartitiom {
	private long[] theArray;
	private int nElems;

	
	public ArrayPartitiom(int max){
		theArray = new long[max];
		nElems = 0;
	}
	
	public void insert(long value){
		theArray[nElems++] = value;
	}
	
	public int patitionIt(int left, int right,long pivot){
		int leftPtr = left - 1; //左指针,在左边数据的的前一位
		int rightPtr = right + 1; //右指针,在右边数据的的后一位
		while(true){
			while(leftPtr < right && theArray[++leftPtr] < pivot); //find bigger item
			while(rightPtr > left && theArray[--rightPtr] > pivot); //find smaller item
			if(leftPtr >= rightPtr)
				break;
			else 
				swap(leftPtr,rightPtr);
		} //end while
		return leftPtr;
	} //end patitionIt method

	private void swap(int leftPtr, int rightPtr) {
		long temp;
		temp = theArray[leftPtr];
		theArray[leftPtr] = theArray[rightPtr];
		theArray[rightPtr] = temp;
		
	} //end swap method
	
	public void display(){
		for(int i =0;i< nElems; i++){
			System.out.print(theArray[i]+" ");
		}
		System.out.println();
	}// end display method
	
	public int size(){
		return nElems;
	}//end size method

}
下面是划分算法的测试程序:
package intdata.InsertionSort;

public class PartitionApp {

    
	public static void main(String[] args) {
		int maxSize = 16;
		ArrayPartitiom arr = new ArrayPartitiom(maxSize);
		
		for(int i = 0;i < maxSize; i++){
			long n = (int) (java.lang.Math.random()*199);
			arr.insert(n);
		}
		
		arr.display();
		
		long pivot = 99;
		int size = arr.size();
		
		System.out.println("pivot = "+pivot);
		
		int partDex = arr.patitionIt(0, size-1, pivot);
		
		System.out.println("patitoin is at index "+partDex);
		
		arr.display();
		
	} //end main method

}
程序的运行结果如下:
102 185 59 142 8 49 150 39 119 1 179 97 16 60 48 151 
pivot = 99  (这个是中枢)
patitoin is at index 9
48 60 59 16 8 49 97 39 1 119 179 150 142 185 102 151 

快速排序算法:

毫无疑问,快速排序是最流行的排序算法,因为有充足的理由,在大多数情况下,快速排序都是最快的,执行时间为O(N*logN) 级。(这只是对内部排序或者说随机存储器内的排序而言,对于在磁盘文件中的数据进行的排序,其他的排序算法可能更好。)
必须注意的是:
快速排序算法本质上通过把一个数组划分为两个子数组,然后递归地调用自身为每一个子数组进行快速排序来实现的。但是,对这个基本的设计还需要进行一些加工。算法还必须要选择枢纽以及对小的划分区域进行排序。


快速排序算法有三个基本的步骤:
  • 1.把数组或者子数组划分成左边(较小的关键字)的一组和右边(较大的关键字)的一组。
  • 2. 调用自身对左边的一组进行排序。
  • 3. 再次调用自身对右边的一组进行排序。
经过一次划分之后,所有在左边子数组的数据项都小于在右边子数组的数据。只要对左边子数组和右边子数组分别进行排序,整个数组就是有序的了。 通过递归的 调用排序算法自身就可以对子数组进行排序了。

看下面的程序:
package intdata.InsertionSort;

public class QuickSort1 {
	
	private static long[] theArray = new long[16];
	
	public static void main(String[] args) {
		
		
		for(int i = 0;i < theArray.length; i++){
			theArray[i] = (int) (java.lang.Math.random()*99);
		}
		
		display();
		recQuickSort(0,theArray.length-1);//theArray.length-1 是最后一个数
		display();
	}

	private static void display() {
		for(int i =0;i< theArray.length; i++){
			System.out.print(theArray[i]+" ");
		}
		System.out.println();
	}//end display method
	
	private static void recQuickSort(int left,int right){
		if(right-left <= 0)
			return;
		else{
			long pivot = theArray[right];
			int partition = partition(left,right,pivot);
			
			recQuickSort(left, partition-1);
			recQuickSort(partition+1, right);
			
		} //end else
	}//  end recQuickSort
	
	private static int partition(int left, int right,long pivot){
		int leftPtr = left - 1; //左指针,在左边数据的的前一位
		int rightPtr = right; //右指针,右边数据
		while(true){
			
			while(theArray[++leftPtr] < pivot); //find bigger item
			while(rightPtr > 0 && theArray[--rightPtr] > pivot); //find smaller item
			
			if(leftPtr >= rightPtr)
				break;
			else 
				swap(leftPtr,rightPtr);
		} //end while
		swap(leftPtr,right);
		return leftPtr;
	} //end patitionIt method

	private static void swap(int leftPtr, int rightPtr) {
		long temp;
		temp = theArray[leftPtr];
		theArray[leftPtr] = theArray[rightPtr];
		theArray[rightPtr] = temp;
		
	} //end swap method

}

程序结果:
55 69 94 38 48 36 62 76 97 16 37 23 82 61 26 92 
16 23 26 36 37 38 48 55 61 62 69 76 82 92 94 97 

2 选择枢纽:
划分方法应该使用什么样的枢纽呢?以下是一些相关的思想:
  1. • 应该选择具体的一个数据项的关键字的值作为枢纽:称这个数据项为pivot ( 枢纽) 。
  2. • 可以选择任意一个数据项作为枢纽。为了简便,我们假设总是选择待划分的子数组最右端的数据项作为枢纽。
  3. • 划分完成之后,如果枢纽被插入到左右子数组之间的分界处,那么枢纽就落在排序之后的最终位置上了。
下图是交换枢纽的位置示意图:

但是上面的枢纽选择方案,在已经有序的数据进行排序时,时间复杂多会变成O(N*N),为了解决这个问题,提出了另一种枢纽选择方案:三项数据取中
如下图所示:

看如下代码:

package intdata.InsertionSort;

public class QuickSort2 {

	/**
	 * 三项数据取中
	 * 解决了对已经有序的数组快速排序需要O(N*N)的问题
	 */
	private static long[] theArray = new long[16];
	
	public static void main(String[] args) {
		
		for(int i = 0;i < theArray.length; i++){
			theArray[i] = (int) (java.lang.Math.random()*99);
		}
		
		display();
		recQuickSort(0,theArray.length-1);//theArray.length-1 是最后一个数
		display();
	}
	
	private static void display() {
		for(int i =0;i< theArray.length; i++){
			System.out.print(theArray[i]+" ");
		}
		System.out.println();
	}//end display method
	
	private static void recQuickSort(int left,int right){
		
		int size = right-left+1;
		
		if(size <= 3){ //处理小划问题,cutoff = 3
			manualSort(left,right); //简单的人工排序
		}
		else{
			long median = medianOf3(left,right);//取三项数据的中间值为划分枢纽
			
			int partition = partition(left,right,median);
			
			recQuickSort(left, partition-1);
			recQuickSort(partition+1, right);
			
		} //end else
	}//  end recQuickSort
	
	private static long medianOf3(int left, int right) {
		
		int center = (left+right)/2;
		if(theArray[left] > theArray[center])
			swap(left,center);
		
		if(theArray[left] > theArray[right])
			swap(left,right);
		
		if(theArray[center] > theArray[right])
			swap(center,right); //三项数据排序取中完成
		
		swap(center,right-1); //put pivot on right;
		return theArray[right-1]; //return median value
		
	}//end medianOf3 method

	private static void manualSort(int left, int right) {
		int size = right-left+1;
		if(size <= 1) 
			return;
		if(size == 2){ //2 sort left and right
			if(theArray[left] > theArray[right])
				swap(left,right);
			return;
		}
		else { //sort is 3   //right = right -1;
			if(theArray[left] > theArray[right -1])
				swap(left,right-1);
			
			if(theArray[left] > theArray[right])
				swap(left,right);
			
			if(theArray[right -1] > theArray[right])
				swap(right-1,right); 
		}
		
	}//end manual method

	private static int partition(int left, int right,long pivot){
		int leftPtr = left; //左指针,左边数据
		int rightPtr = right -1; //右指针,右边数的后一位
		while(true){
			
			while(theArray[++leftPtr] < pivot); //find bigger item
			while(theArray[--rightPtr] > pivot); //find smaller item
			
			if(leftPtr >= rightPtr)
				break;
			else 
				swap(leftPtr,rightPtr);
		} //end while
		swap(leftPtr,right-1); //restore pivot
		return leftPtr;        //return pivot location
	} //end patitionIt method

	private static void swap(int leftPtr, int rightPtr) {
		long temp;
		temp = theArray[leftPtr];
		theArray[leftPtr] = theArray[rightPtr];
		theArray[rightPtr] = temp;
		
	} //end swap method

}
程序结果:
95 28 55 3 0 23 85 57 39 32 36 58 23 61 88 88 
0 3 23 23 28 32 36 39 55 57 58 61 85 88 88 95 


3 处理小划分:

如果使用三数据项取中划分方法,则必须要遵循快速排序算法不能执行三个或者少于三个数据
项的划分的规则。在这种情况下,数字3 被称为切割点(cutoff)。

处理小划分的另一个选择是使用插入排序。当使用插入排序的时候,不用限制以3 为切割点
(cutoff) 。可以把界限定为10 、20 或者其他任何数。试验不同切割点的值以找到最好的执行效率。

看下面的程序:
package intdata.InsertionSort;

public class QuickSort3 {

	/**
	 * 对于小划分的小于10项的数据,用插入算法排序
	 */
	private static long[] theArray = new long[16];
	
	public static void main(String[] args) {
		
		for(int i = 0;i < theArray.length; i++){
			theArray[i] = (int) (java.lang.Math.random()*99);
		}
		
		display();
		recQuickSort(0,theArray.length-1);//theArray.length-1 是最后一个数
		display();
	}
	
	private static void display() {
		for(int i =0;i< theArray.length; i++){
			System.out.print(theArray[i]+" ");
		}
		System.out.println();
	}//end display method
	
	private static void recQuickSort(int left,int right){
		
		int size = right-left+1;
		
		if(size < 10){ //处理小划问题,cutoff = 10
			insertionSort(left,right); //插入算法
		}
		
		else{
			long median = medianOf3(left,right);//取三项数据的中间值为划分枢纽
			
			int partition = partition(left,right,median);
			
			recQuickSort(left, partition-1);
			recQuickSort(partition+1, right);
			
		} //end else
	}//  end recQuickSort
	
	private static void insertionSort(int left, int right) {
		int inner,outer;
		long temp;
		for(outer = left+1;outer <= right;outer++){
			temp = theArray[outer];
			inner = outer;
			
			while(inner > left && theArray[inner-1] >= temp){
				theArray[inner] = theArray[inner -1];
				--inner; //inner--;
			}
			
			theArray[inner] = temp;
		}
			
			
	}

	private static long medianOf3(int left, int right) {
		
		int center = (left+right)/2;
		if(theArray[left] > theArray[center])
			swap(left,center);
		
		if(theArray[left] > theArray[right])
			swap(left,right);
		
		if(theArray[center] > theArray[right])
			swap(center,right); //三项数据排序取中完成
		
		swap(center,right-1); //put pivot on right;
		return theArray[right-1]; //return median value
		
	}//end medianOf3 method

	private static void manualSort(int left, int right) {
		int size = right-left+1;
		if(size <= 1) 
			return;
		if(size == 2){ //2 sort left and right
			if(theArray[left] > theArray[right])
				swap(left,right);
			return;
		}
		else { //sort is 3   //right = right -1;
			if(theArray[left] > theArray[right -1])
				swap(left,right-1);
			
			if(theArray[left] > theArray[right])
				swap(left,right);
			
			if(theArray[right -1] > theArray[right])
				swap(right-1,right); 
		}
		
	}//end manual method

	private static int partition(int left, int right,long pivot){
		int leftPtr = left; //左指针,左边数据
		int rightPtr = right -1; //右指针,右边数的后一位
		while(true){
			
			while(theArray[++leftPtr] < pivot); //find bigger item
			while(theArray[--rightPtr] > pivot); //find smaller item
			
			if(leftPtr >= rightPtr)
				break;
			else 
				swap(leftPtr,rightPtr);
		} //end while
		swap(leftPtr,right-1); //restore pivot
		return leftPtr;        //return pivot location
	} //end patitionIt method

	private static void swap(int leftPtr, int rightPtr) {
		long temp;
		temp = theArray[leftPtr];
		theArray[leftPtr] = theArray[rightPtr];
		theArray[rightPtr] = temp;
		
	} //end swap method

}
程序结果:
81 15 62 41 44 90 51 53 45 65 87 98 69 52 48 12 
12 15 41 44 45 48 51 52 53 62 65 69 81 87 90 98 


基数排序:

基数排序算法

这里的基数排序都是普通的以10 为基数的运算,因为这样更易于讲解。
但是,以2 为基 数实现的基数排序也是非常高效的,这可以利用计算机高速的位运算。这里只考察基数排序,而不 考察与基数排序相似但更复杂一些的基数交换排序。基数这个词的意思是一个数字系统的基。10 是 十进制系统的基数, 2 是二进制系统的基数。

算法思路:

排序包括分别检测关键字的每一个数字,检测从个位(最低有效位)开始。

1.根据数据项个位上的值,把所有的数据项分为10 组。
2. 然后对这10 组数据项重新排列:把所有关键字是以0 结尾的数据项排在最前面,然后是关键字结尾是1 的数据项,照此顺序直到以9 结尾的数据。这个步骤被称为第一趟子排序。
3. 在第二趟子排序中,再次把所有的数据项分为10 组,但是这一次是根据数据项十位上的值来分组的。这次分组不能改变先前的排序顺序。也就是说,第二趟排序之后,从每一组数据项的内部来看,数据项的顺序保持不变:这趟子排序必须是稳定的。
4. 然后再把10 组数据项重新合并,排在最前面的是十位上为0 的数据项,然后是10 位为1 的数据,如此排序直到十位上为9 的数据。
5. 对剩余位重复这个过程。

终于完了:
小结一下:参考书:数据结构(java版)。
  • 希尔排序将增量应用到插入排序,然后逐渐缩小增量。
  •  n-增量排序表示每隔n 个元素进行排序。
  • 被称为间隔序列或者间距序列的数列决定了希尔排序的排序间隔。
  • 常用的间隔序列是由递归表达式h=3*h+1 生成的, h 的初始值为10
  • 一个容纳了1000 个数据的数组,对它进行希尔排序可以是间隔序列为364 ,121 , 40,13 , 4 ,最后是1 的增量排序。
  • 希尔排序很难分析,但是它运行的时间复杂度大约为O(N* (logN内。这比时间复杂度为O(N2 )的排序算法要快,例如 比插入排序快,但是比时间复杂度为O(N*logN) 的算法慢,例如比快速排序慢。

  • 划分数组就是把数组分为两个子数组,在一组中所有的数据项关键字的值都小于指定的
    值,而在另一组中所有数据项关键字的值则大于或等于给定值。
  •  枢纽是在划分的过程中确定数据项应该放在哪一组的值。小于枢纽的数据项都放在左边一组:而大于枢纽的数据项都放在右边一组。
  •  快速排序划分一个数组,然后递归调用自身,对划分得到的两个子数组进行快速排序。
  •  快速排序算法划分时的枢纽是一个特定数据项关键字的值,这个数据项称为pivot (枢纽)。
  •  在快速排序的简单版本中,总是由于数组的最右端的数据项作为枢纽。

  •  快速排序的简单版本,对已经有序(或者逆序)的数据项排序的执行效率只有O(N*N)
  •  更高级的快速排序版本中,枢纽是为"三数据项取中" 划分。
  •  在快速排序已经对大于切割界限的子数组排完序之后,插入排序也可用于整个的数组。
  •  基数排序的时间复杂度和快速排序相同,只是它需要两倍的存储空间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值