【数据结构】搜索二叉树/map/set

  1. 二叉搜索树(搜索二叉树)

1.1.二叉搜索树概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
我们就可以看出他的中序遍历是一个升序序列。
他的查找非常非常的快,虽然一些极端情况下效率的提升不高,后面我们会改进为平衡搜索二叉树会有log(n) 的时间复杂度很快。天生为搜索而生。

1.2.二叉搜索树操作

搜索二叉树的结构(类)

和普通二叉树的结构是一样的,就是插入的时候,需要按照一定的规则插入。
template<class K>
struct BSTreeNode
{
    BSTreeNode<K>* _left;
    BSTreeNode<K>* _right;
    K _key;
    BSTreeNode(const K& val)
        :_key(val)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

template<class K>
class BSTree
{
    typedef BSTreeNode<K> Node;
public:
//默认成员函数
    //无参构造
    BSTree()
        :_root(nullptr)
    {}
    //拷贝构造
    BSTree(const BSTree<K>& tree)//注意传入引用
    {
        //如果不断的插入的方式进行拷贝构造,树的形状会发生改变
        _root = _Copy(tree._root);
    }
    //赋值
    //注意引用返回
    BSTree<K>& operator=(BSTree<K> t)//先拷贝
    {
        swap(this->_root, t._root);
        return *this;
    }
    //析构
    ~BSTree()
    {
        //后续递归删除
        _Destory(_root);
    }
//普通成员函数
//增删改查
    ///后面分模块讲解

private:
    //在类里面写递归都会存在这样一个问题。     
    //一般都会写一个子函数去调用子函数。
    void _Destory(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }
        //后续删除
        _Destory(root->_left);
        _Destory(root->_right);
        delete root;
        root = nullptr;
        //也可以保存子树+前序删除
    }
    Node* _Copy(const Node* root)
    {
        if (root == nullptr)
        {
            return nullptr;
        }
        //注意这里一定要创建一个新的变量,不能直接使用root;
        Node* newroot = new Node(root->_key);
        newroot->_left = _Copy(root->_left);
        newroot->_right = _Copy(root->_right);
        return newroot;
    }
    Node* _root = nullptr;
};

查找

a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
bool find1(const K& val)//迭代版本
{
    if (_root == nullptr) { return false; }
    Node* cur = _root;
    while (cur)
    {
        if (cur->_key > val) { cur = cur->_left; }
        else if (cur->_key < val) { cur = cur->_right; }
        else { return true; }
    }
    return false;
}

bool find2(const K& val)//递归版本
{
    return _FindR(_root, val);
}

bool _FindR(Node* root, const K& val)
{
    if (root == nullptr){return false;}

    if (root->_key > val) { return _FindR(root->_left,val); }
    else if (root->_key < val) { return _FindR(root->_right, val); }
    else{ return true; }
}

插入

a. 树为空,则直接新增节点,赋值给root指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
//这种插入无法控制平衡
//可能是一个歪脖子树
//后面我会讲到怎么插入时候控制平衡(AVL树和红黑树)

bool insert1(const K& val) {
    Node* newnode = new Node(val);
    if (_root == nullptr){
        _root = newnode;
        return true;
    }
    Node* cur = _root;
    Node* prev = nullptr;
    /*while (cur)
    {
        if (cur->_key > val){ prev = cur; cur = cur->_left;}
        else if (cur->_key < val){ prev = cur; cur = cur->_right;}
        else{return false;}    
    }*/
    while (cur)//为什么两个while都可以? 想一想!!
    {
        prev = cur;
        if (cur->_key > val) {  cur = cur->_left; }
        else if (cur->_key < val) {  cur = cur->_right; }
        else { return false; }
    }

    if (prev->_key > val) { prev->_left = newnode; }
    else { prev->_right = newnode; }
    return true;
}

bool insert2(const K& val)//递归版本
{
    return _insertR2(_root, val);
}

bool _insertR2(Node* & root, const K& val)//注意这里的引用(非常巧妙)
{
    if (root == nullptr)
    {
        root = new Node(val);
        return true;
    }
    if (root->_key > val) { return _insertR2(root->_left, val); }
    else if (root->_key < val) { return _insertR2(root->_right, val); }
    else { return false;}
}

删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点

看起来有待删除节点有4中情况,实际情况a可以与情况b或者c合并起来,因此真正的删除过程如下:

情况a/b/c:删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点--直接删除
情况d:在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题--替换法删除

替换:左树最大,右树最小,替换后就是a/b/c三种中的一种了。

