当前位置: 首页 > news >正文

11. STL的使用

目录

1. 常见的容器及其常见问题

1.1 序列式容器

1.1.1 常见问题:

1. string、vector 和 list 的区别?

2. string、vector、list是怎么插入数据的?如何扩容的?

1.2 关联式容器

1.2.1 树形结构的关联式容器

map / set:

1. 底层是什么:树形结构的这4种容器的底层都是红黑树。

2. 一个类型做 map/set 的 key 有什么要求?

3. map的operator[ ]功能是什么?如何实现?

multi_map / multi_set:

1.与各自 map 和 set 版本的不同:

1.2.2 哈希结构的关联式容器

unordered_map / unordered_set:(其实还有unordered_multimap/unordered_multiset)

1. 如何实现的:哈希表

2. unordered_map和map的区别

3. unordered_set 和 set 的区别

4. 一个类型做 unordered_map / unordered_set 有什么要求?

1.3 容器适配器


 

1. 常见的容器及其常见问题

1.1 序列式容器

        对应线性结构,逻辑上是依次存储的,任意位置都能插入,string和vector用的是最多的。string 严格来说虽然不属于STL,但是也比较重要,所以囊括进来做对比。

string:07.string类_stding::length()-CSDN博客

vector:08.vector-CSDN博客

list:09.list-CSDN博客

1.1.1 常见问题:

1. string、vector 和 list 的区别?

用法:

  • string是专门用于处理字符串的容器,string后有 \0,string s
  • vector是动态数组,数组元素类型任意vector<string>、vector<int>...
  • list是双向链表,list<int>

优点:

  • string 支持运算符重载(+, +=等),使用直观;专为字符串设计,接口丰富(find, substr等)
  • vecto支持随机访问O(1);尾插、尾删高效
  • list 任意位置插入删除高效;没有容量概念,按需分配节点;插入删除不会使迭代器失效(除了被删除元素)

缺点:

  • string 某些操作(如插入/删除非末尾字符)效率不高
  • vector 非尾部插入/删除效率低O(n);容量不足时需要重新分配内存并拷贝元素
  • list 不支持随机访问(访问元素O(n)),[ ]会从开头遍历到指定位置;内存开销大(每个元素需要额外存储前后指针)

底层实现:

  • string 通常实现为动态数组,char* _str,size_t _size,size_t _capacity。
  • vector 维护三个指针:起始位置、当前元素末尾、容量末尾:
            typedef T* iterator;
    
            iterator _start;
            iterator _finish;
            iterator _endofstorage;
  •  list 使用双向链表节点结构,每个节点包含数据和前后指针,通常实现为循环链表,有哨兵头节点head
    namespace zzy
    {
        template<class T>
        struct list_node//为什么这里写它?
        {
            list_node<T>* _next;
            list_node<T>* _prev;
            T _val;
     
            explicit list_node(const T& val = T())
                :_next(nullptr)
                ,_prev(nullptr)
                ,_val(val)
            {}
        };
        
        template<class T>
        class list
        {
            typedef list_node<T> Node;
        public:
            list()
            {
                _head = new Node;
                _head->_prev = _head;
                _head->_next = _head;
                _size = 0;
            }
     
        private:
            Node* _head;
            size_t _size;
        };
    }
2. string、vector、list是怎么插入数据的?如何扩容的?

string:

        插入有两种,插入字符、字符串,分成push_back和append,同理在插入时要先检查空间扩容,特色的operate+=就根据参数是char或char*,分别调用push_back和append。

        void reserve(size_t n)
        {
            if(n > _capacity)
            {
                char* tmp = new char[n+1];
                memcpy(tmp, _str, _size+1);
                delete[] _str;
                _str = tmp;
                _capacity = n;
            }
        }
 
        void push_back(char ch)
        {
            if(_size == _capacity)
            {
                reserve(_capacity == 0 ? 4: _capacity*2);
            }
            _str[_size] = ch;
            ++_size;
            _str[_size] = '\0';
        }
 
        void append(const char* str)
        {
            size_t len = strlen(str);
            //如果插入后的大小大于容量,二倍的容量不一定有size+len大
            if(_size + len > _capacity)
            {
                reserve(_size+len);
            }
            memcpy(_str+_size, str, len+1);
            _size += len;
        }
 
        string& operator+=(char ch)
        {
            push_back(ch);
            return *this;
        }
        string operator+=(const char* str)
        {
            append(str);
            return *this;
        }

