sgi_stl slist::sort() 简要分析

本文解析了SinglyLinkedList的sort()方法实现,通过分析slist::sort()源码及模拟过程,详细介绍了其背后的归并排序原理。对于理解链表排序算法具有较高的参考价值。

说是这个暑假复习数据结构,其实进度有点慢。

我还在链表这里,大概是因为想造出更好的轮子,我阅读了 java.util.LinkedList.java 源码以及 sgi_stlstl_slist.h

互联网是个好东西,把我的标题放进搜索框,一下子就有几十条优质的相关内容,可是我为什么还要写呢。这个问题大概可以一句诗作答“纸上得来终觉浅,绝知此事要躬行”。写博客这件事,虽然有很多很多人比我写得好,而且可能我也没有什么有创意的自己的想法,但是只是阅读人家的文字,自己不去动手的话,那么将永远停留在“浅”的层次。

总之,“1”好过没有,即使它只是一个起点。

好了,不废话了。这篇博客基于博主lijun5635的专栏STL List::sort() 解析 ,感谢博主精彩的分析,带给我很多启示。


1、slist::sort() 源码

我研究的 slist 数据结构其实是 Singly Linked List , 而 stllist 实际上是一个 Doubly Linked List,但是我在寻找 slist::sort() 的解释时,意外发现 list::sort() 基本是相同的。下面是 slist::sort() 的源码。

template <class _Tp, class _Alloc>
void slist<_Tp,_Alloc>::sort()
{
  if (this->_M_head._M_next && this->_M_head._M_next->_M_next) {
    slist __carry;
    slist __counter[64];
    int __fill = 0;
    while (!empty()) {
      __slist_splice_after(&__carry._M_head,
                           &this->_M_head, this->_M_head._M_next);
      int __i = 0;
      while (__i < __fill && !__counter[__i].empty()) {
        __counter[__i].merge(__carry);
        __carry.swap(__counter[__i]);
        ++__i;
      }
      __carry.swap(__counter[__i]);
      if (__i == __fill)
        ++__fill;
    }

    for (int __i = 1; __i < __fill; ++__i)
      __counter[__i].merge(__counter[__i-1]);
    this->swap(__counter[__fill-1]);
  }
}

2、 用list 相关函数模拟 slist::sort()

看这类数据结构的算法时,我通常的做法是在纸上演算整个过程,这个通常都很有效,但是比较繁琐,也容易犯错。

在阅读 java.util.LinkedList.java 源码时,我发现一些我觉得很复杂的方法,源码却不是很长,因为基本操作封装得合理,复杂的方法只需要在较低层的方法上,进行严谨的逻辑判断和调用即可。

而在阅读 slist::sort() 时,我刚开始是没有意识到可以自己调用底层方法来实现与之相同的代码的。原因有两点:

  • slist 不是 stl 的标准里的,想要调用其方法的话,也许要自己造完整的轮子。

  • 我以前没有试过。

但是看了上面提到的博主的文章后,我受到了启示,并且因为 list 可以说是 slist 的超集,模拟当然是可以的。下面是我的模拟的代码:

  list<int> carry;
  list<int> counter[64];
  int fill = 0;

  while (!_MT_list.empty()) {
    carry.splice(carry.begin(), _MT_list, _MT_list.begin());

    int i = 0;
    while (i < fill && !counter[i].empty()) {
      counter[i].merge(carry); // after the merger, carray is empty.
      carry.swap(counter[i++]);
    }

    carry.swap(counter[i]);

    if (i == fill)
      ++fill;
  }

  for (int i = 1; i < fill; ++i) 
    counter[i].merge(counter[i - 1]);

测试数据

{7, 11, 2, 1, 8, 20, 5, 99, 33}

插入输出语句后,得到运行过程:
simulate_sort.png

3、运行结果分析

(1)先取第一个元素:7 ,把它存入 counter[0]中;