bool erase1(const K& val)
{
    if (_root == nullptr) { return false; }
    Node* cur = _root;
    Node* prev = nullptr;
    while (cur)
    {
        if (cur->_key > val) { prev = cur; cur = cur->_left; }
        else if (cur->_key < val) { prev = cur; cur = cur->_right; }
        else { 
            //找到了此节点
            //现在cur指向要删除节点。
            //cur是root :prev是nullptr
            //cur不是root:prev是cur的父亲节点.(不为空)
            //要删除的节点如果是叶子节点或者是一个子节点的非叶子节点,都是很好解决的
            if (cur->_left == nullptr)
            {
                if (cur == _root)
                {
                    _root = _root->_right;
                }
                else//cur != _root
                {
                    if (prev->_left == cur) { prev->_left = cur->_right; }
                    else { prev->_right = cur->_right; }
                }
                delete cur;
            }
            else if (cur->_right == nullptr)
            {
                if (cur == _root)
                {
                    _root = _root->_left;
                }
                else//cur != _root
                {
                    if (prev->_left == cur) { prev->_left = cur->_left; }
                    else { prev->_right = cur->_left; }
                }
                delete cur;
            }
            else
            {//处理有两个子节点的节点。
                //现在cur指向要删除节点。
                //cur是root :prev是nullptr
                //cur不是root:prev是cur的父亲节点(并且prev一定有两个儿子。
                //基本思路就是 交换再删除。
                //将cur节点和(左子树的最大节点)/(右子树的最小节点)进行交换。
                //然后在删掉次对应的节点

                Node* del = MinRight(cur->_right);
                int tmp = del->_key;
                erase1(tmp);
                cur->_key = tmp;    
            }
            return true;
        }
    }
    return false;
}


bool erase2(const K& val)
{
    return _EraseR2(_root, val);
}
bool _EraseR2(Node*& root, const K& val)
{
    if (root == nullptr)//注意这里的判断,没找到或者空树
    {
        return false;
    }
    if (root->_key > val) { return _EraseR2(root->_left, val); }
    else if (root->_key < val) { return _EraseR2(root->_right, val); }
    else {
        //找到了
        //root所指向的节点就是要删除的节点。(注意引用)
        if (root->_left == nullptr) { auto tmp = root->_right;  delete root; root = tmp; }
        else if (root->_right == nullptr) { auto tmp = root->_left;  delete root;  root = tmp; }
        else
        {
            Node* tmp = MinRight(root->_right);
            swap(tmp->_key, root->_key);
            _EraseR2(root->_right, val);
        }
        return true;
    }
}
  1. 二叉搜索树的应用

2.1.K模型

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到

的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

用上面我们自己实现的搜索树做一个字典查找:

void test7()
{
    //把词库中的单词都insert进二叉树,
    //然后find这个词即可,
    //找到返回真,找不到返回假

    BSTree<string> dict;  
    dict.insert1("zhang");
    dict.insert1("gao");
    dict.insert1("liu");
    dict.insert1("zhao");

    cout << dict.find1("liu") << endl;
    cout << dict.find1("gao") << endl;
    cout << dict.find1("zhang") << endl;
    cout << dict.find1("zhao") << endl;
    cout << dict.find1("meng") << endl;
    cout << dict.find1("msdf") << endl;
    cout << dict.find1("msdferger") << endl;
}

其实就是快速的查找在不在。后面会有专门的容器set。就是K模型。

2.2.KV模型

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对

该种方式在现实生活中非常常见:

比如 英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文<word, chinese>就构成一种键值对;
再比如 统计单词次数,统计成功后,给定单词就可快速找到其出现的次数, 单词与其出
现次数就是<word, count>就构成一种键值对
#include<string>
#include<iostream>
using namespace std
template<class K, class V>
struct BSTreeNode
{
    BSTreeNode<K,V>* _left;
    BSTreeNode<K,V>* _right;
    K _key;
    V _val;

    BSTreeNode(const K& key, const V& val)
        :_key(key)
        ,_val(val)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

template<class K, class V>
class BSTree
{
    typedef BSTreeNode<K, V> Node;
    typedef BSTree<K,V> Tree;
public:
    //无参构造
    BSTree()
        :_root(nullptr)
    {}
    //拷贝构造
    BSTree(const Tree& tree)//注意传入引用
    {
        //如果不断的插入,这就会树的形状会发生改变
        _root = _Copy(tree._root);
    }

    ~BSTree()
    {
        //后续递归删除
        _Destory(_root);
    }

