C++ STL --map,set

1. 关联式容器

        在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、
forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面
存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。

2. 键值对

        用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然
有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。

SGI-STL中关于键值对的定义:

          

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;


template<class T1,class T2>
struct pair{

    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;
    T2 second;
    pair():first(T1()),second(T2()){}
    pair(const T1&a,const T2&b):first(a),second(b){}

};





int main()
{





system("pause");
return 0;
}

3. 树形结构的关联式容器

根据应用场景的不桶, STL 总共实现了两种不同结构的管理式容器:树型结构与哈希结构。 树型结
构的关联式容器主要有四种: map set multimap multiset 。这四种容器的共同点是:使用平衡搜索树( 即红黑树 ) 作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
翻译:
1. set 是按照一定次序存储元素的容器
2. set 中,元素的 value 也标识它 (value 就是 key ,类型为 T) ,并且每个 value 必须是唯一的。set中的元素不能在容器中修改 ( 元素总是 const) ,但是可以从容器中插入或删除它们。
3. 在内部, set 中的元素总是按照其内部比较对象 ( 类型比较 ) 所指示的特定严格弱排序准则进行排序。
4. set 容器通过 key 访问单个元素的速度通常比 unordered_set 容器慢,但它们允许根据顺序对子集进行直接迭代。
5. set 在底层是用二叉搜索树 ( 红黑树 ) 实现的。

注意:
1. map/multimap 不同, map/multimap 中存储的是真正的键值对 <key, value> set 中只放value,但在底层实际存放的是由 <value, value> 构成的键值对。
2. set 中插入元素时,只需要插入 value 即可,不需要构造键值对。
3. set 中的元素不可以重复 ( 因此可以使用 set 进行去重 )
4. 使用 set 的迭代器遍历 set 中的元素,可以得到有序序列
5. set 中的元素默认按照小于来比较
6. set 中查找某个元素,时间复杂度为: $log_2 n$
7. set 中的元素不允许修改 ( 为什么 ?)
8. set 中的底层使用二叉搜索树 ( 红黑树 ) 来实现。

3.1.2 set的使用

 1. set的模板参数列表

        

T: set 中存放元素的类型,实际在底层存储 <value, value> 的键值对。
Compare set 中元素默认按照小于来比较Alloc: set 中元素空间的管理方式,使用 STL 提供的空间配置器管理。
2. set 的构造
        

3. set 的迭代器
        
        

 4. set的容量

        

 5. set修改操作

        

 6. set的使用举例

        

#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
using namespace std;

template <class T1, class T2>
struct pair
{

    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;
    T2 second;
    pair() : first(T1()), second(T2()) {}
    pair(const T1 &a, const T2 &b) : first(a), second(b) {}
};

