【C++】STL--List使用及其模拟实现

「开学季干货」:聚焦知识梳理与经验分享 10w+人浏览 459人参与

目录

1. list的介绍及使用

1.1. list的介绍

1.2. list的常见接口

2. list的迭代器

3. list的构造

构造空的list:

拷贝构造函数

使用first、last区间构造

析构函数

4. list capacity

5. list常见接口

5.1. insert

5.2. push_front、push_back

5.3. earse

5.4. pop_front、pop_back

5.5. swap

5.6. clear

7.list和vector的对比


1. list的介绍及使用

1.1. list的介绍

list官方文档

  • list是能够在常数范围内任意位置进行插入和删除的序列容器,并且能够进行双向迭代
  • list的底层是双向链表,双向链表中的每一个元素都储存在独立的节点中,每个节点通过指针进行前后联系
  • list和forward_list相似,不同的地方在于forward_list是单链表,只能够向前迭代
  • 与其他容器相比(vector、array、deque),list通常在任意位置插入和删除的执行效率更好
  • 与其他序列式容器相比,list、forward_list的最大缺陷在于不能够做到对任意位置的随机访问。例如:我要访问第六个元素,那就必须要从已知的位置(头或尾)开始,迭代到这个位置,这一操作需要线性的开销。并且list中的每一个节点还需要额外的空间来存储关联的信息(这一点在小数据的大链表中体现的比较多)

1.2. list的常见接口

list中接口比较多,我们学习常见的接口并且了解它的底层原理即可:


2. list的迭代器

在开始讲解list的常见接口之前,我们先来了解一下list中的迭代器:list中的指针是一个自定义类型的指针,该指针指向list中的某一个节点

这里补充一个知识,迭代器的实现有两种方式:

  • 直接使用原生指针,如:vector
  • 对原生指针进行封装,然后重载原生指针需要用到的运算符

list迭代器:

  • 因为指针支持解引用,所以自定义的类中需要重载operator*()
  • 可以通过->来访问成员,所以需要重载operator->()
  • 指针可以++向后移动,所以需要重载operator++()/operator(int);至于向前移动,需要根据需要选择呢是否重载operator--()/operator(int),因为如果是forward_list就不需要这个操作
  • 迭代器需要进行是否相等的比较,所以需要重载operator==()和operator!=()

我们首先实现一个简单的list的iterator:

函数声明接口说明
beginend

返回第一个元素的迭代器 + 返回最后一个元素下一个位置的迭代器

rbeginrend  

 
返回第一个元素的 reverse_iterator,  end 位置 , 返回最后一个元素下一个位置的 reverse_iterator,  begin 位置

注意:

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

//节点类
template <class T>
struct ListNode
{
	ListNode(const T& val = T())
		:_pre(nullptr)
		,_next(nullptr)
		,_val(val)
	{ }

	ListNode<T>* _pre;
	ListNode<T>* _next;
	T _val;
};

//迭代器类
template <class T,class Ref,class Ptr>
struct ListIterator
{
	typedef ListNode<T>* PNode;
	//下面不需要用指针,因为我们要的就是迭代器本身,而并不是迭代器指针
	typedef ListIterator<T, Ref, Ptr> Self;
	/*typedef  T& Ref;
    typedef  T* Ptr;*/

	PNode _node;

	ListIterator(PNode node)
		:_node(node)
	{ }

	Ref operator * ()
	{
		return _node->_val;
	}

	Ptr operator -> ()
	{
		//返回的节点数据的地址
		return &_node->_val;
	}

	Self& operator ++ ()
	{
		_node = _node->_next;
		return *this;
	}

	Self operator ++ ()
	{
		Self tmp(*this);
		_node = _node->_next;
		return tmp;
	}

	Self& operator -- ()
	{
		_node = _node->_pre;
		return *this;
	}

	Self operator -- ()
	{
		Self tmp(*this);
		_node = _node->_pre;
		return tmp;
	}

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

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

};

3. list的构造

构造函数(constructor)接口说明

list()

构造空的 list

list (size_type n, const value_type& val = value_type())

构造的 list 中包含 n 个值为 val 的元素

list (const list& x)

拷贝构造函数

list (InputIterator first, InputIterator last)

 [first, last) 区间中的元素构造 list

  • 构造空的list:

//构造空的list
list()
{
	_head = new Node();
	_head->_next = _head;
	_head->_pre = _head;
}
  • 拷贝构造函数

