STL-list

目录

【本节目标】

1.list的介绍及使用

[1.1 list的介绍(双向链表)](#1.1 list的介绍(双向链表))

[1.2 list的使用](#1.2 list的使用)

[1.2.1 list的构造](#1.2.1 list的构造)

[1.2.2 list iterator的使用(迭代器)](#1.2.2 list iterator的使用(迭代器))

[1.2.3 list capacity(容量)](#1.2.3 list capacity(容量))

[1.2.4 list element access](#1.2.4 list element access)

[1.2.5 list modifiers](#1.2.5 list modifiers)

[1.2.6 list的迭代器失效](#1.2.6 list的迭代器失效)

2.list的模拟实现

2.1节点类

[2.2 list迭代器类](#2.2 list迭代器类)

[2.3 list类](#2.3 list类)

[2.4 输出遇到的问题](#2.4 输出遇到的问题)

[2.5 遇到const迭代器传参时的问题](#2.5 遇到const迭代器传参时的问题)

3.list与vector的对比


【本节目标】

  1. list的介绍及使用

  2. list的深度剖析及模拟实现

  3. list与vector的对比

1.list的介绍及使用

1.1 list的介绍(双向链表)

  1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。

  2. list的底层是双向链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。

  3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝前迭代,已让其更简单高效。

  4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。

  5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

1.2 list的使用

list中的接口比较多,此处类似,只需要掌握如何正确的使用,然后再去深入研究背后的原理,已达到可扩展的能力。以下为list中一些常见的重要接口。

1.2.1 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 |

1.2.2 list iterator的使用(迭代器)

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

注意:

1.begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动

2.rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动

1.2.3 list capacity(容量)

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

1.2.4 list element access

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

1.2.5 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中的有效元素 |

1.2.6 list的迭代器失效

list的底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代

器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

cpp 复制代码
void TestListIterator1()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		// erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给
		其赋值
			l.erase(it);
		++it;
	}
}

删除时会导致迭代器失效,由于删除之后,之前的节点已经删除,但是迭代器还是指在这个位置,没有发生改变,从而导致迭代器失效。改为如下

cpp 复制代码
// 改正
void TestListIterator()
{
	int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
	list<int> l(array, array + sizeof(array) / sizeof(array[0]));
	auto it = l.begin();
	while (it != l.end())
	{
		l.erase(it++); // it = l.erase(it);
	}
}

注意:

list只有删除时(erase)迭代器才会失效,插入时(insert)不会失效。

2.list的模拟实现

2.1节点类

首先将节点封装成一个类,节点类

cpp 复制代码
//节点类
//初始化每个节点
template<class T>//用struct结构体时由于节点的数据全部公开,也可以时class类中的public中
struct ListNode
{
	ListNode<T>* _next;//指向下一个节点
	ListNode<T>* _prev;//指向前一个节点
	T data;


	//节点的构造函数
	//只用构造节点属性
	//默认缺省值为默认无参构造
	ListNode(const T& x = T()):
		_next(nullptr),
		_prev(nullptr),
		data(x)
	{}

};

运用结构体来封装,因为就结构体的默认时public类型,而节点的数据本来就要全部公开。

2.2 list迭代器类

如果物理空间是连续的,迭代器就可以认为是原生指针。由于list迭代器的空间是不连续的,原生指针不满足需求。封装一个类来实现迭代器。由于STL的每个结构都有iterator迭代器,所以可以用内嵌类型解决,即用typedef重命名或者内部类。

list迭代器也就相当于一个节点的指针
ListIterator类

起初的ListIterator类

cpp 复制代码
template<class T>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T> Self;

	Node* _node; //一个迭代器节点

	//迭代器构造
	ListIterator(Node *node):_node(node)
	{}

	++it
	前置++,返回++以后的值
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	it++
	后置++
	Self operator++(int)
	{
		Self tmp(*this);//浅拷贝,即两个迭代器指针指向同一个空间,直接应用默认拷贝构造
		_node = _node->_next;
		return tmp;//拷贝
	}

	--it
	Self& operator--()
	{
		//向前走
		_node = _node->_prev;
		return *this;
	}

	it--
	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	*it
	解引用,返回的是数据
	T& operator*()
	{
		return _node->data;
	}

	==
	比较两个迭代器相等,即比较迭代器的位置(引用/地址)相同
	bool operator==(const Self& it)
	{
		return _node == it._node;
	}

	//!=
	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}

	//->
	//返回的是数据的地址
	T* operator->()
	{
		return &_node->data;
	}

};

前置++、后置++、前置--、后置--都额可以简单应用
重点:

解引用运算符重载

2.3 list类

封装一个list类,成员变量是哨兵位头节点(_head)和计数节点的个数(_size),这是一个起初的list类之后会根据迭代器去变化

typedef ListIterator<T> iterator;是将迭代器名重定义到域内,就相当于只在list类内,即和内部类的功能一样

cpp 复制代码
template<class T>
class list
{
public:
	//重定义节点类名
	typedef ListNode<T> Node;
	//重定义迭代器名,作用域在list域内
	//没有用const迭代器时
	typedef ListIterator<T> iterator;


private:
	Node *_head;//哨兵位
	size_t _size;//链表中节点的个数


};

迭代器的begin和end

cpp 复制代码
//迭代器的引用
iterator begin()
{
	//iterator it(_head->_next);//有名对象
	//return it;
	return iterator(_head->_next);//这是应用的是一个匿名对象
}

iterator end()
{
	return iterator(_head);
}

由于哨兵位不算节点,哨兵位的下一个节点是第一个节点(begin)。

由于双向链表,即end就为_head。
构造函数

起初的构造函数,构造一个哨兵位头节点,_next和_prev都指向_head自己

cpp 复制代码
//构造函数
//默认情况下,只有一个头节点的哨兵位
//并且_head->_next=_head
//_head->_prev=_head
list()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	_size = 0;
}

为了更好些拷贝构造函数,则把构造函数改为以下方式

cpp 复制代码
void empty_init()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	_size = 0;
}
list()
{
	empty_init();
}

根据上述部分,写出拷贝构造

cpp 复制代码
//拷贝构造
//lt2(lt1)
//逐节点拷贝
list(const list<T>& lt)
{
	empty_init();
	//插入
	for (auto& e : lt)
	{
		push_back(e);
	}

}

先构造出哨兵位头节点,之后再逐节点拷贝,即为得到

赋值重载

cpp 复制代码
//交换
void swap(list<T>& lt)
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}

//赋值重载
//lt=lt1;
list<T>& operator=(const list<T> lt)
{
	//交换
	swap(lt);
	reurn* this;
}

插入

尾插

cpp 复制代码
//尾插
//引用插入数据本身
//权限可以缩小,输入的实参可以是const类型数据或者普通类型数据都可以
void push_back(const T& x)
{
	开辟节点,存入数据
	//Node* newnode = new Node(x);
	//Node* tail = _head->_prev;// tail指向原来的最尾部的节点

	//tail->_next = newnode;
	//newnode->_prev = tail;
	//newnode->_next = _head;
	//_head->_prev = newnode;

    //已知insert函数时
	insert(end(), x);
}

insert函数

cpp 复制代码
//c++中要隐藏底层,应用迭代器
//在pos位置插入
void insert(iterator pos, const T& val)
{
	Node *cur = pos._node;//当前位置
	Node* Prev = cur->_prev;
	Node *newnode = new Node(val);

	//Prev newnode cur
	Prev->_next = newnode;
	newnode->_prev = Prev;
	newnode->_next = cur;
	cur->_prev = newnode;

	_size++;
}

头插

cpp 复制代码
//头插
void push_front(const T& x)
{
	insert(begin(), x);
}

删除

erase

cpp 复制代码
//删除
//erase会导致迭代器失效,失效的原因是这个指针已经被释放
//需要更新节点指针,返回下一个节点的迭代器
iterator erase(iterator pos)
{
	Node *cur = pos._node;//当前位置
	Node *Prev = cur->_prev;//前一个位置
	Node *Next = cur->_next;//后一个位置

	Prev->_next = Next;
	Next->_prev = Prev;
	delete cur;//删除当前节点

	_size--;
	return iterator(Next);//匿名对象
	
}

头删和尾删

cpp 复制代码
//头删
void pop_front()
{
	erase(begin());
}

//尾删
void pop_back()
{
	//erase(end() - 1);不能使用,由于没有重载减号的运算符
	erase(--end());
}

2.4 输出遇到的问题

cpp 复制代码
struct A
{
	int _a1;
	int _a2;

	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		, _a2(a2)
	{}
};

void test_list2()
{
	list<A> lt;
	A aa1(1, 1);
	A aa2 = { 1, 1 };
	lt.push_back(aa1);
	lt.push_back(aa2);
	lt.push_back(A(2, 2));
	lt.push_back({ 3, 3 });//多参数可以这样构造
	lt.push_back({ 4, 4 });

	A* ptr = &aa1;
	(*ptr)._a1;
	ptr->_a1;

	list<A>::iterator it = lt.begin();
	while (it != lt.end())
	{
		//*it += 10;
        //cout<<*it<<" ";    //问题出处
		
        cout << (*it)._a1 << ":" << (*it)._a2 << endl;//解决问题2
		
        //解决问题3
        //it.operator->()->_a1;
        //第一个是->是运算符重载,第二个是->原生指针
        cout << it->_a1 << ":" << it->_a2 << endl;
        cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << endl;
		++it;
	}
	cout << endl;
}

由于之前输出时,用的int数据的链表,流插入可以输出结果,但是对应自定义类型时,是无法用流插入输出的。

解决方法有两种

1.写出流插入的运算符重载方法

2.用上面的方法解决:cout<<(*it)._a1<<":"<<(*it)._a2<<endl;即解决问题2

3.如果实在不想写流插入的运算符重载方法,可以运用解决问题3,在ListIterator类中重载->方法
解决问题3

直接在最初的ListIterator类的public部分中添加以下方法

cpp 复制代码
//->
//返回的是数据的地址
T* operator->()
{
	return &_node->data;
}

之后可以应用问题解决3,他直接会去省略一个->,即为it->_a;原本是it.operator->()->_a;由于返回值为A*,所以解引用后可以访问到_a;

2.5 遇到const迭代器传参时的问题

由于上述的代码都是最初的代码,最初的ListIterator类,最初的list类

遇到下面代码时会报错

cpp 复制代码
void PrintList(const list<int>& clt)
{
	list<int>::const_iterator it = clt.begin();
	while (it != clt.end())
	{
		//*it += 10;

		cout << *it << " ";
		++it;
	}
	cout << endl;
}

void test_list3()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

	PrintList(lt);

	list<int> lt1(lt);
	PrintList(lt1);
}

由于传参数是const类型的迭代器,和我们上面写的不同,上面最初的迭代器ListIterator类只有普通方法,没有const方法。
解决问题方法有两种:

1.可以用ctrl+c/v,再写一个ListConstIterator类

如下

cpp 复制代码
//第一种解决const类型的迭代器
//const迭代器类
template<class T>
struct ListConstIterator
{
	typedef ListNode<T> Node;
	typedef ListConstIterator<T> Self;

	Node* _node; //一个迭代器节点

	//迭代器构造
	ListConstIterator(Node* node) :_node(node)
	{}

	++it
	前置++,返回++以后的值
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	it++
	后置++
	Self operator++(int)
	{
		Self tmp(*this);//浅拷贝,即两个迭代器指针指向同一个空间,直接应用默认拷贝构造
		_node = _node->_next;
		return tmp;//拷贝
	}

	--it
	Self& operator--()
	{
		//向前走
		_node = _node->_prev;
		return *this;
	}

	it--
	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	*it
	解引用,返回的是数据
	const T& operator*()
	{
		return _node->data;
	}

	==
	比较两个迭代器相等,即比较迭代器的位置(引用/地址)相同
	bool operator==(const Self& it)
	{
		return _node == it._node;
	}

	//!=
	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}

	//->
	//返回的是数据的地址
	const T* operator->()
	{
		return &_node->data;
	}

};

在list类中也要添加

cpp 复制代码
typedef ListConstIterator<T> const_iterator;

//const迭代器,需要迭代器不能修改,还是迭代器指向的内容?
// 迭代器指向的内容不嫩被修改! const iterator不是我们需要的const迭代器
//以下是迭代器本身不能修改
//const iterator begin()错误
const_iterator begin() const
{
	//iterator it(_head->_next);//有名对象
	//return it;
	return const_iterator(_head->_next);//这是应用的是一个匿名对象
}

const_iterator end() const
{
	return const_iterator(_head);
}

2.由于上述代码过于冗余,两个类的内容非常相似,可以用模板来解决问题

最终的ListIterator类

cpp 复制代码
//迭代器类
// 一个链表指针用迭代器封装,实质上还是一个指针
//迭代器也就相当于指向一个节点的指针
//第二种解决const类型的迭代器问题
//利用模板来解决
template<class T,class Ref,class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T,Ref,Ptr> Self;

	Node* _node; //一个迭代器节点

	//迭代器构造
	ListIterator(Node *node):_node(node)
	{}

	++it
	前置++,返回++以后的值
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

	it++
	后置++
	Self operator++(int)
	{
		Self tmp(*this);//浅拷贝,即两个迭代器指针指向同一个空间,直接应用默认拷贝构造
		_node = _node->_next;
		return tmp;//拷贝
	}

	--it
	Self& operator--()
	{
		//向前走
		_node = _node->_prev;
		return *this;
	}

	it--
	Self operator--(int)
	{
		Self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

	*it
	解引用,返回的是数据
	//T& operator*()
	Ref operator*()
	{
		return _node->data;
	}

	==
	比较两个迭代器相等,即比较迭代器的位置(引用/地址)相同
	bool operator==(const Self& it)
	{
		return _node == it._node;
	}

	//!=
	bool operator!=(const Self& it)
	{
		return _node != it._node;
	}

	//->
	//返回的是数据的地址
	//T* operator->()
	Ptr operator->()
	{
		return &_node->data;
	}

};

最终的list类

cpp 复制代码
//list类
template<class T>
class list
{
public:
	//重定义节点类名
	typedef ListNode<T> Node;
	//重定义迭代器名,作用域在list域内
	//没有用const迭代器时
	/*typedef ListIterator<T> iterator;
	typedef ListConstIterator<T> const_iterator;*/

	//第二种方法解决const迭代器类
	typedef ListIterator<T,T&,T*> iterator;
	typedef ListIterator<T,const T&,const T*> const_iterator;


private:
	Node *_head;//哨兵位
	size_t _size;//链表中节点的个数


    ...
};

用模板实质上也是相当于创建了两个类,只是将创建类的工作都交给了编译器。

3.list与vector的对比

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

相关推荐
别NULL20 分钟前
机试题——疯长的草
数据结构·c++·算法
飞飞-躺着更舒服1 小时前
【QT】实现电子飞行显示器(改进版)
开发语言·qt
武昌库里写JAVA1 小时前
Java成长之路(一)--SpringBoot基础学习--SpringBoot代码测试
java·开发语言·spring boot·学习·课程设计
CYBEREXP20081 小时前
MacOS M3源代码编译Qt6.8.1
c++·qt·macos
ZSYP-S2 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos2 小时前
c++------------------函数
开发语言·c++
yuanbenshidiaos2 小时前
C++----------函数的调用机制
java·c++·算法
程序员_三木2 小时前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
是小崔啊2 小时前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
tianmu_sama2 小时前
[Effective C++]条款38-39 复合和private继承
开发语言·c++