从零开始写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. 在reset css后两个input之间还是出现默认间隔的问题。

    <div class="search_box fl"> <input type="text" class="search_text& ...

  2. 学JAVA第二十三天,List类型和Set类型

    数组,是我们最常用的,但是有时候,我们要用数组,但是又不知道数组的类的长度的时候, 我们java就有一个很好用的工具Collection,这都是java的爸爸的用心良苦,Collection中包含Li ...

  3. LN : leetcode 123 Best Time to Buy and Sell Stock III

    lc 123 Best Time to Buy and Sell Stock III 123 Best Time to Buy and Sell Stock III Say you have an a ...

  4. 将call/apply方法应用于其他对象上的几种方法

    在处理类数组中,发现了两种将数组方法应用于类数组的方法,现将call/apply的常用方式总结一下. 一.当做函数调用 function print_vars(var1,var2){ console. ...

  5. spring.net应用

    经过一段时间的调试,终于把spring.net中关于aop的方面给做个了一个比较完整的Demo.包含异常日志和性能日志.spring.net和log4net配置. http://files.cnblo ...

  6. iOS---设置控件的内容模式

    容易混淆的内容摆放属性: 1. textAligment : 文字的水平方向的对齐方式 取值 NSTextAlignmentLeft = 0, // 左对齐 NSTextAlignmentCenter ...

  7. SQL 索引自动维护计划脚本

    脚本功能: 1,查询数据库中,碎片率在5%以上(官方推荐),有一定数据里的表的索引. 2.如果碎片率在5%<碎片率<=30%  执行重新组织索引.如果在30%以上,执行重建索引 建议在执行 ...

  8. Farseer.net轻量级开源框架 中级篇:UrlRewriter 地址重写

    导航 目   录:Farseer.net轻量级开源框架 目录 上一篇:Farseer.net轻量级开源框架 中级篇: Cookies.Session.Request 下一篇:Farseer.net轻量 ...

  9. PHP正则表达式考察点

    正则表达式的作用 分隔.查找.匹配.替换字符串 正则表达式的组成部分 分隔符 "/" . "#" . "~" 通用原子 \d : 十进制的0 ...

  10. 程序员容易忽略的SQL Server错误集锦

    1.大小写 大写T-SQL 语言的所有关键字都使用大写,规范要求. 2.使用“;” 使用“;”作为 Transact-SQL 语句终止符.虽然分号不是必需的,但使用它是一种好的习惯,对于合并操作MER ...