void TestSet()
{

    int array[] = {1, 3, 4, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
    set<int> s(array, array + sizeof(array) / sizeof(array[0]));
    cout << s.size() << endl;

    //正向打印set的元素,Con打印结果中可以看出,set 可以去重
    for (auto &e : s)
    {
        cout << e << " ";
    }
    cout << endl;

    for (auto it = s.rbegin(); it != s.rend(); it++)
    {
        cout << *it << " ";
    }
    cout << endl;
    cout << s.count(3) << endl;
}

int main()
{

    TestSet();

    system("pause");
    return 0;
}

3.2 map

3.2.1 map的介绍

https://cplusplus.com/reference/map/map/?kw=map

翻译:
1. map 是关联容器,它按照特定的次序 ( 按照 key 来比较 ) 存储由键值 key 和值 value 组合而成的元素。
2. map 中,键值 key 通常用于排序和惟一地标识元素,而值 value 中存储与此键值 key 关联的内容。键值key 和值 value 的类型可能不同,并且在 map 的内部, key value 通过成员类型 value_type绑定在一起,为其取别名称为 pair: typedef pair<const key, T> value_type;
3. 在内部, map 中的元素总是按照键值 key 进行比较排序的。
4. map 中通过键值访问单个元素的速度通常比 unordered_map 容器慢,但 map 允许根据顺序对元素进行直接迭代( 即对 map 中的元素进行迭代时,可以得到一个有序的序列 )
5. map 支持下标访问符,即在 [] 中放入 key ,就可以找到与 key 对应的 value
6. map 通常被实现为二叉搜索树 ( 更准确的说:平衡二叉搜索树 ( 红黑树 ))

3.2.2 map的使用

        1. map的模板参数说明

        

key: 键值对中 key 的类型
T : 键值对中 value 的类型
Compare: 比较器的类型, map 中的元素是按照 key 来比较的,缺省情况下按照小于来比较,一般情况下( 内置类型元素 ) 该参数不需要传递,如果无法比较时 ( 自定义类型 ) ,需要用户自己显式传递比较规则( 一般情况下按照函数指针或者仿函数来传递 )
Alloc :通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的
空间配置器
注意:在使用 map 时,需要包含头文件。

 2. map的构造

 3. map的迭代器

        

 4. map的容量与元素访问

        

 问题:当key不在map中时,通过operator获取对应value时会发生什么问题?

注意:在元素访问时,有一个与 operator[] 类似的操作 at()( 该函数不常用 ) 函数,都是通过key找到与 key 对应的 value 然后返回其引用,不同的是: key 不存在时, operator[] 用默认 value key 构造键值对然后插入,返回该默认 value at() 函数直接抛异常
5. map 中元素的修改

 

【总结】
1. map 中的的元素是键值对
2. map 中的 key 是唯一的,并且不能修改
3. 默认按照小于的方式对 key 进行比较
4. map 中的元素如果用迭代器去遍历,可以得到一个有序的序列
5. map 的底层为平衡搜索树 ( 红黑树 ) ,查找效率比较高 $O(log_2 N)$
6. 支持 [] 操作符, operator[] 中实际进行插入查找。

3.3 multiset

3.3.1 multiset的介绍

        cplusplus.com/reference/set/multiset/?kw=multiset

1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
3. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排序。
4. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
5. multiset底层结构为二叉搜索树(红黑树). 

注意:

1. multiset中在底层中存储的是<value, value>的键值对
2. mtltiset的插入接口中只需要插入即可
3. 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列

5. multiset中的元素不能修改
6. 在multiset中找某个元素,时间复杂度为$O(log_2 N)$
7. multiset的作用:可以对元素进行排序

3.3.2 multiset的使用

此处只简单演示set与multiset的不同,其他接口接口与set相同,同学们可参考set。

#include <iostream>
#include <set>
using namespace std;

void test01()
{
    int arr[] = {2, 1, 3, 9, 6, 0, 5, 8, 4, 7};
    multiset<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));

    for (auto &e : s)
    {
        cout << e << " ";
    }
    cout << endl;
}

int main(int argc, char const *argv[S])
{
    test01();

    system("pause");
    return 0;
}

3.4 multimap

        3.4.1 multimap的介绍

官方文档:http://www.cplusplus.com/reference/map/multimap/?kw=multimap

1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key,
value>,其中多个键值对之间的key是可以重复的。
2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,
value_type是组合key和value的键值对:
typedef pair<const Key, T> value_type;
3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序的。
4. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。
5. multimap在底层用二叉搜索树(红黑树)来实现。

注意:multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以
重复的

3.4.2 multimap的使用

        multimap中的接口可以参考map,功能都是类似的。
注意:
1. multimap中的key是可以重复的。
2. multimap中的元素默认将key按照小于来比较
3. multimap中没有重载operator[]操作(同学们可思考下为什么?)。
4. 使用时与map包含的头文件相同

3.5 在OJ中的使用

692. 前K个高频单词 - 力扣(Leetcode)


class compare{

    public:
        bool operator()(const pair<string,int> & p1,const pair<string,int> & p2){
        
                return p1.second>p2.second||p1.second==p2.second&&p1.first<p2.first;
    
        }

} ;

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        // map<string,int> mp;
        unordered_map<string,int> mp;
        for(int i=0;i<words.size();i++){
            mp[words[i]]++;
        }
        vector<string> ans;
        vector<pair<string,int> > v(mp.begin(),mp.end());
        sort(v.begin(),v.end(),compare());
        for(size_t i=0;i<k;i++){
            ans.push_back(v[i].first);
        }
            return ans;
    }
};

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
       unordered_set<int> s1;
        
       unordered_set<int> s2;
        vector<int> ans;
        for(int i=0;i<nums1.size();i++){
            s1.insert(nums1[i]);
        }
        for(int i=0;i<nums2.size();i++){
            s2.insert(nums2[i]);
        }

        for(auto e:s1){
            auto it=s2.find(e);
            if(it!=s2.end())
                ans.push_back(e);
        }

        return ans;


    }
};

4. 底层结构

前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个
共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

4.1 AVL 树

4.1.1 AVL树的概念

        二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树。

  •  它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在