    //赋值
    //注意引用返回
    Tree& operator=(Tree t)
    {
        swap(this->_root, t._root);
        return *this;
    
    bool insert(const K& key, const V& val) {
        Node* newnode = new Node(key, val);
        if (_root == nullptr) {
            _root = newnode;
            return true;
        }
        Node* cur = _root;
        Node* prev = nullptr;
        /*while (cur)
        {
            if (cur->_key > val){ prev = cur; cur = cur->_left;}
            else if (cur->_key < val){ prev = cur; cur = cur->_right;}
            else{return false;}
        }*/
        while (cur)//为什么两个while都可以? 想一想!!
        {
            prev = cur;
            if (cur->_key > key) { cur = cur->_left; }
            else if (cur->_key < key) { cur = cur->_right; }
            else { return false; }
        }
        if (prev->_key > key) { prev->_left = newnode; }
        else { prev->_right = newnode; }
        return true;
        //这种插入无法控制平衡
    }

    void print() { _print(_root); }
    //在类里面写递归都会存在这样一个问题。     
    //一般都会写一个子函数去调用子函数。


    Node* find(const K& val)
    {
        if (_root == nullptr) { return nullptr; }
        Node* cur = _root;
        while (cur)
        {
            if (cur->_key > val) { cur = cur->_left; }
            else if (cur->_key < val) { cur = cur->_right; }
            else { return cur; }
        }
        return nullptr;
    }

    bool erase(const K& val)
    {
        if (_root == nullptr) { return false; }
        Node* cur = _root;
        Node* prev = nullptr;
        while (cur)
        {
            if (cur->_key > val) { prev = cur; cur = cur->_left; }
            else if (cur->_key < val) { prev = cur; cur = cur->_right; }
            else {
                //找到了此节点
                //现在cur指向要删除节点。
                //cur是root :prev是nullptr
                //cur不是root:prev是cur的父亲节点.(不为空)
                //要删除的节点如果是叶子节点或者是一个子节点的非叶子1节点,都是很好解决的
                if (cur->_left == nullptr)
                {
                    if (cur == _root)
                    {
                        _root = _root->_right;
                    }
                    else//cur != _root
                    {
                        if (prev->_left == cur) { prev->_left = cur->_right; }
                        else { prev->_right = cur->_right; }
                    }
                    delete cur;
                }
                else if (cur->_right == nullptr)
                {
                    if (cur == _root)
                    {
                        _root = _root->_left;
                    }
                    else//cur != _root
                    {
                        if (prev->_left == cur) { prev->_left = cur->_left; }
                        else { prev->_right = cur->_left; }
                    }
                    delete cur;
                }
                else
                {//处理有两个子节点的节点。
                    //现在cur指向要删除节点。
                    //cur是root :prev是nullptr
                    //cur不是root:prev是cur的父亲节点(并且prev一定有两个儿子。
                    //基本思路就是 交换再删除。
                    //将cur节点和(左子树的最大节点)/(右子树的最小节点)进行交换。
                    //然后在删掉次对应的节点

                    Node* del = MinRight(cur->_right);
                    int tmp = del->_key;
                    erase1(tmp);
                    cur->_key = tmp;
                }
                return true;
            }
        }
        return false;
    }






private:
    void _print(Node* root) {
        if (root == nullptr) { return; }
        _print(root->_left);
        cout << '<' << root->_key << ',' << root->_val << '>' << endl;
        _print(root->_right);
    }
    Node* MinRight(Node* root)
    {
        while (root->_left) { root = root->_left; }
        return root;
    }
    void _Destory(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }
        //后续删除
        _Destory(root->_left);
        _Destory(root->_right);
        delete root;
        root = nullptr;
    }
    Node* _Copy(const Node* root)
    {
        if (root == nullptr)
        {
            return nullptr;
        }
        //注意这里一定要创建一个新的变量,不能直接使用root;
        Node* newroot = new Node(root->_key, root->_val);
        newroot->_left = _Copy(root->_left);
        newroot->_right = _Copy(root->_right);
        return newroot;
    }
    Node* _root = nullptr;
};



void kv_test1()
{
    // 输入单词,查找单词对应的中文翻译
    BSTree<string, string> dict;
    dict.insert("string", "字符串");
    dict.insert("tree", "树");
    dict.insert("left", "左边、剩余");
    dict.insert("right", "右边");
    dict.insert("sort", "排序");
    dict.print();

    string str;
    while (cin >> str)
    {
        BSTreeNode<string, string>* ret = dict.find(str);
        if (ret == nullptr)
        {
            cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
        }
        else
        {
            cout << str << "中文翻译:" << ret->_val << endl;
        }
    }
}


void kv_test2()
{
    // 统计水果出现的次数
    string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜",
    "西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" };
    BSTree<string, int> countTree;
    for (const auto& str : arr)
    {
        // 先查找水果在不在搜索树中
        // 1、不在,说明水果第一次出现,则插入<水果, 1>
        // 2、在,则查找到的节点中水果对应的次数++
        //BSTreeNode<string, int>* ret = countTree.Find(str);
        auto ret = countTree.find(str);
        if (ret == NULL)
        {
            countTree.insert(str, 1);
        }
        else
        {
            ret->_val++;
        }
    }
    countTree.print();
}#include<string>
#include<iostream>
using namespace std;


template<class K, class V>
struct BSTreeNode
{
    BSTreeNode<K,V>* _left;
    BSTreeNode<K,V>* _right;
    K _key;
    V _val;