vector:

        尾插的话就先判断容量,不够就扩容,新元素放在最后一个位置上,finish++。

        如果是insert,也是先判断容量,扩容后重新找到要插入的位置(迭代器失效),从尾向前搬移元素,新元素放到pos上,finish++。

        void push_back(const T& x)
        {
            if(_finish == _endofstorage)
            {
                size_t newcapacity = capacity() == 0? 4:capacity*2;
                reserve(newcapacity);
            }
            *_finish = x;
            ++_finish;
        }

        如果要扩的容量大于当前的容量,就扩容,注意保留原大小以及逐个拷贝,不能使用memcpy(涉及到内存管理,memcpy只会复制指针值,不会复制指针指向的内容)

        void reserve(size_t n)
        {
            if(n > capacity())
            {
                size_t sz = size();
                T* tmp = new T[n];
                if(_start)
                {
                    //memcpy(tmp, _start, sizeof(T)*sz);
                    for (size_t i=0; i<sz; i++)
                    {
                        tmp[i] = _start[i];
                    }
                    delete[] _start;
                }
                _start = tmp;
                _finish = _start + sz;
                _endofstorage  = _start + n;
            }
        }

list:

        其实就是双向带头循环链表的插入,没有扩容一说。

1.2 关联式容器

        关联式容器也是用来存储数据的,与序列式容器不同的是,里面存的是<key,value>结构的键值对,在数据检索时比序列式容器效率更高

1.2.1 树形结构的关联式容器

map / set:
1. 底层是什么:树形结构的这4种容器的底层都是红黑树。

各自的规则:

set:

  • set与map/multimap 不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value,value>构成的键值对。
  • set按一定次序存储元素,默认按小于来比较,查找某个元素时,时间复杂度为O(logN)。
  • set中的元素不允许修改,因为用的都是const迭代器。可以从容器中插入和删除。

map:

  • map存储按特定次序(按key比较)由键和值组合而成的元素,为其取别名pair<>
  • key也不允许修改,用的是const key
  • 支持下标访问符,即在[ ]中放入key,可以找到与key对应的value
  • 当key元素不存在时,operator[]用默认的value与key构造键值对然后插入,返回默认value,at()直接抛异常
  • map中的元素如果用迭代器去遍历,可以得到一个有序的序列

效率:

        set 中通过键访问单个元素时比 unodered_set

        map 中通过键值访问单个元素时比 unodered_map

大概实现:

         map和set底层使用的都是红黑树,那怎么让map和set共用一颗红黑树的呢?

使用模板,首先需要对红黑树进行改造

template<class K>
class set
{
private:
    RBTree<K, K> _t;
};

template<class K, class V>
class map
{
private:
    RBTree<K, pair<K, V>> _t;
};

        我们可以从上面的代码看出,set 和 map 的key位置一样,但是value位置,set用的是K,而map用的是一个pair<K, V>,他们是通过同一棵树搞出来的_t,那么RBTree在这个位置就要用一个模板T。

  • setRBTree<K, K>:因为 set 只存 Key,复用 RBTree 时需要传 Value=K

  • mapRBTree<K, pair<const K, V>>:因为 map 存键值对,但比较时只关心 Key

这样设计可以最大程度复用 RBTree,避免为 setmap 分别实现不同的红黑树,set迁就了map

        如下方修改后的tree,如果是set建树,传给模板T的是key,树的结点就会用这个T(key)去定义数据;如果是map建树,传给模板T的是pair<K, V>,树的结点就会用这个T(pair<K, V>)去定义数据。

template<class T>
struct RBTreeNode
{
    RBTreeNode<T>* _left;
    RBTreeNode<T>* _right;
    RBTreeNode<T>* _parent;

    T _data;
    Color _col;
};

template<class K, class T>
class RBTree
{
    typedef RBTreeNode<T> Node;
};

        还有一个问题就是,set,map建树需要比较key,set比较的就是保存的值,map比较的是保存的键值对pair<key,value>key的值。可以看出来set的值可以直接比较,但是 map 需要通过对键值对进行解引用才能得到key的值,两者方式不同。

        我们可以通过仿函数+模板解决这个问题:

首先要再给RBTree添加一个模板,KeyOfT

template<class K, class T, class KeyOfT>
class RBTree
{
    typedef RBTreeNode<T> Node;
};

 然后在map和set中实现各自KeyOfT的类,类中实现一个仿函数,分别取出他们自己的key:

