C++打怪升级(十一)- STL之list

~~~~

  • 前言
  • 1. list是什么
  • 2. list接口函数的使用
    • 1. 构造相关
      • 默认构造
      • n个val构造
      • 迭代器范围构造
      • 拷贝构造
    • 2 赋值运算符重载函数
    • 2 析构函数
    • 3 迭代器相关
      • begin 和 end
      • rbegin 和rend
    • 4 容量相关
      • empty
      • size
    • 5 元素访问相关
      • front
      • back
    • 6 修改相关
      • push_back
      • pop_back
      • push_front
      • pop_front
      • insert - 插入新值
      • erase
      • resize
      • assign
      • swap
      • clear
    • 7 其他操作相关
      • reverse - 逆置
      • sort - 排序
      • unique - 去重
      • merge - 合并
      • remove - 删除值为val的元素
      • remove_if - 删除满足条件的元素
      • splice - 从list对象转移元素到另一个list对象
    • 8 非成员函数
      • swap
  • 3. list的模拟实现
    • 基本框架
      • 定义节点结构
      • 定义链表结构
      • 定义迭代器结构 - 类的封装
        • 迭代器分类
        • 模板类的类型与内部类型的特殊使用方式
        • 版本1:只支持非const对象
          • 基本结构
          • 构造函数
          • *运算符重载
          • ->运算符重载
          • 前置++运算符重载
          • 后置++运算符重载
          • 前置--运算符重载
          • 后置--运算符重载
          • !=运算符重载
          • ==运算符重载
          • 代码汇总
        • 版本2:支持const对象和非const对象
          • 基本结构
          • *运算符重载
          • ->运算符重载
          • 代码汇总
        • 版本3:支持const对象和非const对象且简化写法
          • 基本结构
          • *运算符重载
          • ->运算符重载
          • 前置++
          • 后置++
          • 前置--
          • 后置--
          • !=运算符重载
          • ==运算符重载
          • 代码汇总
      • 构造函数
        • 初始化链表 - 代码复用
        • 无参构造
        • 迭代器范围构造
      • 拷贝构造函数
        • 传统写法 - 自己实现
        • 现代写法 - 复用
      • 赋值运算符重载函数
        • 传统写法 - 自己实现
        • 现代写法 - 复用
      • 析构函数
      • 正向迭代器相关
        • begin
        • end
      • 增删
        • insert
        • erase
        • push_back
        • pop_back
        • push_front
        • pop_front
        • clear
        • swap
        • resize
        • size
      • 关系运算符重载
        • ==
        • !=
  • list与vector比较
    • list与vector排序效率简要分析
    • list与vector迭代器差异再次分析 - 调试
  • 结语

美图

前言

本节将介绍list的使用,之后模拟实现list的基本功能,最后将分析与连续顺序容器(vector)的差异。


1. list是什么

list是逻辑上的顺序容器,实际list的储存中是不连续的,相邻节点之间通过指针进行链接。
list是带头节点的双向循环链表,在所有的链表中结构最复杂,使用也最方便。
list在头部和尾部的插入和删除操作、中间部分的插入删除操作都是常数时间,效率很高;与连续顺序容器相比的优势是中间位置的插入和删除操作时间复杂度是常数时间 O ( 1 ) O(1) O(1),而像vector在中间插入或删除元素往往涉及到元素的移动,时间复杂度是 O ( N ) O(N) O(N)。劣势是list作为不连续存储的顺序容器不能实现随机访问元素 O ( 1 ) O(1) O(1),需要从第一个元素开始依次查找,直到找到或到最后一个元素为止,时间复杂度是 O ( N ) O(N) O(N)
image.png


2. list接口函数的使用

1. 构造相关

初始化list对象

默认构造

声明

list ();

eg

list<int> l1;

n个val构造

声明

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

eg

list<int> l1(5, 7); // l1: 7 7 7 7 7

迭代器范围构造

声明

template <class InputIterator>
list (InputIterator first, InputIterator last);

eg

vector<int> v{1, 2, 3, 4, 5};
list<int> l1(v.begin() + 1, v.end()); // l1: 2 3 4 5

拷贝构造

声明

list (const list& x);

eg

list<int> l1(5, 6); // 6 6 6 6 6
list<int> l2(l1);   // l2: 6 6 6 6 6 

2 赋值运算符重载函数

声明

list& operator= (const list& x);

eg

list<int> l1(4, 3); // l1: 3 3 3 3
list<int> l2(3, 6); // l2: 6 6 6
l2 = l1; // l2: 3 3 3 3

2 析构函数

释放对象申请的所有资源,为对象销毁做好准备。

~list();

3 迭代器相关

迭代器是像指针一样的类型。

begin 和 end

声明

begin

返回第一个对象的所在位置的迭代器。

iterator begin();
const_iterator begin() const;

end

返回最后一个对象所在位置的下一个位置的迭代器。

iterator end();
const_iterator end() const;

eg

list<int> l1{1,2,3,4,5};
list<int>::iterator it = l1.begin();
while (it != l1.end()) {
    cout << *it << " ";
    it++;
}
cout << endl << endl; // 结果为 1 2 3 4 5

rbegin 和rend

声明

rbegin

返回第一个对象的前一个位置的迭代器。

reverse_iterator rbegin();
const_reverse_iterator rbegin() const;

rend

返回最后一个对象的迭代器。

reverse_iterator rend();
const_reverse_iterator rend() const;

