上一章我们讲解了list_node
和 list_iterator
,这一章我们主要来讲讲list
本身的实现。
废话不说,直接看代码
1、 构造函数和成员变量
class list
{
public:
list();
private:
list_node* head_;
list_node* tail_;
};
list::list()
{
head_ = new list_node;
tail_ = new list_node;
head_->next = tail_;
head_->prev = nullptr;
tail_->prev = head_;
tail_->next = nullptr;
}
首先我们在list内部封装了两个list_node的指针,作为前哨兵和尾哨兵,并且在构造的时候,给这两个指针申请内存并互相指向。
两个哨兵模式的某个list实例的内存模型可以用下图表示
在STL中都是用的一个哨兵的模式,感兴趣的同学可以自行画图表示一下,它实际上是一个环状的链式结构。我们这里用两个哨兵的目的是为了让语义更清楚而已。
2、list是否为空
从上面的图很容易看出来,当head_->next
指针直接指向tail_
指针时,该list就为空
bool list::empty() const
{
return head_->next == tail_;
}
3、begin 和 end
同样从上图看,很显然我们可以写出
list_iterator list::begin() { return list_iterator(head_->next); }
list_iterator list::end() { return list_iterator(tail_); }
4、 list的大小
有了begin()
和 end()
迭代器,很容易就可以得到list的大小了。
int list::size() const
{
int size_ = 0;
distance(begin(), end(), size_);
return size_;
}
void list::distance(list_iterator first,list_iterator last,int& result) const
{
for (auto it = first; it != last; ++it)
++result;
}
如果您看过《Effective STL》,那您应该记得有一款条目是说,永远不要用if(size() == 0)而是empty()
来判断一个容器是否为空。从这您就可以端倪来了,是的,对于部分容器来说,调用一次size()
的时间复杂度为O(n),而调用一次empty()
的时间复杂度为O(1),孰优孰劣可想而知了。
条款 4 : 用empty来代替检查size是否为0 (Effective STL 中文版 25页)
5、front 和 back
与上面类似,通过begin 和 end 很快就可以得到 front 和 back的值
int& list::front() const { return *begin();}
int& list::back() const { return *(--end());}
6、插入和添加元素
6.1 插入元素
在添加元素之前,我们先来考虑怎么插入一个元素。首先,在某个节点处插入一个元素,我们还需要考虑是作为前驱(as prev)还是作为后继(as next)插入。通过查看gnu gcc 提供的源代码,STL中是作为前驱来插入的。
在写代码之前,我们可以通过画图等辅助方式来帮助我们分析,毕竟有图才有真相么…
据图可以写出如下代码
private:
list_iterator list::insertAsPerv(list_iterator position,const int &task)
{
list_iterator tmp(new list_node);
*tmp = task;
position.node_->prev->next = tmp.node_;
tmp.node_->prev = position.node_->prev;
tmp.node_->next = position.node_;
position.node_->prev = tmp.node_;
return tmp;
}
public:
list_iterator list::insert(list_iterator position,const int &task)
{
return insertAsPerv(position,task);
}
6.2 在前部和后部添加元素
当插入一个元素时,我们是作为前驱插入的,所以对于push_back
和push_front
我们可以像如下编写
void list::push_back(const int &value){ insert(end(), value); }
void list::push_front(const int &value){ insert(begin(), value); }
7、查找某个元素 find
一般我们用find()
时,通常意味着是在一个无序的数据结构(容器)中查找,对于list中的查找,我们只能遍历所有元素才进行。
list_iterator list::find(list_iterator first, list_iterator last, const int &task)
{
list_iterator iter = first;
while (iter != last)
{
if (*iter == task)
return iter;
++iter;
}
return last;
}
8、测试
写了一些接口之后,我们来写个小demo测试一下我们的程序是否有错误。
#include "list.h"
#include <iostream>
#include <cstdlib>
int main()
{
std::cout<<"-------------- list test -----------------"<<std::endl<<std::endl;
yang::list mylist;
if(mylist.empty())
std::cout<<"this is the empty list !"<<std::endl<<std::endl;
for(int i = 0;i < 5; ++i)
mylist.push_back(i);
auto it = mylist.begin();
std::cout<<"elements are : ";
for(;it != mylist.end(); ++it)
std::cout<<*it<<std::ends;
std::cout<<std::endl<<std::endl;
mylist.push_front(5);
if(!mylist.empty())
std::cout<<"this is not the empty list !"<<std::endl<<std::endl;
std::cout<<"size is "<<mylist.size()<<std::endl<<std::endl;
std::cout<<"front is "<<mylist.front()<<std::endl<<std::endl;
std::cout<<"back is "<<mylist.back()<<std::endl<<std::endl;
it = mylist.begin();
++it;
mylist.insert(it,-99);
std::cout<<"elements are : ";
for(it = mylist.begin();it != mylist.end(); ++it)
std::cout<<*it<<std::ends;
std::cout<<std::endl<<std::endl;
if(mylist.find(mylist.begin(),mylist.end(),-99) != mylist.end())
std::cout<<"find the task -99 ! ..." <<std::endl<<std::endl;
if(mylist.find(mylist.begin(),mylist.end(),100) == mylist.end())
std::cout<<"can not find the task 100 ! ..." <<std::endl<<std::endl;
system("pause");
return 0;
}
// 以上代码均在 win7下,visual studio 2013 测试完成
测试结果如下:
从结果来看,我们写的list暂时还是没有错误的,可喜可贺:)无耻….
总结:本章主要讲了list底层的结构和一些最基本的接口函数,限于篇幅原因,还有一些接口的实现如sort,remove,erase等等我们留到下一章再讲解。
参考资料
STL 源码解析 侯捷著
数据结构(C++语言版)第三版 邓俊辉著
gcc 2.95 version source code