namespace zzy
{
    template<class K,class T>
    class map
    {
    public:
        //仿函数来获取值
        struct MapKofT
        {
            const K& operator()(const pair<K, T>& V)
            {
                return V.first;
            }
        };
        typedef typename RBTree<K, pair<K, T>, MapKofT>::Iterator iterator;
        
        pair<iterator, bool> Insert(const pair<K, T>& data)
        {
            return _tree.insert(data);
        }
 
        iterator begin()
        {
            return _tree.begin();
        }
        iterator end()
        {
            return _tree.end();
        }
        
        T& operator[](const K& k)
        {
            pair<iterator, bool> ret = Insert(pair<K, T>(k, T()));
            return ret.first->second;
        }
    private:
        RBTree<K, pair<K, T>, MapKofT> _tree{};
    };
}
namespace zzy
{
    template<class K>
    class set
    {
    public:
        struct SetKofT
        {
            const K& operator()(const K& k)
            {
                return k;
            }
        };
 
        typedef typename RBTree<K, K, SetKofK>::Iterator iterator;
        
        pair<iterator, bool> Insert(const K& k)
        {
            return _tree.insert(k);
        }
 
        iterator begin()
        {
            return _tree.begin();
        }
        iterator end()
        {
            return _tree.end();
        }
    private:
        RBTree<K, K, SetKofT> _tree{};
    };
}

可以看到他们在建树时也把自己求K的类传给了RBTree。

这时树中的class KeyOfT模板接收的就是他们各自传的 SetKofT 和 MapKofT。

template<class K, class T, class KeyOfT>
class RBTree
{
    typedef RBTreeNode<T> Node;
public:
    bool insert(const T& data)
    {
        KeyOfT kot;
        while(cur)
        {
            if(kot(cur->data) < kot(data))
            {
                parent = cur;
                cur = cur->_right;
            }
            //...
        }
    }
};

        传过来的这个类要在函数中实例化一个对象kot才能用对象去使用其中的仿函数,像上面的代码一样。当然其实是SetKofT 或 MapKofT 类的对象去调用各自的仿函数。

2. 一个类型做 map/set 的 key 有什么要求?

        :可以支持比大小 或者 提供仿函数比较大小

3. map的operator[ ]功能是什么?如何实现?

功能:它提供了对映射元素的快速访问和修改能力。

int main() 
{
    map<string, int> word_counts;
    
    // 插入新元素
    word_counts["apple"] = 5;  // 键"apple"不存在,自动插入
    
    // 修改现有元素
    word_counts["apple"] += 3; // 现在"apple"对应的值是8
    
    // 访问元素
    cout << "apple count: " << word_counts["apple"] << endl;
    
    // 访问不存在的键会创建新元素
    cout << "orange count: " << word_counts["orange"] << endl;
    // 现在map中包含"orange"键,值为0
    
    return 0;
}

实现:

        opeartor[ ]的原理是:用<key,T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中。如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器;如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器。operator[ ]函数最后将insert返回键值对中的value返回。(给key返回value)

        T& operator[](const K& k)
        {
            pair<iterator, bool> ret = Insert(pair<K, T>(k, T()));
            return ret.first->second;
        }
  • 创建一个新的键值对pair<K, T>,其中:

    • 键是传入的k

    • 值是用T()构造的默认值(例如,如果Tint,则值为0)

  • return ret.first->second;

    • ret.firstinsert返回的迭代器

    • ->second访问迭代器指向的键值对中的值部分

    • 返回这个值的引用

multi_map / multi_set:
1.与各自 map 和 set 版本的不同:
特性map / setmultimap / multiset
元素唯一性键/元素必须唯一允许重复键/元素
插入行为key存在插入失败/忽略总是插入新元素
查找返回值返回单个迭代器可能返回多个元素/迭代器范围
典型实现红黑树红黑树
内存占用相对较小可能较大(存储重复元素)
查找count()返回0或1count()返回某元素数量
// map示例
map<int, string> m;
m.insert({1, "a"});  // 成功
m.insert({1, "b"});  // 失败,键1已存在
m[1] = "b";          // 覆盖原有值

// multimap示例
multimap<int, string> mm;
mm.insert({1, "a"});  // 成功
mm.insert({1, "b"});  // 成功,允许重复键
// mm[1] = "c";       // 错误,multimap不支持operator[]

multimap不支持operator[ ]

如果operator[]用于multimap,当键重复时,无法明确应该返回哪个值的引用,导致语义模糊。