(2)然后取第二个元素:11 , 将它与 7 合并后,存入了 counter[1] 中,此时 counter[0] 为空, 现在为止已经取了 2 个元素:{7, 11},而 fill = 1;

(3)取第三个元素: 2,把它存入 counter[0] 中,此时 fill = 2;

(4)取第四个元素:1,把它与 counter[0] 合并得到 {1, 2},然后再与 counter[1] 合并得到 counter[2] = {1, 2, 7, 11},然后 counter[0] 和 counter[1]由于合并操作的缘故全部变成空,此时 fill = 2,整个 counter 中含有的元素个数是4;

(5)取第五个元素,counter[0] 中存入 8,开始与之前的过程相似。此时 fill = 3,counter 中含有的元素个数为5,而 22<5<23

(6)取第六个元素之后,仍然 22<6<23

(7)取第七个元素后, 20+21+22=7

(8)取第八个元素后,由于 23=8 ,所以这次的合并成功将所有已经取到的元素合并到有序,且没有遗漏 counter[3] 之前的 counter[i] 存储的元素,因为它们都为空了。

(9)去第九个元素后, 20+23=9 ,已经取完了所有元素,然后从 1 到 fill - 1 依次合并相邻的 counter[i],得到最终结果。

来张表总结一下:


取第 i 个元素i 的值与2的幂的关系counter所含总元素的表示fill
11 = 20 counter[0]0
22 = 21 counter[1]1
33 = 20+21 counter[0]+ counter[1]2
44 = 22 counter[2].size()2
55 = 20+22 counter[0] + counter[2]3
66 = 21+22 counter[1] + counter[2]3
77 = 20+21+22 counter[0] + counter[1] + counter[2]3
88 = 23 counter[3]3
99 = 20+23 counter[0] + counter[3]4

可以看到不同 fill 代表了不同的层级, 假设 fill = level,则能表示的层级为

[2level1,2level]

当取得的元素个数从 2level1 逐渐增加到 2level 时,便完成了一次从低层向高层的“进位”—— 所有低层的元素全部合并到高层,低层被全部清空。

如果所有元素的个数 size() 刚好等于 2 的某个幂,那么 counter[fill - 1] 就是 sort 的最终结果,但是如果不是那么巧合,那么需要从低层向高层合并得到最终结果。

4、代码分析

从之前的分析可以看到这是一个归并排序:1 个元素加 1 个元素合并成 2 个元素; 2 个元素 加 2 个元素合并成 4 个元素;4 个元素加 4 个元素合并成 8 个元素……以此类推。

然而不是每一个元素都能经历从最底层到最高层的完整合并过程,因为元素个数不一定是 2 的幂,所以需要最后的 for 循环,将经过完整合并过程以及没有经历完整合并过程的元素进行最后的合并,以求得最终结果。

下面是注释形式的更加具体的代码分析。

