从零开始写STL-容器-list

  • List 是STL 中的链表容器,今天我们将通过阅读和实现list源码来解决一下问题:
  • List内部的内存结构是如何实现的?
  • 为什么List的插入复杂度为O(1)?
  • 为什么List的size()函数复杂度为O(n)?

list 容器的幕后英雄 - list 结点

作为一个链表首先要维护数据(模板元素的实例内容),指向前一个节点的指针 和 指向后一个节点的指针。List 的 结点作为list 容器中 元素内容 和 容器组织逻辑的一个中间层。

  • 为什么不能搞成单向链表?

    如果单向链表的话,请您试想一下要删除迭代器pos指向的元素应该怎么操作?我们必须知道前驱后继才能正确进行删除
for(auto it = begin(); it != end(); it++)
{
if(it->next == pos)
{
//...逻辑代码
}
}

这样的单向链表每次寻找一个节点的前驱 后继 都要经过最坏复杂度为O(n)的查询,所有应该实现为双向链表。

list 容器的 迭代器

  • 维护一个指向list_node 的指针
	template<class T>
struct list_iterator : public bidirectional_iterator<T>
{}//继承bidirectional_iterator类 便于类型萃取 和 特定算法应用

list 容器的 逻辑结构

  • 环状链表

    node 表示尾部的一个空白结点,其next 指向list的头节点, pre指向list的尾节点

为什么这样设计?

首先list作为双向链表需要提供向前和向后的迭代能力,这样设计可以在O1的时间获得首尾元素,而且可以避免链表为空时的边界检查(只需要看一下node->pre == node,则为空)

		node_ptr get_node()
{
return data_allocator.allocate(1);
}
node_ptr new_node(const T& x)
{
node_ptr p = get_node();
construct(&p->data, x);
p->next = p->pre = nullptr;
return p;
}

构造函数

		list()
{
//注意 typedef std::allocator<list_node<T>> Data_allocator;
node.ptr = data_allocator.allocate(1);
node.ptr->next = node.ptr->pre = node.ptr;
}
list(const self& rhs) :list(rhs.begin(), rhs.end())
{ }
list(std::initializer_list<T> li):list(li.begin(),li.end())
{ }
template<class c>
list(const c l,const c r)
{
node.ptr = data_allocator.allocate(1);
node.ptr->next = node.ptr->pre = node.ptr;
for (auto it = l; it != r; it++)
{
push_back((*it));//逐个插入到list的末端
}
}
list(size_type n, const T& val)
{
node.ptr = data_allocator.allocate(1);//初始化node
node.ptr->next = node.ptr->pre = node.ptr;
while (n--)
push_back(val);
}

析构函数

遍历列表 销毁数据实例

		~list()
{
if (!empty())
{
for (iterator it = begin(); it != end(); )
{
data_allocator.destroy(&it.ptr->data);
it++;
}
}
}

Modify

  • 注意指针操作的先后顺序
		void push_front(const T& x)
{
node_ptr p = new_node(x);
node.ptr->next->pre = p;
p->next = node.ptr->next;
node.ptr->next = p;
p->pre = node.ptr;
}
void push_back(const T& x)
{
node_ptr p = new_node(x);
node.ptr->pre->next = p;
p->pre = node.ptr->pre;
p->next = node.ptr;
node.ptr->pre = p;
}
void pop_front()
{
node_ptr tmp = node.ptr->next;
node.ptr->next = node.ptr->next->next;
node.ptr->next->pre = node.ptr;
data_allocator.deallocate(tmp,sizeof(list_node));
}
void pop_back()
{
node_ptr tmp = node.ptr->pre;
node.ptr->pre = tmp->pre;
tmp->pre->next = node.ptr;
data_allocator.deallocate(tmp, sizeof(list_node));
}
iterator erase(iterator pos)
{
pos.ptr->pre->next = pos.ptr->next;
pos.ptr->next->pre = pos.ptr->pre;// 前驱 后继 结点的指针操作
node_ptr tmp = pos.ptr->next;
destroy(&pos.ptr->data);//销毁
data_allocator.deallocate(pos.ptr,sizeof(list_node));// 回收内存
return iterator(tmp);
}
iterator erase(iterator first, iterator last)
{
first.ptr->pre->next = last.ptr;
last.ptr->pre = first.ptr->pre;
for (auto it = first; it != last; it++)
destroy(&it.ptr->data);
return first;
}
//The list container is extended by inserting new elements before the element at position.
iterator insert(iterator pos, const T& x)
{
node_ptr p = new_node(x);
pos.ptr->pre->next = p;
p->pre = pos.ptr->pre;
p->next = pos.ptr;
pos.ptr->pre = p;
return pos;
} void insert(iterator pos, size_type sz, const T& x)
{
while (sz--)
insert(pos, x);
}

