C++容器篇,set和map容器

C++容器篇——set和map容器

1. 关联式容器

vector/list/dequeu/forward_list这些容器称为序列式容器,其底层为线性序列的数据结构,存储的数据是元素本身。

关联式容器与其他容器相比同样也是存储数据的,不过是采用<key,value>结构的键值对,在数据检索时比序列式容器的效率高。

2. 键值对

用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量keyvalue,key表示键值,value表示与key对应的信息。下面是关于键值对的定义:

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)
	{}
};

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

树形结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器都是以红黑树作为底层结构,不懂可以参考我写的数据结构之红黑树(C++实现),容器的元素是一个有序的系列。

3.1 set的介绍

set的参考文档

  1. set是按照一定次序存储元素的容器。(有序)
  2. set中,元素的value就是key,并且每个value必须唯一的。set中的元素不能在容器中修改,只能从容器中插入或者删除它们。(唯一)
  3. set内部,set中的元素总是按照内部比较对象所指示的特定严格弱排序准则进行排序。(比较)
  4. set容器通过key访问单个元素比unordered_set慢,但它们允许根据数据对子集进行直接迭代。(访问)
  5. set的底层采用红黑树实现。(实现)

注意

  1. map/multimap不同,map/multimap是存储真正的键值对<key,value>,set只存放value,但底层实际存放的是<value,value>结构。
  2. set插入元素的时候,只需要插入value值即可。
  3. set中的元素不可以重复。
  4. 使用set的迭代器遍历set的元素,是可以获得有序序列。
  5. set中的元素默认是从小到大排序的。
  6. set查找元素的时间复杂度为O(log_2(n))
  7. set的元素不允许修改,会导致底层的红黑树结构错误。
  8. set的底层使用红黑树实现。
3.2 set的使用

set必须包含头文件#include <set>,并且属于std命名空间里面。

int main()
{
    set<int> s;
    s.insert(1);
    s.insert(7);
    s.insert(3);
    s.insert(5);
    s.insert(2);

    auto it = s.begin();
    while(it != s.end())
    {
        cout << *it << " "; // 1 2 3 5 7
        ++it;
    }
    cout << endl;
}

这是一个简单使用set容器的方法,与其他容器几乎一致,并且迭代器只存储了value值,故可以直接进行*解引用操作,然后++it表示移动到下一个元素。最终的结果也是满足set的特性,是有序排序的。

3.2.1 set的定义

set的构造函数

构造函数接口说明
set();无参构造
set(const set& x);拷贝构造
set(InputIterator first,InputIterator last);迭代器构造
  • 第一个是无参构造,这个时候容器里面并没有存放任何数据。但是需要通过<>来指定容器存放的类型。
  • 第二个是拷贝构造。
  • 第三个是通过迭代器(输入迭代器)来构造并初始化。
int main()
{
	set<int> s;
	set<int> s1(s);
    vector<int> v{1,5,2,4,3,0};
	set<int> s2(v.begin(), v.end());
}
3.2.2 set的迭代器

迭代器其实本质上是指针,但是是对指针进行了封装。我们在使用C语言的时候,常常因为指针,甚至多级指针,导致代码异常复杂难看。容器里的迭代器对指针进行改造之后,使用起来更加方便。set迭代器其实调用的是底层红黑树的迭代器。

set获取迭代器

迭代器的使用使用说明
begin()返回指向开始位置的迭代器
end()返回指向末尾元素的下一个位置的迭代器
cbegin()返回指向开始并且为常量的迭代器
cend()返回指向末尾元素的下一个位置的并且为常量的迭代器
rbegin()返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向
rend()返回逆置迭代器,指向开头元素的位置,操作都是往相反反向
crbegin()返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向,并且为常量属性
crend()返回逆置迭代器,指向开头元素的位置,操作都是往相反反向,并且为常量属性
3.2.3 set的容量
容量说明接口说明
size获取容器中实际的个数
empty判断是否为空
3.2.4 set增删查
增删改查接口说明
insert(const value_type& x)在set中插入元素x,实际上插入的是<x,x>构成的键值对,如果成功,返回<该元素在set中的,true>,否则,返回<x在set的位置,false>
erase(iterator pos)删除set中pos位置的元素。
erase(const value_type& x)删除set中值为x的元素,返回删除元素的个数。
swap(set& st)交换set中的元素
find(const value_type& x)返回set中值为x的元素的位置
count(const value_type& x)返回set中值为x的元素的个数
clear清除set的元素

