STL源码笔记(16)—单链表slist

STL单链表slist简介

概述

slist(Single linked list)顾名思义,是一个单向链表,这个容器并不在标准规格之内,在我几年的代码学习生涯中也是第一次听说,既然侯老师的书中提到了,那也还是学习一蛤。

slist与list的主要差别是,前者的迭代器属于单向的Forward Iterator(可读写),后者的迭代器属于双向的Bidirectional Iterator(可以双向读写)。看起来slist的功能应该会不如list,但由于其单向链表的实现,其消耗的空间更小,某些操作更快。

回忆数据结构中在单链表的某个位置插入元素的过程,slist的底层实现就是单链表,因此会遇到我们曾经遇到过的麻烦:在某个位置插入时,必须要用一个指针从头到尾找到待插入位置的前一个位置。这便是在slist的一个大的缺点之一,因此,书中提到,在非起点位置使用insert或erase的算法是不智之举。


slist源码实现

在SGI STL源码中,slist的实现位于stl_slist.h

节点设计

容器的核心就是其底层存储于迭代器设计了,对于节点设计,使用了继承的关系,实际上简单的来说就是单链表的节点:指向下一个节点的指针和数据

代码实现如下:

//stl_slist.h
//单向链表的节点结构
struct _Slist_node_base
{
  _Slist_node_base* _M_next;
};
//使用继承来实现单链表的节点结构:指针+数据
template <class _Tp>
struct _Slist_node : public _Slist_node_base
{
  _Tp _M_data;
};

基于单链表的特性和节点的结构,源码中提供了不少内部全局函数,这些函数不对外开放的,仅仅在某些对外使用的接口实现中直接调用,例如:

//全局函数:单链表节点数,其实就是简单的遍历计数
inline size_t __slist_size(_Slist_node_base* __node)
{
  size_t __result = 0;
  for ( ; __node != 0; __node = __node->_M_next)
    ++__result;
  return __result;
}
//全局函数:已知某一节点,插入新节点于其后
//返回插入节点之后的指针。
inline _Slist_node_base*
__slist_make_link(_Slist_node_base* __prev_node,
                  _Slist_node_base* __new_node)
{
  __new_node->_M_next = __prev_node->_M_next;
  __prev_node->_M_next = __new_node;
  return __new_node;
}

迭代器设计

如上图所示,迭代器同样是使用了继承的方式:

//单向链表的迭代器基本结构
struct _Slist_iterator_base
{
  typedef size_t               size_type;
  typedef ptrdiff_t            difference_type;
  typedef forward_iterator_tag iterator_category;//单向的可读写迭代器

  _Slist_node_base* _M_node;//数据类型,这里父类只包含指针结构

  //构造函数:父类只包含了带参数的构造函数
  _Slist_iterator_base(_Slist_node_base* __x) : _M_node(__x) {}
  void _M_incr() { _M_node = _M_node->_M_next; }//指针向后移动一位

  bool operator==(const _Slist_iterator_base& __x) const {
    return _M_node == __x._M_node;//重载==指针是否相等
  }
  bool operator!=(const _Slist_iterator_base& __x) const {
    return _M_node != __x._M_node;//重载!=指针是否相等
  }
};

//继承关系
//单向链表的迭代器结构
template <class _Tp, class _Ref, class _Ptr>
struct _Slist_iterator : public _Slist_iterator_base
{
  typedef _Slist_iterator<_Tp, _Tp&, _Tp*>             iterator;//定义迭代器类型
  typedef _Slist_iterator<_Tp, const _Tp&, const _Tp*> const_iterator;
  typedef _Slist_iterator<_Tp, _Ref, _Ptr>             _Self;

  typedef _Tp              value_type;
  typedef _Ptr             pointer;
  typedef _Ref             reference;
  typedef _Slist_node<_Tp> _Node;//节点类型

  //构造函数
  //这里由于父类只包含了带参数的构造函数,因此子类只能显示的初始化父类的构造函数
  _Slist_iterator(_Node* __x) : _Slist_iterator_base(__x) {}
  _Slist_iterator() : _Slist_iterator_base(0) {}
  //拷贝构造函数
  _Slist_iterator(const iterator& __x) : _Slist_iterator_base(__x._M_node) {}

  //*访问符重载,返回元素的引用
  reference operator*() const { return ((_Node*) _M_node)->_M_data; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
//->访问符重载,返回元素的地址的引用
  pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */

//前置++重载
  _Self& operator++()
  {
    _M_incr();//直接调用父类函数指针向后移动一位
    return *this;
  }
  //后置++重载
  _Self operator++(int)
  {
    _Self __tmp = *this;
    _M_incr();//直接调用父类函数指针向后移动一位
    return __tmp;
  }
  //这里没有--的重载,因为Forward Iterator的特性不支持双向操作
};

slist的数据结构

有了迭代器设计和节点设计的基础,单链表的实现就非常之简单了。虽然算法实现很简单,但是由于用到了继承关系,设计上看起来就有些复杂了:

//stl_slist.h
//父类定义了空间构造器等
template <class _Tp, class _Alloc>
struct _Slist_base {
  typedef _Alloc allocator_type;
  allocator_type get_allocator() const { return allocator_type(); }