//拷贝构造
list(const list<T>& it)
{
	_head = new Node();
	_head->_next = _head;
	_head->_pre = _head;

	for (auto i : it)
	{
		push_back(i);
	}
}
  • 使用first、last区间构造

//使用first、last区间构造
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
	_head = new Node();
	_head->_next = _head;
	_head->_pre = _head;

	while (first != last)
	{
		push_back(*first);
		++first;
	}
}
  • 析构函数

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

4. list capacity

函数声明接口说明
empty

检测 list 是否为空,是返回 true ,否则返回 false

size

返回 list 中有效节点的个数

// List Capacity
size_t size()const
{
	size_t sz = 0;
	iterator it = begin();
	while (it != end())
	{
		sz++;
		it++;
	}
	return sz;
}
bool empty()const
{
	return size() == 0;
}

5. list常见接口

函数声明

接口说明
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 中的有效元素

5.1. insert

步骤:

  • 先创建一个newnode来接收x
  • 然后创建两个指针pre(指向pos位置的前一个结点),cur(指向pos位置)
  • 然后进行newnod、cur、pre三个结点的连接

iterator insert(iterator pos, const T& x)
{
	Node* newnode = new Node(x);
	Node* cur = pos._node;
	Node* pre = cur->_pre;

	//连接
	pre->_next = newnode;
	newnode->_pre = pre;
	newnode->_next = cur;
	cur->_pre = newnode;

	return newnode;
}

5.2. push_front、push_back

//push_front
void push_front(const T& x)
{
	inserta(begin(), x);
}

//push_back
void push_back()
{
	insert(end(), x);
}

5.3. earse

步骤:

  • 首先创建三个指针cur(pos位置)、pre(pos前一个位置)、next(pos后一个位置)
  • 然后连接pre和next
  • 释放掉cur指针,然后返回next指向结点的迭代器

//earse
iterator earse(iterator pos)
{
	assert(pos != end());

	Node* cur = pos->_node;
	Node* pre = cur->_pre;
	Node* next = cur->_next;

	//连接pre、next
	pre->_next = next;
	next->_pre = pre;
	delete cur;

	return iterator(next);
}

5.4. pop_front、pop_back

//头删
void pop_front()
{
	earse(begin());
}

//尾删
void pop_back()
{
	earse(end());
}

5.5. swap

其实list的交换非常简单,因为我们发现,list类中只有一个成员变量_head,所以我们只需要交换_head的值即可:

void Swap(list<T>& l)
{
	std::swap(_head, l->_head);
}

5.6. clear

将链表的每一个节点释放掉即可,可是使用迭代器然后earse:

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

6. list的迭代器失效

之前和大家讲过,我们可以将迭代器粗浅地先看做指针,而迭代器失效是因为是迭代器指向的空间被释放了。但是list的底层是一个包含头结点的双向链表,所以在插入元素的时候不存在扩容的操作,所以是不会导致迭代器失效的只有在删除结点的时候才会导致迭代器失效,并且失效的只有被删除的一个结点,其他迭代器不会受到影响

来看一下这段代码:

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;
	}
}

会出现访问异常:

修改之后的代码:

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;
	}
}

7.list和vector的对比

vectorlist

动态顺序表,一段连续空间

带头结点的双向循环链表



访
支持随机访问,访问某个元素效率 O(1)不支持随机访问,访问某个元素
效率 O(N)

任意位置插入和删除效率低,需要搬移元素,时间复杂

度为 O(N) ,插入时有可能需要增容,增容:开辟新空

间,拷贝元素,释放旧空间,导致效率更低

任意位置插入和删除效率高,不 需要搬移元素,时间复杂度为

O(1)

底层为连续空间,不容易造成内存碎片,空间利用率

高,缓存利用率高

底层节点动态开辟,小节点容易

造成内存碎片,空间利用率低,缓存利用率低

原生态指针

对原生态指针 ( 节点指针 ) 进行封装

在插入元素时,要给所有的迭代器重新赋值,因为插入

元素有可能会导致重新扩容,致使原来迭代器失效,删

除时,当前迭代器需要重新赋值否则会失效

插入元素不会导致迭代器失效,

删除元素时,只会导致当前迭代

器失效,其他迭代器不受影响

使

需要高效存储,支持随机访问,不关心插入删除效率

大量插入和删除操作,不关心随

机访问


(本篇完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值