multimap<int, string> m;
m.insert({1, "a"});
m.insert({1, "b"});
string val = m[1]; // 应该返回 "a" 还是 "b"?

1.2.2 哈希结构的关联式容器

        最好的查询是,进行很少的比较次数就能将元素找到,所以C++11中,STL又提供了4个unordered系列的关联式容器,与红黑树结构的使用类似,只是底层结构不同。

unordered_map / unordered_set:(其实还有unordered_multimap/unordered_multiset)
1. 如何实现的:哈希表

规则:

        unordered_map 中 key 是不能重复的,因此count函数的返回值最大为1。

        unordered_map 的 operator[ ],实际调用哈希桶的插入操作,用参数key和V()构造一个默认值往底层哈希桶插入,遍历桶内的元素,用 keyoperator== 进行比较,找到匹配的 key,如果key不在哈希桶中,插入成功;如果找到:返回对应的 value

大概实现:

  • 底层实现:基于 指针数组 + 链表/红黑树(解决冲突),数组的每个槽位称为 桶,存储链表的头指针。

  • 通过 哈希函数 计算 Key 的哈希值映射到桶数组的某个位置以完成增删查。

  • 当载荷因子到1时扩容到二倍,遍历所有元素,重新计算哈希并放入桶中。

2. unordered_map和map的区别
特性std::mapstd::unordered_map
数据结构红黑树(RB-Tree)哈希表(Hash Table)(数组 + 链表/红黑树)
是否有序(按键 Key 升序排列)(元素存储顺序取决于哈希函数)
是否需要 operator<必须定义(用于排序)不需要
是否需要 hashoperator==不需要必须定义(用于哈希计算和冲突检测)
内存占用较低较高(桶数组 + 链表/红黑树)

 时间复杂度:

操作std::map (红黑树)std::unordered_map (哈希表)
查找 find()O(log n)平均 O(1),最坏 O(n)(哈希冲突严重时)
插入 insert()O(log n)平均 O(1),最坏 O(n)
删除 erase()O(log n)平均 O(1),最坏 O(n)
遍历 iteration有序遍历(按 Key 排序)无序遍历(顺序取决于哈希桶)
3. unordered_set 和 set 的区别

        和上面的差不多,也是红黑树-哈希表、有序-无序、O(log n)-O(1)、内存占用较高-较低

4. 一个类型做 unordered_map / unordered_set 有什么要求?
  • unordered_mapunordered_set 基于哈希表实现,因此必须能够计算 Key 的哈希值。
  • 必须提供相等比较(operator==),需要通过 operator== 判断两个 Key 是否真正相等。

1.3 容器适配器

10.stack 和 queue_queue 和 stack-CSDN博客

http://www.dtcms.com/a/100027.html

相关文章:

  • springBoot统一响应类型3.4版本
  • LlamaIndex实现(基于PDF|CSV文件)RAG检索增强生成:NaiveRAG
  • 量子计算:未来计算技术的革命性突破
  • 【奶茶经济学的符号暴力本质】
  • 【软件开发】可复用的数据库导入工具类
  • J2EE框架技术 第五章 Spring注入与作用域
  • 【C++】STL库_stack_queue 的模拟实现
  • 【leetcode】通过两种遍历方式构造二叉树
  • 前端页面缓存问题
  • opencv之指纹验证
  • spring-security原理与应用系列:securityFilterChainBuilders
  • 职测-言语理解与表达
  • SD-WAN组网方案
  • pycharm相对路径引用方法
  • C++ 多态:面向对象编程的核心概念(一)
  • Android Product Flavors 深度解析与最佳实践:构建多版本应用的全方位指南
  • Redis的补充和RESP
  • 【工具】BioPred一个用于精准医疗中生物标志物分析的 R 软件包
  • C# StreamReader/StreamWriter 使用详解
  • 什么是 RBAC 权限模型?
  • jmeter web压力测试 压测
  • Android学习总结之算法篇三(打家劫舍)
  • 蓝桥杯—最小公倍数
  • Linux系统之dump命令的基本使用
  • Linux系统禁用swap
  • Xenium | 细胞邻域(Cellular Neighborhood)分析(fixed radius)
  • Spring AI MCP Server + Cline 快速搭建一个数据库 ChatBi 助手
  • QML编程中的性能优化二
  • C语言指针2
  • 2024蓝桥杯省赛C/C++大学B组 题解