机械转码日记【21】list使用及list的模拟实现
创始人
2024-01-16 19:34:13

目录

前言

1.list的使用 

1.2sort和unique 

2.list的模拟 

2.1构造函数

2.2push_back()

2.3迭代器

2.3.1简洁版

2.3.2升级版(重要)

2.4insert和erase与迭代器失效

2.4.1list的迭代器失效

2.5析构函数

2.6深拷贝构造


前言

list是我们数据结构之中的链表,它允许在链表中的任何地方进行时间复杂度O(1)的插入和删除操作。今天我们就来学习一下list这个容器的使用与模拟实现。

1.list的使用 

list的使用和我们之前学的容器都差不多,要说有不同呢?它可以实现头插头删,尾插尾删

void test_list2(){list lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);for (auto e : lt){cout << e << " ";}cout << endl;lt.push_front(10);lt.push_front(20);lt.push_front(30);lt.push_front(40);for (auto e : lt){cout << e << " ";}cout << endl;lt.pop_back();lt.pop_back();lt.pop_front();lt.pop_front();for (auto e : lt){cout << e << " ";}cout << endl;}

1.2sort和unique 

algorithm里面的sort不支持链表,可以看到algorithm头文件里面sort的定义里用到了迭代器相减,但是链表的迭代器不支持相减,且sort为快速排序,是需要三数取中,需要随机访问,但是list的迭代器不支持随机访问。

所以list这个类模板里面增加了一个sort函数: 

再看到我们std里的sort,可以看到它的迭代器类型为randomaccessiterator,但是我们list构造函数里的迭代器类型为inputiterator,所以说迭代器是有分类的,

 从结构来分类们可以分为三类:

  1. 单向,如forward_list,它仅可以支持++。
  2. 双向,如list,map和set,它可以实现++和--。
  3. 随机,如string、vector、deque,它不仅可以实现++、--,也可以实现+和-,也就是说它可以实现随机访问。

在我们使用一些采用了迭代器的库函数的时候,帮助文件常常会提醒我们,是使用什么类型的迭代器,比如algorithm的reverse,它告诉我们这与传双向的迭代器才能用:

 其实传随机迭代器也是可以用的,因为随机迭代器的功能大于双向迭代器的功能。而上面所提到的list的构造函数的Inputiterator是三种迭代器都可以传过去的一种迭代器。

 下面我们就来看一看list::sort是如何使用的:

void test_list3(){list lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_front(10);lt.push_front(20);lt.push_front(30);lt.push_front(40);lt.push_back(1);lt.push_back(1);lt.push_back(1);for (auto e : lt){cout << e << " ";}cout << endl;lt.sort();for (auto e : lt){cout << e << " ";}cout << endl;}
}

tips:链表排序是很慢的,需要排序的数据不要放到链表里面

unique函数可以去重(要求先排序才能去重)

	void test_list3(){list lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_front(10);lt.push_front(20);lt.push_front(30);lt.push_front(40);lt.push_back(1);lt.push_back(1);lt.push_back(1);for (auto e : lt){cout << e << " ";}cout << endl;lt.sort();//有大量数据要排序不建议用list,list底层for (auto e : lt){cout << e << " ";}cout << endl;lt.unique();for (auto e : lt){cout << e << " ";}cout << endl;}
}

2.list的模拟 

从stl_list源码里我们可以看到list一个节点的结构:

因此我们也模仿一下,写出一个节点:

template
struct list_node
{list_node* _next;list_node* _prev;T _data;list_node(const T& val = T()):_next(nullptr), _prev(nullptr), _data(val){}
};

再看看它的迭代器位置,begin()和end()。 

不难猜出,这是一个带头双向循环的链表(因为begin是哨兵位节点,而begin是哨兵位的下一个位置), 那么其实它就是下面的这个的这个结构:

所以我们可以写出它的基本结构为以下,_head为哨兵位的头节点: 

templateclass list{typedef list_node Node;private:Node* _head;};

2.1构造函数

构造函数就是初始化我们的_head,给_head分配一块物理空间,将_head的头尾都指向自己就完成了初始化。

typedef list_node Node;
list()
{_head = new Node();_head->_next = _head;_head->_prev = _head;
}

2.2push_back()

