前言
链表作为一个常见的数据结构,在高频插入删除的场景下有独特的优势,在内存的使用上也极少有浪费可以按需申请。今天我们就来简单的学习一下这种数据结构,链表也有很多不同的实现,我们这里和标准库保持一致,实现带头双线循环链表
具体list类的描述可以参考list - C++ Reference (cplusplus.com)
在不同的编译器下string类的实现各有差异,这里我们使用的是Microsoft Visual Studio Community 2022 (64 位) - Current 版本 17.8.5
链表的结构
在正式学习链表之前,我们先来简单的认识一下带头双向循环链表的结构。
带头指的是有哨兵位的头节点,哨兵位指的是一个没有任何数据的节点,作用是标识首位节点(如果是单向指标识头节点),因为有哨兵位的存在,很多操作得以简化
双向指的是每个节点都有标识上一个节点和下一个节点的地址
循环指的是尾节点不在指向nullptr而是指向哨兵位
首先,我们需要实现一个节点的类,用来描述节点的属性
template<class T>
class listNode
{
public:
//listNode() {}
listNode(const T& a=T())
{
data = a;
}
//private:
listNode* next = nullptr;
listNode* previous = nullptr;
T data = T();
};
这里我们还是使用类模板,以适应存储各种类型的数据
全部属性用public描述,因为这个类在后面要经常被使用到,用struct可以实现一样的效果,用友元类也可以使用private描述,这里就简化这些操作了
此时如果我们需要再链表中使用节点,需要先声明节点的属性
typedef listNode<T> Node;
现在我们就可以来搭建一个链表类的框架出来
#pragma once
#include <iostream>
#include <algorithm>
//#include <assert.h>
using namespace std;
namespace zzzyh
{
template<class T>
class list
{
public:
private:
Node* head;
size_t sz = 0;
};
}
其中head表示哨兵位的地址,size用来标识有效元素的个数
构造函数
allocator是空间配置器,这里我们还是先忽略这个点
第一个是默认构造函数
list()
{
empty_init();
}
这里empty_init是初始化哨兵位,因为经常用到我们可以封装为一个函数
void empty_init()
{
head = new Node;
head->next = head;
head->previous = head;
sz = 0;
}
第二个是使用n个默认值进行初始化
第三个是使用迭代器区间进行初始化
第四个是拷贝构造
list(const list&s)
{
empty_init();
for (auto &x : s)
{
push_back(x);
}
}
顺带,我们将赋值重载也实现
list<T>& operator=(const list&s)
{
list<T> rem(s);
swap(rem);
return *this;
}
这里的push_back功能是尾插一个元素,我们先使用在后面再实现
这里swap可以实现两个链表的交换,还是先使用在后面我们再来实现
这里还是会有深浅拷贝的问题,只有不能两个节点指向同一块空间,不能多次释放一块空间
在最后我们介绍一个c++11引入的构造方式
这里initializer_list是一个类
这个类我们不在这里不展开只介绍使用
#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"
#include <list>
using namespace std;
int main()
{
list<int> list1 = { 1,2,3,4,5 };
list<int> list2({ 6,7,8,9,0 });
for (int i : list1)
{
cout << i << " ";
}
cout << endl;
for (int i : list2)
{
cout << i << " ";
}
cout << endl;
return 0;
}
下面我们来实现一下这种功能
list(initializer_list<T> li)
{
empty_init();
for (auto& a : li)
{
push_back(a);
}
}
这个构造方式不仅仅再list中适用,所有实现这个接口的容器均可以使用,具体哪些容器实现类还需要再查询文档
析构函数
析构函数可以先将使用有效的节点释放(clear)再释放哨兵位头节点
~list()
{
clear();
delete head;
}
我们再来实现一下clear
void clear()
{
iterator beg = begin();
while (beg != end())
{
beg = erase(beg);
}
}
这里的erase可以删除指定迭代器指向的位置,这里还是先使用在后面会实现其功能
迭代器
链表的迭代器相比于前面我们介绍的顺序表要复杂很多,因为链表的内存空间是不连续的,这就意味着++这种操作符需要重载为新的含义
我们这里可以把迭代器实行为一个类来描述,因为只有类才能实现运算符重载
template<class T>
class list_iterator
{
public:
typedef listNode<T> Node;
Node* cut;
list_iterator(Node* c)
:cut(c)
{}
T& operator*() const
{
return cut->data;
}
T* operator->() const
{
return &(cut->data);
}
list_iterator operator++()
{
cut = cut->next;
return *this;
}
list_iterator operator--() {
cut = cut->previous;
return *this;
}
list_iterator operator++(int)
{
cut = cut->next;
return cut->previous;
}
list_iterator operator--(int) {
cut = cut->previous;
return cut->previous;
}
list_iterator operator+(size_t i) {
Node* ret = cut;
while (i != 0)
{
ret = ret->next;
i--;
}
return ret;
}
list_iterator operator-(size_t i) {
Node* ret = cut;
while (i != 0)
{
ret = ret->previous;
i--;
}
return ret;
}
bool operator!=(list_iterator pos) const
{
return cut != pos.cut;
}
bool operator==(list_iterator pos) const
{
return cut == pos.cut;
}
};
同样我们可以实现const迭代器
template<class T>
class list_const_iterator
{
public:
typedef listNode<T> Node;
Node* cut;
list_const_iterator(Node* c)
:cut(c)
{}
const T& operator*() const
{
return cut->data;
}
const T* operator->() const
{
return &(cut->data);
}
list_const_iterator operator++()
{
cut = cut->next;
return *this;
}
list_const_iterator operator--() {
cut = cut->previous;
return *this;
}
list_const_iterator operator++(int)
{
cut = cut->next;
return cut->previous;
}
list_const_iterator operator--(int) {
cut = cut->previous;
return cut->previous;
}
list_const_iterator operator+(size_t i) {
Node* ret = cut;
while (i != 0)
{
ret = ret->next;
i--;
}
return list_iterator(ret);
}
list_const_iterator operator-(size_t i) {
Node* ret = cut;
while (i != 0)
{
ret = ret->previous;
i--;
}
return list_iterator(ret);
}
bool operator!=(list_const_iterator pos) const
{
return cut != pos.cut;
}
bool operator==(list_const_iterator pos) const
{
return cut == pos.cut;
}
};
此时我们发现,这两个类高度相似,我们也可以用到类模板的思想实现
template<class T,class Ref,class Ptr>
class list_iterator
{
public:
typedef listNode<T> Node;
Node* cut;
list_iterator(Node* c)
:cut(c)
{}
Ref operator*()
{
return cut->data;
}
Ptr operator->()
{
return &(cut->data) ;
}
list_iterator operator++()
{
cut = cut->next;
return *this;
}
list_iterator operator--() {
cut = cut->previous;
return *this;
}
list_iterator operator++(int)
{
cut = cut->next;
return cut->previous;
}
list_iterator operator--(int) {
cut = cut->previous;
return cut->previous;
}
list_iterator operator+(size_t i) {
Node* ret = cut;
while (i != 0)
{
ret = ret->next;
i--;
}
return list_iterator(ret);
}
list_iterator operator-(size_t i) {
Node* ret = cut;
while (i != 0)
{
ret = ret->previous;
i--;
}
return list_iterator(ret);
}
bool operator!=(list_iterator pos) const
{
return cut != pos.cut;
}
bool operator==(list_iterator pos) const
{
return cut == pos.cut;
}
};
如果此时我们需要再链表里使用迭代器,同样需要先声明迭代器的属性
typedef list_iterator<T,T&,T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
同理如果上面那两种未使用类模板实现的迭代器在使用时也需要先声明再使用
typedef list_iterator<T> iterator;
typedef list_const_iterator<T> const_iterator;
此时我们可以实现begin和end了
iterator begin()
{
return iterator(head->next);
}
iterator end()
{
return iterator(head);
}
const_iterator begin() const
{
return const_iterator(head->next);
}
const_iterator end()const
{
return const_iterator(head);
}
其他的获得迭代器的函数我们就不再介绍了,和前面介绍过的容器功能相同
容量相关
size(),可以获得有效节点的个数,不包含哨兵位头节点
size_t size()
{
return sz;
}
empty(),判断此链表是否为空(即没有有效元素,还是不包含哨兵位头节点)
bool empty()
{
return sz == 0;
}
访问(查)相关
front(),可以返回首节点的值的引用
back(),可以返回最后一个节点的值的引用
增删改相关
push_back(),可以尾插一个节点
void push_back(const T& a)
{
Node* newNode = new Node(a);
if (head->next == head)
{
newNode->next = newNode->previous = head;
head->next = head->previous = newNode;
}
else
{
newNode->next = head;
newNode->previous = head->previous;
head->previous->next = newNode;
head->previous = newNode;
}
sz++;
//insert(end(), a);
}
insert(),可以在指定迭代器之前的位置插入一个节点,迭代器不失效
void insert(iterator pos, const T& data)
{
if (pos == end())
{
push_back(data);
return;
}
else {
Node* newNode = new Node(data);
if (pos == begin()) {
newNode->previous = head;
newNode->next = head->next;
head->next->previous = newNode;
head->next = newNode;
}
else {
newNode->previous = pos.cut->previous;
newNode->next = pos.cut;
pos.cut->previous->next = newNode;
pos.cut->previous = newNode;
}
}
sz++;
}
push_front(),可以头插一个节点
void push_front(const T& a)
{
insert(begin(), a);
}
erase(),可以删除指定迭代器的节点,并且返回当前迭代器的下一个位置
iterator erase(iterator pos)
{
if (pos.cut == head) {
return pos;
}
Node* left = pos.cut->previous;
Node* right = pos.cut->next;
left->next = right;
right->previous = left;
delete pos.cut;
sz--;
return right;
}
pop_back(),可以尾删尾节点
void pop_back()
{
erase(--end());
}
pop_front(),可以头删头节点
void pop_front()
{
erase(begin());
}
swap(),可以交换两个链表的内容,这里单独实现是为了提高交换效率
void swap(list& rem)
{
swap(sz,rem.sz);
swap(head, rem.head);
}
迭代器失效
这里还是就有迭代器失效的问题,但只要在删除时会失效传入的迭代器,其他迭代器不受影响。标准库和我们这的结局方案相同,在调用删除时会将新的迭代器(即删除元素的下一个位置)返回,及时更新使用即可
结语
以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。
因为这对我很重要。
编程世界的小比特,希望与大家一起无限进步。
感谢阅读!