【C++修炼秘籍】List深度剖析

【C++修炼秘籍】STL-List

☀️心有所向,日复一日,必有精进

☀️专栏《C++修炼秘籍》

☀️作者:早凉

☀️如果有错误,烦请指正,如有疑问可私信联系;


目录

【C++修炼秘籍】STL-List

文章目录

前言

一、list介绍

二、list的使用/接口介绍

构造函数

list iterator的使用

list capacity

list element access

list modifiers

三、list模拟实现

 结点定义

list类(构造函数和基本函数)

迭代器

增删查改 

迭代器失效问题

拷贝构造

 赋值运算符重载

size

有关类名和类型的问题

operator->

list和vector

总结


前言

相较于vector连续线性空间,list就显示的复杂很多,相较于vector,list既有优势也有缺点,两者并无好坏之分,只是各有其适用范围。首先,list的一个好处就是插入删除一个元素,就增加或释放一个元素空间,对空间的利用效率是极高的,对于任意位置的插入删除,list永远是O(1)的时间复杂度。

list和vector作为两个最常被使用的容器,什么时机去使用,也是一个比较复杂的问题,随着文章的深入,会慢慢对这个问题做出解答。


一、list介绍

//list node
template 
struct __list_node {
  typedef void* void_pointer;
  void_pointer next;
  void_pointer prev;
  T data;
};

从标准模板库中我们观察链表结点,可以明显看出,list底层是带头双向循环链表,不支持随机访问

  • 可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
  • 双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
  • list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。
  • 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
  • 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

二、list的使用/接口介绍

构造函数

构造函数( (constructor)) 接口说明
list (size_type n, const value_type& val = value_type()) 构造的list中包含n个值为val的元素
list() 构造空的list
list (const list& x) 拷贝构造函数
list (InputIterator first, InputIterator last) 用[first, last)区间中的元素构造list

list iterator的使用

函数声明 接口说明
begin +
end
返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin +
rend
返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的
reverse_iterator,即begin位置

迭代器就是一个类似于指针的行为,虽然底层实现不是原生指针。但是上层我们可以暂时理解如此。

list capacity

函数声明 接口说明
empty 检测list是否为空,是返回true,否则返回false
size 返回list中有效节点的个数

list element access

函数声明 接口说明
front 返回list的第一个节点中值的引用
back 返回list的最后一个节点中值的引用

list modifiers

函数声明 接口说明
push_front 在list首元素前插入值为val的元素
pop_front 删除list中第一个元素
push_back 在list尾部插入值为val的元素
pop_back 删除list中最后一个元素
insert 在list position 位置中插入值为val的元素
erase 删除list position位置的元素
swap 交换两个list中的元素
clear 清空list中的有效元素

三、list模拟实现

 结点定义

这是模式实现的一个结点,其实我们使用class和struct均可,实现一个构造函数。 

    template 
	struct list_node{
		T _val;
		list_node*_prev;
		list_node*_next;
		list_node(const T& x)
			:_prev(nullptr),
			_next(nullptr),
			_val(x){}
	};

list类(构造函数和基本函数)

 先暂时实现一个list类运行起来,这里其实是使用__list_iterator是一个类,封装迭代器,简单实现push_back,为下面的迭代器实现做准备。

template
	class list{
	public:
		typedef list_node node;
		typedef __list_iterator iterator;
        //typedef __list_const_iterator const_iterator;
		list()//默认构造函数
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}
        template 
		list(InputIterator first, InputIterator last)//迭代器构造函数
		{
			empty_initialize();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		void push_back(T val)
		{
			//新建结点
			node* NewNode = new node(val);
			node* tail = _head->_prev;
			tail->_next = NewNode;
			NewNode->_prev = tail;
			NewNode->_next = _head;
			_head->_prev = NewNode;
		}
		void insert(iterator pos, T val)
		{

		}
	private:
		node*_head;
	};

迭代器