    BSTreeNode(const K& key, const V& val)
        :_key(key)
        ,_val(val)
        , _left(nullptr)
        , _right(nullptr)
    {}
};

template<class K, class V>
class BSTree
{
    typedef BSTreeNode<K, V> Node;
    typedef BSTree<K,V> Tree;
public:
    //无参构造
    BSTree()
        :_root(nullptr)
    {}
    //拷贝构造
    BSTree(const Tree& tree)//注意传入引用
    {
        //如果不断的插入,这就会树的形状会发生改变
        _root = _Copy(tree._root);
    }

    ~BSTree()
    {
        //后续递归删除
        _Destory(_root);
    }

    //赋值
    //注意引用返回
    Tree& operator=(Tree t)
    {
        swap(this->_root, t._root);
        return *this;
    }




    
    bool insert(const K& key, const V& val) {
        Node* newnode = new Node(key, val);
        if (_root == nullptr) {
            _root = newnode;
            return true;
        }
        Node* cur = _root;
        Node* prev = nullptr;
        /*while (cur)
        {
            if (cur->_key > val){ prev = cur; cur = cur->_left;}
            else if (cur->_key < val){ prev = cur; cur = cur->_right;}
            else{return false;}
        }*/
        while (cur)//为什么两个while都可以? 想一想!!
        {
            prev = cur;
            if (cur->_key > key) { cur = cur->_left; }
            else if (cur->_key < key) { cur = cur->_right; }
            else { return false; }
        }
        if (prev->_key > key) { prev->_left = newnode; }
        else { prev->_right = newnode; }
        return true;
        //这种插入无法控制平衡
    }

    void print() { _print(_root); }
    //在类里面写递归都会存在这样一个问题。     
    //一般都会写一个子函数去调用子函数。


    Node* find(const K& val)
    {
        if (_root == nullptr) { return nullptr; }
        Node* cur = _root;
        while (cur)
        {
            if (cur->_key > val) { cur = cur->_left; }
            else if (cur->_key < val) { cur = cur->_right; }
            else { return cur; }
        }
        return nullptr;
    }

    bool erase(const K& val)
    {
        if (_root == nullptr) { return false; }
        Node* cur = _root;
        Node* prev = nullptr;
        while (cur)
        {
            if (cur->_key > val) { prev = cur; cur = cur->_left; }
            else if (cur->_key < val) { prev = cur; cur = cur->_right; }
            else {
                //找到了此节点
                //现在cur指向要删除节点。
                //cur是root :prev是nullptr
                //cur不是root:prev是cur的父亲节点.(不为空)
                //要删除的节点如果是叶子节点或者是一个子节点的非叶子1节点,都是很好解决的
                if (cur->_left == nullptr)
                {
                    if (cur == _root)
                    {
                        _root = _root->_right;
                    }
                    else//cur != _root
                    {
                        if (prev->_left == cur) { prev->_left = cur->_right; }
                        else { prev->_right = cur->_right; }
                    }
                    delete cur;
                }
                else if (cur->_right == nullptr)
                {
                    if (cur == _root)
                    {
                        _root = _root->_left;
                    }
                    else//cur != _root
                    {
                        if (prev->_left == cur) { prev->_left = cur->_left; }
                        else { prev->_right = cur->_left; }
                    }
                    delete cur;
                }
                else
                {//处理有两个子节点的节点。
                    //现在cur指向要删除节点。
                    //cur是root :prev是nullptr
                    //cur不是root:prev是cur的父亲节点(并且prev一定有两个儿子。
                    //基本思路就是 交换再删除。
                    //将cur节点和(左子树的最大节点)/(右子树的最小节点)进行交换。
                    //然后在删掉次对应的节点

                    Node* del = MinRight(cur->_right);
                    int tmp = del->_key;
                    erase1(tmp);
                    cur->_key = tmp;
                }
                return true;
            }
        }
        return false;
    }






private:
    void _print(Node* root) {
        if (root == nullptr) { return; }
        _print(root->_left);
        cout << '<' << root->_key << ',' << root->_val << '>' << endl;
        _print(root->_right);
    }
    Node* MinRight(Node* root)
    {
        while (root->_left) { root = root->_left; }
        return root;
    }
    void _Destory(Node* root)
    {
        if (root == nullptr)
        {
            return;
        }
        //后续删除
        _Destory(root->_left);
        _Destory(root->_right);
        delete root;
        root = nullptr;
    }
    Node* _Copy(const Node* root)
    {
        if (root == nullptr)
        {
            return nullptr;
        }
        //注意这里一定要创建一个新的变量,不能直接使用root;
        Node* newroot = new Node(root->_key, root->_val);
        newroot->_left = _Copy(root->_left);
        newroot->_right = _Copy(root->_right);
        return newroot;
    }
    Node* _root = nullptr;
};