eg

list<int> l1{1,2,3,4,5};

list<int>::reverse_iterator rit = l1.rbegin();
while (rit != l1.rend()) {
    cout << *rit << " ";
    rit++;
}
cout << endl << endl; // 结果为 5 4 3 2 1

4 容量相关

empty

声明
判断list对象是否为空,是返回true;反之返回false。

bool empty() const;

eg

list<int> l1;
l1.empty();      // true
l1.push_back(10);
l1.empty();      // false

size

声明
返回list对象的元素个数

size_type size() const;

5 元素访问相关

front

声明
返回第一个有效元素的引用。
当list为空时调用front函数会产生未定义的行为。

reference front();
const_reference front() const;

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
l1.front(); // 1

back

声明
返回最后一个元素的引用。
当list为空时调用back函数会产生未定义的行为。

reference back();
const_reference back() const;

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
l1.back(); // 5

6 修改相关

push_back

声明
尾插一个新元素

void push_back (const value_type& val);

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
l1.push_back(10); // l1: 1 2 3 4 5 10
l1.size()         // 6

pop_back

声明
尾删一个元素。
当list对象为空时调用该函数程序将会出错。

void pop_back();

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
l1.pop_back(); // l1: 1 2 3 4
l1.size()         // 4

push_front

声明
头插一个新元素

void push_front (const value_type& val);

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
l1.push_front(9); // l1: 9 1 2 3 4 5
l1.size()         // 6

pop_front

声明
头删一个元素
当list对象为空时调用该函数程序将会出错。

void pop_front();

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
l1.pop_front(); // l1: 2 3 4 5
l1.size()         // 4

insert - 插入新值

声明
在迭代器pos指向的元素之前插入一个值为val的元素。

iterator insert (iterator pos, const value_type& val);

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
list<int>::iterator pos = find(l1.begin(), l1.end(), 3);

l1.insert(pos, 9); // l1: 1 2 9 3 4 5

声明
在迭代器pos指向的元素之前插入n个值为val的元素。

void insert (iterator pos, size_type n, const value_type& val);

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
list<int>::iterator pos = find(l1.begin(), l1.end(), 3);

l1.insert(pos, 3, 6); // l1: 1 2 6 6 6 3 4 5

声明
在迭代器pos指向的元素之前插入指定迭代器范围内的所有元素。
指定的迭代器范围是左闭右开区间。

template <class InputIterator>
void insert (iterator pos, InputIterator first, InputIterator last);

eg

list<int> l1{1,2,3,4,5}; // l1: 1 2 3 4 5
vector<int> v{7, 8, 9, 0};
list<int>::iterator pos = find(l1.begin(), l1.end(), 3);

l1.insert(pos, 3, 6); // l1: 1 2 7 8 3 4 5

erase

声明
删除迭代器pos指向的元素

iterator erase (iterator position);

eg

list<int> l1{1, 2, 3, 4, 5}; // l1: 1 2 3 4 5
list<int>::iterator pos = find(l1.begin(), l1.end(), 3);
if (pos != l1.end()) {
    l1.erase(pos);           // l1: 1 2 4 5a
}

声明
删除迭代器范围内的所有元素。

iterator erase (iterator first, iterator last);

eg

list<int> l1{1, 2, 3, 4, 5};    // l1: 1 2 3 4 5
list<int>::iterator pos = find(l1.begin(), l1.end(), 3);
l1.erase(l1.begin(), l1.end()); // l1: 空

resize

声明
设置list对象的大小为n,每个元素都使用val初始化;
如果 n 大于list对象的大小,就尾插新元素直到list对象大小恰好为n;
如果 n 小于list对象的大小,就尾删元素直到list对象大小恰好为n;
如果 n 等于list对象大小,就什么也不做。

void resize (size_type n, value_type val = value_type());

eg

list<int> l1{1,2,3,4,5};	// l1: 1 2 3 4 5
l1.size();					// 5

l1.resize(8, 9);			// l1: 1 2 3 4 5 8 8 8
l1.size();					// 8

l1.resize(3, 1);			// l1: 1 2 3
l1.size();					// 3

assign

用新值替换就值

声明
使用n个val赋值给list对象。
list对象的大小可能发生变化。

void assign (size_type n, const value_type& val);

eg

list<int> l1{1, 2, 3, 4, 5};	// l1: 1 2 3 4 5
l1.size();						// 5
vector<int> v{2, 2, 3, 3};
l1.assign(4, 6);				// l1: 6 6 6 6
l1.size();						// 4

声明
使用指定迭代器范围内的所有元素赋值给list对象。

template <class InputIterator>
void assign (InputIterator first, InputIterator last);

eg

list<int> l1{1, 2, 3, 4, 5};	// l1: 1 2 3 4 5
l1.size();						// 5
vector<int> v{2, 2, 3, 3};
					// 4
l1.assign(v.begin(), v.end());	// l1: 2 2 3 3
l1.size();						// 4

swap

声明
交换两个list对象的内容。

void swap (list& x);

eg

list<int> l1{1, 2, 3, 4, 5};
l1.size();
list<int> l2{2, 2, 3 ,3};
l2.size();

l1.swap(l2);
// l1: 2 2 3 3		size: 4
// l2: 1 2 3 4 5	size: 5

clear

声明
删除list对象的所有元素,并使list对象的大小为0。

void clear();

eg

