【C++】list类

list

  • list是序列容器,允许在序列内任意位置进行恒定时间的插入和擦除操作,并实现双向迭代
  • list容器实现为双链表,双链表可以将所包含的每个元素存储在不同且无关的存储位置。排序通过与每个元素关联前一个元素及其后一个元素的链接来内部保持
  • list与forward_list非常相似:主要区别在于forward_list对象是单链表,因此只能向前迭代,但体积更小、更高效
  • 与其他基础标准序列容器(array,vector,deque)相比,列表在插入、提取和移动元素时通常表现更好,尤其是在容器内已获得迭代器的任意位置,因此在大量使用这些元素的算法中也表现更好,比如排序算法

1、list的使用

1.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

size_type表示无符号整数类型,value_type是第一个模板参数,使用迭代器区间的构造函数是函数模板

c 复制代码
void test_list1()
{
	list<int> lt1;                       //构造空的lt1
	list<int> lt2(10, 1);                //lt2中放10个值为1的元素
	vector<int> v1 = { 1,2,3,4,5,6 };    
	list<int> lt3(v1.begin(), v1.end()); //利用迭代器区间构造lt3
	list<int> lt4(lt3);                  //用lt3构造lt4

  //以数组为迭代器区间构造lt5
	int a[] = { 1,20,3,40 };
	list<int> lt5(a, a + 4);
	
	//列表格式初始化
	list<int> lt6 = { 1,2,3,4,5,6,1,1,1,1 };

	//用迭代器的方式打印lt4中的元素
	list<int>::iterator it4 = lt4.begin();
	while (it4 != lt4.end())
	{
		cout << *it4 << " ";
		++it4;
	}
	cout << endl;
	
	//用范围for的方式遍历
	for (auto e : lt4)
	{
		cout << e << " ";
	}
	cout << endl;
}

1.2 list iterator的使用

此处,大家可暂时将迭代器理解成一个指针,该指针指向list中的某个节点

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

注意:begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动,由于list的底层物理空间不连续,,所以list的迭代器不是原生指针,list的迭代器没有对+和-进行重载,只重载了++和--,lt.begin()+5是不被允许的

c 复制代码
int main() {
    std::list<int> lt = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 1. 正向迭代器 begin() + end()
    std::cout << "正向遍历 list: ";
    std::list<int>::iterator it = lt.begin();
    while (it != lt.end()) {
        std::cout << *it << " ";
        ++it;  // 只能用 ++/--,不支持 it + 5 这种随机访问
    }

    // 2. 反向迭代器 rbegin() + rend()
    std::cout << "反向遍历 list: ";
    std::list<int>::reverse_iterator rit = lt.rbegin();
    while (rit != lt.rend()) {
        std::cout << *rit << " ";
        ++rit;  // 反向迭代器 ++ 会向前移动(向 list 头部方向)
    }
    std::cout << "\n\n";

    // 3. 为什么 lt.begin() + 5 不被允许?
    // 下面这行代码编译会报错,你可以取消注释试试:
    // auto it5 = lt.begin() + 5;

    return 0;
}

1.3 list capacity(容量)

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

1.4 list element access(元素访问)

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

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

注意 :insert插入元素不会导致迭代器失效,list中的insert不会扩容挪动数据,前面说过,此处大家可将迭代器暂时理解成类似于指针,迭代器失效即迭代器所指向的节点的无效,即该节点被删除了 。因为list的底层结构为带头结点的双向循环链表 ,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响

1.6 list operation(对链表的一些操作)

函数声明 接口说明
reverse 对链表进行逆置
sort 对链表中的元素进行排序(稳定排序)
merge 对两个有序的链表进行归并,得到一个有序的链表
unique 对链表中的元素去重
remove 删除具有特定值的节点
splice 将 A 链表中的节点转移到 B 链表

注意:链表排序只能使用list自身的sort()(本质是归并排序)接口,不能使用算法库的sort接口,因为算法库中的的sort底层是通过快排来实现的,而快排中涉及迭代器-迭代器,链表不支持。链表排序时效率低,推荐使用vector进行排序

错误示范

c 复制代码
#include <iostream>
#include <list>
#include <algorithm> // 算法库的 sort

