leetcode思路总结

1. Two Sum

题目描述:

给定一个整数数组nums以及一个数target,求和为target的两个数在数组中的下标。约定对于给定的target有且仅有一组解。

解法一:哈希表

采用哈希表(unordered_map)。依次扫描数组,并判断target-nums[i] 是否存在哈希表中,若存在,则找到了这两个数;否则,将(nums[i],i) 存入哈希表中。

时间复杂度:O(n)
空间复杂度:O(n)

解法二:双指针

采用双指针。首先将数组从小到大排序,然后用双指针从两端开始扫描数组,若两指针指向的数之和小于target,则左指针向右移动;若两指针指向的数之和大于target,则右指针相左移动。

时间复杂度:排序的时间复杂度
空间复杂度:排序的空间复杂度

2. Add Two Numbers

题目描述:

将两个整数用非空链表逆序存储:如 1->2->3 表示整数321,求这两个数之和并用逆序链表表示。
注意:两个链表非空;可能长短不一,如1->2 和3->4->5;注意考虑最高位发生进位的情况,如 5 + 5 = 0 -> 1

链表相关的:注意需要通过链表指针取值时,如 l1->val ,需要注意判断链表指针是否为空;另外可以空出一个结点作为链表的头结点。

解法

链表的对应位相加,并用 carry 记录进位。需注意存在一个链表为空的情况;两个链表长度不一致的情况;最高位发生进位的情况。

c++ code

struct ListNode{
	int val;
	ListNode* next;
	ListNode(int x):val(x),next(NULL){}
};

ListNode* addTwoNumbers(ListNode* l1, ListNode* l2){
	ListNode* result = new ListNode(0); // 多出来一个结点作为头结点
	ListNode* pNode = result;
	int sum = 0;
	int carry = 0;
	while(l1!=NULL || l2!=NULL){
		int x = l1?l1->val:0;
		int y = l2?l2->val:0;
		sum = x + y + carry;
		pNode->next = new ListNode(sum%10);
		pNode = pNode->next;
		carry = sum/10;
		if(l1) l1 = l1->next; // 注意 l1 可能为空
		if(l2) l2 = l2->next;
	}
	if(carry) pNode->next = new ListNode(carry);
	return result->next;
}

3. Longest Substring Without Repeating Characters

题目描述:

给定一个字符串,求无重复字符的最长字串长度。如s=“bbb”, result=1;
s=“abcabc”,result=3。

解法一:暴力法,set,(超时)

我们可以枚举出所有的子串,然后判断各个子串是否无重复字符,并计算子串长度。枚举子串方法,用子串的起始下标 i 和 终点下标 j , 0 ≤ i < j ≤ n 0 \leq i < j \leq n 0i<jn,则子串[i,j) 表示下标 i 到 j-1的子串。

时间复杂度:O(n^3) 检验是否为无重复字符的子串需要遍历子串 j-i,枚举子串为一个二层循环 ∑ i = 0 n − 1 ∑ j = i + 1 n ( j − i ) \sum_{i=0}^{n-1} \sum_{j=i+1}^{n}(j-i) i=0n1j=i+1n(ji)
空间复杂度:O(min(n,m)) 需要用一个set 来检验是否存在重复字符,m 为 字母表字符个数。

int lengthOfLongestSubstring(string s){
	int n = s.size();
	int ans = 0;
	for(int i=0;i<n;++i)
		for(int j=i+1;j<=n;++j)
			if(allUnique(s,i,j)) ans = max(ans,j-i); // 计算子串长度
	return ans;
}
bool allUnique(string s, int start, int end){
	set<char> mySet;
	for(int i=start;i<end;++i){
		char ch = s[i];
		if(mySet.find(ch)!=mySet.end()) return false;
		else mySet.insert(ch);
	}
	return true;
}

优化一:用哈希表

暴力解法对每个子串都需要重头判断是否存在重复字符。事实上,假设[i,j) 不存在重复字符,只需要判断 j 是否在 子串 [i, j) 内,就能判断 [i,j+1) 是否存在重复字符。我们可以用哈希表以O(1)的时间查找元素。

时间复杂度:O(n) 每个元素至多被遍历2遍。
空间复杂度:O(min(m,n)) 哈希表存储元素。