list<int> l1{1, 2, 3, 4, 5};// l1: 1 2 3 4 5
l1.size();					// 5

l1.clear();					// 清空l1
l1.size();					// 0

7 其他操作相关

reverse - 逆置

声明
逆置list对象的所有元素。

void reverse();

eg

list<int> l1{1, 2, 3, 4, 5};	// l1: 1 2 3 4 5
l1.reverse();					// l1: 5 4 3 2 1
reverse(l1.begin(), l1.end());	// l1: 1 2 3 4 5

sort - 排序

声明
对list对象的所有元素进行排序,无参时采用默认排序规则;有参时传入函数指针或仿函数,使用函数或仿函数定义的规则。
排序方法是归并排序。

void sort();

template <class Compare>
void sort (Compare comp);

eg

list<int> l1{1, 3, 5, 2, 4, 6};		// l1: 1 3 5 2 4 6
l1.sort();							// l1: 1 2 3 4 5 6
                                	// 传入函数指针
list<int> l2{ 1, 3, 5, 2, 4, 6 };	// l2: 1 3 5 2 4 6
l2.sort(compare<int>);				// l2: 6 5 4 3 2 1
                                	// 传入函数对象
list<int> l3{ 1, 3, 5, 2, 4, 6 };	// l3: 1 3 5 2 4 6
l3.sort(com<int>());				// l3: 6 5 4 3 2 1

从大到小排序

template<class T>
bool compare(const T& t1, const T& t2) {
	return t1 > t2;
}

从大到小排序

template<class T>
struct com {
	bool operator()(const T& t1, const T& t2) {
		return t1 > t2;
	}
};

unique - 去重

声明
无参数时,删除相邻的重复元素;
有参数时,参数是一个函数指针或函数对象,删除的不再是重复的相邻元素,而是满足传入参数所设置的条件的元素;
unique函数不对list对象所有参数进行排序,所以在使用该函数之前需要额外调用排序函数进行排序,以保证重复的元素是相邻的。

void unique();

template <class BinaryPredicate>
void unique (BinaryPredicate binary_pred);

eg

list<int> l1{3, 2, 3, 1, 1, 2, 2}; // l1: 3, 2, 3, 1, 1, 2, 2
                                   // 不排序直接去重
l1.unique();					   // l1: 3, 2, 3, 1, 2

list<int> l2{3, 2, 3, 1, 1, 2, 2};	// l2: 3, 2, 3, 1, 1, 2, 2
                                	// 先排序在去重
l2.sort();							// l2: 1, 1, 2, 2, 2, 3, 3
l2.unique();						// l2: 1, 2, 3

merge - 合并

声明
合并两个已经排好序的list对象;
无参时,按默认规则把x中的所有元素依次转移到本list对象中,转移的操作为把x的元素的值依次与本list对象元素的值进行比较,如果x的元素的值大于比较的值则把x的元素插入到比较的元素之后;反之就继续比较直到最后一个元素。转移完成后,x对象就为空了。
有参时,区别是传入的函数指针或函数对象定义了比较的规则,转移时不在根据默认默认的规则进行。

void merge (list& x);

template <class Compare>
void merge (list& x, Compare comp);

eg

list<int> l1{1, 9, 7, 6, 4};	// l1: 1 9 7 6 4 size: 5
list<int> l2{3, 2, 8};			// l2: 3 2 8     size: 3

l1.sort();						// l1: 1 4 6 7 9
l2.sort();						// l2: 2 3 8

l1.merge(l2);					// l1: 1 2 3 4 6 7 8 9	size: 8
                                // l2: 					size: 0

remove - 删除值为val的元素

声明

void remove (const value_type& val);

eg

list<int> l1{0, 1, 2, 3, 3, 4, 3}; 	// l1: 0, 1, 2, 3, 3, 4, 3	size: 7
l1.remove(3);						// l1: 0, 1, 2, 4   		size: 4

remove_if - 删除满足条件的元素

声明

template <class Predicate>
void remove_if (Predicate pred);

eg

list<int> l2{0, 1, 2, 3, 4};	// l1: 0, 1, 2, 4		size: 4
l2.remove_if(isOdd());			// l1: 1, 3    			size: 2

是偶数就返回true

struct isOdd {
bool operator()(const int& t) {
    return t % 2 == 0;
}
};

splice - 从list对象转移元素到另一个list对象

声明
把list对象x的元素转移到另一个list对象的pos迭代器位置之前;

// 转移x所有的元素
void splice (iterator pos, list& x);
// 转移x迭代器i指向的元素
void splice (iterator pos, list& x, iterator i);
// 转移x中指定迭代器范围的元素
void splice (iterator pos, list& x, iterator first, iterator last);

eg

list<int> l1{1, 2, 3, 4, 5};				// l1: 1, 2, 3, 4, 5
list<int> l2{6, 7, 8};						// l2: 6, 7, 8
auto pos = find(l1.begin(), l1.end(), 3);
l1.splice(pos, l2);							// l1: 1, 2, 6, 7, 8, 3, 4, 5	size: 8
                                            // l2: 							size: 0

8 非成员函数

swap

声明

template <class T>
void swap (list<T>& x, list<T>& y);

eg

list<int> l1{1, 2, 3};    		// l1: 1, 2, 3		size: 3
list<int> l2{6, 7, 8, 9};	  	// l2: 6, 7, 8, 9	size: 4
swap(l1, l2);					// 交换                            	
								// l1: 6, 7, 8, 9	size: 4
								// l2: 1, 2, 3		size: 3

