zl程序教程

您现在的位置是:首页 >  前端

当前栏目

再探 set/map

2023-09-14 09:15:26 时间

请添加图片描述

set和map底层数据结构都是红黑树,红黑树的data域段为==pair<key, value>==类型。

set篇

放码过来

先来段源码看一下吧。

缩略版,太长了。

//默认排序方法:从小到大排。话说看了这么多都是从小到大排为默认的,数据库也是这样的、vector也是这样的
template <class Key, class Compare = less<Key>, class Alloc = alloc>

class set {
public:
  //key_type 和 value_type 的类型是一样的
  typedef Key key_type;
  typedef Key value_type;
  //key_compare 和 value_compare 也用到了同一个比較函数
  typedef Compare key_compare;
  typedef Compare value_compare;
private:
  //底层采用红黑树来实现
  typedef rb_tree<key_type, value_type, 
                  identity<value_type>, key_compare, Alloc> rep_type;
  rep_type t;  // red-black tree representing set
public:
  typedef typename rep_type::const_pointer pointer;
  typedef typename rep_type::const_pointer const_pointer;
  typedef typename rep_type::const_reference reference;
  typedef typename rep_type::const_reference const_reference;
  //set 的 iterator 定义为红黑树的 const_iterator,这表示 set 的迭代器无法运行写入操作。
  //这是由于 set 的元素有一定次序安排,不同意用户在随意处进行写入操作
  typedef typename rep_type::const_iterator iterator;
  typedef typename rep_type::const_iterator const_iterator;
  typedef typename rep_type::const_reverse_iterator reverse_iterator;
  typedef typename rep_type::const_reverse_iterator const_reverse_iterator;
  typedef typename rep_type::size_type size_type;
  typedef typename rep_type::difference_type difference_type;

  // set 一定要使用 RB-tree 的 insert-unique() 。这样当要插入
  //已经存在的键值时,会选择忽略
  
  set() : t(Compare()) {}  // 传递 Compare() 产生的函数对象给底层的红黑树作为初始化时设定的比較函数
  explicit set(const Compare& comp) : t(comp) {}

  //下面全部的 set 操作行为,RB-tree 都已提供,所以 set 仅仅要调用就可以
  iterator begin() const { return t.begin(); }
  iterator end() const { return t.end(); }
  bool empty() const { return t.empty(); }
  size_type size() const { return t.size(); }
  size_type max_size() const { return t.max_size(); }
  void swap(set<Key, Compare, Alloc>& x) { t.swap(x.t); }

  // insert/erase
  typedef  pair<iterator, bool> pair_iterator_bool; 
  pair<iterator,bool> insert(const value_type& x) { 
    pair<typename rep_type::iterator, bool> p = t.insert_unique(x); 
    return pair<iterator, bool>(p.first, p.second);
  }
  iterator insert(iterator position, const value_type& x) {
    typedef typename rep_type::iterator rep_iterator;
    return t.insert_unique((rep_iterator&)position, x);
  }

  void erase(iterator position) { 
    typedef typename rep_type::iterator rep_iterator;
    t.erase((rep_iterator&)position); 
  }
  
  size_type erase(const key_type& x) { 
    return t.erase(x); 
  }
  
  void clear() { t.clear(); }

  iterator find(const key_type& x) const { return t.find(x); }
  size_type count(const key_type& x) const { return t.count(x); }
  iterator lower_bound(const key_type& x) const {
    return t.lower_bound(x);
  }
  iterator upper_bound(const key_type& x) const {
    return t.upper_bound(x); 
  }
 
};


set的定义

从上述代码中我们可以看出

set的底层结构是红黑树_Rep_type _M_t;

因为是红黑树所以元素自动排序,排序函数为_Compare,默认按照标准函数std::less<_key>进行排序的

当然也可以传入自定义的排序函数:


自定义排序函数

#include <iostream>
#include <set>

using namespace std;

struct cmp {