下面是相关代码演示:

#include <set>
void TestSet()
{
    // 用数组array中的元素构造set
    int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    set<int> s(array, array+sizeof(array)/sizeof(array));
    cout << s.size() << endl;
    // 正向打印set中的元素,从打印结果中可以看出:set可去重
    for (auto& e : s)
    cout << e << " ";
    cout << endl;
    // 使用迭代器逆向打印set中的元素
    for (auto it = s.rbegin(); it != s.rend(); ++it)
    	cout << *it << " ";
    cout << endl;
    // set中值为3的元素出现了几次
    cout << s.count(3) << endl;
}
3.3 map的介绍

map的参考文档

  1. map是关联容器,他是按照特定的次序(根据key比较)存储由<key,value>键值对组合而成的元素。
  2. 在map中,键值key是用于排序且唯一表示元素,而值value中存储与此键值key关联的内容。键值key和value的类型可以不同,并且在map内部,通常会将key和value用pair进行绑定。
  3. map中键值访问单个元素的速度要比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代,获得一个有序的序列。
  4. map支持下标访问符访问key对应的value,map[key]=value
  5. map的底层同样也是红黑树。
3.4 map的使用

map必须包含头文件#include <map>,并且属于std命名空间里面。

int main()
{
    map<int, string> m;
    m[0] = "hello";
    m[1] = "world";
    m[2] = ".";

    auto it = m.begin();
    while (it != m.end())
    {
        cout << it->first << " : " << it->second << " "; // 0 : hello 1 : world 2 : .
        ++it;
    }
    cout << endl;
}

这是一个简单使用map容器的方法,与set容器不同的地方在于它存的是一个<key,value>型,所以构造函数的模板参数需要分别传入key和value的类型,并且可以采用下表进行访问和插入元素,迭代器本质上是一个pair<key,value>类型,所以访问的方式与set也有所不同,first是key值,second是value值,最后,迭代器进行访问元素的时候会按照key排序之后的顺序进行打印。

3.4.1 map的定义

map的构造函数

构造函数接口说明
map();无参构造
  • 第一个是无参构造,构造一个空的map,传入的模板参数有key,value的类型,比较器的类型(默认是less)。
int main()
{
	map<string,int> m;
	map<string,int,greater<string>> m2;
}
3.4.2 map的迭代器

map迭代器与set的迭代器相似,只不过迭代器获取的是一个pair<key,value>的类型,访问元素不可以直接通过解引用进行访问,需要按照pair的方式。

map获取迭代器

迭代器的使用使用说明
begin()返回指向开始位置的迭代器
end()返回指向末尾元素的下一个位置的迭代器
cbegin()返回指向开始并且为常量的迭代器
cend()返回指向末尾元素的下一个位置的并且为常量的迭代器
rbegin()返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向
rend()返回逆置迭代器,指向开头元素的位置,操作都是往相反反向
crbegin()返回逆置迭代器,指向末尾元素下一个位置,操作都是往相反反向,并且为常量属性
crend()返回逆置迭代器,指向开头元素的位置,操作都是往相反反向,并且为常量属性
3.4.3 map的容量
容量说明接口说明
size获取容器中实际的个数
empty判断是否为空
3.4.4 map的元素访问
函数接口说明
mapped_type& operator[](const key_type& k)获取key对应的value值,如果key存在,则无需插入到map中,直接返回key对应的value值,如果key不存在,会将对应的key和value值插入到map中。

上面这个接口等价于

(*((this->insert(make_pair(k,mapped_type()))).first)).second;

首先,对key和value值构建键值对pair,然后调用map中的insert接口,返回一个pair<iterator,bool>,假如key值不存在,那么iterator就是插入之后的迭代器的位置,bool是表示插入成功,然后迭代器本质上存的就是pair<key,value>,所以最后对返回的迭代器拿去second就是获取value值。假如key值已经存在,iterator也会返回key值对应迭代器的位置,然后访问迭代器的second也同样是value值。