$O(log_2 n)$,搜索时间复杂度O($log_2 n$)

4.1.2 AVL树节点的定义

#include <iostream>
#include <set>
#include <map>
#include <list>
#include <unordered_map>
using namespace std;

template <class T>

struct AVTreeNode
{
    AVTreeNode(const T &data) : _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr), _bf(0)
    {
    }

    AVTreeNode<T> *_pLeft;
    AVTreeNode<T> *_pRight;
    AVTreeNode<T> *_pParent;
    T _data;
    int _bf;
};

int main()
{
    AVTreeNode<int> t1(1);
    // cout<<"Hello"<<endl;

    system("pause");
    return 0;
}

4.1.3 AVL树的插入

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么 AVL树的插入过程可以分为两步:

1. 按照二叉搜索树的方式插入新节点

2. 调整节点的平衡因子

class AVLTree
{
public:
    bool Insert(const T &data)
    {
        // 1. 先按照二叉搜索树的规则将节点插入到AVL树中
        // ...

        // 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
        破坏了AVL树
        //  的平衡性

        /*
        pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
        的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
        1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
        2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可

        此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
        1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整
       成0,此时满足
          AVL树的性质,插入成功
        2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
       新成正负1,此
          时以pParent为根的树的高度增加,需要继续向上更新
        3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进
       行旋转处理
        */
        while (pParent)
        {
            // 更新双亲的平衡因子
            if (pCur == pParent->_pLeft)
                pParent->_bf--;
            else
                pParent->_bf++;
            // 更新后检测双亲的平衡因子
            if (0 == pParent->_bf)
            {
                break;
            }
            else if (1 == pParent->_bf || -1 == pParent->_bf)
            {
                // 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲
                为根的二叉树
                // 的高度增加了一层,因此需要继续向上调整
                pCur = pParent;
                pParent = pCur->_pParent;
            }
            else
            {
                // 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
                // 为根的树进行旋转处理
                if (2 == pParent->_bf)
                {
                    // ...
                }
                else
                {
                    // ...
                }
            }
        }
        return true;
    }
};

4.1.4 AVL树的旋转

如果在一棵原本是平衡的 AVL 树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL 树的旋转分为四种:
         1. 新节点插入较高左子树的左侧 --- 左左:右单旋

实现及情况考虑可参考右单旋。 

        3. 新节点插入较高左子树的右侧 --- 左右:先左单旋再右单旋
        

 

 将双旋变成单旋后再旋转,即: 先对 30 进行左单旋,然后再对 90 进行右单旋 ,旋转完成后再考虑平衡因子的更新。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

template <class T>
struct AVLTreeNode
{
    AVLTreeNode(const T &data) : _pLeft(nullptr), _pRight(nullptr),
                                 _pParent(nullptr), _data(data), _bf(0)
    {
    }

    AVLTreeNode<T> *_pLeft;   //该结点在左
    AVLTreeNode<T> *_pRight;  //节点的右子孩
    AVLTreeNode<T> *_pParent; //节点的双亲

    T _data;
    int _bf; //平衡因子
};

class AVLTree
{
public:
    bool Insert(const T &data)
    {
        // 1. 先按照二叉搜索树的规则将节点插入到AVL树中
        // ...

        // 2. 新节点插入后,AVL树的平衡性可能会遭到破坏,此时就需要更新平衡因子,并检测是否
        破坏了AVL树
        //  的平衡性

        /*
        pCur插入后,pParent的平衡因子一定需要调整,在插入之前,pParent
        的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:
        1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
        2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可

        此时:pParent的平衡因子可能有三种情况:0,正负1, 正负2
        1. 如果pParent的平衡因子为0,说明插入之前pParent的平衡因子为正负1,插入后被调整
       成0,此时满足
          AVL树的性质,插入成功
        2. 如果pParent的平衡因子为正负1,说明插入前pParent的平衡因子一定为0,插入后被更
       新成正负1,此
          时以pParent为根的树的高度增加,需要继续向上更新
        3. 如果pParent的平衡因子为正负2,则pParent的平衡因子违反平衡树的性质,需要对其进
       行旋转处理
        */
        while (pParent)
        {
            // 更新双亲的平衡因子
            if (pCur == pParent->_pLeft)
                pParent->_bf--;
            else
                pParent->_bf++;
            // 更新后检测双亲的平衡因子
            if (0 == pParent->_bf)
            {
                break;
            }
            else if (1 == pParent->_bf || -1 == pParent->_bf)
            {
                // 插入前双亲的平衡因子是0,插入后双亲的平衡因为为1 或者 -1 ,说明以双亲
                为根的二叉树
                // 的高度增加了一层,因此需要继续向上调整
                pCur = pParent;
                pParent = pCur->_pParent;
            }
            else
            {
                // 双亲的平衡因子为正负2,违反了AVL树的平衡性,需要对以pParent
                // 为根的树进行旋转处理
                if (2 == pParent->_bf)
                {
                    // ...
                }
                else
                {
                    // ...
                }
            }
        }
        return true;
    }

