C++ set && map

发布于:2025-04-01 ⋅ 阅读:(24) ⋅ 点赞:(0)

1.set和map是什么

set和map是 C++ STL 提供的容器,用于高效的查找数据,底层采用红黑树实现,其中set是Key模型,map是Key-Value模型

set和map的基本使用较为简单,这里不再叙述,直接进入实现环节

2.set和map的底层实现

2.1 节点的定义

既然set和map的底层采用红黑树,那就少不了我们之前实现过的红黑树,直接搬过来复用

但我们实现的红黑树是Key-Value模型,难道要针对Key模型再copy一份红黑树,将节点里面的值修改成Key吗?

STL 中采用模板的方式,只需要一份红黑树,就能完成Key和Key-Value模型的统一

既然如何,红黑树节点的定义中,数据就不能只是key或pair对象,而应当是泛型,外部传递什么类型,数据就是什么类型

template<typename T>
struct RBTreeNode
{
	RBTreeNode(const T& data)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_data(data)
		,_col(RED)
	{}

	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _data;
	Color _col;
};

// set.h
template<typename K>
class set
{
private:
    RBTree<K, K> _tree;
};

// map.h
template<typename K, typename V>
class map
{
private:
    RBTree<K, std::pair<K, V>> _tree;
};

2.2 set和map的设计

同时,插入数据时,插入的值也应当为修改,此时,数据的比较就有问题了,之前的比较都是针对单一的key或pair进行比较,但红黑树这层并不知道外部传递给我的是一个什么类型的数据,只有set/map层知道传递的数据类型是什么,该如何进行数据的比较?

这里的解决方法是使用仿函数,在set/map层传递一个仿函数给红黑树层,该仿函数的功能用于取出key值,如果是set,那就直接取出key值,如果是map,那就取出pair中的first

// set.h
template<typename K>
class set
{
private:
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };
    
private:
    RBTree<K, K, SetKeyOfT> _tree;
};

// map.h
template<typename K, typename V>
class map
{
private:
    struct MapKeyOfT
    {
        const K& operator()(const std::pair<K, V>& kv)
        {
            return kv.first;
        }
    };
    
private:
    RBTree<K, std::pair<K, V>, MapKeyOfT> _tree;
};

// RBTree.h
template<typename K, typename T, typename KeyOfT>
class RBTree
{
public:
	bool insert(const T& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return true;
		}