    /*
    这里将函数设置为常量函数,不然会报错:
    “const cmp”的表达式会丢失一些 const - volatile 限定符以调用“bool cmp::operator ()(const int&, const int&)”
    
    报错内容大概意思为:传入的参数表达式应具有“const myCompare”类型,而你调用的“bool myCompare::operator ()(int,int)”不具备const属性,会丢失const限定,所以无法通过编译。
    */
    bool operator() (int a, int b) const 
    {
        return a > b;
    }
};
int main()
{
    set<int, cmp> s;
    s.insert(5);
    s.insert(3);
    s.insert(6);
    s.insert(7);
    set<int>::iterator it;
    for (it = s.begin(); it != s.end(); it++) {
        cout << *it << endl;
    }//输出7 6 5 3
    return 0;
}

若是元素是结构体,那么能够直接把比较函数写在结构体内:

#include <iostream>
#include <set>
#include <string>

using namespace std;
struct Info {
    string name;
    double score;
	
		//如果不定义这个函数,人家还不给插入
    bool operator < (const Info& a) const {//重载<运算符
        return a.score < score;
    }
};

int main()
{
    set<Info> s;	
    Info info;
    info.name = "Jack";
    info.score = 80;
    s.insert(info);
    info.name = "Tom";
    info.score = 99;
    s.insert(info);
    info.name = "Bill";
    info.score = 100;
    s.insert(info);
    set<Info>::iterator it;
    for (it = s.begin(); it != s.end(); it++) {
        cout << it->name << endl;
        cout << it->score << endl;
    }
    return 0;
}

再来个模板版本的:

#include <iostream>
#include <set>
using namespace std;

/// set默认是从小到大排列,以下是让set从大到小排列
template <typename T>
struct greator
{
    bool operator()(const T& x, const T& y) const
    {
        return x > y;
    }
};

int main(void)
{
    set<int, greator<int>> iset;

    iset.insert(12);
    iset.insert(1);
    iset.insert(24);

    for (set<int>::const_iterator iter = iset.begin(); iter != iset.end(); iter++)
    {
        cout << *iter << " ";
    }
    cout << endl;

    system("pause");
    return 0;
}

这个版本排不了结构体等自定义类型的数据哈,这个跟我没关系,是模板自己的短板。


set的迭代器

set的迭代器都是const的,因此无法通过迭代器修改set元素。

正常,人家的迭代器是主键,要用来排序的。你可以删,但不可以改。
就像数据库的主键一样,你可以整条记录删掉,但是你想修改主键,嘿,不好意思。
键可删,不可改!

元素被修改后,容器并不会自动重新调整顺序,不为什么,因为没提供这个方法。


begin() 、end()

begin()方法返回set的首元素,调用的是红黑树的begin()方法,实际上这个红黑树的begin()方法返回的并不是整棵树的根节点,而是整个树的最左节点。因为set的元素是按顺序排列的,最左节点也是最小节点。

同理end()方法则返回的是最右节点,最小的值。


是否成功插入

有时候吧,就需要判断一下是否成功插入了,我记得以前做过一道算法题就需要用到这个特性,如果重复插入,需要采取一些措施的。
也可以先find,再insert,我嫌麻烦。这样吧:

#include <iostream>
#include <set>
using namespace std;

int main(void)
{
    set<int> iset;

    cout << iset.insert(12).second << endl;
    cout << iset.insert(12).second << endl;

    return 0;
}

pair<iterator, bool> insert(const T & val);
如果 set 的 insert 成员函数的返回值是 pair 模板类对象 x,如果 x.second 为 true,则说明插入成功,此时 x.first 就是指向被插入元素的迭代器;如果 x.second 为 false,则说明要插入的元素已在容器中,此时 x.first 就是指向原有那个元素的迭代器。


元素的检索

使用find()方法对set进行检索。找到则返回元素位置的迭代器,不然返回end()。

这个我熟,就不多说了。

#include <iostream>
#include <set>