Splice 函数

		/* 将 first到last的元素移动到 pos 之前 */
void transfer(iterator pos, iterator first, iterator last)
{
if (pos != last)
{
last.ptr->pre->next = pos.ptr;
first.ptr->pre->next = last.ptr;
pos.ptr->pre->next = first.ptr;
auto tmp = pos.ptr->pre;
pos.ptr->pre = last.ptr->pre;
last.ptr->pre = first.ptr->pre;
first.ptr->pre = tmp;
}
}
void splice(iterator pos, list<T>& rhs)
{
if (*this != rhs)
transfer(pos, rhs.begin(), rhs.end());
}
void splice(iterator pos, list<T>& rhs, iterator first, iterator last)
{
if (*this != rhs)
{
transfer(pos, first, last);
}
}
void splice(iterator pos, list<T>& rhs, iterator it)
{
it.ptr->pre->next = it.ptr->next;
it.ptr->next->pre = it.ptr->pre;
pos.ptr->pre->next = it.ptr;
it.ptr->pre = pos.ptr->pre;
pos.ptr->pre = it.ptr;
it.ptr->next = pos.ptr;
}

merge 函数

注意链表的merge函数 和算法库中含义有所不同,在这里会将参数链表全部合并进来(也就是说在调用完这个函数之后,other 参数应该是空)

		void merge(list<T> &other)
{
auto p1 = begin(), p2 = other.begin();
while (p1 != end() && p2 != other.end())
{
if (*p1 < *p2)
p1++;
else if (*p1 >= *p2)
{
auto tmp = p2;// 注意保存迭代器下一个位置,如果不保存,直接再调用splice之后p2++会怎样?首先p2!=other.end()这个条件永远不会触发,因为p2已经到了this的链表中
tmp++;
splice(p1, other, p2);
p2 = tmp;
}
}
if (!other.empty()) {
splice(end(), other);
}
}

list 归并排序的非递归形式(难点)

非递归形式的 归并排序,因为List并不支持随机存取迭代器,sort不能用于排序list。

归并的思路如下:先建立64个桶,然后从前向后遍历,每次从要排序的list头取出一个元素插入桶中,第一个桶要存的元素最多为1个,第二个桶要存的最多为2个,第K个桶最多为2^K个元素。在插入元素的时候从前向后插入,如果到达当前桶的上限就向后归并。

具体的算法实现极为巧妙!

个人感觉参考了位运算的进位性质

		void sort()
{
if (size() <= 1)
{
return;
}
self carry;
self counter[64];
int fill = 0;
while (!empty())
{
carry.splice(carry.begin(), *this, begin());
int i = 0;
while (i < fill && !counter[i].empty())
{
counter[i].merge(carry);
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]);
}
swap(counter[fill - 1]);
}