3. list的模拟实现

本次list实现是简化版,主要以学习为主,着重关注的是list的基本结构和其迭代器的具体实现。特别是const的迭代器与连续顺序容器的迭代器(以Linux下G++采用的SGI版本为例,VS2019下vector的迭代器被封装为了一个类,不再是原生指针)有着巨大的差异:如vector的迭代器是由原生指针typedef而来的,而list迭代器必须封装为一个类才能达到与vector迭代器类似的行为。

基本框架

list是带头双向循环链表,本次实现参考的是Linux中g++中采用的STL的SGI版。

定义节点结构

由于list可以存放多种类型的数据,需要采用类模板的方式。
包含三个成员变量:

节点指针_prev: 指向前一个节点
节点指针_next: 指向后一个节点
_val: 节点储存该类型对应的元素

image.png
struct和class作用基本相同,只是默认成员变量和成员函数是public的。
下文定义的list类模板需要在类内访问节点的成员变量和成员函数,直接使用struct定义节点结构就可以方便实现需求。

template<class T>
struct __list_node {
    __list_node(const T& val)
        :_val(val)
        ,_prev(nullptr)
        ,_next(nullptr){ }

    __list_node* _prev;
    __list_node* _next;
    T _val;
};

定义链表结构

链表结构list也是一个类模板,因为其节点储存的元素类型不确定;
list的成员变量有两个:

节点指针_phead: 指向头结点,头结点是整个链表的开始但不存放有效元素
_size: 表示当前链表的有效节点个数,不包括头结点

template<class T>
class list {
	typedef __list_node<T> node; // 对节点类实例化重定义以简化写法
public:
	typedef __list_iterator<T> iterator; // 迭代器类型
	typedef __list_const_iterator<T> const_iterator;
private:
    node* _phead;
    size_t _size;
};

定义迭代器结构 - 类的封装

迭代器分类

原生指针实现;
类的封装实现;

模板类的类型与内部类型的特殊使用方式

类名与类型的区别:
普通类:类名就是类型;
类模板:类名+模板参数才是类型,
例外是:在类内可以省略模板参数,只写类名表示类型;
但是并不推荐这种写法,应该写出具体完整的类型,防止以后给自己或他人造成困扰;
例如标准库中list的构造实现:
image.png

版本1:只支持非const对象

迭代器对于链表来说,是一个行为像指针一样的类型。
指针支持的操作有:

解引用*、->、前置++、后置++、前置–、后置–、指针比较操作

原生的节点指针:

  • 不支持++、–操作;
  • 解引用操作*得到的是整个节点而不是预期的节点的值;
  • ->操作得到的是整个节点的地址,而不是节点值的地址;
  • 比较操作是符合的,可以满足比较操作;

如string、vector可以直接借助原生指针实现迭代器类型(当然,不是所有版本的string、vector的迭代器是原生指针,可能是被封装为了一个复杂的类),这依赖于它们天生的物理结构优势-连续存储。

原生指针的迭代器基本不能满足list对迭代器的需求,所以需要把迭代器定义为一个单独的类,依赖运算符重载功能对**运算符*、->、前置++、后置++、前置–、后置–**进行重载以便于满足我们对迭代器的预期。

对迭代器的预期:

  • *得到节点内部的值;
  • ->得到节点内部值的地址;
  • 支持++:迭代器由当前节点指向下一个节点;
  • 支持–:迭代器由当前节点指向前一个节点;
基本结构

迭代器只包含一个节点指针成员变量

template<class T>
struct __list_iterator {
    typedef __list_node<T> node;
    typedef __list_iterator<T> iterator;
    node* _pnode;
};
构造函数

实现基本的构造函数,接受节点地址进行初始化;
由于迭代器类本身不涉及资源的申请和释放,故析构、拷贝构造、赋值运算符重载等函数都不需要显式实现,由编译器生成默认的即可。

__list_iterator(node* pnode)
    :_pnode(pnode){ }
*运算符重载

得到节点内部的值并传引用返回;
由于这个迭代器是非const的,所以迭代器指向的节点也是非const的,故返回类型是非const的:T&。

T& operator*() {
    return _pnode->_val;
}
->运算符重载

返回节点内部值的地址;
由于这是非const迭代器,所以迭代器指向的节点也是非const的,故·返回类型是非const的:T*

T* operator->() {
    return &(_pnode->_val);
}
前置++运算符重载

迭代器指向下一个位置,返回下一个位置的迭代器;
即前置++返回++之前的值;

iterator operator++() {
    _pnode = _pnode->_next;
    return *this;
}
后置++运算符重载

迭代器指向下一个位置,返回当前位置的迭代器;
即后置++返回++之后的值

iterator operator++(int) {
    iterator tmp(*this);
    _pnode = _pnode->_next;
    return tmp;
}
前置–运算符重载

迭代器指向前一个位置,返回前一个位置的迭代器;

iterator operator--() {
    _pnode = _pnode->_prev;
    return *this;
}
后置–运算符重载

迭代器指向前一个位置,返回当前位置的迭代器;

iterator operator--(int) {
    iterator tmp(*this);
    _pnode = _pnode->_prev;
    return tmp;
}
!=运算符重载

两个迭代器不相等即两个迭代器指向的节点不相等;

bool operator!=(iterator it) {
    return _pnode != it._pnode;
}
==运算符重载

