【C++】STL 模拟实现之 list

news2024/11/23 21:12:18

文章目录

  • 一、list 的常用接口及其使用
    • 1、list 一般接口
    • 2、list 特殊接口
    • 3、list 排序的性能分析
  • 二、list 迭代器的实现
    • 1、迭代器的分类
    • 2、list 迭代器失效问题
    • 3、list 迭代器源码分析
    • 4、list 迭代器模拟实现
      • 4.1 普通迭代器
      • 4.2 const 迭代器
      • 4.3 完整版迭代器
  • 三、list 的模拟实现
  • 四、vector 和 list 的区别

一、list 的常用接口及其使用

1、list 一般接口

list 是可以在常数范围内在任意位置进行插入和删除的序列式容器,其底层是带头双向循环链表;list 常用接口的使用和 string、vector 系列容器的接口使用一样,这里我就不再详细介绍,具体使用细节可以查看 list 使用文档 – cplusplus.com。

构造函数

-构造函数( 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

增删查改

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

注意事项

1、由于 list 的物理结构是非连续的 – 前一个节点地址和后一个节点地址的位置关系是随机的,所以 list 不支持随机访问,自然也就不支持 [] 操作;

2、list 不支持 reserve 操作,因为 list 的节点是使用时开辟,使用完销毁,不能预留空间;

2、list 特殊接口

除了上述 STL 容器基本都有的一般接口外,list 还提供一些独有的特殊操作接口,如下:

-函数声明-接口说明
splice将 list1 中的元素转移到 list2 中
remove移除 list 中的指定元素
unique链表去重
merge合并两个链表
sort链表排序
reverse链表逆置

注意事项

1、链表排序只能使用 list 提供的 sort 接口,而不能使用 algorithm 提供的 sort 接口,因为链表物理地址不连续,迭代器为双向迭代器,不支持 + - 操作,而算法库中的 sort 函数需要支持 + - 的随机迭代器;

2、链表去重之前必须保证链表有序,否则去重不完全;

3、两个有序链表合并之后仍然保存有序;

最后,虽然 list 提供了这些具有特殊功能的接口,它们也确实有一定的作用,但是实际上这些特殊接口使用频率非常低,包括 sort 接口 (链表排序的效率太低)。

3、list 排序的性能分析

虽然链表排序只能使用 list 提供的 sort 接口,而不能使用 algorithm 提供的 sort 接口,但是其使用频率仍然非常低,这是由于链表排序的效率太低了,我们可以通过对比两组测试数据来直观的感受链表排序的效率。

测试一:vector 排序与 list 排序性能对比

//vector sort 和 list sort 性能对比 -- release 版本下
void test_op1() {
	srand((size_t)time(0));
	const int N = 1000000;  //100万个数据

	vector<int> v;
	v.reserve(N);
	list<int> lt;
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		v.push_back(e);
		lt.push_back(e);
	}

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

	//list sort
	int begin2 = clock();
	lt.sort();
	int end2 = clock();

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

image-20230227185402161

测试二:list 直接进行排序与将数据拷贝到 vector 中使用 vector 排序后再将数据拷回 list 中性能对比

//list sort 与 将数据转移到 vector 中进行排序后拷贝回来性能对比 -- release 版本下
void test_op2()
{
	srand(time(0));
	const int N = 1000000;  //100万个数据
	list<int> lt1;
	list<int> lt2;
	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		lt1.push_back(e);
		lt2.push_back(e);
	}

	//list sort -- lt1
	int begin1 = clock();
	lt1.sort();
	int end1 = clock();

	// 将数据拷贝到vector中排序,排完以后再拷贝回来 -- lt2
	int begin2 = clock();
	vector<int> v;
	v.reserve(N);
	for (auto e : lt2)  //拷贝
	{
		v.push_back(e);
	}
	sort(v.begin(), v.end());  //排序
	lt2.assign(v.begin(), v.end());  //拷贝
	int end2 = clock();

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

image-20230227191002915

可以看到,list sort 的效率远低于 vector sort,甚至于说,直接使用 list sort 的效率都不如先将数据拷贝到 vector 中,然后使用 vector sort,排序之后再将数据拷贝回 list 中快;至此,我们也能明白为什么 list sort 接口使用的非常少了。

注:在工程中,在 release 版本下测试软件或算法性能得到的结果要比在 debug 版本下得到的结果具有参考意义。


二、list 迭代器的实现

1、迭代器的分类

按照迭代器的功能,迭代器一共可以分为以下三类:

  • 单向迭代器 – 迭代器仅仅支持 ++ 和解引用操作,单链表的迭代器是典型的单向迭代器;
  • 双向迭代器 – 迭代器支持 ++、-- 和解引用操作,但不支持 +、- 操作,list (双向带头循环链表) 是典型的双向迭代器;
  • 随机迭代器 – 迭代器不仅支持 ++、-- 和解引用操作,还支持 +、- 操作,即迭代器能够随机访问,我们前面学习的 string 和 vector 的迭代器是典型的随机迭代器。

2、list 迭代器失效问题

和 vector 不同,list 进行 insert 操作后并不会产生迭代器失效问题,因为 list 插入的新节点是动态开辟的,同时由于 list 每个节点的物理地址是不相关的,所以插入的新节点并不会影响原来其他节点的地址;

但是 list erase 之后会发生迭代器失效,因为 list 删除节点会直接将该节点释放掉,此时我们再访问该节点就会造成越界访问。

3、list 迭代器源码分析

我们知道,迭代器是类似于指针一样的东西,即迭代器要能够实现指针相关的全部或部分操作 – ++、–、*、+、-;对我们之前 string 和 vector 的迭代器来说,迭代器就是原生指针,所以它天然的就支持上述操作;

但是对于 list 来说,list 的节点是一个结构体,同时 list 每个节点的物理地址是不连续的,如果此时我们还简单将节点的指针 typedef 为迭代器的话,那么显然它是不能够实现解引用、++ 等操作的,所以我们需要用结构体/类来对迭代器进行封装,再配合运算符重载等操作让迭代器能够实现解引用、++、-- 等操作

SGI 版本 C++ 源码中对 list 迭代器实现框架如下:

//节点定义
template <class T>
struct __list_node {
    typedef void* void_pointer;
    void_pointer next;
    void_pointer prev;
    T data;
};

//迭代器定义
typedef __list_iterator<T, T&, T*>   iterator;
typedef __list_iterator<T, const T&, const T*>  const_iterator;

//迭代器类
template<class T, class Ref, class Ptr>
struct __list_iterator {
     typedef __list_iterator<T, Ref, Ptr>  self;
     typedef __list_node<T>* link_type;  //节点的指针
     link_type node; //类成员变量
    
     __list_iterator(link_type x) : node(x) {} //将节点指针构造为类对象
    
    //... 使用运算符重载支持迭代器的各种行为
    self& operator++() {...}
    self& operator--() {...}
    Ref operator*() const {...}
};

如上,我们为迭代器专门设计了一个 __list_iterator 类,然后在类内配合运算符重载、模板等操作使得迭代器支持指针的各种行为 (至于这里为什么 __list_iterator 会有三个模板参数,我们会在下面模拟实现中具体解释,当前我们只需要理解其第一个模板参数 T 即可)。

4、list 迭代器模拟实现

4.1 普通迭代器

list 普通迭代器的实现比较简单,只需要一个模板参数 T 来代表数据类型,然后通过运算符重载来实现迭代器的各种操作即可:

//typedef __list_iterator<T> iterator -- 迭代器

template<class T>
struct __list_iterator {
    typedef list_node<T> node;  //将list节点重命名为node
    node* _pnode;  //节点指针作为类的唯一成员变量

    __list_iterator(node* p)  //默认构造
        :_pnode(p)
        {}

    T& operator*()  //解引用
    {
        return _pnode->_data;
    }

    __list_iterator<T>& operator++()  //前置++
    {
        _pnode = _pnode->_next;
        return *this;
    }

    __list_iterator<T>& operator++(int)  //后置++
    {
        __list_iterator<T> it(*this);
        _pnode = _pnode->_next;
        return it;
    }

    __list_iterator<T>& operator--()  //前置--
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    __list_iterator<T>& operator--(int)  //后置--
    {
        __list_iterator<T> it(*this);
        _pnode = _pnode->_prev;
        return it;
    }

    bool operator!=(const __list_iterator<T>& it)  //!=
    {
        return _pnode != it._pnode;
    }
};

4.2 const 迭代器

我们上面的迭代器是普通迭代器,那么如何实现 const 迭代器呢?const 迭代器与普通迭代器的区别在于 – const 迭代器不能修改节点中的数据,即 operator*() 函数的返回值应该是 const T&;

那么部分同学可能会这样来实现 const 迭代器,即在 __list_iterator 类中重载一个返回值为 const T& 的 operator*() 函数,如下:

//typedef __list_iterator<T> iterator -- 迭代器
//typedef const __list_iterator<T> const_iterator  -- const 迭代器
template<class T>
struct __list_iterator {
    T& operator*()  //解引用
    {
        return _pnode->_data;
    }
	
    const T& operator*() const
    {
        return _pnode->_data;
    }
};

但是这样显然是不行的,因为 const __list_iterator<T> const_iterator 中 const 修饰的是 const_iterator 本身,即限制 const_iterator 不能改变,这样会导致 const_iterator 不能进行 ++ 等操作,而并不会限制迭代器解引用后对节点数据的改变。

所以,我们应该为 const 迭代器设计一个单独的类 __list_const_iterator:

//typedef __list_iterator<T> iterator -- 迭代器
template<class T>
struct __list_iterator {
    typedef list_node<T> node;  //将list节点重命名为node
    node* _pnode;  //节点指针作为类的唯一成员变量

    __list_iterator(node* p)  //默认构造
        :_pnode(p)
        {}

    T& operator*()  //解引用
    {
        return _pnode->_data;
    }

    __list_iterator<T>& operator++()  //前置++
    {
        _pnode = _pnode->_next;
        return *this;
    }

    __list_iterator<T>& operator++(int)  //后置++
    {
        __list_iterator<T> it(*this);
        _pnode = _pnode->_next;
        return it;
    }

    __list_iterator<T>& operator--()  //前置--
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    __list_iterator<T>& operator--(int)  //后置--
    {
        __list_iterator<T> it(*this);
        _pnode = _pnode->_prev;
        return it;
    }

    bool operator!=(const __list_iterator<T>& it)  //!=
    {
        return _pnode != it._pnode;
    }
};

//typedef __list_const_iterator<T> const_iterator -- 迭代器
template<class T>
struct __list_const_iterator {
    typedef list_node<T> node;  //将list节点重命名为node
    node* _pnode;  //节点指针作为类的唯一成员变量

    __list_const_iterator(node* p)  //默认构造
        :_pnode(p)
        {}

    const T& operator*()  //解引用
    {
        return _pnode->_data;
    }

    __list_const_iterator<T>& operator++()  //前置++
    {
        _pnode = _pnode->_next;
        return *this;
    }

    __list_const_iterator<T>& operator++(int)  //后置++
    {
        __list_iterator<T> it(*this);
        _pnode = _pnode->_next;
        return it;
    }

    __list_const_iterator<T>& operator--()  //前置--
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    __list_const_iterator<T>& operator--(int)  //后置--
    {
        __list_const_iterator<T> it(*this);
        _pnode = _pnode->_prev;
        return it;
    }

    bool operator!=(const __list_const_iterator<T>& it)  //!=
    {
        return _pnode != it._pnode;
    }
};

可以看到,__list_iterator 类和 __list_const_iterator 类除了类名和 operator*() 函数的返回值不同以外,其他地方完全相同,这就会造成严重的代码冗余。

大佬显然是不会运行这种冗余存在的,所以想到了另一种办法来解决 const 迭代器的问题,那就是向 __list_iterator 中增加一个模板参数,如下:

//const 迭代器 -- 增加模板参数,解决 operator*()返回值问题
//typedef __list_iterator<T, T&> iterator;
//typedef __list_iterator<T, const T&> const_iterator;

//STL源码中大佬的写法,利用多个模板参数来避免副本造成的代码冗余问题
template<class T, class Ref>  
struct __list_iterator  //迭代器类
{
    typedef list_node<T> node;  //重命名list节点
    typedef __list_iterator<T, Ref> Self;  //这里进行重命名是为了后续再添加模板参数时只用修改这一个地方
    node* _pnode;  //节点指针作为类的唯一成员变量

    __list_iterator(node* p)
        :_pnode(p)
        {}

    Ref operator*()  //const迭代器看这个
    {
        return _pnode->_data;
    }

    Self& operator++() //前置
    {
        _pnode = _pnode->_next;
        return *this;
    }

    Self& operator--() //前置
    {
        _pnode = _pnode->_prev;
        return *this;
    }

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

注意事项:对于普通类来说,类名 = 类型;对于模板类来说,类名 != 类型,类型 = 类名 + 模板参数 。(注意:在类内,不管是否存在模板,类名都等于类型,不过为了混淆我们不建议这样使用)

所以我们可以通过传递不同的模板参数来让编译器实例化出两个不同的类,对于上面的类来说表现如下:

//库使用者
list<int, T&>::iterator it;  //普通迭代器
list<int, const T&>::iterator cit;  //const 迭代器

//template<class T, class Ref>
//通过编译器实例化出的两个不同的 __list_iterator 类
//类1、T:int Ref:T&
struct __list_iterato {
    T& operator*() { return _pnode->_data }
};

//类2、T:int Ref:const T&
struct __list_iterato {
    const T& operator*() { return _pnode->_data }
};

总结 const 迭代器实现的两种方法

  • 为 const 迭代器单独写一个类,此类与原来的类只有类名和 operator*() 函数返回值不同,造成代码冗余;
  • 给迭代器类增加一个模板参数 Ref,让编译器根据传入的 Ref 的不同来自动示例化出 const 迭代器类。

4.3 完整版迭代器

从最初的迭代器源码中我们可以看到,源码中迭代器类有三个模板参数,下面,我们来引出这第三个参数。

假设 list 第一个模板参数 T 是一个自定义类型的类 Pos,其类的定义如下:

struct Pos
{
    int _row;
    int _col;

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

那么我们可以通过 list 迭代器以这样的方式来访问 Pos 中的成员变量:

list<Pos> lt(1,2);
list<Pos>::iterator it = lt.begin();
while(it != lt.end())
{
    cout <<(*it)._row<<":"<<(*it)._col<<endl;
    ++it;
}

可以看到,我们需要先解引用得到 list 的节点,但由于此节点 Pos 是结构体类型,所以我们还需要通过 . 的方式来获取结构体成员;但是这样用非常别扭,因为迭代器是模拟指针的行为,而结构体指针访问数据的方式是 类名->变量名,那么我们能否像下面这样用呢?

cout <<it->_row<<":"<<it->_col<<endl;

显然,这样是不行的,因为 it 是一个自定义类型的对象 (__list_iterator 类的对象),而只有结构体指针才能像上面那样访问成员,所以我们需要利用运算符重载让 it 支持 -> 操作,如下:

template<class T, class Ref>
struct __list_iterator {
  T* operator->() { return &_pnode->_data; }  
};
cout <<it->_row<<":"<<it->_col<<endl;

相信绝大部分同学看到 &_pnode->_data 和 it->_row 的组合是懵逼的,这其实是因为 C++ 为了代码的可读性,省略了一个 ->,而原本的调用方式应该是这样的:

cout <<it->->_row<<":"<<it->->_col<<endl;  
//编译器编译时转化为:cout <<it.operator->()->_row<<":"<<it.operator->()->_col<<endl;

如上,由于 __list_iterator 对 -> 运算符进行了重载,所以编译器会将 it-> 转化为 it.operator->(),它得到的是节点数据的地址,也就是 Pos*,所以实际上 Pos* 还需要通过 -> 操作符来得到 _row 和 _col,但是 it->->_row 可读性太差,所以 C++ 省略了一个 ->,而让编译器对其进行处理 – it.operator->()->_row。

image-20230228193735451

但是此时这里又会和前面一样的问题,const 迭代器需要 operator->() 的返回值为 const T*,所以这里我们采取和前面一样的做法 – 再增加一个模板参数,把第三个模板参数作为 operator->() 函数的返回值,使得编译器可以根据传入的参数的不同实例化出不同的 __list_iterator 类

//库使用者
list<int, T&, T*>::iterator it;  //普通迭代器
list<int, const T&, const T*>::iterator cit;  //const 迭代器

//template<class T, class Ref, class Ptr>
//通过编译器实例化出的两个不同的 __list_iterator 类
//类1、T:int Ref:T& Ptr
struct __list_iterato {
    T* operator->() { return &_pnode->_data }
};

//类2、T:int Ref:const T& Ptr:const T*
struct __list_iterato {
    const T* operator->() { return &_pnode->_data }
};

至此,完整的迭代器模拟实现如下

//const 迭代器 -- 增加模板参数,解决 operator*() 返回值与 operator->() 返回值问题
//typedef __list_iterator<T, T&, T*> iterator;
//typedef __list_iterator<T, const T&, const T*> const_iterator;
//STL源码中大佬的写法,利用多个模板参数来避免副本造成的代码冗余问题
template<class T, class Ref, class Ptr>  
struct __list_iterator  //迭代器类
{
    typedef list_node<T> node;  //重命名list节点
    typedef __list_iterator<T, Ref, Ptr> Self;  //这里进行重命名是为了后续再添加模板参数时只用修改这一个地方
    node* _pnode;  //节点指针作为类的唯一成员变量

    __list_iterator(node* p)  //构造
        :_pnode(p)
        {}

    Ref operator*()  //解引用
    {
        return _pnode->_data;
    }

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

    Self& operator++() //前置++
    {
        _pnode = _pnode->_next;
        return *this;
    }

    Self& operator++(int) //后置++
    {
        Self it(*this);
        _pnode = _pnode->_next;
        return it;
    }

    Self& operator--() //前置--
    {
        _pnode = _pnode->_prev;
        return *this;
    }

    Self& operator--(int) //后置--
    {
        Self it(*this);
        _pnode = _pnode->_prev;
        return it;
    }

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

三、list 的模拟实现

list 模拟实现的最大难点在于 list 迭代器类的模拟实现,至于其他的一些成员函数,比如构造、赋值重载、析构等都比较简单,这里我就直接给出最终代码了,大家可以对照着自己模拟实现的代码看一看。

list.h

#pragma once

#include <iostream>
#include <assert.h>
#include <algorithm>

namespace thj {
	template<class T>
	struct list_node  //list 节点结构定义
	{
		list_node<T>* _next;//不加<T>也没错,但是写上好一些
		list_node<T>* _prev;
		T _data;

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

	//迭代器最终版
	//const 迭代器 -- 增加模板参数,解决 operator*() 返回值与 operator->() 返回值问题
	//typedef __list_iterator<T, T&, T*> iterator;
	//typedef __list_iterator<T, const T&, const T*> const_iterator;
	//STL源码中大佬的写法,利用多个模板参数来避免副本造成的代码冗余问题
	template<class T, class Ref, class Ptr>
	struct __list_iterator  //迭代器类
	{
		typedef list_node<T> node;  //重命名list节点
		typedef __list_iterator<T, Ref, Ptr> Self;  //这里进行重命名是为了后续再添加模板参数时只用修改这一个地方
		node* _pnode;  //节点指针作为类的唯一成员变量

		__list_iterator(node* p)
			:_pnode(p)
		{}

		Ref operator*()  //解引用
		{
			return _pnode->_data;
		}

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

		Self& operator++() //前置++
		{
			_pnode = _pnode->_next;
			return *this;
		}

		Self& operator++(int) //后置++
		{
			Self it(*this);
			_pnode = _pnode->_next;
			return it;
		}

		Self& operator--() //前置--
		{
			_pnode = _pnode->_prev;
			return *this;
		}

		Self& operator--(int) //后置--
		{
			Self it(*this);
			_pnode = _pnode->_prev;
			return it;
		}

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

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

	//list 类
	template<class T>
	class list
	{
		typedef list_node<T> node;  //list 的节点
	public:
		typedef __list_iterator<T, T&, T*> iterator;  //迭代器
		typedef __list_iterator<T, const T&, const T*> const_iterator; //const 迭代器

		//迭代器
		iterator begin() {
			return iterator(_head->_next);
		}

		iterator end() {
			//iterator it(_head);
			//return it;

			//直接利用匿名对象更为便捷
			return iterator(_head);
		}

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

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

		void empty_initialize() {  //初始化 -- 哨兵位头结点
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;  //空间换时间,用于标记节点个数
		}

		list() {  //构造,不是list<T>的原因:构造函数函数名和类名相同,而list<T>是类型
			empty_initialize();
		}

		//迭代器区间构造
		template <class InputIterator>
		list(InputIterator first, InputIterator last) {
			empty_initialize();
			while (first != last)
			{
				push_back(*first);
				++first;
				//first++;
			}
		}

		//拷贝构造传统写法
		//list(const list<T>& lt) {
		//	empty_initialize();

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

		// 拷贝构造的现代写法
		//list(const list& lt) 官方库是这样写的,这是由于在类内类名等价于类型,但不建议自己这样写
		list(const list<T>& lt) {
			empty_initialize();  //初始化头结点,防止交换后tmp野指针不能正常的调用析构
			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}

		//赋值重载传统写法
		//list<T>& operator=(const list<T>& lt) {
		//	if (this != &lt)
		//	{
		//		clear();
		//		for (const auto& e : lt)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}

		//赋值重载现代写法
		//list& operator=(list lt)
		list<T>& operator=(list<T> lt) {  //不能加引用,lt是调用拷贝构造生成的
			swap(lt);
			return *this;
		}

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

		void swap(list<T>& lt) {  //交换两个链表,本质上是交换两个链表的头结点
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		size_t size() const {  //增加一个计数的成员,以空间换时间
			return _size;
		}

		bool empty() {  //判空
			return _size == 0;
		}

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

		void push_back(const T& x) {
			//node* newnode = new node(x);
			//node* tail = _head->_prev;
			//tail->_next = newnode;
			//newnode->_prev = tail;
			//newnode->_next = _head;
			//_head->_prev = newnode;

			insert(end(), x);  //复用
		}

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

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

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

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

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

			++_size;
			return iterator(pos);
		}

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

			node* prev = pos._pnode->_prev;
			node* next = pos._pnode->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._pnode;

			--_size;
			return iterator(next);
		}

	private:
		node* _head;
		size_t _size;
	};
}

test.cpp

#include "list.h"

using namespace thj;
using std::cout;
using std::endl;

void test_list1()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.insert(++lt.begin(), 10);
	//iterator 1、内嵌类型 2、像指针一样
	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

void test_list2() {
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_front(10);
	lt.push_front(20);
	lt.push_front(30);
	lt.pop_back();
	lt.pop_back();
	lt.pop_back();
	lt.pop_front();
	lt.pop_front();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

void test_list3() {
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	list<int> lt1(lt);//默认是浅拷贝,指向同一块
	lt.pop_back();
	lt.pop_back();
	for (auto e : lt1)
	{
		cout << e << " ";
	}
	cout << endl;
	list<int> lt3 = lt1;
	for (auto e : lt3)
	{
		cout << e << " ";
	}
}

void test_list4() {
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);

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

	list<int> lt1(lt);
	for (auto e : lt1)
	{
		cout << e << " ";
	}
	cout << endl;
	list<int> lt2 = lt;
	for (auto e : lt2)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << lt.size() << endl;
}

struct Pos {
	int _row;
	int _col;

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

void print_list(const list<Pos>& lt) {
	list<Pos>::const_iterator it = lt.begin(); //重载了,找的另一个类的迭代器
	while (it != lt.end()) {
		//it->_row++;  增加第三个模板参数

		cout << it->_row << ":" << it->_col << endl;
		++it;
	}
	cout << endl;
}

void test_list5() {
	list<Pos> lt;
	Pos p1(1, 1);

	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(p1);
	lt.push_back(Pos(2, 3));
	lt.push_back(Pos(2, 3));

	list<Pos>::iterator it = lt.begin();
	while (it != lt.end()) {
		//cout << (*it)._row << ":" << (*it)._col << endl;
		cout << it->_row << ":" << it->_col << endl;  //编译器为了可读性,做了特殊处理,省略了一个->,
													  //那么省略之前应该是it->->_row
		//cout << it.operator->()->_col << ":" << it.operator->()->_row << endl;
		++it;
	}
	cout << endl;
	print_list(lt);
}

int main() {
	//test_list1();
	//test_list2();
	//test_list3();
	//test_list4();
	test_list5();

	return 0;
}

四、vector 和 list 的区别

vector 和 list 的区别其实就是顺序表和链表的区别,但是由于相较于数据结构初阶我们又增添了 C++ 的相关知识,所以这里我还是重新列举一下二者的异同与优缺:

--vector-list
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素效率 O(1)不支持随机访问,访问某个元素效率 O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度为 O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针 (节点指针) 进行封装
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

05 封装

在对 context 的封装中&#xff0c;我们只是将 request、response 结构直接放入 context 结构体中&#xff0c;对应的方法并没有很好的封装。 函数封装并不是一件很简单、很随意的事情。相反&#xff0c;如何封装出易用、可读性高的函数是非常需要精心考量的&#xff0c;框架中…

Pwn 二进制漏洞审计

PWN的另一个名字是二进制漏洞审计 Pwn和逆向工程一样&#xff0c;是操作底层二进制的&#xff0c;web则是在php层面进行渗透测试 我是从re开始接触CTF的&#xff0c;有一点二进制基础&#xff0c;本文可能会忽略一些基础知识的补充 ”Pwn”是一个黑客语法的俚语词 &#xff0c;…

JS#1 引入方式和基础语法

JavaScript(JS)是一门跨平台, 面向对象的脚本语言, 来控制网页行为的, 它能够是网页可交互一. 引入方式内部脚本与外部脚本内部脚本: 将JS代码定义在HTML页面中外部脚本: 将JS代码定义在外部JS文件中, 然后引入到HTML页面中注意: 在HTML中,JS代码必须位于<script></sc…

纯手动搭建大数据集群架构_记录008_搭建Hbase集群_配置集群高可用---大数据之Hadoop3.x工作笔记0169

首先准备安装包 然后将安装包分发到集群的其他机器上去 然后因为运行hbase需要zookeeper支持,所以这里首先要去,启动zk 走到/opt/module/hadoop-3.1.3/bin/zk.sh 然后 zk.sh start 启动一下,可以看到启动了已经 然后zk.sh status 可以看zookeeper的状态 然后我们再去启动一下…

购买运动耳机应该考虑什么问题、运动达人必备的爆款运动耳机

喜欢运动的小伙伴都知道&#xff0c;运动和音乐是最配的&#xff0c;在运动中伴随着节奏感的音乐能够让自己更兴奋&#xff0c;锻炼的更加起劲儿。在运动耳机方面我也一直都有所研究&#xff0c;购买运动耳机最重要的就是要满足我们运动时候听音乐的需求&#xff0c;从佩戴舒适…

SAP Insurance Analyzer

SAP Insurance Analyzer 是一款用于保险公司财务和风险管理的软件。SAP Insurance analyzer 支持基于 IFRS 17 或 Solvency II 的保险合同估值和计算要求。SAP Insurance Analyzer 于 2013 年 5 月推出&#xff0c;为源数据和结果数据集成了一个预配置的保险数据模型。 源数据…

网上商城系统用户子功能模块

技术&#xff1a;Java、JSP等摘要&#xff1a;网上购物系统又称为网上商城、网络商城、网上商城、网上开店平台、网店管理系统、网店程序、网上购物系统、网上商城系统等。无论是开设个人网上购物商店还是企业网上商城商城&#xff0c;一套好用的网上购物系统都是必须的。网上购…

颠覆你的认知,这3款软件,每一款都非常实用

闲话少说&#xff0c;直上干货。 1、WizTree WizTree是一款最快的磁盘空间分析器&#xff0c;傻瓜式操作&#xff0c;功能还特别强大。它可快速的从硬盘中查找和释放被大量占用的空它将扫描的硬盘驱动器&#xff0c;直观显示哪些文件和文件夹使用的磁盘空间最多&#xff0c;及时…

day22_IO

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、作业 二、缓冲流 三、字符流 四、缓冲字符流 五、匿名内部类 零、 复习昨日 File: 通过路径代表一个文件或目录 方法: 创建型,查找类,判断类,其他 IO …

EPICS synApps介绍

一、synApps是什么&#xff1f; 1&#xff09; 一个用于同步束线用户的EPICS模块集合。 2&#xff09; EPICS模块 alive, autosave, busy, calc, camac, caputRecorder, dac128V, delaygen, dxp, ip, ip330, ipUnidig, love, mca, measComp, modbus, motor, optics, quadEM,…

如何提升权限运行远程桌面客户端?

​ 我们远程支持他人的时候&#xff0c;有些情况下需要管理员权限才能执行操作&#xff0c;比如更新软件。那么如何提升权限运行远程桌面客户端&#xff1f; 如果您使用 Splashtop SOS 软件远程支持客户&#xff0c;可以使用连线提权功能提升至系统管理员权限&#xff0c;方便…

java 1(概要、变量与运算符)

java ——概要、变量与运算符 ✍作者&#xff1a;电子科大不知名程序员 &#x1f332;专栏&#xff1a;java学习指导 各位读者如果觉得博主写的不错&#xff0c;请诸位多多支持&#xff1b;如果有错误的地方&#xff0c;欢迎在评论区指出 目录java ——概要、变量与运算符命令行…

Linux | 压缩和解压文件详细

linux系统中针对不同的文件&#xff0c;有不同的压缩命令。本文对常见压缩和解压命令进行总结zip文件1.1.unzip解压单个文件unzip 命令可以查看和解压缩 zip 文件。该命令的基本格式如下&#xff1a;unzip filename.zip (文件后可添加压缩相关参数)-d 目录名 将压缩文件解压到指…

牛客网--加法模拟器---题号:NC22007

链接&#xff1a;https://ac.nowcoder.com/acm/problem/22007 来源&#xff1a;牛客网 题目描述 牛牛渐入佳境&#xff0c;他准备做一个加法模拟器来玩玩&#xff0c;输入两个数&#xff0c;分别打印这两个数的横式和竖式运算式子。 输入描述: 输入两个整数a, b 以空格隔开…

大家一起做测试的,凭什么你现在拿20k,我却还只有10k?...

最近我发现一个神奇的事情&#xff0c;我一个97年的朋友居然已经当上了测试项目组长&#xff0c;据我所知他去年还是在深圳的一家创业公司做苦逼的测试狗&#xff0c;短短8个月&#xff0c;到底发生了什么&#xff1f; 于是我立刻私聊他八卦一番。 原来他所在的公司最近正在裁…

softmax与simod如何选择?

前言&#xff1a;博主最近在复现代码的时候遇到一个问题&#xff0c;有的代码使用softmax有的使用sigmod&#xff0c;两者使用到底有什么区别呢&#xff1f; 一、softmax函数 1.1公式 &#xff08;一般只用于最后一层进行分类&#xff09;深度学习中使用Softmax进行分类。 1…

数据结构与算法——5.空间复杂度分析

这篇文章让我们来讨论一下空间复杂度 目录 1.概述 2.java中常见内存占用 2.1基本数据类型内存占用情况 2.2计算机访问内存的方式 2.3引用大小 2.4对象大小 2.5一般内存占用 2.6数组占用地址大小 3.算法的空间复杂度 4.小结 1.概述 计算机的软硬件都经历了一个比较漫…

大数据面试题集锦-Hadoop面试题(四)-YARN

你准备好面试了吗?这里有一些面试中可能会问到的问题以及相对应的答案。如果你需要更多的面试经验和面试题&#xff0c;关注一下"张飞的猪大数据分享"吧&#xff0c;公众号会不定时的分享相关的知识和资料。 文章目录1、为什么会产生 yarn,它解决了什么问题&#xf…

Linux系统常用的2种切换用户命令

文章目录一、su命令二、sudo命令总结一、su命令 1、语法&#xff1a;&#xff08;英文全拼&#xff1a;switch user&#xff09; su [-fmp] [-c command] [-s shell] [--help] [--version] [-] [USER [ARG]]2、参数说明&#xff1a; -f或--fast&#xff1a;不必读启动档&…

值得收藏!适合小微企业的万元数字化攻略!

编者按&#xff1a;小微企业数字化之路困难重重&#xff1f;看看这款全新的全面数字化方案&#xff0c;低成本、部署效率、免安装、免维护、数据安全&#xff0c;小微企业的数字化福音&#xff01;关键词&#xff1a;低成本&#xff0c;开箱即用&#xff0c;免安装免维护&#…