int lengthOfLongestSubstring(string s){
	int n = s.size();
	unordered_map<char,int> myHash;
	int ans = 0, i = 0, j = 0;
	while(i<n && j<n){
		char ch = s[j];
		if(myHash.find(ch)==myHash.end()){
// [i,j) 无重复,若[i,j+1)无重复,则找到一个新的无重复子串,计算子串长度,并继续判断[i,j+2)
			myHash[ch] = 1;
			++j;
			ans = max(ans,j-i);
		}
		else{
// [i,j) 为无重复字符,若[i,j+1)重复,则[i+1,j)无重复,继续判断[i+1,j+1)是否重读
			myHash.erase(s[i]);  
			++i;
		}
	}
	return ans;
}

优化二:用vector记录字符所在index, vector vec(256,-1)

假设s[j] 在 [i,j) 区间内有重复,位置为 j ′ j' j。则可以过滤掉[i,j’] 区域。直接从 [j’+1,j+1) 开始判断。可以用哈希表记录字符所在下标,用 [i,j) 记录当前子串范围,
需注意,在哈希表查找s[j+1]时,若找到,需判断下标是否>=1(即是否在当前范围内,若在当前范围内,则字符重复,i+1),并且需要更新哈希表的存储的下标值。

时间复杂度:O(n)
空间复杂度:O(min(m,n)) 用哈希表 unordered_map<char,int> myHash;
空间复杂度:O(m) 用table 如vector vec(256,-1);

int lengthOfLongestSubstring(string s){
	int n = s.size();
	unordered_map<char,int> myHash;
	int ans = 0;
	for(int i=0,j=0;j<n;++j){
	// 当前子串为[i,j),判断s[j] 是否在该子串中出现过
	char ch = s[j];
	if(myHash.find(ch)==myHash.end()){
	// 没有出现过
		myHash[ch] = j;
		ans = max(ans,j-i+1);
	}
	else{
	// 表示在哈希表中,但仍需判断重复字符是否在子串[i,j)范围内
		if(myHash[ch]>=i){
		// 若在子串范围内,即s[j] 与 [i,j) 内的字符重复,需更新 i
			i = myHash[ch] + 1;
		}
		// 更新s[j] 在哈希表中的映射值
		myHash[ch] = j;
		ans = max(ans,j-i+1); // 计算新子串的长度
	}
	}
	return ans;
}
int lengthOfLongestSubstring(string s){
	int n = s.size();
	vector<int> vec(256,-1);
	int ans = 0;
	for(int i=0,j=0;j<n;++j){
		char ch = s[j];
		if(vec[ch]>=i) i = vec[ch]+1;
		vec[ch] = j;
		ans = max(ans,j-i+1);
	}
	return ans;
}

4. Median of Two Sorted Arrays

题目描述:

求两个排序数组的中位数。假设至少一个数组不为空。

解法:

中位数将一个集合均分成元素个数相等的两部分,使得左边的数永远小于右边。因此求两个排序数组的中位数,就是将两个数组划分为左右相等的两部分,中位数为左边最大值与右边最小值的平均值(若总个数为奇数,则令左边的个数总比右边的个数多1,中位数为左边最大值)。

只需要求出其中一个数组一分为二的切分点(由于最终划分后左右两边的个数确定,只要确定了一个数组的切分点,就确定了另一个数组的切分点;至于为啥不能是跳着划分左右两部分,而一定是从某处一分为二,因为是排序数组,假设升序,则左边的永远小于右边);由于是已排序的数组,我们选择对元素个数较少的数组进行划分,查找划分点采用二分法,查找分三种结果:划分点太小,查找区间向右缩小;划分点太大,查找区间向左缩小;划分点正好。

对于已排序数组 nums1 和 nums2,元素个数分别为m, n。假设从i 处将 nums1 分为[0, i) 和 [i, m) 两部分,从 j 处将 nums2 分为 [0, j) 和 [j, n) 两部分。然后将 nums1 左边部分和 nums2 左边部分合并为左部分。同理合并为右部分。只要划分再合并后左右两部分个数相等,且左部分的最大值小于右部分的最小值,则划分正确,中位数为左边最大值和右边最小值的均值(若总个数为奇数,则中位数位左边的最大值)。