using namespace std;
int main()
{
    set<int> s;
    s.insert(5);
    s.insert(3);
    s.insert(6);
    s.insert(7);
    set<int>::iterator it;
    it = s.find(3);
    if(it == s.end()) cout << "Not found" << endl;
    else cout << "Find it!" << endl;
    return 0;//输出Find it!
}

为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?

1、因为存储的是结点,不需要内存拷贝和内存移动。

2、map和set的增删改查速度为都是logn,是比较高效的。


为什么 set 的底层不用hash?

1、用,干嘛不用。用完改名字了,叫 unordered_set。

2、如果只是判断set中的元素是否存在,那么hash显然更合适,因为set 的访问操作时间复杂度是log(N)的,而使用hash底层实现的hash_set是近似O(1)的。然而,set应该更加被强调理解为“集合”,而集合所涉及的操作并、交、差等,即STL提供的如交集set_intersection()、并集set_union()、差集set_difference()和对称差集set_symmetric_difference(),都需要进行大量的比较工作,那么使用底层是有序结构的红黑树就十分恰当了,这也是其相对hash结构的优势所在。


set.lower_bound(x)/upper_bound(x)

用法与find类似,但查找的条件略有不同,时间复杂度O(log n)

s.lower_bound(x)表示查找>=x的元素中最小的一个,并返回指向该元素的迭代器
s.upper_bound(x)表示查找>x的元素中最小的一个,并返回指向该元素的迭代器

举个例子:

在set{3,5,7,8,13,16}中

对于在set中存在的元素,比如8
s.lower_bound(8)返回8所在位置的迭代器
s.upper_bound(8)返回13所在位置的迭代器

对于在set中不存在的元素,比如12
两个函数返回的则都是13所在位置的迭代器

特殊地,
对于比set中最大的元素大的元素,比如20,两个函数返回的都是s.end()。


有一个结构体,里面有两个字符串,如何在一个set中查找这个结构体?

啊,这,刚刚的结构体排序就懵了一下吧,这个哈哈。。
话说这是一道面试题,算上排序,是两道,我在同一场压力面里都遇上了,都没答上来。

前面的排序通过重载了小于号运算符,那判断相等不就重载等于号运算符嘛,就很简单的。

(好吧,其实没有这么简单)

#include <iostream>
#include <set>
#include <string>

using namespace std;
struct Info {
    string name;
    double score;

    //如果不定义这个函数,人家还不给插入
    bool operator < (const Info& a) const {//重载<运算符
        return a.score < score;
    }

    bool operator==(const Info& a) const {
        if (this->name <= a.name) {
            return true;
        }
        else {
            return false;
        }
    }
};

class compoper {
public:
    bool operator() (const Info& left, const Info& right) const {
        if (left.name <= right.name) {
            return true;
        }
        else {
            return false;
        }
    }
};

int main()
{
    set<Info, compoper> s;
    Info info;
    info.name = "Jack";
    info.score = 80;
    s.insert(info);
    info.name = "Tom";
    info.score = 99;
    s.insert(info);
    info.name = "Bill";
    info.score = 100;
    s.insert(info);
    set<Info>::iterator it;
    
    struct Info a;
    //a.name = "Bill";
    a.name = "Bil";

    if (find(s.begin(), s.end(), a) == s.end()) {
        cout << "false" << endl;
    }
    else {
        cout << "true" << endl;
    }
    
   /* string str1 = "abc";
    string str2 = "def";

    cout << (str1 <= str2) << endl;*/

    return 0;
}

除了在结构体内部要配备 “==” 运算符重载之外,还要给 set 传入comp函数。


map篇

放码过来

跟上边的一样,缩减版。

template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>

class map {
public:

  typedef Key key_type;    // 键值型別
  typedef T data_type;        // 真值型別
  typedef pair<const Key, T> value_type;    // 元素型別(键值/真值)
  typedef Compare key_compare;    // 键值比较函式