3.4.5 map的修改
增删改查接口说明
pair<iterator,bool> insert(const value_type& x)在map中插入键值对<key,value>,如果成功,返回新插入位置的迭代器,true,否则返回,key对应位置的迭代器,false
erase(iterator pos)删除map中pos位置的元素。
erase(const value_type& x)删除map中键值对为x的元素
swap(map& m)交换map中的元素
find(const key_type& x)在map中查找key为x的元素,找到返回该元素位置的迭代器,找不到返回end()
count(const key_type& x)返回map中key值为x的元素的个数
clear清除map的元素
#include <iostream>
#include <string>
#include <map>

using namespace std;

void TestMap()
{
	map<string, string> m;
	// 向map中插入元素的方式:
	// 将键值对<"peach","桃子">插入map中,用pair直接来构造键值对
	m.insert(pair<string, string>("peach", "桃子"));
	// 将键值对<"peach","桃子">插入map中,用make_pair函数来构造键值对
	m.insert(make_pair("banan", "香蕉"));
	// 借用operator[]向map中插入元素
	/*
		operator[]的原理是:
		用<key, T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中
		如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器
		如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器
		operator[]函数最后将insert返回值键值对中的value返回
	*/
	// 将<"apple", "">插入map中,插入成功,返回value的引用,将“苹果”赋值给该引用结果
	m["apple"] = "苹果";
	// key不存在时抛异常

	//m.at("waterme") = "水蜜桃";
	cout << m.size() << endl;
	// 用迭代器去遍历map中的元素,可以得到一个按照key排序的序列
	for (auto& e : m)
		cout << e.first << "--->" << e.second << endl;
	cout << endl;
	// map中的键值对key一定是唯一的,如果key存在将插入失败
	auto ret = m.insert(make_pair("peach", "桃色"));
	if (ret.second)
		cout << "<peach, 桃色>不在map中, 已经插入" << endl;
	else
		cout << "键值为peach的元素已经存在:" << ret.first->first << "--->"
		<< ret.first->second << " 插入失败" << endl;
	// 删除key为"apple"的元素
	m.erase("apple");
	if (1 == m.count("apple"))
		cout << "apple还在" << endl;
	else
		cout << "apple被吃了" << endl;
}

[总结]

  1. map中的元素是键值对
  2. map中的key是唯一,并且不能修改
  3. map默认是按照从小到大的顺序对key进行排序。
  4. map如果用迭代器遍历元素,可以获得一个key有序的序列。
  5. map的底层为红黑树,查找效率为O(log_2(n))
  6. map支持[]操作符,可以通过下标进行插入和查找。
3.5 multiset和multimap

这两个容器的与setmap区别在于可以存储多个key值,重点在于count接口,如果是setmap,只会返回1和0,而multisetmultimap可以返回具体的数量。

4. 红黑树模拟实现STL的map与set

在看这之前,最好对红黑树有些许了解,可以看我的博客数据结构之红黑树(C++实现)

4.1 红黑树的迭代器