我们假设 m<n,划分后左边一共为 h a l f L e n = ( m + n + 1 ) / 2 halfLen=(m+n+1)/2 halfLen=(m+n+1)/2个元素。现在对nums1进行划分。i 的初始取值范围为[0,m],当i=0时,nums1左边为0个元素;当i=m时,nums1左边为m个元素;当为i时表示nums1左边为i个元素,左边最小值为nums1[i-1]。则nums2的划分点 j = h a l f L e n − i j=halfLen-i j=halfLeni ,表示nums2左边为 j j j个元素,左边最大值为nums2[j-1]。
n u m s 2 [ j − 1 ] > n u m s 1 [ i ] nums2[j-1]>nums1[i] nums2[j1]>nums1[i],则表示划分点 i i i太小,i 的取值范围更新为 [i+1,m];
n u m s 1 [ i − 1 ] > n u m s 2 [ j ] nums1[i-1]>nums2[j] nums1[i1]>nums2[j],则表示划分点 i i i太大,i 的取值范围更新为 [0, i-1);
若为其他,则表示找到正确的划分点,接下来求中位数。

参考这儿

时间复杂度:O(log(min(m,n)))
空间复杂度:O(1)

double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int m = nums1.size();
        int n = nums2.size();
        if(m>n){
        // 我们选取较短的数组进行切割,这样用时更短 O(log(min(m,n)))
            vector<int> temp = nums1;
            nums1 = nums2;
            nums2 = temp;
            int tmp = m; m = n; n = tmp;
        }
         // 一共m+n 个数,中位数将其分成个数相等的左右两个部分,左边为 halfLen 个数,
        // 当 m+n 为奇数时,左边总比右边多一个数,中位数是左边最大的数;
        // 当 m+n 为偶数时,左边与右边的个数相等,中位数为左边最大数与右边最小数的平均值。
        // 因此只需要找到nums1的切割点,以及对应的nums2的切割点,将二者左部分结合,右部分结合,将所有数均分成两个部分,
        // 且使得左边的最大值小于右边的最小值
        int iMin = 0, iMax = m, halfLen = (m+n+1)/2; 
        while(iMin <= iMax){
            // nums1 的划分点为i,左边为[0,i) 共i个数,右边 m-i个数; 右边最小值为nums1[i]
            int i = (iMin + iMax) / 2; 
            // nums2的划分点为j,左边为[0,j)共j个数,使得num1和num2划分后左边共halfLen个数;
            // 左边最大值为nums2[j-1]
            int j = halfLen - i; 
            if(i<iMax && nums2[j-1]>nums1[i]){
            // 注意边界条件 i<iMax
            // 因为nums2[j-1]总比nums2右半部分小,若nums2左边最大值大于nums1右边最小值,说明 nums2左边划分的个数太多了,即i太小了,划分点 i 应处于 [i+1,iMax] 之间
                iMin = i+1;
            }
            else if(i>iMin && nums1[i-1]>nums2[j]){
            // 因为nums1总比nums1右半部分小,若nums1左边的最小值大于nums右边的最大值,说明 nums1 左边划分的个数太多了,即 i太大了,划分点 i 应处于 [iMin,i-1]之间
                iMax = i-1;
            }
            else{
                int maxLeft = 0;
                if(i==0) {maxLeft = nums2[j-1];} // nums1左边为0个数
                else if(j==0){maxLeft = nums1[i-1]; } // nums2左边为m个数
                else{maxLeft = max(nums1[i-1],nums2[j-1]);} // 取左边的最大值
                if((m+n)%2==1) {return maxLeft;} // 当总个数为奇数时,中位数为左边的最大值
                
                int minRight = 0;
                if(i==m){minRight = nums2[j];}
                else if(j==n){minRight = nums1[i];}
                else{minRight = min(nums1[i],nums2[j]);}
                
                return (maxLeft + minRight) / 2.0;// 注意类型
            }
        }
        return 0.0; // 最后需要返回值
    }

5. Longest Palindromic Substring

题目描述

给定一个字符串,求其最长的回文子串。见这里 。回文是一个正读反读都相同的字符串。