两个迭代器相等即两个迭代器指向的节点相等;

bool operator==(iterator it){
    return !(*this != it);
}
代码汇总
template<class T>
struct __list_iterator {
    typedef __list_node<T> node;
    typedef __list_iterator<T> iterator;
    node* _pnode;

    __list_iterator(node* pnode)
        :_pnode(pnode){ }
	T& operator*() {
        return _pnode->_val;
    }
    iterator operator++() {
        _pnode = _pnode->_next;
        return *this;
    }
    iterator operator++(int) {
        iterator tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }
    iterator operator--() {
        _pnode = _pnode->_prev;
        return *this;
    }
    iterator operator--(int) {
        iterator tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }
    bool operator!=(iterator it) {
        return _pnode != it._pnode;
    }
	bool operator==(iterator it){
        return !(*this != it);
    }
};
版本2:支持const对象和非const对象

版本1只实现了非const的迭代器,对于const对象无法使用非const迭代器,需要实现const迭代器;
也许你有疑问,为什么不是普通迭代器前加上const就是const迭代器呢?

比如:typedef const __list_iterator const_iterator;

首先清楚const迭代器的const实际上修饰的谁?
肯定不是修饰的迭代器本身,因为不管迭代器是const还是非const的都应该能满足++、–操作,而++、–迭代器一定会改变迭代器,故迭代器本身是非const的;而简单的在普通迭代器之前加上const修饰而成的迭代器将会导致迭代器本身是const的而不能进行++、–操作,故其不是const迭代器。
那const修饰的是谁呢?
const迭代器的const,修饰的其实是迭代器指向的节点,特别是包括节点内的值;
const迭代器与普通迭代器的不同在于operator()和->operator返回类型分别是const T&和const T
至于++、–、比较大小操作同普通迭代器相同;
需要一个新的类模板作为const迭代器类,以满足于不同于普通迭代器的*和->操作;

基本结构
template<class T>
struct __list_const_iterator {
    typedef __list_node<T> node;
    typedef __list_const_iterator<T> const_iterator;
    node* _pnode;
};
*运算符重载

返回迭代器指向节点内的值的引用;
由于是const迭代器,所以指向的节点是const的,故返回类型是const T&;

const T& operator*() {
    return _pnode->_val;
}
->运算符重载

返回迭代器指向节点内的值的地址;
由于是const迭代器,所以指向的节点是const的,故返回类型是const T*;

const T* operator->(){
    return &(_pnode->_val);
}
代码汇总
template<class T>
struct __list_const_iterator {
    typedef __list_node<T> node;
    typedef __list_const_iterator<T> const_iterator;
    node* _pnode;

    __list_const_iterator(node* pnode)
        :_pnode(pnode) { }
    const T& operator*() {
        return _pnode->_val;
    }
    const_iterator operator++() {
        _pnode = _pnode->_next;
        return *this;
    }
    const_iterator operator++(int) {
        const_iterator tmp(*this);
        _pnode = _pnode->_next;
        return tmp;
    }
    const_iterator operator--() {
        _pnode = _pnode->_prev;
        return *this;
    }
    const_iterator operator--(int) {
        const_iterator tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }
    bool operator!=(const_iterator it) {
        return _pnode != it._pnode;
    }
	bool operator==(const_iterator it){
        return !(*this != it);
    }
};
版本3:支持const对象和非const对象且简化写法

版本1和版本2分别是普通迭代器的实现和const迭代器的实现,仔细分析发现二者的代码基本上很相似,主要区别是*和->操作返回的类型,普通迭代器返回非const的类型,const迭代器返回const的类型;

为了简化代码书写,选择合并这两个类模板,把两个不同之处分别多增加参数进行表示:

操作的返回类型分为普通类型和const类型,用参数Ref表示;
->操作的返回类型分为普通
类型和const*类型,用参数Ptr表示;

本质仍然是会生成两个迭代器类型,只是不再是由我们自己显式写出,而是由编译器根据类模板及其参数进行推导然后生成两个迭代器类型,所做的工作没有减少,只是交给类编译器承担。

基本结构
template<class T, class Ref, class Ptr>
struct __list_iterator {
    typedef __list_node<T> node;
    typedef __list_iterator<T, Ref, Ptr> Self;
    node* _pnode;
};

typedef __list_iterator<T, Ref, Ptr> Self;
在一开始,重定义了迭代器类型本身,简化后续书写;
如果还需要对迭代器模板参数进行改动只需要在此处进行修改即可,不需要对后续涉及到的代码进行修改,很便利。