  // 以下定义一个 functor,其作用就是唤醒 元素比较函式。
  class value_compare: public binary_function<value_type, value_type, bool> {
	  friend class map<Key, T, Compare, Alloc>;
	  protected :
	    Compare comp;
	    value_compare(Compare c) : comp(c) {}
	  public:
	    bool operator()(const value_type& x, const value_type& y) const {
	      return comp(x.first, y.first);
	    }
  };

private:
  typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;
  rep_type t;  
  
public:
  typedef typename rep_type::pointer pointer;
  typedef typename rep_type::const_pointer const_pointer;
  typedef typename rep_type::reference reference;
  typedef typename rep_type::const_reference const_reference;
  typedef typename rep_type::iterator iterator;
···

  // 注意, map 一定使用 insert_unique() 而不使用 insert_equal()。
  // multimap 才使用 insert_equal()。

  map() : t(Compare()) {}
  explicit map(const Compare& comp) : t(comp) {}

  map(const map<Key, T, Compare, Alloc>& x) : t(x.t) {}
  map<Key, T, Compare, Alloc>& operator=(const map<Key, T, Compare, Alloc>& x)
  {
    t = x.t;
    return *this;
  }

  key_compare key_comp() const { return t.key_comp(); }
  value_compare value_comp() const { return value_compare(t.key_comp()); }
 
  void swap(map<Key, T, Compare, Alloc>& x) { t.swap(x.t); }

  pair<iterator,bool> insert(const value_type& x) { return t.insert_unique(x); }

  void erase(iterator position) { t.erase(position); }
  void clear() { t.clear(); }

  iterator find(const key_type& x) { return t.find(x); }
  iterator lower_bound(const key_type& x) {return t.lower_bound(x); }
  iterator upper_bound(const key_type& x) {return t.upper_bound(x); }
};

map的迭代器

map的所有元素都会根据元素的键值自动排序。map的所有元素都是pair,同时拥有实值(value)和键值(key)。pair的第一元素为键值,第二元素为实值。map不允许有两个相同的键值。

如果通过map的迭代器改变元素的键值,这样是不行的,因为map元素的键值关系到map元素的排列规则。任意改变map元素键值都会破坏map组织。如果修改元素的实值,这是可以的,因为map元素的实值不影响map元素的排列规则。因此,map iterator既不是一种constant iterators,也不是一种mutable iterators。


自定义排序

#include <iostream>
#include <string>
#include <map>
#include <functional>
using namespace std;

/// map默认是从小到大排列,以下是让map从大到小排列
template <typename T>
struct greator
{
    bool operator()(const T x, const T y) const
    {
        return x > y;
    }
};

int main(void)
{
    map<int, string, greator<int>> imap;

    imap[3] = "333";
    imap[1] = "333";
    imap[2] = "333";

    for (map<int, string>::const_iterator iter = imap.begin(); iter != imap.end(); iter++)
    {
        cout << iter->first << ": " << iter->second << endl;
    }

    system("pause");
    return 0;
}

[] 运算符重载函数

  _Tp& operator[](const key_type& __k) {
  	//首先查找__k 是否已经存在
    iterator __i = lower_bound(__k);
    // __i->first is greater than or equivalent to __k.
    //如果__k 不存在 ,直接插入一个__key ,值为_Tp()默认值
    if (__i == end() || key_comp()(__k, (*__i).first))
      __i = insert(__i, value_type(__k, _Tp()));
    //
    return (*__i).second;
  }

寻找一个元素是否存在于map当中,可以用这种方式,还不错哟。


C++map迭代器的++操作是如何实现的?

++操作就是中序遍历。
但是别看遍历一次二叉树的复杂度是O(N),一旦拿二叉树遍历跟vector的遍历来比较,性能就大大降低了。对于几万甚至数十万数百万规模的数据,性能相差达到十几倍。因为二叉树不像数组那样在连续的空间上访问,cache miss多,而且基于指针的父子节点关系,在访问的时候多了一层指针地址的间接性。


到这儿吧,哥哥还有事儿呢。