int main() {
    std::list<int> lst = {5, 2, 8, 1, 9};

    // 尝试用算法库的 std::sort 给 list 排序
    // 这里会编译失败!
    std::sort(lst.begin(), lst.end()); 

    for (auto x : lst) {
        std::cout << x << " ";
    }
    return 0;
}

正确示范

c 复制代码
#include <iostream>
#include <list>

int main() {
    std::list<int> lst = {5, 2, 8, 1, 9};

    // 用 list 自身的 sort() 成员函数(稳定排序,底层是归并排序)
    lst.sort(); 

    std::cout << "排序后的 list: ";
    for (auto x : lst) {
        std::cout << x << " ";
    }
    std::cout << "\n";

    return 0;
}

输出结果

排序后的 list: 1 2 5 8 9

推荐使用vector:

c 复制代码
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
#include <ctime>  // 用于 clock()

using namespace std;

int main() {
    // 生成 10 万个随机数(数据越大差距越明显)
    const int SIZE = 100000;
    vector<int> vec;
    list<int> lst;

    // 先填充相同的数据
    for (int i = 0; i < SIZE; ++i) {
        int num = rand() % 100000;
        vec.push_back(num);
        lst.push_back(num);
    }

    // ======================
    // 1. 计时 vector 排序
    // ======================
    clock_t start_vec = clock();
    sort(vec.begin(), vec.end());  // 算法库 sort
    clock_t end_vec = clock();
    double time_vec = double(end_vec - start_vec) / CLOCKS_PER_SEC;

    // ======================
    // 2. 计时 list 排序
    // ======================
    clock_t start_lst = clock();
    lst.sort();  // list 自身成员函数
    clock_t end_lst = clock();
    double time_lst = double(end_lst - start_lst) / CLOCKS_PER_SEC;

    // ======================
    // 输出结果
    // ======================
    cout << "数据量:" << SIZE << endl;
    cout << "vector 排序耗时:" << time_vec << " 秒" << endl;
    cout << "list   排序耗时:" << time_lst << " 秒" << endl;

    return 0;
}

输出结果

数据量:100000

vector 排序耗时:0.002 秒

list 排序耗时:0.015 秒

迭代器的三种类型

迭代器类型 支持操作 典型容器 说明
单向迭代器 ++(仅向前移动) std::forward_list 只能从前往后遍历,不能反向,也不能随机访问
双向迭代器 ++ / --(前后移动) std::list、std::set、std::map 可以向前、向后遍历,但不支持 +/-/+=/-=/ 下标访问,it + n 是非法的
随机访问迭代器 ++ / -- / + / - / += / -= / [] std::vector、std::deque、原生数组 可以像指针一样任意跳跃,支持 it + n、it[n] 等操作

2、list的模拟实现

2.1 list的节点

c 复制代码
struct list_node
{
	T _data;
	list_node<T>* _next;
	list_node<T>* _prev;

	list_node(const T& x = T())
		:_data(x) 
		, _next(nullptr)
		, _prev(nullptr)
	{}
};

2.2 list的成员变量

c 复制代码
class list
{
	typedef list_node<T> Node;
public:
//成员函数
private:
	Node* _head;
	size_t _size = 0;
}

typedef受到访问限定符的限制,没写默认是private,链表的本质是数据结构,所以只需要存储一个链表的头节点即可

2.3 list的迭代器

list的迭代器不能使用原生指针,如果使用原生指针,那么得到的是一个节点,而我们希望得到节点里面存储的元素,因此需要对迭代器进行封装,然后将一些运算符进行重载实现迭代器的效果

const迭代器

c 复制代码
template<class T, class Ref, class Ptr>
//Ref = T&, Ptr = T*
struct __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> Self;
	Node* _node;

	__list_iterator(Node* node)
		:_node(node)
	{}

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

	Ptr 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) const
	{
		return _node != it._node;
	}

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