		KeyOfT kot;
		Node* cur = _root, * parent = nullptr;
		while (cur)
		{
			if (kot(data) < kot(cur->_data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (kot(data) > kot(cur->_data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else return false;
		}

		Node* newnode = new Node(data);
		if (kot(data) < kot(parent->_data)) parent->_left = newnode;
		else parent->_right = newnode;
		newnode->_parent = parent;
        // .......
		return true;
	}
    
private:
	Node* _root = nullptr;
};

至此,set/map的整体框架建立完毕

set/map中,红黑树的定义时,还多传递了一个K类型,这样set有两个K类型,会不会很多余?

虽然插入数据不同的类型要采用不同的比较方式,但双方的查找操作都是根据key进行查找的,因此,多添加的K类型是为了方便map进行find操作的

2.3 迭代器

接下来,便是迭代器的设计了

在vector和list中,迭代器都是对指针的封装,再通过重载*和->运算符,达到屏蔽底层,上层统一遍历的效果

这里我们的设计思想也是一样的

template<typename T, typename Ref, typename Ptr>
struct __RBreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __RBreeIterator<T, Ref, Ptr> Self;

	__RBreeIterator(Node* node)
		:_node(node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	Node* _node;
};

唯一不同的是对于红黑树,++操作该如何实现?

红黑树的遍历要采用中序遍历才有意义,左子树 根 右子树

遍历到到某一节点时,如果左子树不为空,那么要一直遍历,直到左子树为空

左子树遍历完了,接下来访问根

根访问完了,如果右子树不为空,下一个应该访问右子树的最左节点

如果右子树为空,表明当前子树已经访问完了,此时,如果父亲是祖父的右子树,表明父亲的那颗子树也访问完了,要一直向上返回,直到当前节点是其父亲的左子树

在这里插入图片描述

在这里插入图片描述

template<typename T, typename Ref, typename Ptr>
struct __RBreeIterator
{
    typedef RBTreeNode<T> Node;
	typedef __RBreeIterator<T, Ref, Ptr> Self;
    // ...
	Self& operator++()
	{
		if (_node->_right != nullptr)
		{
			_node = _node->_right;
			while (_node->_left) _node = _node->_left;
		}
		else
		{
			Node* parent = _node->_parent;
			while (parent && _node == parent->_right)
			{
				_node = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}

	Node* _node;
};

set和map在 STL 中是双向迭代器,意味着能迭代器不仅支持++操作,也支持–操作,而–操作无非反过来遍历

在这里插入图片描述

在这里插入图片描述

Self& operator--()
{
    if (_node->_left != nullptr)
    {
        _node = _node->_left;
        while (_node->_right) _node = _node->_right;
    }
    else
    {
        Node* parent = _node->_parent;
        while (parent && _node == parent->_left)
        {
            _node = parent;
            parent = parent->_parent;
        }
        _node = parent;
    }
    return *this;
}

规定迭代器的begin为红黑树的最左节点,end为空

template<typename K, typename T, typename KeyOfT>
class RBTree
{
public:
	typedef RBTreeNode<T> Node;
	typedef __RBreeIterator<T, T&, T*> iterator;

	iterator begin()
	{
		Node* left = _root;
		while (left && left->_left) left = left->_left;
		return iterator(left);
	}

	iterator end()
	{
		return iterator(nullptr);
	}
    // ...
}

而set/map,无论是插入操作,还是迭代器,无非是对红黑树的接口的封装罢了

// set.h
template<typename K>
class set
{
private:
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };

public:
    typedef typename RBTree<K, const K, SetKeyOfT>::iterator iterator;

    bool insert(const K& key)
    {
        return _tree.insert(key);
    }

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

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

private:
    RBTree<K, const K, SetKeyOfT> _tree;
};

// map.h
template<typename K, typename V>
class map
{
private:
    struct MapKeyOfT
    {
        const K& operator()(const std::pair<K, V>& kv)
        {
            return kv.first;
        }
    };

public:
    typedef typename RBTree<K, std::pair<const K, V>, MapKeyOfT>::iterator iterator;

    bool insert(const std::pair<K, V>& kv)
    {
        return _tree.insert(kv);
    }

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

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

private:
    RBTree<K, std::pair<const K, V>, MapKeyOfT> _tree;
};

2.4 map中operator[]

STL 中map支持使用[]运算符进行value的修改,使用起来相当方便

实际上,其内部设计利用了insert操作,将insert的返回值修改为std::pair<iterator, bool>

使用[]时,会传递一个key值,希望拿到key对应value的引用,这样就能方便修改value

因此,可以在[]内部调用insert,如果key不存在则插入成功,返回新节点的迭代器 + true的pair对象,否则返回值中的pair的second为false,表示插入失败

template<typename K, typename V>
class map
{
    // ...
public:
    typedef typename RBTree<K, std::pair<const K, V>, MapKeyOfT>::iterator iterator;

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

2.5 拷贝构造 + 赋值重载

set和map都只有RBTree一个成员,直接调用红黑树的拷贝构造和赋值重载即可

template<typename K, typename T, typename KeyOfT>
class RBTree
{
public:
	typedef RBTreeNode<T> Node;
	typedef __RBreeIterator<T, T&, T*> iterator;
	typedef __RBreeIterator<T, const T&, const T*> const_iterator;

	~RBTree()
	{
		this->destroy(_root);
		_root = nullptr;
	}

	RBTree() = default;

	RBTree(const RBTree<K, T, KeyOfT>& t)
	{
		_root = this->create(t._root);
		_root->_parent = nullptr;
	}

	RBTree<K, T, KeyOfT>& operator=(RBTree<K, T, KeyOfT> t)
	{
		std::swap(_root, t._root);
		return *this;
	}
    
    // ...
    
private:
	void destroy(Node* root)
	{
		if (root == nullptr) return;
		destroy(root->_left);
		destroy(root->_right);
		delete root;
		root = nullptr;
	}

	Node* create(Node* root)
	{
		if (root == nullptr) return nullptr;

		Node* newnode = new Node(root->_data);
		newnode->_col = root->_col;
		newnode->_left = create(root->_left);
		if (newnode->_left) newnode->_left->_parent = newnode;
		newnode->_right = create(root->_right);
		if (newnode->_right) newnode->_right->_parent = newnode;
		return newnode;
	}

private:
	Node* _root = nullptr;
}

剩下的还有const迭代器和find的实现了,由于较为简单,不再详讲

**注意:**还有个细节,我们在传递红黑树的模板参数时,将第二个模板参数中的K加上了const

如果是set,那就是const K,如果是map,那就是pair<const K, V>,这是为了防止使用迭代器修改节点中的值,为什么不直接使用const迭代器呢?如果使用const迭代器,那么map中的Key和Value都不能修改,而map是要能支持修改Value的

2.6 测试

void Test_set()
{
    set<int> s;
    std::vector<int> v = { 1, 3, 2, 8, 11, 9, 4, 5, 7, 10, 6 };
    for (auto& e : v) s.insert(e);

    std::cout << "正向遍历: ";
    for (auto& e : s) std::cout << e << " ";
    std::cout << std::endl;

    std::cout << "反向遍历: ";
    set<int>::iterator it = s.find(11);
    while (it != s.end())
    {
        std::cout << *it << " ";
        --it;
    }
    std::cout << std::endl;

    std::cout << "拷贝构造: s2->";
    set<int> s2(s);
    for (auto& e : s2) std::cout << e << " ";
    std::cout << std::endl;

    std::cout << "赋值重载: ";
    set<int> s3(s);
    s3.insert(1);
    s3 = s;
    std::cout << "s3->";
    for (auto& e : s3) std::cout << e << " ";
    std::cout << "    s->";
    for (auto& e : s) std::cout << e << " ";
}

在这里插入图片描述

void Test_map()
{
    std::vector<std::string> v = { "apple", "tassel", "watermelon", "apple", "banana", "tassel", "orange", "apple" };
    map<std::string, int> m;
    for (auto& e : v) m[e] ++;
    for (auto& it : m)
    {
        std::cout << it.first << " " << it.second << std::endl;
    }
}

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到