深入理解C++系列(10)——lsit

⭐️博主: 此生决int-@CSDN博客
** 速胜派就是最大的投降派!!!**

**         🔥热门专栏🔥**

      深入理解 C++ 系列算法系列

      快速复习系列Java 速通系列


文章目录

  • 深入理解C++------list链表
  • 一、list的使用
    • 1,list介绍:
    • [2、list 不支持 `operator\[\]`](#2、list 不支持 operator[])
    • [3、list 的访问和遍历方式](#3、list 的访问和遍历方式)
    • 4,list相关接口⭐️
    • [5、使用 list 接口时需要注意的地方(坑!)](#5、使用 list 接口时需要注意的地方(坑!))
        • [① 不支持随机访问](#① 不支持随机访问)
        • [② insert()、erase() 的位置必须是迭代器](#② insert()、erase() 的位置必须是迭代器)
        • [③ insert()、erase() 不会导致其它迭代器失效(被删除节点除外)](#③ insert()、erase() 不会导致其它迭代器失效(被删除节点除外))
        • [④ erase() 会返回下一个位置,](#④ erase() 会返回下一个位置,)
        • [⑤ sort() 不能使用 std::sort()](#⑤ sort() 不能使用 std::sort())
        • [⑥ reverse() 也尽量使用成员函数](#⑥ reverse() 也尽量使用成员函数)
        • [⑦ merge() 合并前必须保证两个链表都有序](#⑦ merge() 合并前必须保证两个链表都有序)
        • [⑧ unique() 只能删除连续重复元素](#⑧ unique() 只能删除连续重复元素)
        • [⑨ splice() 是剪切,不是复制](#⑨ splice() 是剪切,不是复制)
        • [⑩ list 最大优势不是遍历,而是频繁插入删除操作](#⑩ list 最大优势不是遍历,而是频繁插入删除操作)
    • 5,迭代器的类型:迭代器的简单介绍⭐️⭐️
    • [二、list 模拟实现------知识点 & 易错点总结⭐️⭐️⭐️](#二、list 模拟实现——知识点 & 易错点总结⭐️⭐️⭐️)
    • 五,补充知识:
      • [**1、iterator 为什么设计成三个模板参数?⭐️⭐️⭐️**](#1、iterator 为什么设计成三个模板参数?⭐️⭐️⭐️)
      • [**10、operator->() 为什么返回指针?⭐️⭐️⭐️**](#10、operator->() 为什么返回指针?⭐️⭐️⭐️)
  • 三、总结与回顾
    • [一、list 的底层结构](#一、list 的底层结构)
    • [二、list 与 vector 最大区别](#二、list 与 vector 最大区别)
    • [三、list 最重要的接口](#三、list 最重要的接口)
  • 下期预告
  • 结语

上期回顾

上一篇我们主要学习了 vector ,重点掌握了 vector的相关接口,以及模拟实现了vector,正式开始了STL的学习。如果还有印象模糊的地方,建议先回顾上一篇内容,再继续阅读本文。

深入理解C++------list链表

精简目录:

一、list的使用

1、list介绍

2、list 不支持 operator\[\]

3、list 的访问和遍历方式

4、list相关接口 ⭐

5、使用 list 接口时需要注意的地方(坑!)

6、迭代器的类型:迭代器的简单介绍 ⭐⭐

二、list 模拟实现------知识点 & 易错点总结 ⭐⭐⭐

1、list 底层结构

2、关于模板

3、接口实现

4、主播在模拟实现时犯的那些错!

5、补充知识

三、总结与回顾

四,下期预告

stack 和 queue

五,结语

一、list的使用

1,list介绍:

1、C++里面list 的底层结构

  • list 底层采用 带头双向循环链表 实现。

2、list 的构造方式

list 的构造方式与前面学习过的 vectorstring 基本一致,都是 STL 容器统一的设计风格,因此学习起来并不陌生。

构造方式 说明 示例
默认构造 创建一个空链表 list<int> lt;
指定元素个数 创建 n 个元素,每个元素为默认值 list<int> lt(10);
指定元素个数和值 创建 n 个值为 val 的元素 list<int> lt(10, 1);
区间构造 用一段迭代器区间构造 list<int> lt(v.begin(), v.end());
拷贝构造 用已有 list 创建新对象 list<int> lt2(lt1);
初始化列表构造(C++11) 使用 {} 初始化 list<int> lt{1,2,3,4};
移动构造(C++11)了解 转移资源,提高效率 list<int> lt2(std::move(lt1));
例如:
cpp 复制代码
list<int> lt;                    // 空链表:{}

list<int> lt(10);                // 创建10个元素,每个元素默认初始化为0:{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}

list<int> lt(10, 1);             // 创建10个元素,每个元素初始化为1:{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}

list<int> lt(v.begin(), v.end()); // 用区间[v.begin(), v.end())中的元素初始化链表:内容与v完全相同

list<int> lt2(lt1);              // 拷贝构造:lt2的内容与lt1完全相同

list<int> lt{1, 2, 3, 4};        // 列表初始化(C++11):{1, 2, 3, 4}

2、list 不支持 operator[]

vector 由于采用连续存储,可以通过地址计算实现随机访问,因此支持:operator[],时间复杂度为 O(1)

list 的节点分散在内存各处,访问第 i 个元素必须沿着链表逐个遍历,因此时间复杂度为 O(N)

所以,list没有提供operator\[\]


3、list 的访问和遍历方式

由于不能随机访问,list 主要通过:

  • front()back()
  • 迭代器(Iterator)
  • 范围 for来访问元素。

最常见的遍历方式

cpp 复制代码
for (auto e : lt)
{
    cout << e << " ";
}

或者:

cpp 复制代码
for (auto it = lt.begin(); it != lt.end(); ++it)
{
    cout << *it << " ";
}

4,list相关接口⭐️

大多数与vector类似:

接口 作用 时间复杂度(典型)
push_back() 尾插 O(1)
push_front() 头插 O(1)
pop_back() 尾删 O(1)
pop_front() 头删 O(1)
front() 获取首元素 O(1)
back() 获取尾元素 O(1)
begin() / end() 获取迭代器 O(1)
insert() 指定位置插入 O(1)(已知位置)
erase() 删除指定位置 O(1)(已知位置)
clear() 清空链表 O(N)
size() 元素个数 O(1)(现代 STL 实现)
empty() 是否为空 O(1)
reverse() 链表逆置 O(N)
sort() 链表排序 O(N log N)
链表特有的:
merge() 合并两个有序链表 O(N+M)
unique() 删除连续重复元素 O(N)
remove() 删除指定值 O(N)
splice() 剪切)节点转移并且不拷贝数据 O(1)(单节点)或与转移区间长度相关
cpp 复制代码
  //==================== splice ====================
    list<int> lt1{1, 2, 3, 4};
    list<int> lt2{10, 20, 30};
    // 将lt2中的所有节点转移到lt1末尾(不会拷贝数据,只修改节点链接)
    lt1.splice(lt1.end(), lt2);
    // lt1:1 2 3 4 10 20 30
    // lt2:空
    //==================== remove ====================
    list<int> lt3{1, 2, 3, 2, 4, 2, 5};
    // 删除所有值为2的节点
    lt3.remove(2);
    // lt3:1 3 4 5
    //==================== unique ====================
    list<int> lt4{1, 1, 2, 2, 2, 3, 4, 4, 5};
    // 删除连续重复元素(必须是相邻的重复元素)
    lt4.unique();
    // lt4:1 2 3 4 5
    //==================== merge ====================
    list<int> lt5{1, 3, 5, 7};
    list<int> lt6{2, 4, 6, 8};
    // 合并两个有序链表
    // 合并后lt5仍然有序,lt6变为空
    lt5.merge(lt6);
    // lt5:1 2 3 4 5 6 7 8
    // lt6:空

5、使用 list 接口时需要注意的地方(坑!)

① 不支持随机访问

list 底层是双向链表,因此不能像 vector 一样随机访问元素

下面的代码都是错误的:

cpp 复制代码
list<int> lt{1,2,3,4};
cout << lt[0] << endl;      // ❌ 不支持[]
cout << lt.at(0) << endl;   // ❌ 没有at()
auto it = lt.begin() + 2;   // ❌ 迭代器不能+2

如果想访问第 3 个元素,只能:

cpp 复制代码
auto it = lt.begin();
advance(it, 2);   // 向后移动2次
cout << *it << endl;

原因:

链表节点在内存中不是连续存储的,无法通过地址直接定位到第 n 个元素。


② insert()、erase() 的位置必须是迭代器

不像 vector 可以使用下标,list 的插入删除都是基于迭代器

cpp 复制代码
list<int> lt{1,2,3,4};
auto it = lt.begin();
++it;
lt.insert(it, 100); // 在2前面插入100
lt.erase(it);       // 删除2

③ insert()、erase() 不会导致其它迭代器失效(被删除节点除外)

这是 list 最大的优势。

例如:

cpp 复制代码
list<int> lt{1,2,3,4};

auto it = lt.begin();   // 指向1

lt.push_back(5);
lt.push_front(0);

cout << *it << endl;    // 仍然是1

对于链表来说:

  • 插入节点
  • 删除其它节点

都不会影响已有节点的位置。

只有被删除节点对应的迭代器会失效。

例如:

cpp 复制代码
auto it = lt.begin();

lt.erase(it);

// it已经失效
// *it;   // ❌ 未定义行为
对比与vector

vector可能失效,因为可能扩容!

cpp 复制代码
vector<int> v{1, 2, 3, 4};
    auto it = v.begin(); // 指向1
    // 假设这里触发扩容
    v.push_back(5);
    // it可能已经失效(未定义行为)
    // cout << *it << endl; // ❌ 不安全

注意: 如果 push_back() 没有触发扩容,那么已有元素不会移动,原来的迭代器仍然有效。因此 vector 的迭代器是否失效,取决于是否发生了扩容。

erase后也是失效,并且后面的所有迭代器也全都失效了!

cpp 复制代码
vector<int> v{1, 2, 3, 4, 5};
auto it = v.begin() + 1; // 指向2
v.erase(it);
// it失效

从删除位置开始,到 end() 之间的所有迭代器全部失效。

一句话总结!

list:节点不移动,所以迭代器很稳定;只有删除的那个节点对应的迭代器失效。

vector:元素可能移动(扩容或搬移),所以迭代器容易失效。


④ erase() 会返回下一个位置,
cpp 复制代码
 list<int> lt{1, 2, 3, 4, 5};
    // 找到值为3的位置
    auto it = lt.begin();
    ++it;
    ++it; // it指向3
    // 删除3,并返回下一个节点(即4)的迭代器
    it = lt.erase(it);
    cout << *it << endl; // 输出:4
    // 此时链表内容:1 2 4 5
    //如果要用erase来删除整个链表的内容:
    auto it = lt.begin(); // 从头开始遍历删除
    while (it != lt.end())
    {
        // 删除当前节点,并返回下一个节点的位置
        it = lt.erase(it);
    }

⑤ sort() 不能使用 std::sort()
cpp 复制代码
sort(lt.begin(), lt.end());   // ❌

这是错误的。因为:std::sort() 需要随机访问迭代器 。而 list 只有双向迭代器。

应该调用成员函数:

cpp 复制代码
lt.sort();   // ✔

⑥ reverse() 也尽量使用成员函数
cpp 复制代码
lt.reverse();

成员函数专门针对链表实现,效率更高。


⑦ merge() 合并前必须保证两个链表都有序

例如:

cpp 复制代码
list<int> lt1{1,3,5};
list<int> lt2{2,4,6};

lt1.merge(lt2);

结果:

text 复制代码
1 2 3 4 5 6

如果链表无序:

cpp 复制代码
list<int> lt1{3,1,5};
list<int> lt2{6,2,4};

lt1.merge(lt2); // ❌ 结果不可预期(不会自动排序)

正确做法应是先排序再合并!

cpp 复制代码
lt1.sort();
lt2.sort();
lt1.merge(lt2);

⑧ unique() 只能删除连续重复元素

如果想删除所有重复元素,一般先排序:

cpp 复制代码
lt.sort();
lt.unique();

⑨ splice() 是剪切,不是复制

例如:

cpp 复制代码
list<int> lt1{1,2,3};
list<int> lt2{4,5,6};

lt1.splice(lt1.end(), lt2);

结果:

text 复制代码
lt1:1 2 3 4 5 6
lt2:空

splice = 剪切(节点转移),节点直接移动过去,没有任何数据拷贝,因此效率非常高。


⑩ list 最大优势不是遍历,而是频繁插入删除操作

5,迭代器的类型:迭代器的简单介绍⭐️⭐️

1、迭代器的简单认识

迭代器(Iterator) 可以理解为 STL 为所有容器提供的一种统一访问元素的方式

迭代器类型 支持操作 典型容器/对象
输入迭代器(Input Iterator) ++*(只读) istream_iterator
输出迭代器(Output Iterator) ++*(只写) ostream_iterator
前向迭代器(Forward Iterator) ++ forward_listunordered_mapunordered_set
双向迭代器(Bidirectional Iterator) ++-- listsetmap
随机访问迭代器(Random Access Iterator) ++--+-[] vectorstringdeque

对于 STL 容器的学习,我们主要关注后三种:前向迭代器、双向迭代器和随机访问迭代器。输入迭代器和输出迭代器主要用于流(输入输出)操作,后续学习算法时再接触即可。

二、list 模拟实现------知识点 & 易错点总结⭐️⭐️⭐️

具体实现见博主的 Gitee 仓库,博主的 Gitee 链接:财哥

一、list 底层结构

  • list 底层采用带头双向循环链表实现。
  • 每个节点都包含三部分:
    • _data:存储数据。
    • _next:指向后继节点。
    • _prev:指向前驱节点。
  • 链表中始终存在一个哨兵节点(头节点)

二、关于模板

  • template<class T> 决定链表存储的数据类型。
  • 为了同时支持普通迭代器和 const 迭代器,迭代器设计成三个模板参数:
cpp 复制代码
template<class T,class Ref,class Ptr>

其中:

  • Ref 控制 operator*() 返回类型。
  • Ptr 控制 operator->() 返回类型。

最终利用 typedef 得到两种迭代器:

cpp 复制代码
typedef list_iterator<T,T&,T*> iterator;
typedef list_iterator<T,const T&,const T*> const_iterator;

这样就无需再写一个 const_iterator 类。


三、接口实现

1、list_node ------链表的节点类,

每个节点维护:

  • 数据
  • 前驱指针
  • 后继指针
cpp 复制代码
template<class T>
struct list_node
{
    T _data;//数据
    list_node<T>* _next;//后继指针
    list_node<T>* _prev;//前驱指针
};

2、iterator ------迭代器类⭐️⭐️⭐️

list 的迭代器本质就是对节点指针进行封装

至少需要实现:

  • operator*
  • operator->
  • operator++
  • operator--
  • 前置/后置 ++ --
  • == 与 !=
operator*
cpp 复制代码
Ref operator*()
{
    return _node->_data;
}

operator->()

一定要明确他的作用:

cpp 复制代码
it->_a1

实际上会被编译器翻译成

cpp 复制代码
it.operator->()->_a1

所以必须返回

cpp 复制代码
&_node->_data;

而不是返回对象本身。

cpp 复制代码
Ptr operator->()
{
    return &_node->_data;
}

前置++、--
cpp 复制代码
Self& operator++()//前置++
{
	_node = _node->_next;
	//return _node;注意返回类型
	return *this;
}

后置++、--

注意:

后置 ++ 返回修改前的值。返回值不能加引用

cpp 复制代码
Self operator++(int)
{
    Self tmp(_node);
    _node = _node->_next;
    return tmp;
}

注意返回值不能是引用!!!。



3、empty_init() ⭐️

由于多个构造函数都会初始化哨兵节点,因此将重复代码提取出来。

cpp 复制代码
void emptry_init()
{
	Node* head = new Node();
	_head = head;
	//哨兵节点没有很好的初始化
	_head->_next = _head;
	_head->_prev = _head;
	//size也没有初始化
	_size = 0;
}

注意:

  1. _next_prev 都必须指向自己。
  2. 别忘了初始化 _size

4、默认构造

默认构造直接调用初始化函数即可。

cpp 复制代码
list()
{
    emptry_init();
}

5、begin() / end()

由于采用带头双向循环链表:

  • begin 指向第一个有效节点
  • end 指向哨兵节点
cpp 复制代码
iterator begin()
{
    return _head->_next;
}

iterator end()
{
    return _head;
}

同时需要提供 const 版本


6、insert() ⭐️⭐️⭐️

插入操作是整个 list 最核心的接口。

代码核心:

cpp 复制代码
void insert(iterator pos, const T& x)
{
	//Node newnode(x);不能定义在栈上
	Node* newnode = new Node(x);
	newnode->_next = pos._node;
	newnode->_prev = pos._node->_prev;
	//pos._node->_prev->_next = newnode;
	pos._node->_prev->_next = newnode;
	pos._node->_prev = newnode;
	//别忘了维护size
	_size++;
}

最后可以优化为:

cpp 复制代码
//insert返回的是新插入元素的位置
template<class T>
typename list<T>::iterator list<T>::insert(iterator pos, const T& x)//	typename list<T>::iterator list类里面的iterator类型,不是变量 list<T>::insert(iterator pos, const T& x)
{
	
	//Node newnode(x);不能定义在栈上
	Node* newnode = new Node(x);
	newnode->_next = pos._node;
	newnode->_prev = pos._node->_prev;
	//pos._node->_prev->_next = newnode;
	pos._node->_prev->_next = newnode;
	pos._node->_prev = newnode;
	//别忘了维护size
	_size++;
	return newnode;
}

7、push_back()

尾插实际上就是:

cpp 复制代码
insert(end(), x);

8、push_front()

头插实际上就是:

cpp 复制代码
insert(begin(), x);

9、erase() ⭐️⭐️⭐️

代码核心:

cpp 复制代码
void erase(iterator pos)
{
	//不能删除end
	assert(pos != end());
	pos._node->_prev->_next = pos._node->_next;
	pos._node->_next->_prev = pos._node->_prev;
	//delete (*pos);不对!!!
	// delete  *(pos._node);也不对!!
	// delete 的参数必须是一个指针,但是释放的是这个指针所指向的对象。
	delete pos._node;
	//别忘了维护size
	size--;
}

注意:

  1. 不能删除 end()
  2. delete 的对象是指针,而不是对象。
  3. 返回下一个位置的迭代器,方便连续删除。

10、pop_back()

尾删直接调用:

cpp 复制代码
erase(--end());

11、pop_front()

头删直接调用:

cpp 复制代码
erase(begin());

12、clear() ⭐️⭐️⭐️

不能简单移动头指针。必须真正释放所有节点。

由于erase 后,it 已经失效。所以,必须先保存下一个位置。

cpp 复制代码
void clear()
{
	//这个不能简单的移动指针,而要全部删除
	//for (auto it = begin(); it != end(); ++it)
	//{
	//	//erase(it);经典问题,迭代器失效
	//	/*iterator tmp = it;
	//	erase(tmp);*///这样也是不对的,还是失效
	//}
	// erase 后 tmp 会失效,因此必须提前保存下一个位置
	auto it = begin();
	while (it != end())
	{
		auto tmp = it;
		++it;//必须先让他走到下一个位置
		erase(tmp);
	}
	//最简单的写法:
	/*while (!empty())
	{
		pop_back();
	}*/
}

13、析构函数
cpp 复制代码
~list()
{
    clear();
    delete _head;
}

14、拷贝构造 ⭐️⭐️

先初始化空链表。再依次尾插。

cpp 复制代码
list(const list& l)
{
    emptry_init();
    for(auto it=l.begin();it!=l.end();++it)
    {
        push_back(*it);
    }
}

注意:

  1. 不要直接复制 _head
  2. 必须深拷贝所有节点

15、initializer_list 构造

支持:

cpp 复制代码
list<int> lt={1,2,3,4};

实现思路:

cpp 复制代码
for(const auto& e:il)
{
    push_back(e);
}

坑:

  1. initializer_list 是只读序列。
  2. 不支持 []
  3. 一般使用范围 for 遍历。

16、swap()

交换两个 list:

cpp 复制代码
	void swap(list& l)
{
	std::swap(_head, l._head);
	//忘记交换size
	std::swap(_size, l._size);
}

17、operator= ⭐️⭐️

采用 Copy-Swap 写法:

cpp 复制代码
list& operator=(list l)
{
    swap(l);
    return *this;
}

18、size()
cpp 复制代码
return _size;

19、empty()
cpp 复制代码
return _size==0;

四,主播在模拟实现时犯的那些错!

1,初始化列表写法错误(今天出现 2 次)

初始化列表:要用括号初始化!!!

cpp 复制代码
list()
    :_head(nullptr)
    ,_size(0)
{}

2,begin() / end() 返回错误

注意我们实现的是:带头双向循环链表:,有哨兵节点!

3,节点不要申请到栈上

一般都要用new 来申请空间

4,delete 理解错误

new的时候是T*什么什么,所以,delete后面也是跟指针 ,就是和malloc类似

5,忘记维护 _size

6,assert判断写反

7,重复维护 _size

erase已经维护了size

8,注意:erase后迭代器失效!但是可以接收继续使用

9,initializer_list 构造函数

支持:begin,end,size;

建议:以后每写一个接口,都建议自己问一句:这个接口会不会改变链表的结构?如果会,需要同步维护哪些成员 ?还有,不要忘记写返回值!!!

五,补充知识:

1、iterator 为什么设计成三个模板参数?⭐️⭐️⭐️

小知识:如果两份代码逻辑完全相同,仅仅类型不同,就应该考虑使用模板来消除重复代码。

为了能够同时支持普通迭代器和 const 迭代器,如果只使用一个模板参数 T,我们通常需要分别实现两个迭代器类

cpp 复制代码
template<class T>
struct list_iterator
{
    T& operator*();
    T* operator->();
    ......
};
template<class T>
struct list_iterator
{
   const T& operator*();
   const T* operator->();
   ......
};

我们可以发现:普通迭代器const 迭代器 的区别其实只有两个地方:

  • operator*() 返回值不同
  • operator->() 返回值不同

除了这两个返回值,其余代码(++--==!= 等)完全一样。

如果分别写两个类:那么 90% 的代码都会重复。

因此 STL 采用三个模板参数:

cpp 复制代码
template<class T,class Ref,class Ptr>

其中:

cpp 复制代码
Ref决定operator*()
Ptr决定operator->()

于是只需要写一份代码:

cpp 复制代码
Ref operator*()
{
    return _node->_data;
}
Ptr operator->()
{
    return &_node->_data;
}

那他什么时候会实例化?

当你在后面用到的时候,比如:

cpp 复制代码
list_iterator<T, T&, T*> begin()
{
    return _head->_next;
}
list_iterator<T, const T&, const T*> begin() const
{
    return _head->_next;
}

那当编译的时候,他就可以分别实例化出类似于

cpp 复制代码
struct list_iterator<int, int&,int*>
{
    int& operator*();
    int* operator->();
    ......
};
struct list_iterator<int, const int&, const int*> 
{
   const int& operator*();
   const int* operator->();
   ......
};

也就是说,这里是用来两套模版,

第一层 T 用来决定容器存储的数据类型(如 int、double、string)

第二层 T、Ref、Ptr 用来决定迭代器访问元素时返回的类型,是const修饰还是普通的,从而复用普通迭代器和 const 迭代器的大部分代码。

我说白了,这就是把两段只有类型不同的代码把类型换成一个通用的名字和并成一份代码,然后,后面根据不同的模版参数实例化出不同类型的代码!!

最后再利用 typedef(或 using)为这两个具体类型分别起一个名字:

cpp 复制代码
typedef list_iterator<T,T&,T*> iterator;
typedef list_iterator<T,const T&,const T*> const_iterator;

总结:

三个模板参数并不是为了支持不同的数据类型,而是为了复用普通迭代器和 const 迭代器的大部分实现,只让返回值不同。


10、operator->() 为什么返回指针?⭐️⭐️⭐️

我们首先要明白,在迭代器里面,我们想要达到什么样的效果

cpp 复制代码
struct AA
{
    int _a1;
};
list<AA> lt;
auto it = lt.begin();
it->_a1 = 10;

想要就是迭代器it->_a1 可以访问到list里面存的自定义类型里面的数据!

那么,我们要访问到AA里面的数据肯定是要这样子的

cpp 复制代码
AA*->_a1

因此,operator->() 必须返回一个指针。

正确实现:

cpp 复制代码
Ptr operator->()
{
    return &_node->_data;//&这里是取地址符号哦!
}

千万不要写成:

cpp 复制代码
return _node->_data;

总结:

operator->() 之前,一定先想清楚它是怎样被调用的。

it->_a1

实际上等价于:

cpp 复制代码
it.operator->()->_a1;

所以返回值必须是 指针,而不是对象,也不是引用。


三、总结与回顾

一、list 的底层结构

list 底层采用带头双向循环链表实现。

每个节点包含:

  • _data:存储数据
  • _next:后继节点
  • _prev:前驱节点

整个链表始终存在一个哨兵节点(head)


二、list 与 vector 最大区别

vector list
连续存储 链式存储
支持 [] 不支持 []
随机访问 O(1) 随机访问 O(N)
尾插效率高 任意位置插删效率高
扩容可能导致迭代器失效 节点不移动,迭代器更稳定

一句话:

vector 擅长访问;list 擅长插删。


三、list 最重要的接口

大部分与vector一样,list新增的

cpp 复制代码
merge()//合并两个有序链表
remove()//删除指定值  
unique()//删除连续重复元素
splice()//剪切
  1. 不支持\[\]、at()、begin()+n 因为:list 只有双向迭代器,不支持随机访问。
  2. 插入、删除必须使用迭代器
  3. erase() 返回的是下一个位置的迭代器
  4. sort() 要调用成员函数。
  5. merge() 前必须保证两个链表有序
  6. unique() 只能删除连续重复元素如果想去重,一般先排序:
  7. splice()剪切节点,不是复制数据。因此效率非常高
  8. operator->() 必须返回指针,而不是对象。

下期预告

stack和queue

结语

**  本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。**

**  也欢迎订阅我的深入理解 C++系列专栏快速复习系列专栏算法系列专栏。**


**  愿每一次敲下键盘,都比昨天更进一步!** **  愿每一行代码落下,都让未来多一种可能!**

相关推荐
NoteStream2 小时前
【C语言基础】分支和循环(上)
c语言·开发语言·c++·经验分享·笔记·算法·c#
2401_827499992 小时前
C++(黑马)05-提高编程
java·开发语言·c++
进击的程序猿~3 小时前
Go Interface源码深度解析指南
开发语言·后端·golang
hold?fish:palm3 小时前
链表的基本原理和实现(C++版本)
数据结构·c++·链表
jufeng13074 小时前
【系列:MiniKV 原理剖析 · 第 5 篇】
linux·网络·c++·软件工程
Data_Journal4 小时前
如何使用 Java 和 Jsoup 解析 HTML
大数据·开发语言·数据库·python·scrapy
吃好睡好便好4 小时前
MATLAB仿真框图2
开发语言·matlab·仿真·simulink
豆沙沙包?4 小时前
C++-程序的内存模型(P84-P88)
java·jvm·c++
jufeng13074 小时前
【系列:MiniKV 原理剖析 · 第 8 篇(完结篇)】
linux·c++·log4j·软件工程·makefile