template <class _Tp, class _Alloc>
void slist<_Tp,_Alloc>::sort()
{
  // 如果 this->size() < 2,返回。
  if (this->_M_head._M_next && this->_M_head._M_next->_M_next) {
    // carry,命名的含义大概是作为传输的媒介。
    slist __carry; 
    // counter,计数器,这里大概是广义的寄存器含义。
    slist __counter[64];
    // fill,“填满”,fill 指示 counter 中下一个将被填充的层次。
    int __fill = 0;

    // bool empty() const { return this->_M_head._M_next == 0; }
    while (!empty()) { 
      // 取链表中第一个元素,取出后,链表的第一个元素将变成原来首元素的下一个。
      __slist_splice_after(&__carry._M_head,
                           &this->_M_head, this->_M_head._M_next);

      int __i = 0;
      // 如果 __counter[__i] 为空,那么它是可以存放元素的层次。
      while (__i < __fill && !__counter[__i].empty()) {
        __counter[__i].merge(__carry); // merge 后, __carry 为空
        __carry.swap(__counter[__i]); // swap 后,__counter[__i] 为空
        ++__i;
      }
      // 将合并保存到被合并底层的高一层。
      __carry.swap(__counter[__i]); 

      // __i == __fill,表示第fill层以下已经全部合并,并被清空
      // __fill 需要增加,以进入下一个元素存储范围,需要合并的层次增加了一层
      if (__i == __fill)
        ++__fill;
    }

    // 如果 __counter[__fill-1] 没有包含 *this 的所有元素的话,
    // 说明元素个数在 (2^(fill-1), 2^fill) 之间,有部分元素滞留在底层
    // 需要进行最终的合并。
    for (int __i = 1; __i < __fill; ++__i)
      __counter[__i].merge(__counter[__i-1]);
    this->swap(__counter[__fill-1]);
  }
}
<think>我们正在讨论SGI STL中multimap的实现及其使用方式。SGI STLSTL的一个早期实现,由Silicon Graphics公司开发,它对C++标准库的发展有重要影响。multimap是关联容器的一种,允许重复的键值,每个键可以关联多个值。 ### 1. SGI STL中multimap的实现 在SGI STL中,`multimap`通常基于红黑树(一种自平衡二叉查找树)实现。红黑树保证了元素的有序性,并且插入、删除和查找操作的时间复杂度均为$O(\log n)$。 #### 关键实现点: - **内部结构**:`multimap`由红黑树节点构成,每个节点存储一个键值对(`pair<const Key, T>`)。 - **允许重复键**:与`map`不同,`multimap`允许键重复,因此插入时不需要检查键的唯一性。 - **排序**:元素按照键的升序(默认)排列,也可通过自定义比较函数改变排序方式。 #### 核心代码结构(简化): ```cpp template <class Key, class T, class Compare = less<Key>, class Alloc = alloc> class multimap { public: typedef Key key_type; typedef T data_type; typedef pair<const Key, T> value_type; typedef Compare key_compare; // 内部使用红黑树作为底层结构 typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type; rep_type t; // 红黑树对象 // 迭代器 typedef typename rep_type::iterator iterator; // 插入操作(允许重复) iterator insert(const value_type& x) { return t.insert_equal(x); } // 查找操作 iterator find(const key_type& k) { return t.find(k); } // 其他成员函数:erase, size, begin, end, etc. }; ``` 在SGI STL中,`rb_tree`是实现`multimap`和`map`的基础。`multimap`使用`insert_equal`来插入允许重复的键,而`map`使用`insert_unique`。 ### 2. multimap的使用方式 以下是一些基本的使用示例: #### 示例1:插入和遍历 ```cpp #include <map> #include <iostream> int main() { std::multimap<int, std::string> mmap; // 插入元素 mmap.insert(std::make_pair(1, "apple")); mmap.insert(std::make_pair(2, "banana")); mmap.insert(std::make_pair(1, "apricot")); // 允许重复键 // 遍历所有元素 for (auto it = mmap.begin(); it != mmap.end(); ++it) { std::cout << it->first << ": " << it->second << std::endl; } return 0; } ``` 输出: ``` 1: apple 1: apricot 2: banana ``` #### 示例2:查找特定键的所有值 ```cpp // 查找键为1的所有值 auto range = mmap.equal_range(1); for (auto it = range.first; it != range.second; ++it) { std::cout << it->second << std::endl; } ``` 输出: ``` apple apricot ``` #### 示例3:删除元素 ```cpp // 删除键为1的所有元素 mmap.erase(1); ``` ### 3. 性能与注意事项 - **性能**:插入、删除和查找操作的时间复杂度为$O(\log n)$。 - **内存**:由于基于红黑树,每个节点需要额外的空间存储颜色和指针。 - **迭代顺序**:元素按键的升序遍历(默认)。 ### 相关问题 1. SGI STL中`multimap`与`map`的实现有何异同? 2. 如何在`multimap`中实现自定义排序? 3. `multimap`与`unordered_multimap`在性能和应用场景上有何区别? 4. 如何高效地删除`multimap`中满足特定条件的元素? 以上内容基于SGI STL的实现原理和C++标准库的通用实践。由于SGI STL是早期实现,现代C++标准库可能有细节调整,但核心思想一致[^1][^2][^3]。</think>### SGI STL中multimap的实现及使用方式 #### 1. **底层实现原理** SGI STL的`multimap`基于**红黑树(Red-Black Tree)** 实现,这是一种自平衡二叉搜索树。关键特性包括: - **节点结构**:每个节点存储键值对 `pair<const Key, T>`,键(Key)不可修改 - **允许重复键**:与`map`不同,`multimap`允许插入相同键的多个值 - **自动排序**:元素按键的升序排列(可通过自定义比较函数修改) - **时间复杂度**: - 插入/删除/查找:$O(\log n)$ - 遍历:$O(n)$ 简化代码结构: ```cpp template <class Key, class T, class Compare = less<Key>> class multimap { private: typedef rb_tree<Key, pair<const Key, T>, Compare> RepType; RepType t; // 红黑树核心容器 public: iterator insert(const value_type& x) { return t.insert_equal(x); // 关键:允许重复插入 } // ... 其他成员函数 }; ``` #### 2. **核心操作示例** ##### (1) 基础使用 ```cpp #include <map> using namespace std; multimap<string, int> scoreMap; // 插入重复键 scoreMap.insert({"Alice", 90}); scoreMap.insert({"Alice", 85}); // 允许相同键 // 范围插入 scoreMap.insert({{"Bob", 78}, {"Bob", 82}}); ``` ##### (2) 元素访问 ```cpp // 遍历所有元素(按键升序) for (auto it = scoreMap.begin(); it != scoreMap.end(); ++it) { cout << it->first << ": " << it->second << endl; } // 查找特定键的所有值 auto range = scoreMap.equal_range("Alice"); for (auto it = range.first; it != range.second; ++it) { cout << "Alice's score: " << it->second << endl; } ``` ##### (3) 删除操作 ```cpp // 删除特定键的所有实例 size_t count = scoreMap.erase("Bob"); // 返回删除数量 // 删除单个元素 auto pos = scoreMap.find("Alice"); if (pos != scoreMap.end()) { scoreMap.erase(pos); } ``` #### 3. **高级特性** - **自定义排序**: ```cpp struct CaseInsensitiveCompare { bool operator()(const string& a, const string& b) const { return lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(), [](char c1, char c2) { return tolower(c1) < tolower(c2); }); } }; multimap<string, int, CaseInsensitiveCompare> customMap; ``` - **内存管理**: SGI STL通过**分配器(Allocator)** 控制内存,避免直接使用`new/delete`,符合游戏开发等场景的严格内存控制要求[^3]。 #### 4. **性能优化建议** 1. **批量操作**:使用`insert(InputIter first, InputIter last)`提升插入效率 2. **迭代器稳定性**:删除元素时不影响其他迭代器(除被删除元素的迭代器) 3. **键类型选择**:优先使用轻量级键类型(如`int`而非`string`)减少比较开销 #### 5. 典型应用场景 - 电话簿(同名多号码) - 股票交易记录(同股票代码多笔交易) - 多值索引数据库查询 --- ### 相关问题 1. SGI STL中`multimap`与`map`的底层实现有何本质区别? 2. 如何在`multimap`中高效删除满足特定条件的元素? 3. 红黑树如何保证$O(\log n)$的操作复杂度?其平衡规则是什么? 4. 对比`multimap`与`unordered_multimap`在不同场景下的性能差异。 [^1]: SGI STL实现参考了红黑树等经典数据结构设计 [^2]: 容器扩展特性源于实际开发需求,非理论推测 [^3]: 内存分配器设计满足游戏等高性能场景要求
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值