void kv_test1()
{
    // 输入单词,查找单词对应的中文翻译
    BSTree<string, string> dict;
    dict.insert("string", "字符串");
    dict.insert("tree", "树");
    dict.insert("left", "左边、剩余");
    dict.insert("right", "右边");
    dict.insert("sort", "排序");
    dict.print();

    string str;
    while (cin >> str)
    {
        BSTreeNode<string, string>* ret = dict.find(str);
        if (ret == nullptr)
        {
            cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
        }
        else
        {
            cout << str << "中文翻译:" << ret->_val << endl;
        }
    }
}


void kv_test2()
{
    // 统计水果出现的次数
    string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果", "香蕉", "苹果", "香蕉" ,"西瓜" ,"苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果","苹果",
    "西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜",
    "西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" ,"西瓜" };
    BSTree<string, int> countTree;
    for (const auto& str : arr)
    {
        // 先查找水果在不在搜索树中
        // 1、不在,说明水果第一次出现,则插入<水果, 1>
        // 2、在,则查找到的节点中水果对应的次数++
        //BSTreeNode<string, int>* ret = countTree.Find(str);
        auto ret = countTree.find(str);
        if (ret == NULL)
        {
            countTree.insert(str, 1);
        }
        else
        {
            ret->_val++;
        }
    }
    countTree.print();
}

我们以后不用自己遭轮子,后面会有专门的容器map。就是K模型。

  1. 二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:(logN)

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:(N/2)

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插

入关键码,二叉搜索树的性能都能达到最优?

那么我们后续章节学习的AVL树和红黑树就可以上场了。

  1. 二叉树经典面试题

1. 二叉树创建字符串。OJ链接

class Solution {
public:
    string tree2str(TreeNode* root) {
        if(root == nullptr)
        {
            return "";
        }
        string str;
        str+=to_string(root->val);
        if(root->left != nullptr || root->right != nullptr) {
            str += '(';
            str += tree2str(root->left);
            str += ')';
        }
        if(root -> right!= nullptr){
            str += '(';
            str += tree2str(root->right);
            str += ')';
        }
        return str;
    }
};

2. 二叉树的分层遍历1。OJ链接

方法一:直接深度递归(dfs+递归的层数)

class Solution {
public:
    void _levelOrder(vector<vector<int>>& vv, TreeNode* root, int c)
    {
        if(root == nullptr){return;}
        if(vv.size()<= c)
        {//注意提前开空间和判断是否需要开空间
            vv.push_back(vector<int>());
        }
        vv[c].push_back(root->val);
        _levelOrder(vv,root->left,c+1);
        _levelOrder(vv,root->right,c+1);
    }
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vv ;
        _levelOrder(vv, root,0);
        return vv;
    }
};

方法二:队列(queue) + 广度递归(bfs)

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        if(root == nullptr)
        {
            return vector<vector<int>>();
        }
        queue<TreeNode*> q;
        vector<vector<int>> vv;
        //先把根节点搞进去,启动循环
        q.push(root);
        int line =1;
        int i =0;
        while(!q.empty())
        {
            vv.push_back(vector<int>());
            while(line--)
            {//循环line次
                TreeNode* tmp = q.front();//取出对头元素如vv
                q.pop();
                if(tmp->left) { q.push(tmp->left);}
                if(tmp->right) { q.push(tmp->right);}
                vv[i].push_back(tmp->val);
            }
            i++;
            line = q.size();
        }
        return vv;
    }
};

3. 二叉树的分层遍历2。OJ链接

将第二题的结果逆置即可

class Solution {
public:
    vector<vector<int>> levelOrderBottom(TreeNode* root) {
                if(root == nullptr)
        {
            return vector<vector<int>>();
        }
        queue<TreeNode*> q;
        vector<vector<int>> vv;
        //先把根节点搞进去,启动循环
        q.push(root);
        int line =1;
        int i =0;
        while(!q.empty())
        {
            vv.push_back(vector<int>());
            while(line--)
            {//循环line次
                TreeNode* tmp = q.front();//取出对头元素如vv
                q.pop();
                if(tmp->left) { q.push(tmp->left);}
                if(tmp->right) { q.push(tmp->right);}
                vv[i].push_back(tmp->val);
            }
            i++;
            line = q.size();
        }
        reverse(vv.begin(),vv.end());
        return vv;
    }
};

4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。OJ链接

方法一:判断一个在此节点左边,一个在此节点右边,此节点就是最近公共祖先

class Solution {
public:
    //判断是否在树中
    bool Intree(TreeNode* root, TreeNode* p)
    {
        if(root == nullptr){ return false;}
        if(root == p){ return true;}
        return Intree(root->left, p)||Intree(root->right,p);
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root == p || root == q)
        {
            return root;
        }
        bool pinleft = Intree(root->left, p);//p是否在左树
        bool qinleft = Intree(root->left, q);//q是否在左树
        if(pinleft && qinleft)
        {//都在左边
            return lowestCommonAncestor(root->left,  p,  q);
        }
        else if(!pinleft && !qinleft)
        {//都在右边
            return lowestCommonAncestor(root->right,  p,  q);
        }
        else{
            return root;
        }
    }
};