*运算符重载
Ref operator*() {
    return _pnode->_val;
}
->运算符重载
Ptr operator->() {
    return &(_pnode->Val);
}
前置++
Self& operator++() {
    _pnode = _pnode->_next;
    return *this;
}
后置++
Self operator++(int) {
    Self tmp = *this;
    ++*this;
    return tmp;
}
前置–
Self& operator--() {
    _pnode = _pnode->_prev;
    return *this;
}
后置–
Self operator--(int) {
    Self tmp(*this);
    _pnode = _pnode->_prev;
    return tmp;
}
!=运算符重载
bool operator!=(const Self& it) const{
    return _pnode != it._pnode;
}
==运算符重载
bool operator==(const Self& it) const {
    return !(*this != it);
}
代码汇总
template<class T, class Ref, class Ptr>
struct __list_iterator {
    typedef __list_node<T> node;
    typedef __list_iterator<T, Ref, Ptr> Self;
    node* _pnode;
    __list_iterator(node* pnode)
        :_pnode(pnode) { }
    Ref operator*() {
        return _pnode->_val;
    }
    Ptr operator->() {
        return &(_pnode->Val);
    }
    Self& operator++() {
        _pnode = _pnode->_next;
        return *this;
    }
    Self operator++(int) {
        Self tmp = *this;
        ++*this;
        return tmp;
    }
    Self& operator--() {
        _pnode = _pnode->_prev;
        return *this;
    }
    Self operator--(int) {
        Self tmp(*this);
        _pnode = _pnode->_prev;
        return tmp;
    }
    bool operator!=(const Self& it) const{
        return _pnode != it._pnode;
    }
    bool operator==(const Self& it) const {
        return !(*this != it);
    }
};

构造函数

初始化链表 - 代码复用

当定义一个list对象时,将会调用构造函数对该对象进行初始化:申请一个节点作为头结点,并使节点指针均指向自己。
除了无参的构造函数需要这个功能,迭代器范围做参数的构造函数、拷贝构造函数也需要该功能,为了减少代码冗余,便把申请头结点并处理的功能代码单独抽出来作为一个函数供其他需要的构造函数调用。

void initialize() {
    _phead = new node(T());
    _phead->_next = _phead;
    _phead->_prev = _phead;
    _size = 0;
}
无参构造
list() {
    initialize();
}
迭代器范围构造

范围: [ f i r s t , l a s t ) [first, last) [first,last)

template<class InputIterator>
list(InputIterator first, InputIterator last) {
    initialize();
    while (first != last) {
        push_back(*first);
        ++first;
    }
}

拷贝构造函数

传统写法 - 自己实现

申请并初始化头结点
遍历链表lt,依次取节点中的元素尾插到本链表this中

list(const list<T>& lt) {
    initialize();
    node* cur = lt._phead->_next;
    while (cur != lt._phead) {
        push_back(cur->_val);
        cur = cur->_next;
    }
}
现代写法 - 复用

复用迭代器范围的构造函数先构造一个局部list对象tmp
交换tmp内指针_phead和this内指针_phead,之后除了构造函数作用域tmp对象将自动销毁

list(const list<T>& lt) {
    initialize();
    
    list<T> tmp(lt.begin(), lt.end());
    swap(tmp);
}

赋值运算符重载函数

传统写法 - 自己实现

避免自己给自己赋值,特殊判断一下,比较两个list对象用到了!=的运算符重载
赋值之前先依次delete本身的所有节点

list<T>& operator=(const list<T>& lt) {
    if (*this != lt) {
        clear();
        node* cur = lt._phead->_next;
        while (cur != lt._phead) {
            push_back(cur->_val);
            cur = cur->_next;
        }
    }

    return *this;
}
现代写法 - 复用

由传引用传参变为传值传参构造局部list对象lt
交换本对象this和lt内部的_phead指针

list<T>& operator=(list<T> lt) {
    swap(lt);
    return *this;
}

析构函数

delete释放所有申请的节点,包括头节点
复用了clear函数实现出头节点之外的所有节点的delete释放

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

正向迭代器相关

begin

头结点不是储存数据节点,第一个储存数据的有效节点是头结点的下一个节点

iterator begin() {
    return iterator(_phead->_next);
}
const_iterator begin() const {
    return const_iterator(_phead->_next);
}
end

循环链表,最后一个节点的下一个节点是头结点

iterator end() {
    return iterator(_phead);
}
const_iterator end() const {
    return const_iterator(_phead);
}

增删

insert

在pos迭代器指向的节点之前插入一个新节点

image.png
image.png

iterator insert(iterator pos, T val) {
    // prev cur newnode
    node* newnode = new node(val);
    node* cur = pos._pnode;
    node* prev = cur->_prev;

    newnode->_prev = prev;
    prev->_next = newnode;
    newnode->_next = cur;
    cur->_prev = newnode;
    ++_size;
    return iterator(newnode);
}
erase
iterator erase(iterator pos) {
    assert(pos != end());
    // prev cur next
    node* cur = pos._pnode;
    node* prev = cur->_prev;
    node* next = cur->_next;

    prev->_next = next;
    next->_prev = prev;
    delete cur;
    --_size;
    return iterator(prev);
}
push_back
void push_back(const T& val){
    // phead  tail  newnode
    insert(end(), val);
}
pop_back
void pop_back() {
    erase(--end());
}
push_front
void push_front(const T& val) {
    insert(begin(), val);
}
pop_front
void pop_front() {
    erase(begin());
}
clear
void clear() {
    iterator it = begin();
    while (it != end()) {
        it = erase(it);
    }
}
swap
void swap(list<T>& lt) {
    std::swap(_phead, lt._phead);
}
resize
void resize(size_t n, const T& val = T()) {
    size_t sizes = size();
    if (n > sizes) {
        while (sizes < n) {
            push_back(val);
            ++sizes;
        }
    }
    else if (n < sizes) {
        while (n < sizes) {
            pop_back();
            --sizes;
        }
    }
}
size
size_t size() const {
    return _size;
}

关系运算符重载

==
bool operator==(const list<T>& lt) const {
    return _phead == lt._phead;
}
!=
bool operator!=(const list<T>& lt) const {
    return !(*this == lt);
}

