【C++】拆分详解 - list

文章目录

  • 一、list的介绍
  • 二、list的使用
    • [1. 构造](#1. 构造)
    • [2. 迭代器](#2. 迭代器)
    • [3. 增 删 查 改](#3. 增 删 查 改)
    • [4. list 迭代器失效问题](#4. list 迭代器失效问题)
    • [5. list 排序问题](#5. list 排序问题)
  • 三、list的模拟实现
    • [0. 整体框架](#0. 整体框架)
    • [1. 迭代器类](#1. 迭代器类)
      • [1.1 operator->](#1.1 operator->)
      • [1.2 临时对象](#1.2 临时对象)
      • [1.3 const_iterator](#1.3 const_iterator)
    • [2. list类](#2. list类)
      • [2.1 begin / end](#2.1 begin / end)
      • [2.2 构造 / 析构 / 拷贝构造 / 赋值重载](#2.2 构造 / 析构 / 拷贝构造 / 赋值重载)
      • [2.3 增 删 查 改](#2.3 增 删 查 改)
  • 四、list与vector的对比

一、list的介绍

  • 底层是带头双向链表结构,需要额外空间保存节点信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)
  • 在常数范围内支持任意位置的插入和删除,效率通常优于array、vector和deque。
  • 不支持随机访问,必须从头/尾开始找,访问特定元素需要线性时间。

二、list的使用

1. 构造

构造函数声明(constructor) 功能说明
list() default】无参构造
list (size_type n, const value_type& val = value_type()) fill】构造并初始化填充n个val
list (const list& x) copy】拷贝构造
list (InputIterator first, InputIterator last) range】使用迭代器区间进行初始化构造
cpp 复制代码
void TestList1()
{
    list<int> l1;                         // 构造空的l1
    list<int> l2(4, 100);                 // l2中放4个值为100的元素
    list<int> l3(l2.begin(), l2.end());  // 用l2的[begin(), end())左闭右开的区间构造l3
    list<int> l4(l3);                    // 用l3拷贝构造l4

    // 以数组为迭代器区间构造l5 (数组名的
    int array[] = { 16,2,77,29 };
    list<int> l5(array, array + sizeof(array) / sizeof(int));

    // 列表初始化(initializer list) --> C++11
    // 以下三种写法都是一样的效果
    list<int> l6({ 1,2,3,4,5 });
    list<int> l66{ 1,2,3,4,5 };
    list<int> l666 = { 1,2,3,4,5 };

    list<int>::iterator it = l5.begin();
    while (it != l5.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    for (auto& e : l6)
        cout << e << " ";

    cout << endl;
}

2. 迭代器

iterator的使用 功能说明
begin + end (重点) 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator

  1. begin与end为正向迭代器,对迭代器执行++操作,迭代器向后移动
  2. rbegin(end)与rend(begin)为反向迭代器,对迭代器执行++操作,迭代器向前移动
  3. list中的迭代器为双向迭代器,只支持++, -- 即只能前后移动一个节点
cpp 复制代码
// 注意:遍历链表只能用迭代器和范围for
void PrintList(const list<int>& l)
{
    // 注意这里l是const对象,调用返回的是const_iterator对象
    for (list<int>::const_iterator it = l.begin(); it != l.end(); ++it)
    {
        cout << *it << " ";
        // *it = 10; 编译不通过
    }

    cout << endl;
}

void TestList2()
{
    int array[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    list<int> l(array, array + sizeof(array) / sizeof(array[0]));
    // 使用正向迭代器正向list中的元素
    // list<int>::iterator it = l.begin();   // C++98中语法
    auto it = l.begin();                     // C++11之后推荐写法
    while (it != l.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    // 使用反向迭代器逆向打印list中的元素
    // list<int>::reverse_iterator rit = l.rbegin();
    auto rit = l.rbegin();
    while (rit != l.rend())
    {
        cout << *rit << " ";
        ++rit;
    }
    cout << endl;
}

3. 增 删 查 改

接口名称 功能说明
size 获取有效节点个数
empty 判断是否为空
front 获取第一个节点的值
back 获取最后一个节点的值
reserve 改变vector的capacity
push_front 头插
push_back 尾插
pop_front 头删
pop_back 尾删
insert 指定位插入
erase 指定位删除
swap 交换两个list的成员
clear 删除所有节点
cpp 复制代码
// list插入和删除
// push_back/pop_back/push_front/pop_front
void TestList3()
{
    int array[] = { 1, 2, 3 };
    list<int> L(array, array + sizeof(array) / sizeof(array[0]));

    // 在list的尾部插入4,头部插入0
    L.push_back(4);
    L.push_front(0);
    PrintList(L);

    // 删除list尾部节点和头部节点
    L.pop_back();
    L.pop_front();
    PrintList(L);
}

// insert /erase 
void TestList4()
{
    int array1[] = { 1, 2, 3 };
    list<int> L(array1, array1 + sizeof(array1) / sizeof(array1[0]));

    // 获取链表中第二个节点
    auto pos = ++L.begin();
    cout << *pos << endl;

    // 在pos前插入值为4的元素
    L.insert(pos, 4);
    PrintList(L);

    // 在pos前插入5个值为5的元素
    L.insert(pos, 5, 5);
    PrintList(L);

    // 在pos前插入[v.begin(), v.end)区间中的元素
    vector<int> v{ 7, 8, 9 };
    L.insert(pos, v.begin(), v.end());
    PrintList(L);

    // 删除pos位置上的元素
    L.erase(pos);
    PrintList(L);

    // 删除list中[begin, end)区间中的元素,即删除list中的所有元素
    L.erase(L.begin(), L.end());
    PrintList(L);
}

// resize/swap/clear
void TestList5()
{
    // 用数组来构造list
    int array1[] = { 1, 2, 3 };
    list<int> l1(array1, array1 + sizeof(array1) / sizeof(array1[0]));
    PrintList(l1);

    // 交换l1和l2中的元素
    list<int> l2;
    l1.swap(l2);
    PrintList(l1);
    PrintList(l2);

    // 将l2中的元素清空
    l2.clear();
    cout << l2.size() << endl;
}

4. list 迭代器失效问题

  • list 不是整块的连续物理空间,不存在扩容 / 缩容的概念,只有删除时会引发迭代器失效。(详细说明参考博主的vector解析中的 二、5.迭代器失效问题
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())
    {
        // erase()函数执行后,it所指向的节点已被删除,因此it无效,在下一次使用it时,必须先给其赋值
            l.erase(it);
        // it = l.erase(it); 正确写法
        ++it;
    }
}

5. list 排序问题

结论:

  • 一般不用:效率极其低下,接近vector的1/4,甚至比先拷贝给vector,排好序再拷贝回来的效率低一半(release版本)
cpp 复制代码
// vector与list排序性能比较
void test_op1()
{
    srand(time(0));
    const int N = 10000000;

    list<int> lt1;
    list<int> lt2;

    vector<int> v;

    for (int i = 0; i < N; ++i)
    {
        auto e = rand() + i;
        lt1.push_back(e);
        v.push_back(e);
    }

    int begin1 = clock();
    // 排序
    sort(v.begin(), v.end());
    int end1 = clock();

    int begin2 = clock();
    lt1.sort();
    int end2 = clock();

    printf("vector sort:%d\n", end1 - begin1);
    printf("list sort:%d\n", end2 - begin2);
}

//先将list元素拷贝给vector,在vector中排好序再拷贝回list的性能
void test_op2()
{
    srand(time(0));
    const int N = 10000000;

    list<int> lt1;
    list<int> lt2;

    for (int i = 0; i < N; ++i)
    {
        auto e = rand() + i;
        lt1.push_back(e);
        lt2.push_back(e);
    }

    int begin1 = clock();
    // 拷贝vector

    vector<int> v(lt2.begin(), lt2.end());
    // 排序
    sort(v.begin(), v.end());

    // 拷贝回lt2
    lt2.assign(v.begin(), v.end());

    int end1 = clock();

    int begin2 = clock();
    lt1.sort();
    int end2 = clock();

    printf("list copy vector sort copy list sort:%d\n", end1 - begin1);
    printf("list sort:%d\n", end2 - begin2);
}

三、list的模拟实现

0. 整体框架

cpp 复制代码
template<class T>
struct ListNode
{
	ListNode<T>* _next;
	ListNode<T>* _prev;
	T _data;

	ListNode(const T& data = T())
		:_next(nullptr)
		,_prev(nullptr)
		,_data(data)
	{}
};

template<class T>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T, Ref, Ptr> Self;
	Node* _node; //迭代器本体,唯一成员变量 ---> 指针

	ListIterator(Node* node)
		:_node(node)
	{}
	// ... 各类迭代器行为
};


template<class T>
class list
{
	typedef ListNode<T> Node;
public:
	//typedef Node* iterator;  直接使用原生指针定义迭代器,不符合所需的迭代器行为,需要格外定义一个类进行封装
	typedef ListIterator<T, T&, T*> iterator;
	typedef ListIterator<T, const T&, const T*> const_iterator;

	// ...各类函数接口

private:
	Node* _head;
};
  1. list类封装 有关节点操作的相关接口

  2. ListNode类 封装节点构造相关

  3. ListIterator 封装list的迭代器相关

    • 链表物理空间不连续,不能通过++iterator​ 找下一个节点,必须用一个类封装原生指针然后手动控制其行为(重载运算符)使之符合

      (迭代器的最终效果要像原生指针对于数组一样使用,但是不是全部的功能都要实现,比如list的双向迭代器就不能iterator + 4​,最多只能找前一个和后一个,因为效率太低,设计者没有必要支持)

    • 不需要显式写析构 / 拷贝构造 / 赋值:

      1. 节点的释放由list类控制,迭代器不涉及资源管理,默认生成的析构就够用
      2. ListIterator类唯一的成员变量(本体)就是指针,我们只是规范了它的行为,则拷贝 / 赋值时需要的就是浅拷贝,同一个指针指向同一块空间

1. 迭代器类

cpp 复制代码
//V1 版本:写两个迭代器类,一个是普通迭代器,一个是const 迭代器
template<class T>
struct ListIterator
{
	typedef ListNode<T> Node;
	//typedef ListIterator<T, Ref, Ptr> Self;
	typedef ListIterator<T> Self;
	Node* _node; //迭代器本体,指针

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

	//迭代器行为
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

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

	//后置++
	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;

		return tmp;
	}

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

		return tmp;
	}

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

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

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

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

template<class T>
struct ListConstIterator
{
	typedef ListNode<T> Node;
	typedef ListConstIterator<T> Self;
	Node* _node;

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

	//...略,与普通迭代器一模一样

	const T& operator*() 
	{
		return _node->_data;
	}

	const T* operator->()
	{
		return &_node->_data;
	}
	// ...略,同上
};

//V2 版本:使用多参数模板,交给编译器实现const迭代器类
//const 和 非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)
	{}

	//迭代器行为
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

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

	//后置++
	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;

		return tmp;
	}

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

		return tmp;
	}

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

	Ptr operator->()
	{
		return &_node->_data;
	}

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

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

1.1 operator->

专门给没有重载流插入<<运算符的类提供的(对于自定义类型,流插入可以直接使用,而自定义类型需要类设计者自己重载控制),下见示例类 Pos。该类没有重载流插入,我们只能使用最原始的方法,利用指针手动访问该类的成员变量_row, _col

  • cout << (*it)._row 该段代码很好理解,先解引用得Pos类对象,再用Pos类对象访问其成员
  • cout << it->_row 功能同上,先获取Pos对象指针再使用Pos类对象指针访问其成员,但是仔细观察一下,是不是少了一个箭头啊?实际上这是编译器的省略行为,该段代码等同于cout << it.operator->()->_row 即先调用迭代器类中 重载的-> 获取Pos类对象的指针再用 原生-> 访问其成员
cpp 复制代码
struct Pos
{
	int _row;
	int _col;

	Pos(int row = 0, int col = 0)
		:_row(row)
		, _col(col)
	{}
};

void test_list2()
{
	list<Pos> lt1;
	lt1.push_back(Pos(100, 100));
	lt1.push_back(Pos(200, 200));
	lt1.push_back(Pos(300, 300));

	list<Pos>::iterator it = lt1.begin();
	while (it != lt1.end())
	{
		//cout << (*it)._row << ":" << (*it)._col << endl;
		// 为了可读性,省略了一个->
		cout << it->_row << ":" << it->_col << endl;
		//cout << it->->_row << ":" << it->->_col << endl;
		cout << it.operator->()->_row << ":" << it.operator->()->_col << endl;

		++it;
	}
	cout << endl;
}

1.2 临时对象

begin返回的是临时对象,临时对象具有常性,那为什么返回的迭代器可以++,---呢?

为了使用的便利,编译器经过了特殊处理,实际上临时对象的常性介于const和非const之间,可以调用非静态成员函数,也可以被修改,但是不能被非const对象引用

1.3 const_iterator

  1. const迭代器 专给const类对象调用,类似于 const T* ,迭代器本身可以修改,指向的内容不可以通过迭代器修改(const是对迭代器本身的修饰,如果用其他方法修改其指向的内容,迭代器管不着)

    故对迭代器行为中涉及解引用且修改的进行约束:

    • 对迭代器解引用行为要返回const类型值(通过const迭代器获取的指向内容 不可以修改)
  2. 如何写?

    方法1:再创建一个const版本的迭代器类

    方法2:使用多参数模板,交给编译器干(实际上还是会创建两个类)

cpp 复制代码
//方法1:
template<class T>
struct ListConstIterator
{
	typedef ListNode<T> Node;
	typedef ListConstIterator<T> Self;
	//const Node* _node; 为了直接使用多模板参数省事,可以不加const,因为迭代器的行为完全由我们手动控制,
	//只要我们不在其解引用时修改即可。但是如果有人手动调用迭代器内部的指针进行解引用修改是可以的(没大病不会这么干)
	Node* _node;
	ListConstIterator(Node* node) 
		:_node(node) 
	{}
	//...略,与普通迭代器一模一样

	const T& operator*() 
	{
		return _node->_data;
	}

	const T* operator->()
	{
		return &_node->_data;
	}
};

//方法2:
//const 和 非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)
	{}

	//迭代器行为
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}

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

	//后置++
	Self operator++(int)
	{
		Self tmp(*this);
		_node = _node->_next;

		return tmp;
	}

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

		return tmp;
	}

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

	Ptr operator->()
	{
		return &_node->_data;
	}

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

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

2. list类

2.1 begin / end

cpp 复制代码
iterator begin()
{
	return iterator(_head->_next); //类的匿名对象,下同
}

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

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

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

2.2 构造 / 析构 / 拷贝构造 / 赋值重载

cpp 复制代码
//头结点创建(链表为空),由于多个接口都要调用,故单独提出来封装成函数
void empty_init()
{
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;
}

list()
{
	empty_init();
}

list(initializer_list<T> il)
{
	empty_init();

	for (const auto& e : il)
		push_back(e);
}

//lt2(lt1)
list(const list<T>& lt)
{
	empty_init();

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

list<T>& operator=(list<T> lt)
{
	swap(_head, lt._head);

	return *this;
}

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

2.3 增 删 查 改

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

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

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

	return iterator(newnode);
}

iterator erase(iterator pos)
{
	assert(pos != end());

	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	prev->_next = cur->_next;
	next->_prev = prev;
	delete cur;

	return iterator(next);
}

void push_back(const T& x)
{
	insert(end(), x);
}

void pop_back()
{
	erase(--end());
}

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

void pop_front()
{
	erase(begin());
}

四、list与vector的对比

vector list
底层结构 动态顺序表,一段连续空间 带头结点的双向循环链表
随机访问 支持随机访问,访问某个元素效率O(1) 不支持随机访问,访问某个元素效率O(N)
插入和删除 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器 原生态指针 对原生态指针(节点指针)进行封装
迭代器失效 插入,删除时 删除时
使用场景 需要高效存储,支持随机访问,不关心插入删除效率 大量插入和删除操作,不关心随机访问
相关推荐
IT规划师2 小时前
数据结构 - 散列表,三探之代码实现
数据结构·散列表·哈希表
Y.O.U..2 小时前
STL学习-容器适配器
开发语言·c++·学习·stl·1024程序员节
lihao lihao2 小时前
C++stack和queue的模拟实现
开发语言·c++
TT哇3 小时前
【Java】数组的定义与使用
java·开发语言·笔记
姆路3 小时前
QT中使用图表之QChart概述
c++·qt
西几3 小时前
代码训练营 day48|LeetCode 300,LeetCode 674,LeetCode 718
c++·算法·leetcode
武子康3 小时前
大数据-187 Elasticsearch - ELK 家族 Logstash Filter 插件 使用详解
大数据·数据结构·elk·elasticsearch·搜索引擎·全文检索·1024程序员节
风清扬_jd3 小时前
Chromium HTML5 新的 Input 类型week对应c++
前端·c++·html5
南东山人4 小时前
C++静态成员变量需要在类外进行定义和初始化-error LNK2001:无法解析的外部符号
c++
lqqjuly4 小时前
C++ 中回调函数的实现方式-函数指针
开发语言·c++