解答:中心扩展法

一个回文字符串必定根据中心对称。即要么从一个字符开始向两边扩展如 bab; 要么从两个字符开始向两边扩展 cbaabc。
假设当前最长回文子串范围为[start, end] 。初始时令start=end=0;
我们从 i=0 开始直到 i<s.size() 扩展,尝试找到最长回文子串。每次要么从单个字符s[i]向两边扩展,要么从两个字符s[i], s[i+1] 向两边扩展。

时间复杂度:O(n^2)
空间复杂度:O(1)

string longestPalindrome(string s) {
       if(s=="" || s.length()<1)return ""; // string 是类,不能用NULL来判断是否为空。可以用s.empty(),s.size(),s==""
       int start = 0, end = 0;  // 记录当前最长回文子串的范围[start,end]
       for(int i=0;i<s.length();++i){
           int len1 = expandAroundCenter(s,i,i); // 由一个元素开始向两边扩张
           int len2 = expandAroundCenter(s,i,i+1); // 由2个元素开始向两边扩张
           int len = max(len1,len2);
           if(len>end-start+1){  // 回文中心为i,长度为len,计算出回文子串的范围
               start = i-(len-1)/2;
               end = i+len/2;
           }
       }
       return s.substr(start,end-start+1);
    }
    int expandAroundCenter(string s,int left, int right){
        // 判断[left,right]是否为回文,若为回文,则继续向两边扩张,并判断是否仍为回文
        int L = left, R = right;
        while(L>=0 && R<s.size() && s[L]==s[R]){
            L--;
            R++;
        }
        // 此时真正的回文为[L+1,R-1]
        return R-L-1; // 回文长度为 (R-1)-(L+1)+1=R-1-L-1+1=R-L-1
    }

6. ZigZag Conversion

题目描述:

给定一个字符串,例如“HELLOWORLD”,按 Z 字形排列:

        c0        c1        c2
  r0    H         O         L
  r1    E    L    W    R    D
  r2    L         O

然后按行读取成“HOLELWRDLO”,因此字符串“HELLOWORLD” 转换成“HOLELWRDLO”。请实现该转换 string convert(string s, int numRows)。

解法:找规律

以上述为例。我们先看 c 0 c_0 c0列 ‘HEL’,假设一共有 numRows 行,则 c 0 c_0 c0 r i r_i ri行对应的字符为 s[i] (0<=i<numRows)。 r 0 r_0 r0 c 0 c_0 c0列字符为 H,对应于s[0], c 1 c_1 c1列字符为 O,对应于s[4], c 2 c_2 c2列字符对应于s[8],设 pro = numRows ∗ 2 − 2 *2-2 22,则 c j c_j cj列字符对应于 s[ 0 + j ∗ 0+j* 0+jpro]。同样对于最后一行, c j c_j cj列字符对应于 s[numRows − 1 + j ∗ -1 +j* 1+jpro]。
现在我们来看中间的行,我们可以发现在 c 1 c_1 c1 c 2 c_2 c2列前要多一个字符,可以计算出 r i r_i ri c j c_j cj列前多出的字符对应的是s[0+j*pro-i],( 0 < i < n − 1 0 < i < n-1 0<i<n1, j > 0 j>0 j>0)。

然后我们处理边界情况,当 numRows=2时,无中间行,于是不需要加上中间字符;当numRows==1或者字符串s为空时,只需返回原字符串。

    string convert(string s, int numRows) {
        if(s=="" || s.size()<1 || numRows==1) return s;
        
        int n=s.size();
        vector<string> result(numRows,"");
        for(int i=0;i<numRows;++i){
        // r0列
            if(i<n){
                result[i]+=s[i];
            }
        }    
                           
        for(int i=0;i<numRows;++i){
            int echo=1,pro=numRows*2-2;
            int index = i+echo*pro;
            
            if(numRows>2&&i!=0&&i!=numRows-1){
            // 中间行
                int index_1 = 0+echo*pro-i; // 这是中间多出的那个字符的下标
                while(index_1<n){
                    result[i]+=s[index_1];
                    if(index<n){
                        result[i]+=s[index];
                        ++echo;
                        index = i+echo*pro;
                        index_1 = 0+echo*pro-i;
                    }
                    else break;
                }               
            }
            else{
            // 首末行
                while(index<n){
                    result[i]+=s[index];
                    ++echo;
                    index = i+echo*pro;
                }
            }
        }
        string res = "";
        for(int i=0;i<numRows;++i)
            res+=result[i];
        return res;    
    }