方法二:记录下从根节点到连个节点的路劲,然后找最近公共祖先

class Solution {
public:

    bool getpath(vector<TreeNode*>& path, TreeNode* root, TreeNode* x)
    {
        if(root == nullptr )
        {
            return false;
        }

        path.push_back(root);
        if(root == x)
        {
            return true;
        }

        if(getpath(path, root->left, x) || getpath(path, root->right, x))
        {
            return true;
        }

        path.pop_back();
        return false;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        vector<TreeNode*> ppath;
        vector<TreeNode*> qpath;

        getpath(ppath, root, p);
        getpath(qpath, root, q);

        if(ppath.empty()){ return nullptr;}
        if(qpath.empty()){ return nullptr;}

        int i =0;
        while(i<ppath.size()&& i<qpath.size() && ppath[i] == qpath[i])
        {
            ++i;
        }
        return ppath[i-1];
    }
};

方法三:直接递归

class Solution {
public:
    TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q) {
        if (root == nullptr || root == p || root == q)
            return root;

        TreeNode *left = lowestCommonAncestor(root->left, p, q);
        TreeNode *right = lowestCommonAncestor(root->right, p, q);
        
        if (left && right)
            return root;
        return left ? left : right;
    }
};

5. 二叉树搜索树转换成排序双向链表。OJ链接

class Solution {
public:

    void _Convert(TreeNode*& prev, TreeNode* cur)
    {
        if(cur == nullptr)
        {
            return ;
        }
        _Convert(prev, cur->left);
        //在这里处理
        cur->left = prev;
        if(prev)
        {
            prev->right = cur;
        }
        prev = cur;
        _Convert(prev, cur->right);
    }  
    TreeNode* Convert(TreeNode* pRootOfTree) {
        if(pRootOfTree == nullptr){return nullptr;}
        TreeNode* prev = nullptr;
        TreeNode* cur = pRootOfTree;
        _Convert(prev, cur);
        while(cur->left)
        {
            cur = cur->left;
        }
        return cur;
    }
};

6. 根据一棵树的前序遍历与中序遍历构造二叉树。 OJ链接

class Solution {
public:

    TreeNode* _buildtree(vector<int>& preorder, vector<int>& inorder, int& preroot , int inleft,int inright)
    {

        if(inleft > inright)
        {
            return nullptr;
        }
        int tmp = inleft;
        for(int i = inleft; i<= inright; i++)
        {
            if(inorder[i] == preorder[preroot])
            {
                tmp = i;
                break;
            }
        }
        //inorder: [ ]  tmp  [ ]
        TreeNode* root = new TreeNode(preorder[preroot++]);
        root->left = _buildtree(preorder,inorder,preroot,inleft,tmp-1);
        root->right = _buildtree(preorder,inorder,preroot, tmp+1,inright);
        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int preroot = 0;
        return _buildtree(preorder,inorder,preroot,0,inorder.size()-1);

    }
};

7. 根据一棵树的中序遍历与后序遍历构造二叉树。OJ链接

class Solution {
public:
    TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder,int& postroot, int inleft,int inright)
    {
        if(inleft > inright)
        {
            return nullptr;
        }

        int tmp = inleft;
        for(int i =inleft;i<=inright;++i)
        {
            if(inorder[i] == postorder[postroot])
            {
                tmp =i;
            }
        }
        //inorder :  [inleft, tmp-1] tmp [tmp+1,inright]
        TreeNode* root = new TreeNode(postorder[postroot--]);
        root->right = _buildTree(inorder,postorder,postroot,tmp+1,inright); 
        root->left = _buildTree(inorder,postorder,postroot,inleft,tmp-1);
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int postroot = postorder.size()-1;
        return _buildTree(inorder,postorder,postroot,0,inorder.size()-1);
    }
};

8. 二叉树的前序遍历,非递归迭代实现 。OJ链接

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        //非递归
        vector<int> ret;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || st.empty() != true)
        {
            while(cur)
            {
                st.push(cur);
                ret.push_back(cur->val);
                cur = cur->left;
            }
            TreeNode* tmp = st.top();
            //ret.push_back(tmp->val);
            st.pop();
            cur = tmp->right;
        }
        return ret;
    }
};

9. 二叉树中序遍历 ,非递归迭代实现。OJ链接

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        //非递归
        vector<int> ret;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || st.empty() != true)
        {
            while(cur)
            {
                st.push(cur);
                //ret.push_back(cur->val);
                cur = cur->left;
            }
            TreeNode* tmp = st.top();
            ret.push_back(tmp->val);
            st.pop();
            cur = tmp->right;
        }
        return ret;
    }
};