迭代器的好处是可以方便遍历,并且数据结构的底层实现与用户透明。如果想要给红黑树添加迭代器,需要考虑以下问题:

  1. begin()与end()

    我们知道begin()和end()是一段前闭右开区间,红黑树的最小结点在最左侧结点,故对红黑树一直往左子树遍历,直到遇到cur->left == nullptr时。end()为最大结点的下一个位置,我们知道end()结点的下一个是为空,故返回nullptr即可。

  2. operator++()和operator–()

    • 当前进行++操作,如果当前结点的右子树存在,那么就找到右孩子的最左侧结点,故为当前结点的下一个结点。如果当前结点的右子树不存在,那么就从祖先结点中去寻找,找到当前结点不等于父节点的右孩子的那个结点cur != parent->right,故当前结点为下一个结点。
    • 当前进行–操作,如果当前结点的左子树存在,那么就找到左子树的最右侧的结点,故为当前结点的前一个结点。如果当前结点的左子树不存在,那么就从祖先结点中去寻找,找到当前结点不等于父节点的左孩子的那个结点cur != parent->left,故当前结点为上一个结点。
  3. 迭代器实现如下:

    template<class T, class Ref, class Ptr>
    struct __RBTreeIterator {
        typedef RBTreeNode<T> Node;
        typedef __RBTreeIterator<T,Ref,Ptr> Self;
    
        __RBTreeIterator(Node *node)
                : _node(node)
        {}
    
    
        Ref operator*()
        {
            return _node->_data;
        }
    
        Ptr operator->()
        {
            return &_node->_data;
        }
    
        bool operator!=(const Self& s) const
        {
            return _node != s._node;
        }
    
        bool operator==(const Self& s) const
        {
            return _node == s._node;
        }
    
        Self& operator++()
        {
            if(_node->_right)
            {
                // 下一个是右子树的最左节点
                Node *left = _node->_right;
                while(left->_left)
                    left = left->_left;
                _node = left;
            }
            else
            {
                // 找祖先里面孩子不是祖先最右的那个
                Node *parent = _node->_parent;
                Node *cur = _node;
                while(parent && cur == parent->_right)
                {
                    cur = cur->_parent;
                    parent = parent->_parent;
                }
                _node = parent;
            }
    
            return *this;
        }
    
        Self& operator--()
        {
            if(_node->_left)
            {
                // 下一个是右子树的最左节点
                Node *right = _node->_left;
                while(right->_right)
                    right = right->_right;
                _node = right;
            }
            else
            {
                // 找祖先里面孩子不是祖先最左的那个
                Node *parent = _node->_parent;
                Node *cur = _node;
                while(parent && cur == parent->_left)
                {
                    cur = cur->_parent;
                    parent = parent->_parent;
                }
                _node = parent;
            }
    
            return *this;
        }
    
        Node *_node;
    };
    
    typedef RBTreeNode<T> Node;
    typedef __RBTreeIterator<T,T&,T*> iterator;
    
    iterator begin()
    {
        Node* left = _root;
        while(left && left->_left)
            left = left->_left;
    
        return iterator(left);
    }
    
    iterator end()
    {
        return iterator(nullptr);
    }
    
4.2 改造红黑树
/*
	因为关联式容器存储的是<key,value>的键值对,而set和map的底层都是红黑树。
	那么我们知道set其实存的是<value,value>,而map才是存储<key,value>。
	那么,这样会导致我们如果按照原先的红黑树写法,就必须设计两个红黑树。
	为了方便,我们设计一个仿函数类,这个类是通过value来获取key的一个仿函数。
*/

// 红黑树的结点只存储value值
template<class T>
struct RBTreeNode {
    RBTreeNode<T> *_left;
    RBTreeNode<T> *_right;
    RBTreeNode<T> *_parent;

    T _data;
    Colour _col;

    RBTreeNode(const T &data)
            : _left(nullptr), _right(nullptr), _parent(nullptr), _data(data) {}
};

template<class K, class T, class KeyOfT>
struct RBTree {
    typedef RBTreeNode<T> Node;
    typedef __RBTreeIterator<T,T&,T*> iterator;
private:
    Node *_root = nullptr;
};
4.3 map的模拟实现
namespace Ming
{
    template<class K,class V>
    class map
    {
        // 设计一个map的仿函数类
        struct MapKeyOfT
        {
            const K& operator()(const pair<K,V>& kv)
            {
                return kv.first;
            }
        };
    public:
        // map的value值其实是对key和value进行包装的一个键值对
        typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;

        iterator begin()
        {
            return _t.begin();
        }

        iterator end()
        {
            return _t.end();
        }

        pair<iterator,bool> insert(const pair<K,V>& kv)
        {
            return _t.Insert(kv);
        }

        V& operator[](const K& key)
        {
            pair<iterator,bool> ret = insert(make_pair(key,V()));
            return ret.first->second;
        }

    private:
        RBTree<K,pair<K,V>,MapKeyOfT> _t;
    };
}
4.4 set的模拟实现
namespace Ming {
    template<class K>
    class set {
        struct SetKeyOfT {
            const K &operator()(const K &key) {
                return key;
            }
        };

    public:
        // set的key和value都是同一个值
        typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;

        iterator begin() {
            return _t.begin();
        }

        iterator end() {
            return _t.end();
        }

        pair<iterator,bool> insert(const K &key) {
            return _t.Insert(key);
        }

    private:
        RBTree<K, K, SetKeyOfT> _t;
    };
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值