为什么这里要单独解析list的迭代器,在上文vector的迭代器我们使用的是原生指针,是因为vector是一块连续的线性空间,而list不保证结点空间连续,那么如何使得list的迭代器能像vector进行相同的上层操作,忽视底层细节,这是一个很大的问题。我们只有解决了迭代器问题才能解决模拟实现的大部分问题。

list::iterator it = It.begin();

while(it!=It.end())
{
	cout<<*it<<" ";
	++it;
}

 其实这个类里面就是一个结点的指针,能够指向结点,在使用C++的运算符重载,就能实现类似++的效果;

1、内嵌类型定义在类里面

2、像指针一样,所指像指针一样:解引用和++/--(像指针一样的行为)

 【C++修炼秘籍】List深度剖析_第1张图片❓为什么迭代器不能用<,其实vector和string可以使<,但是list迭代器不能使用小于?

                我们说了迭代器底层不是一个连续空间,虽然我们使用的++,--但是并不是表面上的样子;

这其实就是迭代器的价值,封装底层实现 ,让我们忽视底层细节,我们在使用其他STL库时,统一的访问形式降低了使用成本,迭代器默默承受了一切,但是当你实现迭代器你就发现,那有什么岁月静好,只是有人替你负重前行。

但是,我们学习要关注底层;接下来,让我们走进list迭代器的模拟实现!! 

template 
	class __list_iterator//迭代器
	{
		typedef list_node node;
		node* _pnode;
	public:
		__list_iterator(node* p)
			:_pnode(p)
		{}
		T& operator*()
		{
			return _pnode->_val;
		}
		bool operator != (__list_iterator& it)//参数一定加引用
		{
			return it._pnode != _pnode;
		}
		void operator++()//这里实现有些问题其实是应该携带返回值的,返回*this;
		{
			_pnode =_pnode->_next;
		}
	};

【C++修炼秘籍】List深度剖析_第2张图片❓但是还是存在一些问题,const迭代器呢?

(const迭代器和普通迭代器的区别就是不可通过*it去修改值。)

如果只是如下这样是解决不了问题的;

	// ++cit 这样的话,可以解引用,但是不能++
		/*const T& operator*() const
		{
		return _pnode->_data;
		}*/

    template 
	class __list_const_iterator
	{
		typedef list_node node;
		node* _pnode;
	public:
		__list_const_iterator(node* p)
			:_pnode(p)
		{}
		const T& operator*()
		{
			return _pnode->_val;
		}
		bool operator != (__list_const_iterator& it)//参数一定加引用
		{
			return it._pnode != _pnode;
		}
		void operator++()//这里实现有些问题其实是应该携带返回值的,返回*this;
		{
			_pnode = _pnode->_next;
		}
	};

 我们单独实现了const_iterator的一个类但是,大佬之所以为大佬,就是代码的复用,能写出来常人难以形容的代码;这里简单复现一下:

template 
	class __list_iterator//迭代器
	{
		typedef list_node node;
		typedef __list_iterator Self;
		node* _pnode;
	public:
		__list_iterator(node* p)
			:_pnode(p)
		{}
		/*T& operator*()//我这里自己写的就很迷,
		{
			return _pnode->_val;
		}*/
		Ref& operator*()//返回值不同不构成重载而且不是const迭代器这里传回的返回值也不是const
		{
			return _pnode->_val;
		}
		bool operator != (const Self& it)//参数一定加引用
		{
			return it._pnode != _pnode;
		}
		Self&  operator++()
		{
			_pnode =_pnode->_next;
			return *this;
		}
		Self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}
	};

增删查改 

insert 在list position 位置中插入值为val的元素
iterator insert(iterator pos, const T& val)
		{
			node* NewNode = new node(val);
			node* cur = pos._pnode;
			node* pre = cur->_prev;

			pre->_next = NewNode;
			NewNode->_prev = pre;
			NewNode->_next = cur;
			cur->_prev = NewNode;
			return iterator(pos);
		}