从零开始写STL-容器-list的更多相关文章

  1. 从零开始写STL—容器—vector

    从0开始写STL-容器-vector vector又称为动态数组,那么动态体现在哪里?vector和一般的数组又有什么区别?vector中各个函数的实现原理是怎样的,我们怎样使用会更高效? 以上内容我 ...

  2. 从零开始写STL - 智能指针

    从零开始写STL - 智能指针 智能指针的分类及其特点: scoped_ptr:初始化获得资源控制权,在作用域结束释放资源 shared_ptr: 引用计数来控制共享资源,最后一个资源的引用被释放的时 ...

  3. 从零开始写STL—栈和队列

    从零开始写STL-栈和队列 适配器模式 意图:将一个类的接口转换成客户希望的另外一个接口.适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作. 主要解决:主要解决在软件系统中,常常要将 ...

  4. 从零开始写STL—模板元编程之any

    any class any; (since C++17) The class any describes a type-safe container for single values of any ...

  5. 从零开始写STL—functional

    function C++11 将任意类型的可调用(Callable)对象与函数调用的特征封装到一起. 这里的类是对函数策略的封装,将函数的性质抽象成组件,便于和algorithm库配合使用 基本运算符 ...

  6. 从零开始写STL—哈希表

    static const int _stl_num_primes = 28; template<typename T, typename Hash = xhash<T>> cl ...

  7. 从零开始写STL—模板元编程之tuple

    tuple Class template std::tuple is a fixed-size collection of heterogeneous values. It is a generali ...

  8. 从零开始写STL—set/map

    这一部分只要把搜索树中暴露的接口封装一下,做一些改动. set源码剖析 template<typename T> class set { public: typedef T key_typ ...

  9. STL容器

    啦啦啦,今天听啦高年级学长讲的STL容器啦,发现有好多东西还是有必要记载的,毕竟学长是身经百战的,他在参加各种比赛的时候积累的经验可不是一天两天就能学来的,那个可是炒鸡有价值的啊,啊啊啊啊啊 #inc ...

随机推荐

  1. 程序猿工具——svn

    一个项目的产生,都需要团队中的开发人员互相协作.它的简单,方便深深吸引了我. svn的使用,有2部分组成--svn服务器.svn客户端.svn服务器一般团队之间只要有一个安装就可以了. 在学习安装sv ...

  2. 外文翻译 《How we decide》赛场上的四分卫

    本书导言翻译 为了能看懂这一章,先做了如下的功课: 百度百科 四分卫 国家橄榄球联盟中文站 在2002年超级碗赛场上,比赛的时间仅剩80秒,两队比分持平.新英格兰爱国者队于17码的位置执球,他们的对手 ...

  3. String的用法——判断功能

    package cn.itcast_03; /* String的判断功能: 1.boolean equals(Object obj):字符串的内容是否相同,区分大小写 2.boolean equals ...

  4. Nginx反向代理node,实现让静态文件在同一域

    Nginx反向代理node,实现让静态文件在同一域 原文https://github.com/zhuangZhou/Blog/issues/4 不管是Vue还是React,还是传统的网站,与node服 ...

  5. 微信小程序组件解读和分析:九、form表单

    form表单组件说明: 表单,将组件内的用户输入的<switch/> <input/> <checkbox/> <slider/> <radio/ ...

  6. 一致性hash学习

    一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似.一致性哈希修正了CARP使用的简 单哈 ...

  7. iOS循环引用

    iOS循环引用 当前类的闭包/Block属性,用到了当前类,就会造成循环引用 此闭包/Block应该是当前类的属性,我们经常对Block进行copy,copy到堆中,以便后用. 单方向引用是不会产生循 ...

  8. Python界面编程之六----布局

    布局(转载于–学点编程吧)通过实践可知采用了布局之后能够让我们的程序在使用上更加美观,不会随着窗体的大小发生改变而改变,符合我们的使用习惯. 绝对位置程序员以像素为单位指定每个小部件的位置和大小. 当 ...

  9. Mysql基本操作、C++Mysql简单应用、PythonMysql简单应用

    MySql基本操作 -- 当指定名称的数据库不存在时创建它并且指定使用的字符集和排序方式 CREATE DATABASE IF NOT EXISTS db_name CHARACTER SET UTF ...

  10. new Buffer 生成二进制数据

    node编辑环境下: > new Buffer("admin")<Buffer 61 64 6d 69 6e> 通过post请求,服务端接收到是流数据,必须把流数 ...