目录
前言
我们前面重点介绍了string的使用和模拟实现以及vector的使用及模拟实现。下面我们来说说list的模拟实现。
list是一个带头双向循环链表
,除了不支持任意位置的随机访问,其余的操作它的效率都是非常高的,下面就来学习一下list容器。
list介绍
简单的带头双向循环链表如下:
list的介绍:
- list是可以在常数范围内
任意位置
进行插入
和删除
的序列式容器,且该容器可以前后双向迭代
。 list的底层是带头双向链表结构
,且每个元素存储在互不相关的独立节点中,在节点中通过指针指向前一个元素和后一个元素。- list和forward_list非常相似,
forward_list是单链表。
- 与
其他序列式容器
(array,vector,deque)相比
,list在任意位置进行插入,移除元素的效率更高。
- 与其他序列式容器相比,list和forward_list
最大的缺陷是不支持任意位置的随机访问
。
对于list的相关接口函数的学习可以看:cplusplus—list。
list相关函数接口的使用与vector和string类型,遍不再细讲,直接进入list的模拟实现。
list的模拟实现
总体结构
list不像前面的vector和string只需一个类即可完成所有操作,而是分成了三个类:节点类
、``迭代器类以及链表类
。
节点类
由于在需要插入链表节点时都需要创造节点,因此我们将节点封装为一个类
,在有需要时直接new一个即可,方便快捷。
每个节点都由三部分组成:指向前一个节点的指针(_prve),指向后一个节点的指针(_next),以及数据本身(_data)。
template<class T>
struct ListNode//struct在c++中是默认公有
{
ListNode* _prev;
ListNode* _next;
T _data;
ListNode(const T& data = T())//使用匿名对象作为缺省参数传参
:_prev(nullptr)
,_next(nullptr)
,_data(data)
{}
};
由于我们后面在进行链表的各种操作时会频繁用到这三个变量,因此我们直接使用struct
默认公有即可。
使用匿名对象
作为缺省参数传参
:
- 若
是内置类型
,则C++为了兼容模版,同样也可以调用内置类型的默认构造
- 若
T是自定义类型
,则直接调用它的默认构造
迭代器类
节点类的类型过长,我们typedef一下,缩短名字。
typedef ListNode<T> Node;
在前面vector和string的迭代器使用中,我们都是直接用的原生指针T作为他们的迭代器,那list是否可以直接用Node来作为迭代器呢。答案是否定的。
因为链表是由一个个节点构成,每个节点都是独立new出来的,它们的空间是不连续的
,Node*是内置类型只能直接++或- -,这对于不连续的空间是行不通的。
//封装Node*成为一个类控制它的行为
template<class T>//每次使用模板时都需要加上
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T> Self;
Node* _node;
//运算符重载控制行为
};
链表类
链表类来实现链表的各个增删查改的功能,是最主要的类,其成员变量包括了一个哨兵位节点。
template<class T>
class List
{
typedef ListNode<T> Node;//节点
typedef ListIterator<T> iterator;//迭代器
public:
//....
private:
Node* _head;//哨兵位节点
};
默认成员函数
一个自定义类型有空间申请释放的类,最重要的默认成员函数都是必须要写的,包括构造
,拷贝构造
,赋值重载
,析构
。
构造函数
但凡我们要创建一个链表
,无论是构造还是拷贝构造,都需要创造一个头结点
,因此我们直接把头结点写成一个函数,后面直接调用即可:
//创建头结点(哨兵位),后续各种构造都会使用上,因此将其写成函数
void empty_init()
{
_head = new Node;//申请一个Node类型的空间
_head->_next = _head;
_head->_prev = _head;
}
对于默认构造函数,我们只需要创造头结点即可:
List()//默认构造,创建一个头结点即可
{
empty_init();
}
迭代器区间构造初始化
template<class InputIterator>
List(InputIterator first, InputIterator end)//函数模板——支持任意迭代器的迭代区间进行初始化
{
empty_init();
while (first != end)
{
push_back(*first);
first++;
}
}
带参构造,n是有n个数据,x是数据值。
List(size_t n, const T& x = T())
{
empty_init();
for (size_t i = 0; i < n; i++)
{
push_back(x);
}
}
List(int n, const T& x = T())
{
empty_init();
for (int i = 0; i < n; i++)
{
push_back(x);
}
}
我们多写一个list(int n, const T& val = T())是因为如果不写时,我们写list< int > v(3,5)时会优先匹配上迭代器构造,此时InputIterator会被替代成为int,而解引用就会报错:
error C2100: 非法的间接寻址
list中还有一个initializer_list构造函数
,这使得其可以多参数初始化构造,即:
List<int> ls1 = { 1,2,3,4 };
模拟实现也很简单:
List(initializer_list<T> il)
{
empty_init();//首先创建头结点
for (auto& e : il)
{
push_back(e);
}
}
拷贝构造
同样在拷贝构造前要先初始化创建头结点:
//lt2(lt1)
List(const List<T>& lt)//拷贝构造
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
赋值重载
赋值重载用的传值传参
,会调用拷贝构造,则lt就是lt3的一个拷贝,把lt1的节点和lt的节点进行了交换,lt1就拿到了lt的节点,lt拿到了lt1的节点,但是lt出了作用域直接销毁,没有影响。
//lt1 = lt3
List<T>& operator=(List<T> lt)
{
swap(_head, lt._head);
return *this;
}
析构函数
析构函数只需要把每个节点给销毁即可。
~List()
{
clear();//清理各个节点
deplete _head;
_head = nullptr;
}
clear()涉及到迭代器以及删除函数,后面再讲解。
迭代器实现
上面我们只是写了迭代器类的一个框架,下面来把这个类完整实现。
双向迭代器
vector和string都是使用的双向迭代器,包括前置++,后置++,前置- -以及后置- - 。
上面我们也说过为何要重载这些运算符:
因此我们封装一个迭代器类,再通过运算符重载来控制它的行为。
typedef ListIterator<T> Self;
Self& operator++()//前置++
{
_node = _node->_next;
return *this;//返回++之后
}
Self operator++(int)//后置++
{
Self tmp(_node);
_node = _node->_next;
return tmp;
}
Self& operator--()//前置--
{
_node = _node->_prev;
return *this;
}
Self operator--(int)//后置--
{
Self tmp(_node);
_node = _node->_prev;
return tmp;
}
当然还有随机迭代器
,即随机移动迭代器的位置,即+n或-n
,当然对于链表是不支持
的,我们也不用去实现。
迭代器的其他功能
对于迭代器,不只是要实现++和- -那么简单,在调用迭代器进行遍历时,我们会使用到解引用,以及判断相等的操作。而对于如果链表节点中带的是自定义类型就还会有取节点指针地址访问其成员。
因此迭代器的其他功能还有以下几个:
解引用
:operator*判断是否相等
:operator==或operator!=取节点指针地址
:operator->
T& operator*()//解引用
{
return _node->_data;
}
bool operator==(const Self& it)//判断相等
{
return _node = it._node;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
T* operator->()//取当前节点指针的地址
{
return &_node->_data;
}
对于前面两个解引用和判断相等都比较好理解,下面我们来讲讲取当前节点指针的地址这点,先看代码:
struct Date
{
Date(int year = 1970, int month = 01, int day = 01)
:_year(year)
,_month(month)
,_day(day)
{}
int _year;
int _month;
int _day;
};
void test2()
{
List<Date> ls;
ls.push_back(Date(2024, 10, 01));
ls.push_back(Date(2025, 11, 02));
ls.push_back(Date(2026, 12, 03));
auto it1 = ls.begin();
while (it1 != ls.end())
{
cout << it1->_year << "年" << it1->_month << "月" << it1->_day << "日" << endl;
it1++;
}
}
结果如下:
由代码可以发现,我们可以直接通过迭代器it1->
的方式来访问自定义类型中的成员
,当然这也更方便我们操作,就会有人好奇它是如何得来的,下面来解释一下:
- list迭代器本身也是一个Node*的一个指针,想要访问其中自定义类型的成员,就需要:
(*迭代器).成员
,这样的方式写起来比较难受,因此就有了operator->重载 - 重载operator->实际上返回的是指向当前节点指针的地址,而需要访问其中的成员,则需再加上->操作符,因此整个流程就变成了:
it1.operator->()->_year;
根据运算符重载的规则又可以写成:
it1->->_year;
而编译器又对其进行了优化,所以就变成了:
it1->_year;
用多参数模板完成最终的迭代器类
迭代器不只有iterator
,还有const_iterator
。
我们在实现iterator时,用的一个模版:
typedef ListIterator<T> iterator;//迭代器
当我们还想要const_iterator时,就要再写一个ListConstIterator类:
typedef ListConstIterator<T> const_iterator;//const迭代器
而写出的ListConstIterator这个类,本质上和ListIterator这个类没有太大差别,此时就会造成代码冗余
,于是我们就想到了使用模板来解决。单一个参数模版可能还不太够,我们使用到多参数模板
。
参数如下:
T
:节点中值的普通类型Ref
:节点中值的引用Ptr
:节点中值的指针
由此,完整的迭代器类如下:
//封装Node*成为一个类控制它的行为
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)
{}
Ptr operator->()//取当前节点的指针
{
return &_node->_data;
}
Ref operator*()//解引用
{
return _node->_data;
}
Self& operator++()//前置++
{
_node = _node->_next;
return *this;//返回++之后
}
Self operator++(int)//后置++
{
Self tmp(_node);
_node = _node->_next;
return tmp;
}
Self& operator--()//前置--
{
_node = _node->_prev;
return *this;
}
Self operator--(int)//后置++
{
Self tmp(_node);
_node = _node->_prev;
return tmp;
}
bool operator==(const Self& it)//判断相等
{
return _node = it._node;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
在list链表类中就可以对iterator和const_iterator这两个的begin()和end()函数进行定义:
template<class T>
class List
{
typedef ListNode<T> Node;
typedef ListIterator<T, T&, T*> iterator;//声明两种不同类型的迭代器
typedef ListIterator<T, const T&, const T*> const_iterator;
public:
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);
}
};
这样,对于普通对象还是const对象都可以从容应对了。
list的容量相关和数据访问
empty()和size()
- 对于
判空
,只需判断begin()和end()是否相等
,相等则为空。 - 对于
统计大小
,只需要迭代器访问一遍计数
即可。
bool empty() const
{
return begin() == end();
}
size_t size() const
{
int count = 0;
auto it = begin();
while (it != end())
{
count++;
it++;
}
return count;
}
front()和back()
对于数据访问,库里面给出的接口只有访问第一个数据和最后一个数据,代码也比较简单。
T front()
{
return *begin();
}
const T front() const
{
return *begin();
}
T back()
{
return *(--end());
}
const T back() const
{
return *(--end());
}
list的修改操作
任意位置插入和删除
任意位置插入和删除就是给了个迭代器位置pos
,在pos位置前插入或删除pos位置的节点。
对于任意位置插入
步骤如下:
- 找到
pos位置
对应的节点cur
- 新建一个节点
newnode
- 找到cur的前一个节点
prev
- 此时将
prev,newnode,cur三个节点链接在一起
即可。
代码如下:
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
//prev newnode cur
cur->_prev = newnode;
newnode->_next = cur;
newnode->_prev = prev;
prev->_next = newnode;
return iterator(newnode);
}
对于任意位置删除
步骤如下:
- 首先
判断链表不能为空
- 找到
pos位置的节点cur
,以及前一个节点prev
和后一个节点next
- 将
prev和next链接在一起
删除cur节点
- 此时
pos迭代器会失效
,pos指向的节点被释放了
,因此要返回next节点
代码如下:
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
next->_prev = prev;
prev->_next = next;
delete cur;
return iterator(next);
}
头尾插删
对于头尾插删在数据结构的链表中已经有详细讲述,这里便不再多说。
学了前面那么多,我们已经学会了代码复用,直接复用insert和erase即可:
void push_back(const T& x = T())
{
push_back(end(), x);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
清理
只需要把除头结点外的所有节点全部删除即可。
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
完整代码
list.h
#pragma once
#include<iostream>
#include<assert.h>
#include<algorithm>
using namespace std;
//list是带头双向循环链表
namespace bit
{
//定义节点类
template<class T>
struct ListNode//struct在c++中是默认公有
{
ListNode* _prev;
ListNode* _next;
T _data;
ListNode(const T& data = T())//使用匿名对象作为缺省参数传参
:_prev(nullptr)
,_next(nullptr)
,_data(data)
{}
};
//封装Node*成为一个类控制它的行为
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)
{}
Ptr operator->()//取当前节点的指针
{
return &_node->_data;
}
Ref operator*()//解引用
{
return _node->_data;
}
Self& operator++()//前置++
{
_node = _node->_next;
return *this;//返回++之后
}
Self operator++(int)//后置++
{
Self tmp(_node);
_node = _node->_next;
return tmp;
}
Self& operator--()//前置--
{
_node = _node->_prev;
return *this;
}
Self operator--(int)//后置++
{
Self tmp(_node);
_node = _node->_prev;
return tmp;
}
bool operator==(const Self& it)//判断相等
{
return _node = it._node;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
template<class T>
class List
{
typedef ListNode<T> Node;
//我们想像vector一样,直接用指针作为迭代器,但是对Node是行不通的,因为它不是连续的空间,不能直接++或--,迭代器的操作它进行不了
//Node* 作为内置类型,我们不能修改它的行为,所以我们可以通过用类来封装Node*,再通过运算符重载来控制它的行为
//typedef Node* iterator;
typedef ListIterator<T, T&, T*> iterator;//声明两种不同类型的迭代器
typedef ListIterator<T, const T&, const T*> const_iterator;
public:
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);
}
//创建头结点(哨兵位),后续各种构造都会使用上,因此将其写成函数
void empty_init()
{
_head = new Node;//申请一个Node类型的空间
_head->_next = _head;
_head->_prev = _head;
}
List()//默认构造,创建一个头结点即可
{
empty_init();
}
template<class InputIterator>
List(InputIterator first, InputIterator end)//函数模板——支持任意迭代器的迭代区间进行初始化
{
empty_init();
while (first != end)
{
push_back(*first);
first++;
}
}
List(size_t n, const T& x = T())//带参构造
{
empty_init();
for (size_t i = 0; i < n; i++)
{
push_back(x);
}
}
List(int n, const T& x = T())
{
empty_init();
for (int i = 0; i < n; i++)
{
push_back(x);
}
}
List(initializer_list<T> il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
List(const List<T>& lt)//拷贝构造
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
List<T>& operator=(List<T> lt)//赋值重载
{
swap(_head, lt._head);
return *this;
}
~List()
{
clear();//清理各个节点
delete _head;
_head = nullptr;
}
bool empty() const
{
return begin() == end();
}
size_t size() const
{
int count = 0;
auto it = begin();
while (it != end())
{
count++;
it++;
}
return count;
}
T front()
{
return *begin();
}
const T front() const
{
return *begin();
}
T back()
{
return *(--end());
}
const T back() const
{
return *(--end());
}
void push_back(const T& x )
{
/*Node* newnode = new Node(x);
Node* tail = _head->_prev;
tail->_next = newnode;
_head->_prev = newnode;
newnode->_next = _head;
newnode->_prev = tail;*/
insert(end(), x);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
//prev newnode cur
cur->_prev = newnode;
newnode->_next = cur;
newnode->_prev = prev;
prev->_next = newnode;
return iterator(newnode);
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
next->_prev = prev;
prev->_next = next;
delete cur;
return iterator(next);
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
private:
Node* _head;//哨兵位节点
};
struct Date
{
Date(int year = 1970, int month = 01, int day = 01)
:_year(year)
,_month(month)
,_day(day)
{}
int _year;
int _month;
int _day;
};
}
感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。