erase 删除list position位置的元素
iterator erase(iterator pos)
		{
			assert(pos != end());

			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;

			prev->_next = next;
			next->_prev = prev;

			delete pos._pnode;

			return iterator(next);
		}
push_front 在list首元素前插入值为val的元素
pop_front 删除list中第一个元素
push_back 在list尾部插入值为val的元素
pop_back 删除list中最后一个元素
        void push_back(T val)
		{
			//新建结点
		/*	node* NewNode = new node(val);
			node* tail = _head->_prev;
			tail->_next = NewNode;
			NewNode->_prev = tail;
			NewNode->_next = _head;
			_head->_prev = NewNode;*/
			insert(end(), val);
		}
		void pop_front()
		{
			erase(begin());
		}

		void pop_back()
		{
			erase(--end());
		}

 我们看到大多数代码均是复用出来的,所以代码复用真的很好,要学会“偷懒”。还是那句话

❗️代码复用真的好好 

迭代器失效问题

在vector中,insert和erase是失效的,但是在list迭代器中insert迭代器不失效;erase迭代器必然失效,空间结点都没了;

所以在erase后注意迭代器的使用;

拷贝构造

在拷贝构造前,先实现下析构函数,至于为什么,请看下文,实现拷贝构造去准备工作;

clear 清空list中的有效元素
    void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

其实这里就是我们要为什么先实现迭代器,实现了迭代器基本的模拟实现就完成了一半,还有一半,我认为是拷贝构造;

 析构函数

        ~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

 【C++修炼秘籍】List深度剖析_第3张图片❓如果我们不写拷贝构造,那么默认的拷贝构造是否能用? 

 【C++修炼秘籍】List深度剖析_第4张图片

报错了,如果把析构删掉,这段程序就通过了,所以要完成析构我们才能够正确的认识,这个默认的拷贝构造是一个浅拷贝,那就是同一块空间析构两次,大问题呀!

 这里实现一个比较牛的写法,第一次见,我看不懂,但大受震撼!【C++修炼秘籍】List深度剖析_第5张图片

        void swap(list& lt)
		{
			std::swap(_head, lt._head);
		}

		list(const list& it)
		{
			empty_initialize();//这里如果不单独开辟一个空间就会出现问题
			list tmp(it.begin(), it.end());//迭代器构造函数
			swap(tmp);
		}

 ❗️注意:empty_initialize();这个函数就是开辟一个头结点,这里如果不开头结点,交换给临时变量list,在析构时会默认存在头结点,所以会调用clear(),这时候对空访问会出才能问题。

 赋值运算符重载

	    list& operator=(list lt)
		//list& operator=(list lt) // 不建议
		{
			swap(lt);
			return *this;
		}

这真的很无敌,可能拷贝构造体现不出来,但是赋值运算符重载很无敌;

这里参数必须是传参而不是引用,直接调用拷贝构造;直接换;

size

size 返回list中有效节点的个数

 size这个是比较麻烦的,如果你使用范围for进行遍历,一次可能效率还好,但是频繁size效率是让人难以忍受的;以空间换时间的方法,增加4个字节存储size;

这里还有一个问题,我们之前已经写了很多代码,如果加会不会很麻烦,实际上代码复用的好处就出现了,直接在复用的地方加就好;

有关类名和类型的问题

在类模板中,例如:list,list为类名,list为类型,构造函数名为list而不是list;参数都是lsit但是C++中有些坑,虽然C++中坑太多了

【C++修炼秘籍】List深度剖析_第6张图片

 【C++修炼秘籍】List深度剖析_第7张图片❓诶?诶?怎么,又要拿出那个图片了 【C++修炼秘籍】List深度剖析_第8张图片

  在类中,类名就是类型,所以这样也没问题,但是不建议这么使用,我们只要知道有这个样子的事情就好了;

operator->

struct P
{
	int _row;
	int _col;

