本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录前一修订版后一修订版 | 前一修订版 | ||
cs:programming:cpp:boolan_cpp:stl_generic_3 [2024/01/14 13:46] – 移除 - 外部编辑 (未知日期) 127.0.0.1 | cs:programming:cpp:boolan_cpp:stl_generic_3 [2024/01/14 13:47] (当前版本) – ↷ 链接因页面移动而自动修正 codinghare | ||
---|---|---|---|
行 1: | 行 1: | ||
+ | ======STL与泛型编程 第三周====== | ||
+ | 本页内容是作为 //Boolan// C++ 开发工程师培训系列的笔记。\\ | ||
+ | <wrap em> | ||
+ | ---- | ||
+ | ====Deque概述==== | ||
+ | |||
+ | //Deque// 是一种**分段连续**的容器。这个容器的特点是**双向开口**,并通过一个**控制器**来**串联**一系列的**缓冲区**(// | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 从上图我们可以看出 //Deque// 的大体结构: | ||
+ | * 数据结构部分: | ||
+ | * 数据单元:// | ||
+ | * 数据控制中心: 由一个 //Vector// 实现。 //Deque// 通过该区域存储一系列指向不同 //Buffer// 的指针,将 //Buffer// 从逻辑上串联起来。 | ||
+ | * 迭代器部分: | ||
+ | * 迭代器的组成: | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * 首尾迭代器 '' | ||
+ | 下面来看看这些结构和功能都是如何实现的。 | ||
+ | |||
+ | ===Deque的基础结构实现=== | ||
+ | |||
+ | ==控制器== | ||
+ | |||
+ | 我们知道,// | ||
+ | <code cpp linenums: | ||
+ | class deque { | ||
+ | public: | ||
+ | typedef T value_type; | ||
+ | .... | ||
+ | protected: | ||
+ | typedef pointer* map_pointer; | ||
+ | protected: | ||
+ | map_pointer map; | ||
+ | size_type map_size; | ||
+ | .... | ||
+ | }; | ||
+ | </ | ||
+ | 因为我们在控制器中存储的是指向 //Buffer// 的指针,因此我们需要**指向指针的指针**这种类型。同时,我们也需要维护一个 '' | ||
+ | |||
+ | ==迭代器== | ||
+ | |||
+ | 为了匹配接口,我们需要两个迭代器:'' | ||
+ | <code cpp linenums: | ||
+ | class deque { | ||
+ | public: | ||
+ | | ||
+ | protect: | ||
+ | | ||
+ | | ||
+ | .... | ||
+ | }; | ||
+ | </ | ||
+ | 这两个迭代器一个指向 //Deque// 的开头元素,另外一个指向 //Deque// 的末尾元素的后一位。跟别的容器一样,// | ||
+ | <code cpp linenums: | ||
+ | iterator begin() { return start; } | ||
+ | iterator end() { return finish; } | ||
+ | iterator size() { return finish - start; } | ||
+ | </ | ||
+ | |||
+ | ===迭代器的结构实现=== | ||
+ | |||
+ | 迭代器在 //Deque// 中扮演了非常重要的角色。我们知道 //Deque// 在物理上不是完全连续的;为了像维护顺序容器一样维护 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 除了C++要求的五条基本信息以外,根据本节开头的图示,我们还需要使用迭代器来维护当前的 // | ||
+ | <code cpp linenums: | ||
+ | T* cur; | ||
+ | T* first; | ||
+ | T* last; | ||
+ | map_pointer node; //T** | ||
+ | </ | ||
+ | 同时,迭代器类也定义了两个类型,方便以后重载使用: | ||
+ | <code cpp linenums: | ||
+ | typedef size_t size_type; | ||
+ | typedef _deque_iterator self; | ||
+ | </ | ||
+ | |||
+ | ===Buffer大小的计算方法=== | ||
+ | |||
+ | 在 //G 2.9// 的版本中,// | ||
+ | <code cpp linenums: | ||
+ | size_t BuffSiz = 0; | ||
+ | </ | ||
+ | 这里的 '' | ||
+ | <code cpp linenums: | ||
+ | inline size_t _deque_buf_size(size_t n, size_t sz) { | ||
+ | return n != 0 ? n : | ||
+ | (sz < 512 ? size_t(512 / sz) : size_t(1)); | ||
+ | } | ||
+ | </ | ||
+ | 上面的代码主要分如下几个步骤: | ||
+ | - 如果 '' | ||
+ | - 如果 '' | ||
+ | - 在 '' | ||
+ | - 如果元素大小大于 '' | ||
+ | |||
+ | ===Insert操作的实现=== | ||
+ | |||
+ | '' | ||
+ | * 首先判断插入点是否在容器的头部或者尾部 | ||
+ | * 如果在头部,使用 '' | ||
+ | * 如果在尾部,使用 '' | ||
+ | * 如果不在头部或者尾部,调用函数 '' | ||
+ | * 如果在插入点前面的元素个数较少(距离小于 '' | ||
+ | * 否则使用 '' | ||
+ | 具体的实现代码如下: | ||
+ | <code cpp linenums: | ||
+ | iterator insert(iterator position, const value_type& | ||
+ | //if insert at start | ||
+ | if (position.cur == start.cur) { | ||
+ | push_front(x); | ||
+ | return start; | ||
+ | //if insert at end | ||
+ | } else if (position.cur == finish.cur) { | ||
+ | push_back(x); | ||
+ | iterator tmp = finish; | ||
+ | --tmp; | ||
+ | return tmp; //return the iterator that point to the last element | ||
+ | } else { | ||
+ | return insert_aux(position, | ||
+ | } | ||
+ | |||
+ | /* insert_aux */ | ||
+ | template <class T, class Alloc, size_t BufSize> | ||
+ | typename deque<T, Alloc, BufSize>:: | ||
+ | deque<T, Alloc, BufSize>:: | ||
+ | difference_type index = pos - start; // get the distance from insert position to start | ||
+ | value_type x+copy = x; | ||
+ | //distance < size() / 2 means the insert point is closer to the start | ||
+ | if (index < size() / 2) { | ||
+ | push_front(front()); | ||
+ | ... | ||
+ | copy(front2, | ||
+ | } else { | ||
+ | push_back(back()); | ||
+ | ... | ||
+ | copy_backward(pos, | ||
+ | } | ||
+ | *pos = x_copy; | ||
+ | return pos; | ||
+ | } | ||
+ | </ | ||
+ | \\ | ||
+ | |||
+ | ====Deque如何模拟连续空间==== | ||
+ | |||
+ | //Deque// 的迭代器是**随机访问**类型的,但其数据结构并不是物理连续的。因此,我们需要对 //Deque// 的迭代器的某些功能进行某些运算的重载,以让其达到随机访问的效果。由于 //Deque// 是由一个个缓冲区组成的,而这些缓冲区并不连续。因此,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 作为一个随机访问的迭代器,是应该具有以下的功能的: | ||
+ | * '' | ||
+ | * 自增 / 自减 功能。 | ||
+ | * '' | ||
+ | |||
+ | ===指针基本功能的实现=== | ||
+ | |||
+ | //Deque// 中 '' | ||
+ | <code cpp linenums: | ||
+ | reference operator* () const { return *cur; } | ||
+ | pointer operator-> | ||
+ | </ | ||
+ | ===“-”的重载=== | ||
+ | |||
+ | <WRAP center round important 100%> | ||
+ | 注:这里的相减操作是指的**两个迭代器之间的相减操作**,与迭代器移位操作的重载不同,请勿混淆! | ||
+ | </ | ||
+ | |||
+ | \\ | ||
+ | 在 STL 中,迭代器相减代表了两个迭代器之间的距离。对于 //Deque// 来说,判断其两个迭代器之间的距离要稍微复杂一些。作为分段连续的容器,我们不仅要考虑两个迭代器在同一个 //Buffer// 里的情况,同时也要考虑两个**迭代器分别处于不同**的 //Buffer// 中的情况。我们可以将该距离的计算分为两步: | ||
+ | - 计算出两个迭代器中包含了多少个完整的 //Buffer// | ||
+ | - 计算出两个迭代器所在 //Buffer// 中剩余的元素个数 | ||
+ | 综合起来可以用下面的代码实现: | ||
+ | <code cpp linenums: | ||
+ | operator - (const self& x) const { | ||
+ | return difference_type(buffer_size()) * (node - x.node -1) + (cur - first) + (x.last - x.cur); | ||
+ | } | ||
+ | </ | ||
+ | 这里的 '' | ||
+ | |||
+ | ===“++”和“--”的重载=== | ||
+ | |||
+ | 自增 | ||
+ | - 切换迭代器至下一位 | ||
+ | - 如果此时迭代器处于最后一个元素的后一位 '' | ||
+ | - 通过控制器执行跨区操作 | ||
+ | - 将当前迭代器指向新区的起点 | ||
+ | 整个流程用代码表现出来就是: | ||
+ | <code cpp linenums: | ||
+ | self& operator++() { | ||
+ | ++cur; | ||
+ | if(cur == last) { | ||
+ | set_node(node + 1); // jump to the next Buffer | ||
+ | cur == first; //set cur iterator to the first | ||
+ | } | ||
+ | return *this; | ||
+ | } | ||
+ | </ | ||
+ | 这里的跨区操作通过 '' | ||
+ | <code cpp linenums: | ||
+ | void set_node(map_pointer new_node) { | ||
+ | node = new_node; //switch to the new buffer | ||
+ | first = *new_node; // set first, make it points to the start of the new buffer | ||
+ | last = first + difference_type(buffer_size()); | ||
+ | } | ||
+ | </ | ||
+ | 上面说到的是前自增版本。后自增版本与 '' | ||
+ | |||
+ | ===“+=”和“-=”的重载=== | ||
+ | |||
+ | '' | ||
+ | - 判断当前位置与移位操作量相加后是否在同一 //Buffer// 中 | ||
+ | - 如果是,直接进行 '' | ||
+ | - 如果不是 | ||
+ | - 计算相隔的 //Buffer// | ||
+ | - 切换到正确的 //Buffer// | ||
+ | - 切换到正确的元素 | ||
+ | 上述流程可以用以下代码实现: | ||
+ | <code cpp linenums: | ||
+ | self& operator += (difference_type n) { | ||
+ | difference_type offset = n + (cur - first); | ||
+ | if (offset >= 0 && offset < difference_type(buffer_size())) // offset in buffer | ||
+ | cur += n; | ||
+ | //offset out of current buffer | ||
+ | else { | ||
+ | //whether the offset is going forward or backward (using this condition to represent both += and -=) | ||
+ | difference_type node_offset = offset > 0 ? offset / difference_type(buffer_size())) | ||
+ | : | ||
+ | //switch to the right buffer | ||
+ | set_node(node + node_offset); | ||
+ | //move iterator to the right position | ||
+ | cur = fist + (offset - node_offset * difference_type(buffer_size())); | ||
+ | } | ||
+ | | ||
+ | </ | ||
+ | 还有一些有趣的事情: | ||
+ | * '' | ||
+ | * '' | ||
+ | * 而取下标 '' | ||
+ | |||
+ | ===控制器的扩充=== | ||
+ | |||
+ | 前面说到过 //Deque// 的控制器使用了 '' | ||
+ | |||
+ | ===Queue和Stack=== | ||
+ | |||
+ | 严格意义上来说, //Queue// 和 //Stack// 应该被称为**容器适配器**,因为他们是依托基本的容器来实现的。之所以放到 //Deque// 章节讨论,是因为依托 //Deque// 实现的方式效率**最高**。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 首先来看看 //Queue// 和 //Stack// 都需要什么样的功能: | ||
+ | \\ | ||
+ | \\ | ||
+ | //Queue// 是先进先出的数据结构,因此需要实现的函数主要有: | ||
+ | * 查看元素:'' | ||
+ | * 操作元素: '' | ||
+ | //Deque// 和 //List// 都可以通过自身的函数'' | ||
+ | \\ | ||
+ | \\ | ||
+ | 而 //Stack// 因为是先进先出,因此需要实现的主要函数有: | ||
+ | * 查看元素:'' | ||
+ | * 操作元素:'' | ||
+ | '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 因此我们可以得出一个结论:不管什么样的容器,只要其操作可以实现 //Queue// 和 //Stack// 的数据结构操作,那这些容器都可以作为 //Queue// 和 //Stack// 的底层容器。由这个结论我们可以推一些反例出来: | ||
+ | * 关系容器是没法实现 //Queue// 和 //Stack// 的 | ||
+ | * //Vector// 只能实现 //Stack// | ||
+ | 不过这里比较 tricky 的是,尽管 //Vector// 不能实现 // | ||
+ | \\ | ||
+ | \\ | ||
+ | 除此之外,// | ||
+ | * **不允许遍历** | ||
+ | * **不允许使用迭代器** | ||
+ | 因为以上两种方式都可能破坏这两种数据结构的特性(先进先出,先进后出)。 | ||
+ | |||
+ | ====红黑树作为容器的使用==== | ||
+ | |||
+ | 本系列第一周课程中提到过关系容器的底层实现都是红黑树。为了了解关系容器,我们需要先明白红黑树这个数据结构的一些特点。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 红黑树是**平衡搜索二叉树**的一种,通过节点的红黑属性来达到平衡的效果。所谓的平衡,就是指在树不会有远比其他的分支长的分支导致影响搜索的效率。 | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | // | ||
+ | |||
+ | ===红黑树的遍历=== | ||
+ | |||
+ | 红黑树默认的遍历顺序是**从左到右**的;按照该方法做完遍历以后,红黑树中所有的元素会呈现**已经排序**的状态。红黑树是有迭代器的;在 //STL// 中,最左端的元素被 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 但是在编程层面中,该操作是允许的。// | ||
+ | |||
+ | ===红黑树的插入=== | ||
+ | |||
+ | 在红黑树中,我们可以根据 //Key// 值来决定是否插入元素: | ||
+ | * '' | ||
+ | * '' | ||
+ | |||
+ | ===红黑树的结构=== | ||
+ | |||
+ | 红黑树作为一种较为底层的容器,并不是经常拿到台面上来使用的。然而红黑树也具有自身的 //API//: | ||
+ | <code cpp linenums: | ||
+ | template< | ||
+ | </ | ||
+ | 从上面的代码我们发现红黑树需要五种类型的数据: | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 上述的类型通过下面的代码组成了红黑树这个类的结构: | ||
+ | <code cpp linenums: | ||
+ | class rb_tree { | ||
+ | .... | ||
+ | protected: | ||
+ | typedef _rb_tree_node< | ||
+ | size_type node_count; // elements count in rb_tree | ||
+ | link_type header; | ||
+ | Compare key_compare; | ||
+ | }; | ||
+ | </ | ||
+ | ===红黑树的使用=== | ||
+ | |||
+ | 根据上述的信息我们可以很简单的建立一个红黑树: | ||
+ | <code cpp linenums: | ||
+ | rb_tree< | ||
+ | </ | ||
+ | * 第一个 '' | ||
+ | * 第二个 '' | ||
+ | * 第三个 '' | ||
+ | * 第四个 '' | ||
+ | 除此之外,红黑树还有几个成员函数:'' | ||
+ | |||
+ | ===G4.9的改动=== | ||
+ | |||
+ | 和其他容器一样,// | ||
+ | |||
+ | ====关系容器的详解==== | ||
+ | |||
+ | ===Set / Multiset=== | ||
+ | |||
+ | //Set / Multiset// 是以红黑树为底层结构实现的容器,其拥有以下特点: | ||
+ | * 元素自动排序。 | ||
+ | * 排序的依据是 //Key// 值,在 //Set / Multiset// 中,// | ||
+ | * //Set / Multiset// 提供迭代器遍历,遍历过后容器元素处于排序状态。 | ||
+ | * //Set / Multiset// 本身禁止使用迭代器修改元素值。 | ||
+ | * //Set// 和 // | ||
+ | |||
+ | ==Set实现中的相关注意事项== | ||
+ | |||
+ | * //Set// 一共需要三个参数:// | ||
+ | * //Set// 类中唯一使用的数据类型是 '' | ||
+ | * //Set// 中的操作都会最终转化为红黑树的操作。 | ||
+ | * //GNU// 使用了 '' | ||
+ | * //Set// 中的迭代器类型为 '' | ||
+ | |||
+ | ===Map / Multimap=== | ||
+ | |||
+ | //Map / Multimap// 也是基于以红黑树实现的容器。但不同于 //Set// 的是,// | ||
+ | <code cpp linenums: | ||
+ | map<int, string> myMap; // a map with elements that have int key and string data | ||
+ | </ | ||
+ | 因为 //Map// 其元素独特的结构,对于取值函数,则不能使用 '' | ||
+ | \\ | ||
+ | \\ | ||
+ | 由于 //Map// 中涉及到修改元素的 '' | ||
+ | |||
+ | ==Operator“[]”的重载== | ||
+ | |||
+ | 运算符 '' | ||
+ | - 首先检测 '' | ||
+ | - 如果 '' | ||
+ | - 如果不存在,在恰当的位置建立一个指定 '' | ||
+ | \\ | ||
+ | 在实现中,// | ||
+ | \\ | ||
+ | \\ | ||
+ | 因为返回值必须是一个迭代器,因此 '' | ||
+ | |||
+ | ====HashTable详解==== | ||
+ | |||
+ | 哈希表(// | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 设想我们有一大堆的物品,每个物品都有自己的编号。某一天,我们希望把这些物品按照编号一个一个的存到集装箱中对应编号储物柜里。假设我们有 $2^{32}$ 这么多件物品,那我们就需要设计数量相同的储物柜来存放这些物品。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 这样的设计其实是很浪费的;很可能你的储物柜根本就没有存满,只是因为编号要求一一对应,不得不只装一个物品。而有没有可能用小于物品数量的空间来表示这么多物品呢?答案就是按类存储。而哈希表恰恰就是做的这件事情。 | ||
+ | \\ | ||
+ | \\ | ||
+ | 还是那 $2^{32}$ 的物品,如果要用小于 | ||
+ | \\ | ||
+ | \\ | ||
+ | 哈希表最终采取了**编号表加链表**的形式来处理这个问题。我们将物品的编号按照** //编号 % 编号表的大小// | ||
+ | |||
+ | ===Rehashing=== | ||
+ | |||
+ | 上面的方法通过纵向扩展解决了存储问题。但当我们想要找某个物品的时候,假如一个单元里的物品特别多,如果按照从前到后的模式寻找下去,那效率岂不是太低了? | ||
+ | \\ | ||
+ | \\ | ||
+ | 为了解决这个问题,哈希表采用了 // | ||
+ | \\ | ||
+ | \\ | ||
+ | < | ||
+ | <img src="/ | ||
+ | </ | ||
+ | </ | ||
+ | \\ | ||
+ | \\ | ||
+ | 我们来看看 //GNU// 的哈希表是怎样施行上图的策略的: | ||
+ | * 桶容器的增长条件:元素大于桶的数量(经验,无数学依据)。 | ||
+ | * 桶的元素增长策略:起始元素是 '' | ||
+ | * 增长完毕之后,哈希表将对所有元素按照**末除**的策略进行**重新分桶**。 | ||
+ | 我们把这整个过程称为 // | ||
+ | |||
+ | ===哈希表的结构实现=== | ||
+ | |||
+ | 相对于红黑树,哈希表的实现需要额外解决几个具体的问题:转换编号的策略, 分桶(即元素的相等规则)策略。// | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | * '' | ||
+ | 具体的实现如下: | ||
+ | <code cpp linenums: | ||
+ | template <class Value, class Key, class HashFcn, class ExtractKey, class EqualKey, class Alloc = alloc> | ||
+ | </ | ||
+ | 上述的参数作为类型具体表现在哈希表类中的情况: | ||
+ | <code cpp linenums: | ||
+ | hasher hash; // HashFcn | ||
+ | key_equal equals; // EqualKey | ||
+ | ExtractKey get_key; | ||
+ | typedef _hashtable_node< | ||
+ | vector< | ||
+ | size_type num_elements; | ||
+ | </ | ||
+ | 值得注意的是,'' | ||
+ | <code cpp linenums: | ||
+ | _hastable_node* next; | ||
+ | Value val; | ||
+ | </ | ||
+ | 当然,需要维护这些数据,哈希表也需要有迭代器。哈希表中的迭代器通过自增和自减遍历整个哈希表。但因为哈希表是以桶来存储元素的,因此经常会出现跨桶移动迭代器的情况。为此,哈希表中的迭代器额外设置了一个**指向桶容器的指针** '' | ||
+ | <code cpp linenums: | ||
+ | _hastable_node* next; | ||
+ | node* cur; | ||
+ | hashtable* ht; //point to bucket vector | ||
+ | </ | ||
+ | ===哈希函数=== | ||
+ | |||
+ | 哈希函数的作用就是利用元素的某个属性来生成元素的编号,这个编号我们也称为 //Hash Code// | ||
+ | \\ | ||
+ | \\ | ||
+ | 当然,哈希表对不同的 // | ||
+ | <code cpp linenums: | ||
+ | //Generic | ||
+ | template< | ||
+ | __STL_TEMPLATE_NULL struct hash< | ||
+ | size_t operator() (char x) const { return x; }; | ||
+ | </ | ||
+ | 对于基本的可以用数值表示的类型,哈希函数的处理策略都是返回其本身的值作为元素的编号。而对C字符串来说,则有如下的计算方式: | ||
+ | <code cpp linenums: | ||
+ | inline size_t _stl_hash_string(const char* s) { | ||
+ | unsigned long h = 0; //h is hash_code | ||
+ | for (; *s, ++s) | ||
+ | h = 5*h + *s; // h of " | ||
+ | </ | ||
+ | 上面这个方法也同样没有什么数学含义。这个方法的唯一目的在于希望尽可能的使编号离散化。因为编号越离散,越杂乱,越不容易发生重叠,这样造成单元链表变长的几率则越少。理论上来说,一个哈希表的最优情况是做到单元链表的平衡。 | ||
+ | \\ | ||
+ | \\ | ||
+ | <WRAP center round help 100%> | ||
+ | 注:G.2.9并未提供 //STL String// 的哈希函数,G.4.9 则提供了。 | ||
+ | </ | ||
+ | |||
+ | ====Unordered容器==== | ||
+ | |||
+ | 本节内容并无新内容。具体详情请参考[[cs: | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | ---- | ||
+ | ~~DISQUS~~ |