list与vector比较

list与vector排序效率简要分析

算法库algorithm中的sort函数底层使用快速排序实现,不支持对list进行排序。
list内部实现了自己的排序函数sort,该函数底层使用归并排序实现。
快速排序和归并排序的时间复杂度都是 O ( N ∗ l n N ) O(N*lnN) O(NlnN),但是实际上比较算法库中的sort函数和list内部的sort函数效率时,对同一组数,list内部实现的排序函数运行效率明显低于算法库中的排序函数。
使用同一组数不同情况下效率的比较:

  1. 对vector进行排序;
  2. list的元素先拷贝vector中,对vector排序再拷贝回list中;
  3. 对list进行排序;
void testSort() {
	int n = 100000;
	vector<int> v1;
	vector<int> v2;
	list<int> l1;
	list<int>l2;
	srand(time(0));
	for (int i = 0; i < n; ++i) {
		int val = rand();
		l1.push_back(val);
		l2.push_back(val);
	}
	v1.reserve(n);
	v2.reserve(n);
	for (auto& e : l1) {
		v1.push_back(e);
	}

	// vector  对vector进行排序
	int begin1 = clock();
	sort(v1.begin(), v1.end());
	int end1 = clock();

	// list->vector->list  list的元素先拷贝vector中,对vector排序再拷贝回list中
	int begin2 = clock();
	for (auto& e : l1) {
		v2.push_back(e);
	}
	sort(v2.begin(), v2.end());
	int i = 0;
	for (auto& e : l1) {
		e = v2[i++];
	}
	int end2 = clock();

	// list  对list进行排序
	int begin3 = clock();
	l2.sort();
	int end3 = clock();

	cout << "vector:             " << end1 - begin1 << endl;
	cout << "list->vector->list: " << end2 - begin2 << endl;
	cout << "list:               " << end3 - begin3 << endl;
}

image.png

list与vector迭代器差异再次分析 - 调试

相同点:

  • 迭代器的行为相似,都支持*、->、++、–、比较大小、范围for等操作;
  • 都不支持迭代器相加操作;

不同点:

  • vector迭代器是一个原生指针实现(不排除不同版本STL的vector迭代器也是类的封装实现),list迭代器是类的封装实现;
  • vector的数据连续存储,支持迭代器+1、-1、迭代器之间的相减操作,++后指向相邻的下一个位置,–后指向相邻的前一个位置;list的节点在堆上随机存储,节点之间在储存上没有先后顺序,通过节点内的指针逻辑上链接相邻的节点,不支持+1、-1、迭代器相减操作,++后指向不相邻的下一个位置,–后指向不相邻的前一个位置。

image.png


结语

本节简要介绍了list的接口函数,重点实现了list的主要接口函数,特别是封装的迭代器。list由于是节点链接,insert之后直接插入了一个新的节点,原迭代器还指向原节点,并不会导致迭代器失效问题,与连续容器不同;erase之后,删除了原有节点,原迭代器指向了被删除的节点,迭代器是失效的,与连续容器相同。


T h e The The E n d End End

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/155317.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【OJ比赛日历】快周末了,不来一场比赛吗? #11.18-11.24 #15场