    // 旋转之前,60的平衡因子可能是-1/0/1,旋转完成之后,根据情况对其他节点的平衡因子进
    // 行调整
    void _RotateLR(PNode pParent)
    {
        PNode pSubL = pParent->_pLeft;
        PNode pSubLR = pSubL->_pRight;

        // 旋转之前,保存pSubLR的平衡因子,旋转完成之后,需要根据该平衡因子来调整其他节
        点的平衡因子
        int bf = pSubLR->_bf;

        // 先对30进行左单旋
        _RotateL(pParent->_pLeft);

        // 再对90进行右单旋
        _RotateR(pParent);
        if (1 == bf)
            pSubL->_bf = -1;
        else if (-1 == bf)
            pParent->_bf = 1;
    }
};

int main()
{

    system("pause");
    return 0;
}
        4. 新节点插入较高右子树的左侧 --- 右左:先右单旋再左单旋
参考右左双旋。
总结: 假如以pParent 为根的子树不平衡,即 pParent 的平衡因子为 2 或者 -2 ,分以下情况考虑:
        
1. pParent 的平衡因子为 2 ,说明 pParent 的右子树高,设 pParent 的右子树的根为 pSubR
pSubR 的平衡因子为 1 时,执行左单旋 ,当pSubR 的平衡因子为 -1 时,执行右左双旋
2. pParent 的平衡因子为 -2 ,说明 pParent 的左子树高,设 pParent 的左子树的根为 pSubL 当pSubL 的平衡因子为 -1 是,执行右单旋当pSubL 的平衡因子为 1 时,执行左右双旋旋转完成后,原pParent 为根的子树个高度降低,已经平衡,不需要再向上更新。

 

4.1.5 AVL树的验证

AVL 树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证 AVL 树,可以分两步:
1. 验证其为二叉搜索树
如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
2. 验证其为平衡树
每个节点子树高度差的绝对值不超过 1( 注意节点中如果没有平衡因子 ) 节点的平衡因子是否计算正确.

4.1.6 AVL树的删除(了解)  

因为 AVL 树也是二叉搜索树,可按照二叉搜索树的方式将节点删除,然后再更新平衡因子,只不
错与删除不同的时,删除节点后的平衡因子更新,最差情况下一直要调整到根节点的位置。
具体实现学生们可参考《算法导论》或《数据结构 - 用面向对象方法与 C++ 描述》殷人昆版。

4.1.7 AVL树的性能

 AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这 样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操 作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时, 有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数 据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。

3. 验证用例
请同学们结合上述代码按照以下的数据次序,自己动手画 AVL 树的创建过程,验证代码
是否有漏洞。

4.2 红黑树

4.2.1 红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是RedBlack。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。

4.2.2 红黑树的性质

1. 每个结点不是红色就是黑色
2. 根节点是黑色的 
3. 如果一个节点是红色的,则它的两个孩子结点是黑色的 
4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点 
5. 每个叶子结点都是黑色的 ( 此处的叶子结点指的是空结点 )
思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点
个数的两倍?

 4.2.3 红黑树节点的定义

        

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

enum Color
{
    RED,
    BLACK
};
template <class valueType>

struct RBTreeNode
{

    RBTreeNode(const valueType &data = valueType(), Color color = RED) : _pLeft(nullptr), _pRight(nullptr),
                                                                         _pParent(nullptr), _data(data), _color(color) {}

    RBTreeNode<valueType> *_pLeft;
    RBTreeNode<valueType> *_pRight;
    RBTreeNode<valueType> *_pParent;

    valueType _data; //节点的值域
    Color _color;    //结点的颜色
};

int main()
{

    system("pause");
    return 0;
}
思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?

4.2.4 红黑树结构

为了后续实现关联式容器简单,红黑树的实现中增加一个头结点,因为跟节点必须为黑色,为了
与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点, pLeft
域指向红黑树中最小的节点, _pRight 域指向红黑树中最大的节点,如下:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值