  //构造函数,初始化指针
  _Slist_base(const allocator_type&) { _M_head._M_next = 0; }
  ~_Slist_base() { _M_erase_after(&_M_head, 0); }

protected:
  typedef simple_alloc<_Slist_node<_Tp>, _Alloc> _Alloc_type;//空间构造器类型
  _Slist_node<_Tp>* _M_get_node() { return _Alloc_type::allocate(1); }//分配一个节点
  void _M_put_node(_Slist_node<_Tp>* __p) { _Alloc_type::deallocate(__p, 1); }//释放一个节点空间

  //删去指定元素的后一个位置的元素
  _Slist_node_base* _M_erase_after(_Slist_node_base* __pos)
  {
    _Slist_node<_Tp>* __next = (_Slist_node<_Tp>*) (__pos->_M_next);
    _Slist_node_base* __next_next = __next->_M_next;
    __pos->_M_next = __next_next;
    destroy(&__next->_M_data);//释放节点
    _M_put_node(__next);//释放空间
    return __next_next;
  }
  //删去区间内的所有元素
  _Slist_node_base* _M_erase_after(_Slist_node_base*, _Slist_node_base*);

protected:
  _Slist_node_base _M_head;//“头指针”,但事实上并不是指针
};  

#endif /* __STL_USE_STD_ALLOCATORS */

//根据代码来看,删除应该是前闭后开
template <class _Tp, class _Alloc>
_Slist_node_base*
_Slist_base<_Tp,_Alloc>::_M_erase_after(_Slist_node_base* __before_first,
                                        _Slist_node_base* __last_node) {
  _Slist_node<_Tp>* __cur = (_Slist_node<_Tp>*) (__before_first->_M_next);//记录区间的前一个位置
  while (__cur != __last_node) {
    _Slist_node<_Tp>* __tmp = __cur;
    __cur = (_Slist_node<_Tp>*) __cur->_M_next;
    destroy(&__tmp->_M_data);
    _M_put_node(__tmp);
  }
  __before_first->_M_next = __last_node;
  return __last_node;
}
//stl_slist.h
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class slist : private _Slist_base<_Tp,_Alloc>
{
private:
  typedef _Slist_base<_Tp,_Alloc> _Base;//父类类型定义
//...
//创建特定元素值构造节点(内部函数)
  _Node* _M_create_node(const value_type& __x) {
    _Node* __node = this->_M_get_node();
    __STL_TRY {
      construct(&__node->_M_data, __x);//直接构造
      __node->_M_next = 0;
    }
    __STL_UNWIND(this->_M_put_node(__node));
    return __node;//返回指针
  }
  //创建元素值为0的节点(内部函数)
  _Node* _M_create_node() {
    _Node* __node = this->_M_get_node();
    __STL_TRY {
      construct(&__node->_M_data);
      __node->_M_next = 0;
    }
    __STL_UNWIND(this->_M_put_node(__node));
    return __node;
  }
explicit slist(const allocator_type& __a = allocator_type()) : _Base(__a) {}//构造函数,指定空间配置器类型
//此外,还有许多用到其内部函数的构造函数,例如_M_insert_after_range等,这里就不一一列出。
};

除了上述简单介绍的构造和析构操作外,slist作为容器,它应该有一些容器统一的接口实现吧,根据STL的习惯,插入操作会将新元素插入于指定位置的前面,而非之后,作为一个单项链表,slist没有任何方便的办法可以回头定出前一个位置(没有prev指针),基于效率考虑,slist不提供push_back()只提供push_front()函数,这样插入顺序和元素次序就会相反。

//stl_slist.h
//首尾迭代器,包含头结点
  iterator begin() { return iterator((_Node*)this->_M_head._M_next); }
  const_iterator begin() const
    { return const_iterator((_Node*)this->_M_head._M_next);}

  iterator end() { return iterator(0); }
  const_iterator end() const { return const_iterator(0); }

  //调用内部函数求size大小
  size_type size() const { return __slist_size(this->_M_head._M_next); }

  //判断是否为空
  bool empty() const { return this->_M_head._M_next == 0; }