注意

  • 这里类名不能直接叫iterator,因为每种容器的迭代器底层实现都不同,即可能会为每一种容器都单独实现一个迭代器,如果都直接使用iterator,会导致命名冲突,其次,迭代器类不需要写析构函数、拷贝构造函数、赋值运算符重载函数,直接使用默认生成的(浅拷贝)即可
  • 如果想实现const迭代器,不可以写成typedef const list_iterator<T> const_iterator,const迭代器本质是限制迭代器指向的内容不能修改,而const迭代器自身可以修改,可以指向其他节点,这种写法cnost限制的是迭代器本身,让迭代器无法实现++等操作。为了控制迭代器指向的内容不能修改,可以通过控制operator*的返回值实现。但仅仅只有返回值不同无法构成函数重载。为了实现一个类里面实现两个operator*让他俩一个返回普通的T&,一个返回const T&,可以在普通迭代器的基础上,再传递一个模板参数,让编译器帮忙生成

2.4 list的成员函数

2.4.1 构造函数

c 复制代码
list()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
}

list本质是一个带头双向链表

2.4.2 拷贝构造函数

c 复制代码
// lt2(lt1)
// list(const list& lt)
list(const list<T>& lt)
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	for (const auto& e : lt)
	{
		push_back(e);
	}

//用花括号列表直接初始化
list(initializer_list<T> il)
{
	empty_init();
	for (const auto& e : il)
	{
		push_back(e);
	}
}

2.4.3 赋值运算符重载

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

// lt1 = lt3
//list& operator=(list lt)
list<T>& operator=(list<T> lt)
{
	//现代写法
	swap(lt);

	return *this;
}

注意:构造函数和赋值运算符重载函数的形参和返回值类型可以只写类名list,不需要写完整的类型list,但不推荐,容易混淆,效率上也没有区别

2.4.4 迭代器相关

c 复制代码
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;

iterator begin()
{
	return iterator(_head->_next);
}

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

const_iterator begin() const
{
	return const_iterator(_head->_next);
}

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

2.4.5 insert

c 复制代码
iterator insert(iterator pos, const T& val)
{
	Node* cur = pos._node;
	Node* newnode = new Node(val);
	Node* prev = cur->_prev;

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

	++_size;

	return iterator(newnode);
}

2.4.6 erase

c 复制代码
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);
	return next;
}

2.4.7 push_back

c 复制代码
void push_back(const T& x)
{
	insert(end(), x);
}

2.4.8 push_front

c 复制代码
void push_front(const T& x)
{
	insert(begin(), x);
}

2.4.9 pop_back

c 复制代码
void pop_back()
{
	erase(--end());
}

2.4.10 pop_front

c 复制代码
void pop_front()
{
	erase(begin());
}

2.4.11 size

c 复制代码
size_t size() const
{
	/*size_t n = 0;
	for (auto& e : *this)
	{
		++n;
	}
	return n;*/

	return _size;
}
  • 第一种方法:定义一个计数器n,初始值为0,每走一个元素,计数器n就加1,循环结束后,n的值为链表的元素总数
  • 第二种方法:利用成员变量中的_size进行记录

2.4.12 clear

c 复制代码
void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

2.4.13 析构函数

c 复制代码
~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

析构函数会释放头节点,clear函数不会释放头节点

如果文章中有错误或不足,欢迎大家指正交流。

相关推荐
minji...1 小时前
Linux 网络套接字编程(六)TCP的通信是全双工的,自定义协议的定制,序列化和反序列化
linux·运维·服务器·网络·c++
ximu_polaris1 小时前
设计模式(C++)-行为型模式-策略模式
c++·设计模式·策略模式
迷途之人不知返1 小时前
List的学习
数据结构·c++·学习·list
6Hzlia1 小时前
【Hot 100 刷题计划】 LeetCode 23. 合并 K 个升序链表 | C++ 顺序合并
c++·leetcode·链表
今夕资源网2 小时前
Visual C++运行库合集 V104.0 一个github免费开源的项目VisualCppRedist AIO
开发语言·c++·dll修复工具·dll修复·运行库·修复软件
syagain_zsx2 小时前
剖析“继承”,清晰易懂
开发语言·c++
Season4502 小时前
C++中论在类中成员变量定义顺序的重要性
开发语言·c++
拳里剑气2 小时前
C++算法:前缀和
开发语言·c++·算法·前缀和
啊我不会诶2 小时前
Codeforces Round 1091 (Div. 2) and CodeCraft 26
c++·算法