10. 二叉树的后序遍历 ,非递归迭代实现。OJ链接

方法一:变样的前序遍历+逆置

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ret;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || st.empty() != true)
        {
            while(cur)
            {
                st.push(cur);
                ret.push_back(cur->val);
                cur = cur->right;
            }
            TreeNode* tmp = st.top();
            //ret.push_back(tmp->val);
            st.pop();
            cur = tmp->left;
        }
        reverse(ret.begin(),ret.end());
        return ret;
    }
};

方法二:标记

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ret;
        stack<TreeNode*> st;
        TreeNode* cur= root;
        TreeNode* prev= nullptr;
        while(cur || !st.empty())
        {
            while(cur)
            {
                st.push(cur);
                cur = cur->left;
            }
            TreeNode*  tmp = st.top();
            //if(tmp->right == prev) //小心死循环
            if(tmp->right == nullptr || tmp->right == prev) 
            {//第二次取到
                ret.push_back(tmp->val);
                prev = tmp;
                st.pop();
            }
            else{
                //第一次取到
                cur = tmp->right;
            }    
        }
        return ret;
    }
};
  1. set

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中的底层使用二叉搜索树(红黑树)来实现

5.1.关联式容器

在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、
forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面
存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?

关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的

键值对,在数据检索时比序列式容器效率更高

关联式容器的数据和数据之间由强烈的关联关系。数据存放的位置很特殊。就像set的KV模型一样。

5.2.STL库中set的使用

首先set是一个平衡搜索二叉树。搜索效率非常高。这个结构天生为搜索而生。(logN)

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

说明文档

intset
注意insert的第一个接口的返回值。
pair的第一个参数是此元素插入位置的迭代器,第二个位置反映是否插入成功。
lower_bound和upper_bound
#include <iostream>
#include <set>

int main()
{
    std::set<int> myset;
    std::set<int>::iterator itlow, itup;

    for (int i = 1; i < 10; i++) myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90

    //可以理解为lower_bound(25)是 >= 25 位置的迭代器
    itlow = myset.lower_bound(25);  //30位置的迭代器
    itlow = myset.lower_bound(30);  //30位置的迭代器

    //可以理解为upper_bound(65)是 > 65 位置的迭代器
    itup = myset.upper_bound(65);   //70位置的迭代器

    myset.erase(itlow, itup);                     // 10 20 70 80 90

    std::cout << "myset contains:";
    for (std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
        std::cout << ' ' << *it;
    std::cout << '\n';

    return 0;
}
equal_range
也就是同时为返回lower_bound和upper_bound的值
// set::equal_elements
#include <iostream>
#include <set>

int main ()
{
  std::set<int> myset;

  for (int i=1; i<=5; i++) myset.insert(i*10);   // myset: 10 20 30 40 50

  std::pair<std::set<int>::const_iterator,std::set<int>::const_iterator> ret;
  ret = myset.equal_range(30);

  std::cout << "the lower bound points to: " << *ret.first << '\n';
  std::cout << "the upper bound points to: " << *ret.second << '\n';

  return 0;
}

  1. multiset

和set接口啊结构啊 都相同,但是multiset允许键值冗余,也就是可以插入相同的元素

set是去重排序
multiset就是排序不去重

find返回的是中序遍历第一个匹配的元素的迭代器。

erase可能不是删除一个节点,可能是删除多个,只要匹配全部删除。

这就是为什么set会有一个接口是count,统计数量。

  1. 键值对(pair)(KV键值对)

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息

比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。

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

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)
{}
};
  1. 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通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