	P(int row = 0, int col = 0)
		:_row(row)
		, _col(col)
	{}
};

 如果说我们想打印一个结构化数据,会出现一些问题,

【C++修炼秘籍】List深度剖析_第9张图片

最好的解决方法是重载-> 

很怪很怪

        T* operator->()
		{
			return &_pnode->_val;
		}

 其实就是it.operator->()返回结点数据的地址,就是一个p*,但是p*怎么访问,其实就是一个it->->_row就是it.operator()->_row,但是写两个箭头,很让人无语,所以编译器为了可读性,给我们做了特殊处理。

 【C++修炼秘籍】List深度剖析_第10张图片❓这里又又有问题,如果是const呢?

 

struct P
{
	int _row;
	int _col;

	P(int row = 0, int col = 0)
		:_row(row)
		, _col(col)
	{}
};
void print_list(const my::list

& lt) { my::list

::const_iterator it = lt.begin(); while (it != lt.end()) { it->_col++; cout << it->_col << ":" << it->_col << endl; ++it; } cout << endl; } int main() { my::list

lt; P p1(1, 1); lt.push_back(p1); lt.push_back(p1); lt.push_back(p1); lt.push_back(P(2, 2)); lt.push_back(P(3, 3)); my::list

::iterator it = lt.begin(); while (it != lt.end()) { cout << it->_col << ":" << it->_col << endl; //cout << (*it)._col <<":"<< (*it)._col << ""; ++it; } cout << endl; print_list(lt); }

这个迭代器是有问题的呀,发现居然是能修改的;

所以可以再次修改迭代器的代码:

        typedef __list_iterator iterator;
		typedef __list_iterator const_iterator;
template 
	class __list_iterator//迭代器
	{
	public:
		typedef list_node node;
		typedef __list_iterator Self;
		node* _pnode;

		__list_iterator(node* p)
			:_pnode(p)
		{}
		Ptr operator->()
		{
			return &_pnode->_val;
		}
		Ref& operator*()
		{
			return _pnode->_val;
		}
		bool operator != (const Self& it)
		{
			return it._pnode != _pnode;
		}
		
		Self&  operator++()
		{
			_pnode =_pnode->_next;
			return *this;
		}
		Self& operator--()
		{
			_pnode = _pnode->_prev;
			return *this;
		}
	};

无敌, 类型typedef让我们修改很无敌,方便我们修改;

list和vector

vector list
底 层 结 构 动态顺序表,一段连续空间 带头结点的双向循环链表
随 机 访 问 支持随机访问,访问某个元素效率O(1) 不支持随机访问,访问某个元素
效率O(N)
插 入 和 删 除 任意位置插入和删除效率低,需要移动元素,时间复杂
度为O(N),插入时有可能需要增容,增容:开辟新空
间,拷贝元素,释放旧空间,导致效率更低
任意位置插入和删除效率高,不
需要搬移元素,时间复杂度为
O(1)
空 间 利 用 率 底层为连续空间,不容易造成内存碎片,空间利用率
高,缓存利用率高
底层节点动态开辟,小节点容易
造成内存碎片,空间利用率低,
缓存利用率低
迭 代 器 原生指针 对原生态指针(节点指针)进行封装
迭 代 器 失 效 在插入元素时,要给所有的迭代器重新赋值,因为插入
元素有可能会导致重新扩容,致使原来迭代器失效,删
除时,当前迭代器需要重新赋值否则会失效
插入元素不会导致迭代器失效,
删除元素时,只会导致当前迭代
器失效,其他迭代器不受影响
使 用 场 景 需要高效存储,支持随机访问,不关心插入删除效率 大量插入和删除操作,不关心随
机访问

总结

好了List告一段落,所有细节都是难以,只有亲自实现才能够注意到;把握好不同容器的使用时机,才能够方便我们的使用。下期再见!!

【C++修炼秘籍】List深度剖析_第11张图片 

你可能感兴趣的:(c++,list,链表,数据结构,stl)