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

news2024/11/16 22:01:45
  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;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/737795.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【爆肝帝,花费3个月整理】金九银十面试季,2023年字节跳动所有,软件测试面试题拿走不谢!(附详细答案解析)

前言 最近有收到一些不同公司的面试题&#xff0c;像字节跳动、网易、美团等&#xff0c;趁着有时间&#xff0c;给大家梳理下&#xff0c;说不定面试能派上用场&#xff0c;大概给大家从以下几个方面来做了整理&#xff1a; 个人信息&#xff1a;(工作/实习经验&#xff0c;…

2023 ciscn 华东北分区赛 pwn minidb

2023 ciscn 华东北分区赛 pwn minidb 没去打比赛&#xff0c;做了一下&#xff0c;本地通了&#xff0c;不知道远程可不可以 结构体 00000000 Data struc ; (sizeof0x40, mappedto_8) 00000000 type dd ? 00000004 flag dd ? 00000008 database_name dq ? 00000010 pair d…

用html+javascript打造公文一键排版系统1:设计界面

近日&#xff0c;有同事抱怨收到的文件没有按公文要求进行排版&#xff0c;不得不自已动手帮他们擦PP排版&#xff0c;感慨每天都在做这些无意义的事情&#xff0c;浪费生命&#xff01; 于是打算用用htmljavascript打造公文一键排版系统。 首先是设置界面&#xff0c;主要包…

优化成本,探索WhatsApp API发送更经济的OTP验证信息

在现代的数字化世界中&#xff0c;安全性和使用者验证变得至关重要。随着移动应用程序和在线服务的普及&#xff0c;一次性密码&#xff08;OTP&#xff09;验证已经成为确保使用者身份验证的主要手段之一。然而&#xff0c;对于许多企业来说&#xff0c;发送OTP验证信息可能会…

fileinclude

看题目提示&#xff0c;应该是一道文件包含的题目&#xff0c;打开环境后直接告诉我flag在flag.php里 但是因为不知道绝对路径&#xff0c;不能直接利用file读取 查看源码后&#xff0c;发现里面嵌入了一段php代码 代码审计 首先&#xff0c;通过if( !ini_get(display_errors) …

springboot中banner.txt文件说明

springboot中banner.txt文件说明 通常在启动springboot项目的时候&#xff0c;&#xff0c;控制台会打印一些东西 比如&#xff1a; 如何自定义控制台输出的图形化符号 只需要在项目resources目录下创建一个banner.txt文件即可&#xff0c;因为启动的时候系统会自己检查该…

7.10作业

闹钟 mainWindow.ccp TCP服务器 #include "mainwindow.h" #include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this);server new QTcpServer(this);}MainWindow::~MainWi…

PDF文件转换成CAD图纸怎么做?简单好用的转换方法分享

CAD文件可以进行更加复杂的编辑&#xff0c;例如添加图层、修改线条颜色和粗细等&#xff0c;而PDF文件则只能进行简单的编辑操作。CAD软件中还可以添加文字注释、标注、尺寸和符号&#xff0c;这些功能大大提高了设计的灵活性和精度。下面给大家分享几种能够将PDF文件转换成CA…

交流充电桩通信方式和模块设计介绍

交流充电桩是新能源汽车充电系统的主要设备之一&#xff0c;可分为即插即用、刷卡取电和联网对接云端三种。即插即用&#xff0c;用户直接将充电枪连接到车辆上&#xff0c;就可以开始充电&#xff1b;刷卡取电&#xff0c;用户可以使用刷卡等方式取得充电权限&#xff0c;并根…

打印机一直重复打印不停止

打印一张纸&#xff0c;打印机一直重复打印不停止这个问题其实很简单&#xff0c;一般情况下是因为双向打印不兼容的问题&#xff1b; 选中打印机&#xff0c;点击右键&#xff0c;在弹出的菜单中选择“打印机属性” 在弹出的窗口中点“端口”&#xff0c;将“启用双向支持”前…