push_back()就是尾插,即在尾部插入一个新节点,器基本逻辑为

  1. 找到尾巴
  2. 根据要尾插的值构造一个新节点
  3. 将新节点的头和尾与原链表连接起来
		void push_back(const T& x){//找尾Node* tail = _head->_prev;//构造一个新节点Node* newnode = new Node(x);//将新节点与其他节点连接起来//_head  tail  newnodetail->_next = newnode;newnode->_next = _head;_head->_prev = newnode;}

2.3迭代器

2.3.1简洁版

链表的迭代器不是原生指针,且物理空间不一定是连续的(++操作不一定是在下一个物理位置),所以我们可以采用一个自定义类型去进行封装,用运算符重载去支持++等行为:

    template struct __list_iterator{typedef list_node Node;Node* _node;__list_iterator(Node* node)//通过节点的指针就可以构造一个迭代器:_node(node){}T& operator*()//*it,解引用,返回data的引用{return _node->_data;}__list_iterator& operator++()//迭代器++,返回++之后的迭代器{_node = _node->_next;return *this;}bool operator!=(const __list_iterator& it){return _node != it._node;}};

 那么有了迭代器,我们就要实现end(),begin()等函数返回迭代器对应的位置:

		iterator begin(){//return iterator(_head->_next);return _head->_next;//也可以返回这个,因为单参数的构造支持隐式类型的转换}iterator end()//注意不是_head->_next,因为end指向的是最后一个有效数据的下一个位置{return _head;}

现在我们就可以实验以下我们刚刚写的东西了,可见是写的没有问题的:

2.3.2升级版(重要)

刚刚的迭代器还是着实有点简陋的,一些运算符重载还没有写出来,由于我们迭代器的使用是要像指针一样的,所以还需要支持以下的函数:

	templatestruct __list_iterator{typedef list_node Node;typedef __list_iterator self;Node* _node;__list_iterator(Node* node):_node(node){}T& operator*(){return _node->_data;}T* operator->(){ //return &(operator*());return &_node->_data;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const self& it){return _node != it._node;}bool operator==(const self& it){return _node == it._node;}};

注意到,我们上面新增加了一个self,就是迭代器类模板类型本身 ,这是为了省略写__list_iterator这么长一大串。

同时我们也看到上面迭代器类模板里面没有显式写出析构函数和拷贝构造。这是因为:

  1. 迭代器这个类模板,不需要析构函数,虽然这个类里面有Node*这个指针,但是这个指针并不属于迭代器,它不能把节点给释放了。
  2. 拷贝构造和赋值重载也不需要在迭代器类模板里面写出来,因为默认生成的浅拷贝就可以满足需求,因为我们在这里不需要深拷贝,我们操作的就是同一片空间。

但是我们看到stl_list里面迭代器的类模板有三个参数,这是为什么呢?我们能写成三个参数吗? 

首先我们先像一个问题,如果我们不使用三个参数,而是一个,我们如何去实现const迭代器呢?我认为大多数人是直接再写一个const_list_iterator这个类模板,然后把list_iterator类模板 的成员函数再移到里面:

	templatestruct const__list_iterator{typedef list_node Node;typedef __list_iterator self;Node* _node;__list_iterator(Node* node):_node(node){}const T& operator*(){return _node->_data;}const T* operator->(){ //return &(operator*());return &_node->_data;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}bool operator!=(const self& it){return _node != it._node;}bool operator==(const self& it){return _node == it._node;}};

但是以上的代码有个问题,就是程序的复用性很差,在软件工程里面是很在意程序复用性的,如果代码的复用性不好,那么可修改性就会很差。

所以为了增强程序的复用性,大佬们增加了两个模板参数Ref和Ptr:

	template struct __list_iterator{typedef list_node Node;typedef __list_iterator self;Node* _node;__list_iterator(Node* node):_node(node){}Ref operator*(){return _node->_data;}self& operator++(){_node = _node->_next;return *this;}self operator++(int){self tmp(*this);_node = _node->_next;return tmp;}self& operator--(){_node = _node->_prev;return *this;}self operator--(int){self tmp(*this);_node = _node->_prev;return tmp;}bool operator !=(const self& it){return _node != it._node;}bool operator ==(const self& it){return _node == it._node;}Ptr operator ->(){return &_node->_data;}};

然后在list类模板里面再定义迭代器模板传进去的参数,如果是const迭代器就传进去const T&和const T*,const迭代器的begin和end返回const迭代器就行了:

template class list{typedef list_node Node;public:typedef __list_iterator iterator;typedef __list_iterator const_iterator;const_iterator begin() const{return const_iterator(_head->_next);}const_iterator end() const{return const_iterator(_head);}} 

2.4insert和erase与迭代器失效

前一节vector的迭代器失效里面我们已经知道insert和erase要返回迭代器,那么list需要吗?我们先写一个什么都不返回的版本:

	    //在pos的前一个位置插入一个值	void insert(iterator pos, const T& x){Node* newNode = new Node(x);Node* cur = pos._node;Node* prev = cur->_prev;//prev newnode curprev->_next = newNode;newNode->_prev = prev;newNode->_next = cur;cur->_prev = newNode;}void erase(iterator pos){assert(pos != end());//不能把哨兵位删掉了Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;// prev  nextprev->_next = next;next->_prev = prev;delete cur;}

再依此去实现头插和尾插尾删:

		void push_front(const T& x){insert(begin(), x);}void pop_back(){erase(--end());}void pop_front(){erase(begin());}

2.4.1list的迭代器失效

我们先用上节博客检验vector的insert的迭代器失效的方法去检验list的insert是否存在迭代器失效的问题:

	void test_list4(){list lt;lt.push_back(1);lt.push_back(2);lt.push_back(2);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);lt.push_back(6);//要求在偶数的前面插入这个偶数*10auto it1 = lt.begin();while (it1 != lt.end()){if (*it1 % 2 == 0){lt.insert(it1, *it1 * 10);}it1++;}for (auto e : lt){cout << e << " ";}cout << endl;}

可以看到程序运行正常:

因此使用list的insert函数时不会出现迭代器失效的问题,因为list不会像vector一样发生扩容,而且它底层也是碎片化的,一个迭代器指向的就是一个节点,是不会存在野指针和意义变了的问题。但是为了和stl的其它类模板的迭代器适配呢,库里面的insert还是返回了迭代器的值,他是返回了新插入的那个元素的迭代器:

 那么使用erase时,它的迭代器会不会失效呢? 

void test_list5(){list lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);lt.push_back(6);auto it1 = lt.begin();while (it1 != lt.end()){if (*it1 % 2 == 0){lt.erase(it1);}else{++it1;}}for (auto e : lt){cout << e << " ";}cout << endl;}

可以看到程序运行出现错误,说明erase存在迭代器失效的问题。

原因在哪呢?

其实很简单,在erase被调用之后,it1已经被释放了,变成了野指针,对it1这个野指针进行++,程序就崩溃了,所以如何解决呢?只需要及时更新erase之后的迭代器:

		iterator erase(iterator pos){assert(pos != end());Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;// prev  nextprev->_next = next;next->_prev = prev;delete cur;return iterator(next);//返回被删除元素的下一个迭代器}void test_list5(){list lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);lt.push_back(5);lt.push_back(6);auto it1 = lt.begin();while (it1 != lt.end()){if (*it1 % 2 == 0){//lt.erase(it1);it1 = lt.erase(it1);}else{++it1;//这里it1已经被delete了,是一个野指针,对野指针进行++,程序就崩了}}for (auto e : lt){cout << e << " ";}cout << endl;}

2.5析构函数

在实现析构函数时,我们不要太着急,可以先实现clear,然后析构时复用clear之后,再把哨兵位给释放掉。

		~list(){clear();delete _head;_head = nullptr;}void clear(){iterator it = begin();while (it != end()){it = erase(it);}//哨兵不会没有}

2.6深拷贝构造

传统写法:

		list(const list& lt){_head = new Node();_head->_next = _head;_head->_prev = _head;for (auto e : lt){push_back(e);}}

现代写法:

		//swapvoid swap(list& lt){std::swap(lt._head, _head);}//迭代器区间构造template list(InputIterator first, InputIterator last){_head = new Node();_head->_next = _head;_head->_prev = _head;while (first != last){push_back(*first);++first;}}//深拷贝现代写法//lt2(lt1)list(const list& lt){_head = new Node();_head->_next = _head;_head->_prev = _head;list tmp(lt.begin(), lt.end());swap(tmp);}

上一篇:英风吹“哭马傲”

下一篇:python作业8

相关内容

热门资讯

北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...