解法二:按字符串遍历

我们可以按顺序遍历字符串,用curRow记录当前行,用goingDown记录当前方向(向上还是向下),模拟将字符串写成z字形的过程。

string convert(string s, int numRows){
	if(s=="" || s.size()<1 || numRows<=1) return s;
	int n = s.size();
	vector<string> vec(min(numRows,n),"");
	int curRow = 0;
	bool goingDown = false; // 当当前行为首行或者末行时,下一步要改变方向
	for(int i=0;i<n;++i){
		vec[curRow]+=s[i]; // 当前行
		if(curRow==0 || curRow==numRows-1) goingDown = !goingDown; // 下一步方向
		curRow += goingDown? 1:-1; // 更新当前行为下一步的行		
	}
	string ret = "";
	for(int i=0;i<vec.size();++i) ret+=vec[i];
	return ret;
}

解法三:按行读取

这种解法跟第一种解法思路是一样的,我们遍历行,找到每一行每一个字符对应的是什么。
注意:字符窜大小为n,行数为numRows,按行遍历时, 0 ≤ i < n u m R o w s 0 \leq i < numRows 0i<numRows,这里的numRows 不要写成 n 啊啊啊啊啊,写错好几次了,还检查不出来错误!!!!

时间复杂度:O(n)
空间复杂度:O(n)

string convert(string s, int numRows){
	if(s=="" || numRows==1) return s;
	int n = s.size();
	string ret="";
	int cycleLen = 2*numRows-2;
	for(int i=0;i<numRows;++i){
	// 按行遍历
		for(int j=0;j+i<n;j+=cycleLen){
		// j 为 距离 i 的间隔,j+i 为该行下一个字符的下标;
		// 注意第i行的第1个字符下标为i,因此j的初始值为0。
			ret += s[j+i];
			if(i>0 && i<numRows-1 && j+cycleLen-i<n){
			// 当为中间行的时候,会多出一个字符
				ret += s[j+cycleLen-i];
			}
		}
	}
	return ret;	
}

7. Reverse Integer

题目描述

给定一个32位有符号整数,将整数反转,如123变成321。注意反转后若超过整数范围 [ − 2 31 , 2 31 − 1 ] [-2^{31},2^{31}-1] [231,2311],则返回0。

解法

这里我们需要再补充一下负数取余的知识。
余数的概念:存在唯一整数商 q q q和唯一实数 r r r使得: m = q ∗ d + r m = q*d+r m=qd+r,且 0 ≤ ∣ r ∣ < ∣ d ∣ 0\leq |r| < |d| 0r<d
对于正整数,7%5=2,默认商最小的原则,即商为1(而不是取商为2,余数为-3,实际上这样也是满足余数定义的。。。。)。
对于负整数,-7%5=-2,c++默认商最大的原则,即商为-1(而不是取商为-2,余数为3,事实上有其他的语言采用这种。)
总结一下,同号相除取余数大家都默认商最小的原则,即商要尽量向0靠;异号相除取余数看情况,有的取商最小的原则,有的取商最大的原则。

因此,对于c++而言,123%10=3,-123%10=-3。
反转:
pop = x % 10; x /= 10;
rev = rev*10+pop; // 首先要判断 rev*10 是否溢出,再要判断 rev*10+pop 是否溢出。

另外,需要考虑反转后溢出的情况,分为两种情况:
r e v 10 > 2 31 − 1 10 \frac{rev}{10} > \frac{2^{31}-1}{10} 10rev>102311,则rev 溢出
r e v 10 = 2 31 − 1 10 \frac{rev}{10}=\frac{2^{31}-1}{10} 10rev=102311且rev%10>7,则rev 溢出。
同理对于负数溢出。
注意 2 31 − 1 2^{31}-1 2311 末位为7, − 2 31 -2^{31} 231的末位为8。