CompHub[1] 实时聚合多平台的数据类(Kaggle、天池…)和OJ类(Leetcode、牛客…&#xff09;比赛。本账号会推送最新的比赛消息&#xff0c;欢迎关注&#xff01; 以下信息仅供参考&#xff0c;以比赛官网为准 目录 2023-11-18&#xff08;周六&#xff09; #5场比赛2023-11-19…

受电诱骗快充取电芯片XSP08:PD+QC+华为+三星多种协议9V12V15V20V

目前市面上很多家的快充充电器&#xff0c;都有自己的私有快充协议&#xff0c;如PD协议、QC协议、华为快充协议、三星快充协议、OPPO快充协议等待&#xff0c;为了让它们都能输出快充电压&#xff0c;就需要在受电端也增加快充协议取电芯片XSP08&#xff0c;它可以和充电器通讯…

我记不住的getopt_long的那些参数和返回值

前言&#xff1a;最近在学习面向Linux系统进行C语言的编程&#xff0c;通过查询man手册和查看网络上的各种文章来获取一点点的知识&#xff0c;重点是看完手册还是一脸懵逼&#xff0c;搞不懂手册里面再说啥&#xff0c;而本篇文章将记录一下学习getopt_long的那些参数和返回值…

IDEA无法查看源码是.class,而不是.java解决方案?

问题&#xff1a;在idea中&#xff0c;ctrl鼠标左键进入源码&#xff0c;但是有时候会出现无法查看反编译的源码&#xff0c;如图&#xff01; 而我们需要的是方法1: mvn dependency:resolve -Dclassifiersources 注意&#xff1a;需要该模块的目录下&#xff0c;不是该文件目…

计算机毕业设计选题推荐-幼儿园管理微信小程序/安卓APP-项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

LeetCode(24)文本左右对齐【数组/字符串】【困难】

目录 1.题目2.答案3.提交结果截图 链接&#xff1a; 文本左右对齐 1.题目 给定一个单词数组 words 和一个长度 maxWidth &#xff0c;重新排版单词&#xff0c;使其成为每行恰好有 maxWidth 个字符&#xff0c;且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单…

spring中的DI

【知识要点】 控制反转&#xff08;IOC&#xff09;将对象的创建权限交给第三方模块完成&#xff0c;第三方模块需要将创建好的对象&#xff0c;以某种合适的方式交给引用对象去使用&#xff0c;这个过程称为依赖注入&#xff08;DI&#xff09;。如&#xff1a;A对象如果需要…

flutter web 中嵌入一个html

介绍 flutter web 支持使用 HtmlElementView嵌入html import dart:html; import dart:ui as ui; import package:flutter/cupertino.dart;class WebWidget extends StatelessWidget {const WebWidget({super.key});overrideWidget build(BuildContext context) {DivElement fr…

flutter绘制弧形进度条

绘制&#xff1a; import package:jade/utils/JadeColors.dart; import package:flutter/material.dart; import dart:math as math; import package:flutter_screenutil/flutter_screenutil.dart;class ArcProgressBar extends StatefulWidget{const ArcProgressBar({Key key…

Linux系统进程——进程的退出、子进程退出的收集、孤儿进程

进程退出 进程退出主要分为两种&#xff1a;正常退出、异常退出 正常退出 正常退出分为以下几种&#xff1a; 1.main函数调用return 2.进程调用exit(),标准c库 3.进程调用 _exit() 或者 _Exit() &#xff0c;属于系统调用 4.进程最后一个线程返回 5.最后一个线程调用pthrea…

晨控CK-FR08读卡器与汇川PLC连接EtherCAT通讯手册

晨控CK-FR08读卡器与汇川PLC连接EtherCAT通讯手册 晨控CK-FR08系列是一款基于射频识别技术的高频RFID标签读卡器&#xff0c;读卡器工作频率为13.56MHZ&#xff0c;支持对I-CODE 2、I-CODE SLI等符合ISO15693国际标准协议格式标签的读取。 读卡器同时支持标准工业通讯协议Eth…

ubuntu20源码编译搭建SRS流媒体服务器

第一、下载源码 下载源码&#xff0c;推荐用Ubuntu20&#xff1a; git clone -b develop https://gitee.com/ossrs/srs.git第二、编译 2.1、切换到srs/trunk目录&#xff1a; cd srs/trunk2.2、执行configure脚本 ./configure2.3、执行make命令 make2.4、修改conf/rtmp.c…

通讯录实现之进阶版将通讯录数据保存在文件中(完整代码)

我们在之前的博客中已经写过两版通讯录了&#xff1a; 第一版是用C语言实现了通讯录&#xff0c;但是通讯录的存储人数信息是固定的&#xff0c;用完就没有了 感兴趣的可以转到对应博客看一下&#xff0c;附带链接&#xff1a;第一版通讯录 第二版是在第一版的基础上动态开辟…

从零开始 通义千问大模型本地化到阿里云通义千问API调用

从零开始 通义千问大模型本地化到阿里云通义千问API调用 一、通义千问大模型介绍 何为“通义千问”&#xff1f; “通义千问大模型”是阿里云推出的一个超大规模的语言模型&#xff0c;具有强大的归纳和理解能力&#xff0c;可以处理各种自然语言处理任务&#xff0c;包括但…

DAC实验(DAC 输出三角波实验)(DAC 输出正弦波实验)

DAC 输出三角波实验 本实验我们来学习使用如何让 DAC 输出三角波&#xff0c;DAC 初始化部分还是用 DAC 输出实验 的&#xff0c;所以做本实验的前提是先学习 DAC 输出实验。 使用 DAC 输出三角波&#xff0c;通过 KEY0/KEY1 两个按键&#xff0c;控制 DAC1 的通道 1 输出两种…

文旅媒体有哪些?如何邀请到现场报道?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 中国文旅产业在近年来得到了持续而快速的发展。从产业端看&#xff0c;中国文旅产业呈现出新的发展趋势&#xff0c;其中“文旅”向“文旅”转变成为显著特点。通过产业升级和空间构建&a…

Shell编程基础(3)- Shell的位置参数

Shell编程基础&#xff08;3&#xff09;- Shell的位置参数 Shell Scripting Essentials (3) – Locative Parameters of Shell Scripting 前文介绍过shell变量。当声明shell变量时&#xff0c;只需要在代码行写出变量名称即可;在输入行用read命令要求用户输入&#xff0c;在…

Day48 力扣动态规划 : 647. 回文子串 |516.最长回文子序列 |动态规划总结篇

Day48 力扣动态规划 : 647. 回文子串 &#xff5c;516.最长回文子序列 &#xff5c;动态规划总结篇 647. 回文子串第一印象看完题解的思路dp递推公式初始化递归顺序 实现中的困难感悟代码 516.最长回文子序列第一印象我的尝试遇到的问题 看完题解的思路dp递推公式初始化 实现中…

设计基于STM32F103C8T6微控制器的巡线小车

巡线小车是一种能够在一条预定线追踪路径的小车&#xff0c;广泛应用于工业自动化、物流仓储、智能家居等领域。本设计将使用STM32F103C8T6微控制器来实现一个基础的巡线小车。 硬件组成&#xff1a;1. STM32F103C8T6微控制器开发板&#xff1a;作为巡线小车的核心控制器&…

双剑合璧:基于Elasticsearch的两路召回语义检索系统,实现关键字与语义的高效精准匹配

搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术细节以及项目实战(含码源) 专栏详细介绍:搜索推荐系统专栏简介:搜索推荐全流程讲解(召回粗排精排重排混排)、系统架构、常见问题、算法项目实战总结、技术…