  //在头部插入元素
  void push_front(const value_type& __x)   {
    __slist_make_link(&this->_M_head, _M_create_node(__x));
  }
   //在头部删除元素
  void pop_front() {
    _Node* __node = (_Node*) this->_M_head._M_next;
    this->_M_head._M_next = __node->_M_next;
    destroy(&__node->_M_data);
    this->_M_put_node(__node);
  }
时间: 2024-10-29 19:07:26

STL源码笔记(16)—单链表slist的相关文章

STL源码笔记(15)—堆和优先级队列(二)

STL源码笔记(15)-堆和优先级队列 优先级队列的源码实现基于heap的操作,底层容器默认是vector. 优先级队列简介 优先级队列跟队列类似,一端插入一端删除,不同的是,优先级队列的元素入队后会根据其优先级进行调整,默认情况下优先级高的将优先出队,在SGI STL中,优先级队列的功能保证由heap实现:stl_heap.h中,heap的分析见:STL堆源码分析 优先级队列构造函数 默认情况下,优先级队列使用vector作为底层容器,使用less作为比较函数,其在源码中的定义声明如下: te

STL源码笔记(14)—堆和优先级队列(一)

STL源码笔记(14)-堆和优先级队列 priority_queue是拥有权值观念的queue,跟queue类似,其只能在一端push,一端pop,不同的是,每次push元素之后再容器内部元素将按照一定次序排列,使得pop得到的元素始终是当前权值的极大值. 很显然,满足这个条件就需要某些机制了,缺省情况下使用max-heap大顶堆来实现,联想堆排序的实现,使用大顶完成序列从小到大的排序,过程大概是: 把堆的根元素(堆中极大值)交换到最后 堆的长度减1 这样每次取出堆中的极大值完成排序,刚好与优先

STL源码笔记(12)—序列式容器之deque(二)

STL源码笔记(12)-序列式容器之deque(二) 再谈deque数据结构 我们知道deque是通过map管理很多个互相独立连续空间,由于对deque_iterator的特殊设计,使得在使用的时候就好像连续一样.有了deque_iterator的基础(例如重载的操作符等),对于我们实现容器的一些方法就十分方便了.与vector一样,deque也维护一个start,和finish两个迭代器,start指向容器中的一个元素,finish指向最后一个元素的后一个位置(前闭后开),从微观上讲,star

STL源码笔记(18)—平衡二叉树AVL(C++封装+模板)

AVLTree平衡二叉树 在几年前刚学数据结构时,AVL-Tree只是一个仅仅需要掌握其概念的东西,今非昔比,借看STL源码剖析的契机希望从代码层面将其拿下. 1.简介 二叉查找树给我们带来了很多方便,但是由于其在有序序列插入时就会退化成单链表(时间复杂度退化成 O(n)),AVL-tree就克服了上述困难.AVL-tree是一个"加上了平衡条件的"二叉搜索树,平衡条件确保整棵树的深度为O(log n). AVL树是最先发明的自平衡二叉查找树.在AVL树中任何节点的两个子树的高度最大差

STL源码笔记(17)—二叉排序树BST(C++封装)

二叉排序树BST STL中还有一类非常重要的容器,就是关联容器,比如map啊set啊等等,这些容器说实话,在应用层上还不能完全得心应手(比如几种容器效率的考虑等等),更别说源码了,因此这一部分打算稳扎稳打,好好做做笔记研究一番. 说到关联容器,我们想到了什么AVL树,红黑树等等,但大多时候我们仅仅局限于知道其名字,或者知道其概念,俗话说"talk is cheap,show me the code",因此,我打算从他们的祖爷爷二叉排序树开始下手.(其实,侯老师的书上也是这么安排的哈)

通读《STL源码剖析》之后的一点读书笔记

[QQ群: 189191838,对算法和C++感兴趣可以进来] 直接逼入正题. Standard Template Library简称STL.STL可分为容器(containers).迭代器(iterators).空间配置器(allocator).配接器(adaptors).算法(algorithms).仿函数(functors)六个部分. 迭代器和泛型编程的思想在这里几乎用到了极致.模板或者泛型编程其实就是算法实现时不指定具体类型,而由调用的时候指定类型,进行特化.在STL中,迭代器保证了ST

《STL源码剖析》---stl_alloc.h阅读笔记

这一节是讲空间的配置与释放,但不涉及对象的构造和析构,只是讲解对象构造前空前的申请以及对象析构后空间怎么释放. SGI版本的STL对空间的的申请和释放做了如下考虑: 1.向堆申请空间 2.考虑了多线程.但是这节目的只是讲解空间配置与释放,因此忽略了多线程,集中学习空间的申请和释放. 3.内存不足时的应变措施 4.考虑到了内存碎片的问题.多次申请释放小块内存可能会造成内存碎片. 在C++中,内存的申请和释放是通过operator new函数和operator delete函数,这两个函数相当于C语

《STL源码剖析》---stl_slist.h阅读笔记

slist(single linked list)是单向链表.它不是STL的标准,它与标准list的主要不同在于迭代器.slist的迭代器是Forward iterator,而list的迭代器是Bidirectional iterator,所以slist有着更多的限制.从另一方面看,slist消耗空间更小,一些操作更快.由于slist是单向的,所以在查找迭代器的前一个结点时比较麻烦,要从头开始找.也就是说slist在头结点插入和删除,在其他位置操作代价都比较大. G++ 2.91.57,cygn

《STL源码剖析》---stl_tree.h阅读笔记

STL中,关联式容器的内部结构是一颗平衡二叉树,以便获得良好的搜索效率.红黑树是平衡二叉树的一种,它不像AVL树那样要求绝对平衡,降低了对旋转的要求,但是其性能并没有下降很多,它的搜索.插入.删除都能以O(nlogn)时间完成.平衡可以在一次或者两次旋转解决,是"性价比"很高的平衡二叉树. RB-tree(red black tree)红黑树是平衡二叉树.它满足一下规则 (1)每个节点不是红色就是黑色. (2)根节点是黑色. (3)如果节点为红色,则其子节点比为黑色. (4)任何一个节