map是搜索二叉树的KV模型。
但是每个元素是pair类型的,因为c++不支持同时返回两个值。
所有map把每个元素设定为pair(键值对)
key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
insert
插入的是一个pair类型的数据
int main()
{
    map<int, int> ma;
    ma.insert(pair<int, int>(1, 10000));
    ma.insert(make_pair(99, 990000));
    ma.insert(pair<int, int>(6, 60000));
    ma.insert(make_pair(10, 100000));
    ma.insert(pair<int, int>(4, 40000));
    ma.insert(make_pair(8, 80000));
    ma.insert(make_pair(10, 100000));
    //make_pair(key,val);//就会返回一个pair类型
    //函数模板,自动推导。
    //template <class T1, class T2>
    //pair<T1, T2> make_pair(T1 x, T2 y)
    //{
    //    return (pair<T1, T2>(x, y));
    //}

    for (const `auto& e : ma)
    {
        cout << '<' << e.first << ',' << e.second << '>' << endl;
    }
    //<1, 10000>
    //<4, 40000>
    //<6, 60000>
    //<8, 80000>
    //<10, 100000>
    //<99, 990000>
    map<int, int> ::iterator it = ma.begin();
    while(it != ma.end())
    {
        cout << '<' << it->first << ',' <<it->first << '>' << endl;
        //注意编译器的优化, it->->first优化为 it->first
    }
    //<1, 10000>
    //<4, 40000>
    //<6, 60000>
    //<8, 80000>
    //<10, 100000>
    //<99, 990000>
    return 0;
}
operator[]
//等价于下面的方式
mapped_type& operator[] (const key_type& k)
{
    return (*((this->insert(make_pair(k, mapped_type()))).first)).second;
}
//pair<iterator,bool> insert (const value_type& val);
//insert的返回值是pair类型。pair中first是一个指向插入元素迭代器,如果此元素存在,insert就相当于一个查找功能,也返回迭代器。
//也就等价于一下方式
mapped_type& operator[] (const key_type& k)
{
    pair<iterator,bool> ret = insert(make_pair(k, mapped_type());
    iterator it = ret.first;
    return it->second;
}
map的【】 有三种功能
1.插入<key,val>
2.修改val值
3.key值存在就是查找
int main()
{
    map<string, string> dict;
    dict.insert(make_pair("排序", "sort"));
    dict.insert(make_pair("左", "left"));
    dict.insert(make_pair("右", "right"));
    dict.insert(make_pair("字符", "char"));
    dict.insert(make_pair("字符串", "string"));
    dict["左"] = "_left";
    //[ ]可以充当修改
    for (auto e : dict)
    {
        cout << '<' << e.first << ',' << e.second << '>' << endl;
    }
    cout << endl;

    auto ret_pair = dict.insert(make_pair("右", "_right"));
    //这里是无法修改的。
    //这里插入失败。搜索树中只比较Key。
    cout << ret_pair.first->first << endl;
    cout << ret_pair.first->second << endl;
    cout << ret_pair.second << endl;//0
    for (auto e : dict)
    {
        cout << '<' << e.first << ',' << e.second << '>' << endl;
    }
    return 0;
}
at
这里和【】 不是一样的,其他容器中的at是和【】 一样的,
map的at,如果key不在map里面会报异常。

map的【】很好用。但是要确定好是不是自己想要的行为。

总结

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

和set一样的框架一样

map是不允许键值冗余的
multimap是允许键值冗余的

区别:

multimap没有【】,因为允许数据冗余,【】的返回值不确定。

find一样是中序的第一个的迭代器。

其他的模仿set和multiset。

10.map和set的oj题目

前K个高频词汇

// bool compare(const pair<int,string>& l,const pair<int,string>& r)
// {
//     return l.first > r.first;
// }
bool compare(const pair<int,string>& l,const pair<int,string>& r)
{
    return l.first > r.first || (l.first == r.first && l.second < r.second);
}

class Solution {
public:
    //仿函数
    // struct compare
    // {
    // public:
    //     bool operator()(const pair<int,string>& l,const pair<int,string>& r)
    //     {
    //         return l.first > r.first;
    //         return l.first > r.first|| (l.first == r.first && l.second < r.second);
    //     }
    // };
    vector<string> topKFrequent(vector<string>& words, int k) {
        //统计次数,并且记录
        map<string,int> dict;
        for(auto& e : words)
        {
            dict[e]++;
        }

        //转移入数组
        vector<pair<int,string>> v;
        for(auto& kv: dict)
        {
            v.push_back(make_pair(kv.second, kv.first));
            //此时是按照字典序排序的,
        }
    
        //使用默认的比较函数的时候, pair<>也重载有 > 和 < 等运算符号
        //second和first都相等的时候,才是相等,比大小是:先比较first,若first相等比较second
        //不符合此题目要求
        //需要重载,比较函数.

        //sort(v.begin(),v.end());
        //sort排序底层是快速排序,是不稳定的排序.想稳定要写重载比较函数
        sort(v.begin(),v.end(),compare);
        //stable_sort(v.begin(), v.end());
        //stable_sort排序底层归并排序,是稳定的排序

        //stable_sort(v.begin(), v.end(), compare);

        //截取前 k 个,返回
        vector<string> ret;
        for(int i =0 ;i<k;i++)
        {
            ret.push_back(v[i].second);
        }
        return ret;
    }
};

集合的交集

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {

        //对两个数组去重加排序
        set<int> s1(nums1.begin(),nums1.end());
        set<int> s2(nums2.begin(),nums2.end());
        auto it1= s1.begin();
        auto it2= s2.begin();

        //双指针走,找相同的值,记录返回
        vector<int> ret;
        while(it1 != s1.end() && it2 != s2.end())
        {
            if(*it1 == *it2)
            {
                ret.push_back(*it1);
                it2++;
                it1++;
            }
            else{
                if(*it1 > *it2)
                {
                    it2++;
                }
                else{
                    it1++;
                }
            }
        }
        return ret;
    }
};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小峰同学&&&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值