实现流程编排设计器的心路历程

接上回《「AntV」使用AntV X6实现流程编排设计器》一文说到&#xff0c;流程编排设计器的实现方案是将低代码引擎和AntV X6作为画布相结合。 为什么会有这样的想法&#xff1f; 可行性 起因是业务中有用到低代码引擎的场景&#xff0c;它的交互形式、页面结构正好符合流程编…

超级实用~低生物量的样本如何进行污染控制

上次小编主要介绍了低生物量比如口腔、阴道等样本的常见微生物和污染物&#xff0c;但是测序技术的高灵敏度也放大了样本中DNA污染的影响&#xff0c;那么对于低生物量的样本如何进行污染控制就至关重要了~ 2019年在《Contamination in Low Microbial Biomass Microbiome Studi…

vue + el-table点击表头改变其当前样式

废话不多说&#xff0c;先看效果&#xff1a; 网上找了一大圈没有符合的&#xff0c;只能自己看着搞&#xff1a; 直接贴代码&#xff1a; <el-tableref"table":data"tableData"borderstripesort-change"changeColumn"><el-table-colu…

vue语法详解

以下页面就是用vue开发的 模板语法 注意 模板语法不能在标签属性中用 文本插值 {{ msg }} 使用JavaScript表达式 {{ number 1 }} {{ ok ? YES : NO }} {{ message.split().reverse().join() }} 使用HTML 双大括号将会将数据插值为纯文本&#xff0c;而不是HTML&…

Apikit 自学日记:测试数据集

测试数据集 添加数据集的变量 在测试用例详情页面中&#xff0c;您可以点击上方的 测试数据 标签&#xff0c;进入用例的数据管理页面。在这里您可以添加多组测试数据&#xff0c;以及每组测试数据的变量。 在添加数据集前&#xff0c;我们需要设置数据集中存在什么变量。可以…

Qt:记录一下好看的配色

qss代码 窗体背景色 background-color: #ED6927; border-top-left-radius:35px;border-top-right-radius:35px;border-bottom-right-radius:0px; border-bottom-left-radius:0px;background-color: #203A32; border-radius:35px; border-top-left-radius:0px;border-top-righ…

如何用一部手机进行人体全身三维扫描

人体建模的应用真的是涵盖到了我们生活中的方方面面&#xff0c;真人潮玩、服饰定制、医疗康复、3D数字人等等领域&#xff0c;都离不开人体建模。 提到给人体建模&#xff0c;大家脑海里第一个浮现的画面&#xff0c;大多会是坐在电脑屏幕前&#xff0c;打开某个熟悉的建模的…

从本地目录和S3目录生成Classpath字符串的最佳实践

从一个目录生成Classpath字符串是一个非常常见的问题&#xff0c;在使用命令提交一个Java、Spark作业时会经常遇到。通常遇到的遇到情况是&#xff1a;将本地目录下的Jar文件拼接成一个Classpath字符串&#xff0c;这时&#xff0c;通常我们可以使用这样的命令&#xff1a; sp…

《八角笼中》电影我表达不出的好

昨天看完王宝强主演的《八角笼中》内心久久不能平静&#xff0c;里面有很多触动我的点&#xff0c;如果是写作高手&#xff0c;一定能从这部影片中捕捉到很多可写的话题。其实我也有&#xff0c;只是一直在思索要如何定主题&#xff0c;把内心深处那些想法表达出来&#xff0c;…

【HDC.Cloud 2023】华为云区块链分论坛内容值得再读!

Web3是全新的互联网世界&#xff0c;它不仅仅是一个技术革新&#xff0c;更是一种新的生态系统。在这个新的生态系统中&#xff0c;区块链技术、去中心化应用、智能合约等技术被广泛应用&#xff0c;为人们带来了更加安全、透明、去中心化的互联网体验。 然而&#xff0c;Web3的…