时间复杂度: O ( log ⁡ 10 ( x ) ) O(\log_{10}(x)) O(log10(x))
空间复杂度:O(1)

int reverse(int x) {
    int rev = 0;
    while(x){
        int pop = x % 10;
        x /= 10;
        if(rev>INT_MAX/10 || (rev==INT_MAX/10 && pop>7)) return 0;
        if(rev<INT_MIN/10 || (rev==INT_MIN/10 && pop<-8)) return 0;
        rev = rev * 10 + pop;
    }
    return rev;
}

8. String to Integer (atoi)

题目描述

将字符串转换成整数。
首先抛弃前置空格,然后是符号,再然后是数字,直到第一个非数字字符为止。
如“ -567and cd” 输出-567。
如果不存在除前置空格以外的前置数字,则输出0。
注意限制为32位整数 [ − 2 − 31 , 2 31 − 1 ] [-2^{-31},2^{31}-1] [231,2311],若溢出,则输出INT_MAX 或者 INT_MIN。

解答

首先处理前置空格。
然后处理符号。
最后处理数字。(注意判断是否溢出)

int myAtoi(string str){
if(str=="") return 0;
int n = str.size();
int ret = 0;
int sign = 1;
int i = 0;

while(i<n&&str[i]==' ')++i; // 处理前置空格
if(i<n && str[i]=='-'){  // 注意符号后面必须跟一个数字才算有效
	sign = -1;
	++i;
}
else if(i<n && str[i]=='+'){
	sign = 1;
	++i;
}

for(;i<n;++i){
	if(str[i]>='0' && str[i]<='9'){
		int num = str[i] - '0';
        num = sign*num;
        if((ret>INT_MAX/10)||(ret==INT_MAX/10 && num>7)) return INT_MAX;
        if((ret<INT_MIN/10)||(ret==INT_MIN/10 && num<-8)) return INT_MIN;
		ret = ret*10+num; 
	}
	else break;
}
return ret;
}

9. Palindrome Number

题目描述

判断一个数字是否为回文。如1221 是回文。-5不是回文。
思路一:将数字转换成字符串,然后判断字符串是不是回文。但是需要额外的内存空间。
思路二:将数字反转,然后判断反转后的数是否和原来的数相等。但是要注意反转后的数是否超过int的范围。

解法:将数字反转一半,判断这一半和剩下的一半是否相等。

如1221,反转一半 为12=12。如何判断是否反转了一半:当剩下的x<=rev时。
若数字为偶数位如1221,只要 x = = r e v x==rev x==rev;
若数字为奇数位如12321,只要 x = = r e v / 10 x==rev/10 x==rev/10;
特殊情况:负数都不是回文。末位为0的也不是回文。如10反转为01。
(注意末位为0的情况要单独拎出来,因为反转后为0==1/10,会被误判为true)

bool isPalindrome(int x){
	if(x<0 || (x%10==0 && x!=0)) return false;
	int rev = 0;
	while(x>rev){
		rev = rev*10 + x%10;
		x = x/10;
	}
	return x==rev || x==rev/10;
}

10. Regular Expression Matching

题目描述:正则表达式匹配

‘.’:匹配任意一个字符。
‘*’:匹配0个或多个前面的字符。
“.*”:表示匹配0个或多个任意个字符。

解法一:递归

假设不含’’,只要依次匹配就行。难点是如何匹配 ‘*’。
假设’
‘前面的字符和s对应的字符不匹配,则略过’ *’,继续看后面的字符是否匹配;
假设’*'前面的字符和s对应的字符匹配,此时有可能匹配0个或多个,采用递归,继续判断。

bool isMatch(string s, string p){
	if(p=="") return s==""; // 递归出口
	// 匹配首字符
	bool first_match = (s!="" && (s[0]==p[0] || p[0]=='.'));
	if(p.size()>=2 && p[1]=='*'){
		// 若第二个字符为*,则可能匹配0个字符或多个字符
		return isMatch(s,p.substr(2)) || (first_match && isMatch(s.substr(1),p));
	}
	else{
		// 否则,只需要看首字符是否匹配,再递归
		return first_match && isMatch(s.substr(1),p.substr(1));
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值