✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨个人主页:余辉zmh–****博客
✨文章所属专栏:c++篇–****博客
文章目录
- 前言
- 一.list的基本概念和使用
- 基本概念
- 基本操作使用
- 二.模拟实现list
- 1.节点类(_list_node)模拟实现
- 2.迭代器类(_list_iterator)模拟实现
- 基本框架
- 成员函数
- 3.链表类(list)模拟实现
- 基本框架
- 默认成员函数
- 迭代器成员函数
- 插入成员函数
- 删除成员函数
- 三.模拟实现完整代码
- `list.h`文件
- `test.cpp`文件
前言
在C++编程中,数据结构是理解和应用算法的基础。列表list
作为线性数据结构的一种,它提供了有序的元素集合,支持高效的插入和删除操作。掌握list
的基本使用和模拟实现,不仅有助于加深对数据结构的理解,也是提高编程能力的关键。
本文将深入探讨list
的基本概念、常见操作以及模拟实现方法。我们将从list
的定义开始,逐步介绍如何使用标准库中的list
类,并进一步探讨如何手动模拟实现一个简单的list
数据结构。通过理论与实践的结合,帮助读者更好地理解和运用list
t数据结构。
一.list的基本概念和使用
在C++中,list
是一种双向链表容器,属于标准模板库(STL)的一部分。
基本概念
-
定义:
list
是一个序列容器,它允许非连续内存分配,并通过双向链表来管理元素。这意味着list
支持常数时间内的任意位置插入和删除操作,但随机访问元素的时间复杂度较高(因为要顺序查找)。 -
结构:
list
由一系列节点组成,每个节点包含数据部分(_val)和指向前一个(_prev)及后一个(_next)节点的指针。这种结构使得list
在插入和删除操作时不需要移动大量数据,只需调整指针即可。 -
迭代器:
list
提供了双向迭代器,支持向前和向后遍历元素。但需要注意的是,list
的迭代器不是随机访问迭代器(因为内存地址不是连续的),因此不支持通过加减整数来直接访问某个位置的元素,只能(++/–)。
基本操作使用
-
创建和初始化:
-
使用默认构造函数创建一个空
list
对象:std::list<int> mylist;
-
使用初始化列表创建
list
:std::list<int> mylist={1,2,3,4,5};
-
-
添加元素:
-
在末尾添加元素:
//mylist:1,2,3,4,5 //调用尾插函数push_back() mylist.push_back(6);
-
在开头添加元素:
//mylist:1,2,3,4,5,6 //调用头插函数push_front() mylist.push_front(0);
-
在指定位置插入元素:
//mylist:0,1,2,3,4,5,6 //获取begin位置的迭代器 std::list<int>::iterator it=mylist.begin(); //迭代器it++,指向下一个位置的迭代器 it++; //在迭代器it位置前,也就是元素1前插入10 //调用插入函数insert() it=mylist.insert(it.10);
-
-
删除元素:
-
删除指定位置的元素:
//mylist:0,10,1,2,3,4,5,6 //删除第二个元素10 std::list<int>::iterator it=mylist.begin(); it++; //调用删除函数erase() it=erase(it);
-
删除第一个元素:
//mylist:0,1,2,3,4,5,6 //调用头删函数pop_front mylist.pop_front();
-
删除最后一个元素:
//mylist:1,2,3,4,5,6 //调用尾删函数pop_back mylist.pop_back();
-
清空整个
list
://调用清空函数clear() mylist.clear();
-
-
访问元素:
- 由于
list
不是随机访问容器,因此不能通过索引直接访问元素。但可以使用迭代器遍历list
来访问元素:std::list<int>::iterator it=mylist.begin(); while(it!=mylist.end()){ cout<<*it<<" " } cout<<endl;
- 由于
-
大小和容量:
-
获取
list
中元素的数量://调用size()函数 size_t sz=mylist.size();
-
判断
list
是否为空://调用empty()函数 bool ret=mylist.empty();
注意:list和string,vector这两个类不同的是,list的大小也就是存储的元素个数就是容量,不再区分,size和capacity
-
-
排序和查找:
-
对
list
进行排序://list类中有自己的sort()函数,也可以使用算法库函数里面的 mylist.sort();
-
查找元素(返回迭代器):
//list类中没有find()函数,所以只能有算法库函数里面的 //查找值为4的迭代器位置 auto it=mylist.find(mylist.begin(),mylist.end(),4);
-
二.模拟实现list
上面我们了解完了如何使用list,接下来,我们将自己模拟实现一个属于自己的list,模拟实现可以帮助我们更好地了解list的底层结构,让我们更好的使用和理解list这个数据结构,而list作为一个双向循环链表,实现一个基本的list需要分别设计三个不同的类,节点类(_list_node
),用来封装节点结构;链表类(list
),用来封装链表的成员函数和链表结构;迭代器类(_list_iterator
),用来封装迭代器。
注意:list
类和vector
类,string
类不同的是,list
类需要对迭代器单独使用一个类进行封装,主要原因是因为它们的底层数据结构特性和访问方式的不同
-
list的底层数据结构:
-
list
是基于链表实现的,链表的节点在内存中的存储位置是不连续的。 - 为了在链表中实现顺序访问(即,通过迭代器逐一访问链表中的元素),需要设计一个迭代器类来封装链表节点的指针(可以理解为,迭代器类中的成员变量表示的是一个节点的指针),并通过运算符重载(如
++
、--
、*
等)来模拟指针的行为。 - 迭代器类内部维护一个指向当前节点的指针,并通过这些运算符重载来移动到下一个或上一个节点,或访问当前节点的数据。
-
-
vector和string的底层数据结构:
-
vector
和string
是基于数组实现的,它们的元素在内存中是连续存储的。 - 由于数组元素的连续性,可以直接使用原生指针(即数组元素的地址)作为迭代器,而无需额外的封装。
-
原生指针自然支持顺序访问(通过指针加减来访问相邻元素),因此
vector
和string
的迭代器本质上就是原生指针。
-
模拟实现的整体框架:
namespace Mylist{
template<class T>
struct _list_node{
//.....
};
//迭代器类封装
template<class T,class Ref,class Ptr>
class _list_iterator{
//.....
};
//链表类封装
template<class T>
class list{
//.....
};
}
1.节点类(_list_node)模拟实现
-
代码实现:
template<class T> struct _list_node{ //成员变量 _list_node<T>* _prev; _list_node<T>* _next; T _val; //构造函数 _list_node(const T& val=T()) :_prev(nullptr) ,_next(nullptr) ,_val(val) {} };
-
实现原理:
- 定义三个成员变量表示一个节点的结构:指向前一个节点的指针
_prev
,指向后一个节点的指针_next
,存储的数据_val
- 构造函数初始化时,两个指针都设置为空指针,参数
val=T()
表示创建的匿名对象(在上一篇vector的模拟实现中有用到,不理解的可以看上一篇文章),自定义类型不如string类型调用对应的构造函数,内置类型也可以调用构造函数初始化。
- 定义三个成员变量表示一个节点的结构:指向前一个节点的指针
2.迭代器类(_list_iterator)模拟实现
基本框架
-
代码实现:
template<class T,class Ref,class Ptr> struct _list_iterator{ typedef _list_node<T> node; typedef _list_iterator<T,Ref,Ptr> self; //成员变量 node* _node; //构造函数 _list_iterator(node* node) :_node(node) {} //....其他成员函数 };
-
实现原理:
-
_list_iteartor
是一个模板类,模版参数有三个,这里参数T比较好理解,表示的是存储的数据类型,而后面两个比较难理解,这里先不做解释,在后面讲到相关函数时在详细解释。 - 用
typedef
对两个类类型名重定义(方便实用),注意,模板类和普通的类有一点不同,普通类,类名就是类型;而模版类,类名加上模版参数才是类型。 - 成员变量是一个节点的指针,构造函数将参数节点指针初始化为迭代器(其实本质上还是指针,因为迭代器就是一种指针类型)。
-
成员函数
1.operator++
函数:
-
代码实现:
//前置++ self& operator++(){ _node=_node->_next; return *this; } //后置++ self operator++(int){ self tmp(*this); _node=_node->_next; return tmp; }
-
实现原理:
- 因为list的迭代器不能直接加减常数,所以只能++或–,而++又分为前置++和后置++
- 前置++,迭代器本身需要+1,并且返回的是+1后的迭代器,直接让当前节点指针指向下一个节点即可,返回类型是
self&(_list_iterator<T,Ref,Ptr>&)
- 后置++,拷贝构造一个新的迭代器用来保存+1前的迭代器,对原本的迭代器+1,返回保存的迭代器,返回类型时
self(_list_iterator<T,Ref,Ptr>)
,因为保存的迭代器是一个局部变量,函数调用结束会销毁,所以不能用引用类型的返回。 - 后置++参数中的int是用来和前置++构成函数重载,进行区分
2.operator--
函数:
-
代码实现:
//前置-- self& operator--(){ _node=_node->_prev; return *this; } //后置-- self operator--(int){ self tmp(*this); _node=_node->_prev; return tmp; }
-
实现原理:
- –又分为前置–和后置–
- 前置–,迭代器本身需要-1,并且返回的是-1后的迭代器,直接让当前节点指针指向前一个节点即可,返回类型是
self&(_list_iterator<T,Ref,Ptr>&)
- 后置–,拷贝构造一个新的迭代器用来保存-1前的迭代器,对原本的迭代器-1,返回保存的迭代器,返回类型时
self(_list_iterator<T,Ref,Ptr>)
,因为保存的迭代器是一个局部变量,函数调用结束会销毁,所以不能用引用类型的返回。 - 后置–参数中的int是用来和前置–构成函数重载,进行区分
3.operator!=
函数:
-
代码实现:
bool operator!=(const selt& it){ return _node!=it._node; }
-
实现原理:
- 左参数迭代器和右参数迭代器判断是否不等,左参数迭代器是this,右参数迭代器是it
- 直接返回两个迭代器是否是同一个节点的指针
4.operator*
函数:
-
代码实现:
T& operator*(){ return _node->_val; } const T& operator*(){ return _node->_val; } //使用模版参数合二为一 Ref operator*(){ return _node->_val; }
-
实现原理:
- 首先迭代器是一个指向节点的指针,对其解引用*,表示获取节点中的数据域(
_val
)存储的数据 - 返回节点中的数据域(
_val
),返回类型是T&
- 这里有一个注意点,如果是对
const
类型的迭代器解引用*,直接将返回类型写成T&
就会导致const
类型的无法调用该函数,写成const T&
才可以调用,所以为了解决这个问题,引入了第二个模版参数Ref
,用来自动替换这两个不同的类型。
- 首先迭代器是一个指向节点的指针,对其解引用*,表示获取节点中的数据域(
5.operator->
函数:
-
代码实现:
T* operator->(){ return &_node->_val; } const T* operator->(){ return &_node->_val; } //使用模版参数合二为一 Ptr operator->(){ return &-node->_val; }
-
实现原理:
-
->
运算符重载和*
不同的是,operator*
,返回的是节点中存储的数据_val的地址,返回类型是T*类型 - 和
operator*
一样,如果有const
类型的迭代器使用->
,返回类型直接写成T*
会导致const
类型的无法调用该函数,写成const T*
类型才可以调用,为了解决这个问题,引入了第三个模版参数Ptr
,用来自动替换这两个不同的类型。
-
3.链表类(list)模拟实现
基本框架
- 代码实现:
template<class T>
class list{
//节点类类型重定义,因为只在类内部使用,所以设置成私有
typedef _list_node<T> node;
public:
//迭代器类类型重定义
typedef _list_iterator<T,T&,T*> iterator;
typedef _list_iterator<T,const T&,const T*> const_iterator;
//...成员函数
private:
//成员变量
node* _head;
size_t _size;
};
-
实现原理:
- 将节点类类型重定义,设置成私有,迭代器类类型重定义,设置成公有
- 成员变量声明一个哨兵节点,声明一个大小用来记录链表的节点个数
默认成员函数
1.构造函数代码实现:
void empty_init(){
_head=new node;
_head->_next=_head;
_head->_prev=_head;
_size=0;
}
//构造函数
list(){
empty_init();
}
-
实现原理:
- 用一个函数将初始化的内容分开存放,这样方便拷贝构造函数直接调用
- 定义一个哨兵节点,该节点的前节点指针和后节点指针都指向该节点,将大小初始化为0
2.拷贝构造函数代码实现:
list(const list<T>& lt){
empty_init();
for(auto& e:lt){
push_back(e);
}
}
-
实现原理:
- 先用函数
empty_init()
函数初始化一个空链表,也就是被拷贝的链表 - 用范围for循环,将被拷贝的链表中的节点数据依次插入到拷贝的链表中完成拷贝
- 先用函数
3.赋值运算符重载:
swap(list<T>& lt){
std::swap(_head,lt._head);
std::swap(_size,lt._size);
}
list<T>& operator=(list<T> lt){
swap(lt);
return *this;
}
-
实现原理:
- 用一个自己实现的交换函数,交换两个链表的哨兵节点和大小,就能达到赋值的效果
- 最后将被赋值的链表返回,返回类型是链表类类型
list<T>
,返回要引用返回
4.析构函数:
void clear(){
auto it=begin();
while(it!=end()){
it=erase(it);
}
_size=0;
}
~list(){
clear();
delete _head;
_head=nullptr;
}
-
实现原理:
- 用一个自己实现的清空函数,通过迭代器来依次删除链表中的每个节点,再讲大小置为0
- 析构函数调用清空函数后,在通过
delete
函数,释放哨兵节点,最后置为空指针
迭代器成员函数
1.begin()
函数代码实现:
//普通对象类型的迭代器
iterator begin(){
return _head->_next;
}
//const对象类型的迭代器
const_iterator begin()const {
return _head->_next;
}
-
实现原理:
直接返回哨兵节点的下一个节点即可
2.end()
函数代码实现:
//普通对象类型的迭代器
iterator end(){
return _head;
}
//const对象类型的迭代器
const_iterator end()const {
return _head;
}
-
实现原理:
直接返回哨兵节点即可
插入成员函数
1.insert()
函数代码实现:
iterator insert(iterator pos const T& x){
node* newnode=new node(x);
node* cur=pos._node;
node* prev=cut->_prev;
prev->_next=newnode;
newnode->_next=cur;
cur->_prev=newnode;
newnode->_prev=prev;
_size++;
return pos
}
-
实现原理:
- 先创建需要插入的新节点newnode,然后找pos迭代器指向的当前节点cur,再找到pos当前指向的节点的前节点prev
- 将新节点插入到节点cur和prev中间即可
2.push_back()
尾插函数代码实现:
void push_back(const T& x){
//相当于在哨兵节点前插入
insert(end(),x);
}
3.push_front()
头插函数代码实现:
void push_front(const T& x){
//相当于在第一个节点前插入,也就是哨兵节点的后一个节点
insert(begin(),x);
}
删除成员函数
1.erase()
删除函数代码实现:
iteraotr erase(iterator pos){
assert(pos!=end());
node* cur=pos._node;
node* prev=cur->_prev;
node* next=cur->_next;
prev->_next=next;
next->_prev=prev;
return next;
}
-
实现原理:
- 首先断言判断pos是否是指向哨兵节点的迭代器,因为不能将哨兵节点删除
- 找到当前迭代器pos指向的节点,再找到前节点prev,后节点next
- 将pos指向的节点删除,返回next指向的节点,也就是删除节点的下一个节点
2**pop_back()
尾删函数代码实现:**
void pop_back(){
//相当于删除最后一个节点,也就是哨兵节点的前一个节点
erase(--end);
}
3.pop_front()
头删函数代码实现:
void pop_front(){
//相当于删除第一个节点,也就是哨兵节点的后一个节点
erase(begin());
}
三.模拟实现完整代码
list.h
文件
#include<iostream>
#include<algorithm>
#include<assert.h>
using namespace std;
namespace Mylist
{
//节点类封装
template<class T>
struct list_node
{
//成员变量,前节点指针,后节点指针,存储的数据val
list_node<T>* _next;
list_node<T>* _prev;
T _val;
//构造函数 创造节点
list_node(const T& val=T())
:_next(nullptr)
,_prev(nullptr)
,_val(val)
{}
};
//迭代器类封装
template<class T,class Ref,class Ptr>
struct _list_iterator
{
typedef _list_iterator<T,Ref,Ptr> self;
typedef list_node<T> node;
//成员变量 一个节点的指针
node* _node;
//构造函数 将传入的节点指针变为迭代器
_list_iterator(node* node)
:_node(node)
{}
Ref operator*(){
return _node->_val;
}
Ptr operator->(){
return &_node->_val;
}
bool operator!=(const self& it){
return _node!=it._node;
}
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;
}
};
//链表类封装
template<class T>
class list
{
typedef list_node<T> node;
void empty_init(){
_head=new node;
_head->_next=_head;
_head->_prev=_head;
_size=