list的模拟实现
- 一.list与vector
-
-
- 1.底层结构的本质区别
- 2.模拟实现的核心差异
-
- 2.1数据存储的方式
- 2.2 初始化的过程
- 2.3 插入元素的操作
- 2.4 删除元素的操作
- 2.5 访问元素的效率
- 3.总结
-
- 二.头文件list.h
-
-
-
- **命名空间与模板**
-
- **核心数据结构**
-
- **构造函数**
-
- **模板参数设计**
-
- **核心成员**
-
- **构造与析构**
-
- **操作符重载**
-
- **类型定义**
-
- **迭代器接口**
-
- **内存管理**
-
- **构造函数**
-
- **初始化列表构造函数**
-
- **交换方法**
-
- **赋值运算符重载**
-
- **链表初始化**
-
- **清空操作**
-
- **首尾操作**
-
- **插入操作**
-
- **删除操作**
-
-
- 三.头文件test.cpp
-
-
-
- **打印函数**
-
- **测试函数1**
-
- **测试函数3:迭代器操作测试**
-
- **测试函数4:插入删除测试**
-
-
一.list与vector
顺序表与链表因底层结构不同而导致的模拟实现差异。
1.底层结构的本质区别
- 顺序表:像一排连续的"格子",所有元素按顺序依次放进这些格子里,每个格子紧挨着下一个,中间没有空隙。整个结构占用一整块连续的内存空间,元素的位置(比如第1个、第2个)和它们在内存中的物理位置完全对应。
- 链表:像一串"珠子",每个珠子(称为"节点")分为两部分:一部分装数据,另一部分装一个"指向"下一个珠子的"钩子"(指针或引用)。这些珠子在内存中可能分散在不同位置,全靠钩子串联起来形成逻辑上的顺序,物理位置和逻辑位置无关。
2.模拟实现的核心差异
基于上述底层结构,两者的实现逻辑(比如怎么存数据、怎么加元素、怎么删元素)完全不同:
2.1数据存储的方式
-
顺序表 :
实现时需要先划定一块固定大小的连续空间(比如先准备10个格子),用一个"容器"管理这些格子,同时记录当前装了多少元素(比如已用3个格子)和总共能装多少(10个)。元素的位置可以直接算出来:第
i
个元素一定在第1个元素往后数i
个格子的位置,所以能直接找到。 -
链表 :
实现时不需要预先划定空间,而是用一个"头指针"记录第一个节点的位置,每个节点都是单独创建的(需要时才申请内存)。每个节点除了数据,必须带一个"钩子"指向后面的节点,最后一个节点的钩子指向"空",表示结束。元素的位置无法直接算,只能从第一个节点开始,顺着钩子一个个找。
2.2 初始化的过程
-
顺序表 :
核心是"圈定初始空间"。比如一开始决定能装5个元素,就申请一块能放下5个元素的连续内存,然后标记"当前装了0个元素"。如果后续元素超过5个,就需要"扩容"------重新申请一块更大的连续内存(比如能装10个),把原来的元素搬过去,再用新空间替代旧空间。
-
链表 :
核心是"确定起点"。通常初始化时,要么让头指针指向"空"(表示链表为空),要么先创建一个"哨兵节点"(不存数据,专门用来统一操作),头指针指向这个哨兵节点,同时标记"当前有0个元素"。后续添加元素时,再一个个创建新节点,用钩子连起来即可,不需要提前规划总容量。
2.3 插入元素的操作
-
顺序表 :
如果要在第
i
个位置插入元素,必须先检查当前空间是否够用(不够就扩容),然后把第i
个及之后的所有元素都往后"挪一个位置"(腾出第i
个格子),最后把新元素放进第i
个格子,再更新"已用元素数"。比如在"[1,2,3]"的第2个位置插4,需要先把2、3往后挪,变成"[1, ,2,3]",再放4,结果是"[1,4,2,3]"。
-
链表 :
如果要在第
i
个位置插入元素,不需要挪动其他元素,只需要:- 新建一个节点,存入数据;
- 找到第
i-1
个节点,把它的钩子从原来指向第i
个节点,改成指向新节点; - 再让新节点的钩子指向原来的第
i
个节点。
比如在"1→2→3"的2前面插4,只需要让1的钩子指向4,4的钩子指向2,就变成"1→4→2→3",其他节点不需要动。
2.4 删除元素的操作
-
顺序表 :
如果要删除第
i
个元素,需要先把第i
个元素去掉,然后把第i+1
个及之后的所有元素往前"挪一个位置"(填补空缺),最后更新"已用元素数"。比如删除"[1,2,3,4]"的第2个元素(2),需要把3、4往前挪,变成"[1,3,4]",中间不能留空隙(否则不符合连续空间的特性)。
-
链表 :
如果要删除第
i
个元素,只需要找到第i-1
个节点,把它的钩子从原来指向第i
个节点,改成指向第i+1
个节点,然后把第i
个节点的内存释放掉即可。比如删除"1→4→2→3"中的4,只需要让1的钩子直接指向2,4就被"摘"下来了,其他节点位置不变。
2.5 访问元素的效率
-
顺序表 :
因为元素位置能直接计算(第
i
个元素的位置 = 起点 +i
×元素大小),所以访问任意位置的元素时,一步就能找到,效率很高(时间复杂度O(1)
)。 -
链表 :
因为元素位置靠钩子串联,访问第
i
个元素时,必须从第一个节点开始,顺着钩子一个个数到第i
个,效率较低(时间复杂度O(n)
)。
3.总结
顺序表的底层是"连续内存",决定了它的实现依赖"空间预分配+位置计算",操作时需要挪动元素但访问快;
链表的底层是"离散节点+指针",决定了它的实现依赖"动态创建+指针串联",操作时无需挪动元素但访问慢。
这种底层结构的差异,是两者实现方式和性能特性的根本原因。
二.头文件list.h
cpp
#pragma once
#include<iostream>
using namespace std;
namespace yl
{
//定义单个结点结构
template<typename T>
struct _list_node
{
_list_node<T>* _prev;
_list_node<T>* _next;
T _data;
_list_node(const T& x)
:_data(x)
, _prev(nullptr)
, _next(nullptr)
{
}
};
1. 命名空间与模板
- 代码位于
yl
命名空间内,避免命名冲突 - 使用模板
template<typename T>
实现泛型,支持任意数据类型
2. 核心数据结构
_list_node
结构体表示链表的节点- 包含三个成员变量:
_prev
:指向前驱节点的指针_next
:指向后继节点的指针_data
:存储节点数据的模板类型变量
3. 构造函数
- 带参数的构造函数
_list_node(const T& x)
- 初始化节点数据
_data
为传入值 - 将
_prev
和_next
指针初始化为nullptr
设计特点
- 双向链表结构,支持双向遍历
- 节点独立管理前后连接关系
- 模板设计保证了良好的扩展性
cpp
// 链表迭代器模板
// T: 数据类型,Ref: 引用类型,Ptr: 指针类型
template<typename 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->_data; }
// 箭头操作符:返回节点数据的指针
Ptr operator->() { return &_node->_data; }
// const版本箭头操作符:用于常量迭代器
Ptr operator->() const { return &_node->_data; }
// 前置++:移动到下一个节点并返回自身引用
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;
}
// 不等比较操作符:比较两个迭代器是否指向不同节点
bool operator!=(const Self& it) const {
return it._node != this->_node;
}
// 析构函数:迭代器不拥有节点内存,无需释放
~_list_iterator() {}
};
以上这段代码实现了双向链表的迭代器
4. 模板参数设计
- 三个模板参数:
T
(数据类型)、Ref
(引用类型)、Ptr
(指针类型) - 通过分离引用和指针类型,支持普通迭代器和常量迭代器
5. 核心成员
Node* _node
:指向当前节点的指针- 类型别名:
Self
(自身类型)和Node
(节点类型)
6. 构造与析构
- 构造函数:接收节点指针初始化迭代器
- 析构函数:空实现(迭代器不负责内存管理)
7. 操作符重载
- 解引用操作符
*
:返回节点数据引用 - 箭头操作符
->
:返回节点数据指针(含const版本) - 自增操作符
++
:前置和后置版本(支持双向移动) - 自减操作符
--
:前置和后置版本(支持双向移动) - 不等比较
!=
:比较节点指针是否相等
迭代器特性
- 双向迭代器:支持前后移动
- 符合STL迭代器规范(实现必要操作符)
- 轻量级设计:仅包含一个指针成员
内存管理
- 迭代器不拥有节点内存
- 节点生命周期由链表容器管理
cpp
template<typename T>
class list {
public:
typedef _list_node<T> Node; // 链表节点类型
typedef _list_iterator<T, T&, T*> iterator; // 普通迭代器
typedef _list_iterator<T, const T&, const T*> const_iterator; // 常量迭代器
// 返回指向第一个元素的迭代器
iterator begin() { return _head->_next; }
// 返回指向尾后位置的迭代器
iterator end() { return _head; }
// 常量版本的begin()和end()
const_iterator begin() const { return const_iterator(_head->_next); }
const_iterator end() const { return const_iterator(_head); }
// 默认构造函数
list() { empty_init(); }
// 析构函数:释放链表内存
~list() {
clear(); // 清空所有数据节点
delete _head; // 释放头节点
}
// 拷贝构造函数
list(const list<T>& lt) {
empty_init(); // 初始化空链表
for (auto& e : lt) { // 逐个复制元素
push_back(e);
}
}
这段代码实现了双向链表的容器类,下面是重点内容概括:
8. 类型定义
- 嵌套类型定义:
Node
(节点类型) - 迭代器类型:
iterator
:普通迭代器(引用类型T&
,指针类型T*
)const_iterator
:常量迭代器(引用类型const T&
,指针类型const T*
)
9. 迭代器接口
begin()
:返回指向第一个元素的迭代器end()
:返回指向尾后位置的迭代器- 提供常量版本,支持常量对象遍历
10. 内存管理
- 哨兵节点(头节点
_head
)设计:- 空链表时
_head->_next == _head
- 简化边界条件处理
- 空链表时
- 析构函数:
- 调用
clear()
释放所有数据节点 - 释放头节点内存
- 调用
11. 构造函数
- 默认构造函数:调用
empty_init()
初始化空链表 - 拷贝构造函数:
- 初始化空链表
- 通过范围for循环逐个复制元素
. 关键方法
empty_init()
:初始化头节点,构建循环链表clear()
:清空所有数据节点但保留头节点
cpp
// 初始化列表构造函数
list(initializer_list<T> il) {
empty_init(); // 初始化空链表
for (auto& e : il) { // 使用初始化列表中的元素填充链表
push_back(e);
}
}
// 交换两个链表的内容
void swap(list<T> lt) {
std::swap(_head, lt._head); // 交换头节点指针
std::swap(_size, lt._size); // 交换元素数量
}
// 赋值运算符重载
list<T>& operator=(const list<T> il) {
swap(il); // 通过交换实现拷贝赋值
return *this;
}
// 初始化空链表
void empty_init() {
_head = new Node(T()); // 创建头节点,初始化为T的默认值
_head->_next = _head; // 头节点的next指向自身
_head->_prev = _head; // 头节点的prev指向自身
_size = 0; // 链表大小初始化为0
}
// 清空链表但保留头节点
void clear() {
iterator it = begin();
while (it != end()) { // 遍历所有数据节点
it = erase(it); // 删除当前节点并获取下一个节点
}
}
以上cpp代码继续完善了双向链表容器类,新增了初始化列表构造、赋值运算符等功能:
12. 初始化列表构造函数
- 支持
list<int> lst = {1, 2, 3}
语法 - 调用
empty_init()
初始化头节点 - 通过范围for循环和
push_back()
逐个添加元素
13. 交换方法
swap(list<T> lt)
:交换两个链表的内容- 直接交换头节点指针和元素数量
- 时间复杂度O(1)
14. 赋值运算符重载
- 采用拷贝并交换(Copy-and-swap)惯用法
- 通过值传递接收参数,自动处理自我赋值问题
- 高效释放原有资源并获取新资源
15. 链表初始化
empty_init()
:- 创建头节点,初始化为
T()
- 构建循环链表结构(
_head
的prev
和next
指向自身) - 初始化
_size
为0
- 创建头节点,初始化为
16. 清空操作
clear()
:- 使用迭代器遍历所有数据节点
- 调用
erase(it)
删除节点并更新迭代器 - 保留头节点,保持链表结构完整性
cpp
// 在链表头部插入元素
void push_front(const T& x) { insert(begin(), x); }
// 在链表尾部插入元素
void push_back(const T& x) { insert(end(), x); }
// 删除链表尾部元素
void pop_back() { erase(--end()); }
// 删除链表头部元素
void pop_front() { erase(begin()); }
// 在指定位置前插入元素,返回新节点的迭代器
iterator insert(iterator pos, const T& x) {
Node* cur = pos._node; // 当前位置的节点
Node* newnode = new Node(x); // 创建新节点
Node* prev = cur->_prev; // 当前位置的前一个节点
// 调整指针连接新节点
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
_size++; // 更新链表大小
return iterator(newnode); // 返回指向新节点的迭代器
}
// 删除指定位置的节点,返回下一个位置的迭代器
iterator erase(iterator pos) {
Node* cur = pos._node; // 当前位置的节点
Node* prev = cur->_prev; // 前一个节点
Node* next = cur->_next; // 下一个节点
// 调整指针跳过当前节点
prev->_next = next;
next->_prev = prev;
delete cur; // 释放当前节点内存
_size--; // 更新链表大小
return iterator(next); // 返回下一个位置的迭代器
}
private:
Node* _head; // 头节点指针(哨兵节点)
size_t _size = 0; // 链表元素数量
};
}
以上代码完善了双向链表的核心操作接口:
17. 首尾操作
push_front/push_back
:通过insert
在首尾插入元素pop_front/pop_back
:通过erase
删除首尾元素- 时间复杂度均为O(1)
18. 插入操作
insert(iterator pos, const T& x)
:- 在
pos
前插入新节点 - 调整四个指针完成插入
- 返回指向新节点的迭代器
- 在
- 保持迭代器有效性(除被插入位置外)
19. 删除操作
erase(iterator pos)
:- 删除
pos
指向的节点 - 调整两个指针跳过待删节点
- 释放节点内存并返回下一位置迭代器
- 删除
- 注意:删除后原迭代器失效
指针调整逻辑
- 插入时需修改四个指针:前驱节点的next、新节点的prev/next、后继节点的prev
- 删除时需修改两个指针:前驱节点的next、后继节点的prev
- 头节点参与循环链表维护
数据成员
Node* _head
:哨兵节点,构成循环链表size_t _size
:记录元素数量,插入/删除时维护
三.头文件test.cpp
cpp
#include"list.h"
// 打印链表内容的模板函数
template<class T>
void print(const yl::list<T>& lt) {
auto it = lt.begin();
while (it != lt.end()) {
cout << *it << " ";
++it;
}
cout << endl;
}
// 测试函数1:测试基本功能
void test01() {
yl::list<int> lt1;
lt1.push_back(1);
lt1.push_back(2);
lt1.push_back(3);
lt1.push_back(4);
lt1.push_back(5);
print(lt1);
}
// 测试函数2:测试拷贝构造和初始化列表
void test02() {
yl::list<int> lt1 = { 0, 1, 2, 3, 4, 5 };
yl::list<int> lt2 = lt1;
print(lt2);
}
1. 打印函数
- 模板函数
print(const yl::list<T>& lt)
- 使用迭代器遍历链表并输出元素
- 支持任意可输出类型(需重载
operator<<
)
2. 测试函数1
- 功能:测试基本插入操作
- 步骤 :
- 创建空链表
lt1
- 尾插5个元素(1-5)
- 打印链表(预期输出:1 2 3 4 5)
- 创建空链表
- 验证点 :
push_back
功能- 迭代器遍历正确性
- 测试函数2
- 功能:测试初始化列表和拷贝构造
- 步骤 :
- 使用初始化列表构造
lt1
(元素0-5) - 通过拷贝构造创建
lt2
- 打印
lt2
(预期输出:0 1 2 3 4 5)
- 使用初始化列表构造
- 验证点 :
- 初始化列表构造函数
- 拷贝构造函数(深拷贝)
cpp
// 测试函数3:测试迭代器操作
void test03() {
yl::list<int> lt;
lt.push_back(10);
lt.push_back(20);
lt.push_back(30);
cout << "测试前置++: ";
auto it = lt.begin();
++it;
cout << *it << endl;
cout << "测试后置++: ";
it = lt.begin();
it++;
cout << *it << endl;
cout << "测试前置--: ";
it = lt.end();
--it;
cout << *it << endl;
cout << "测试后置--: ";
it = lt.end();
it--;
cout << *it << endl;
}
// 测试函数4:测试插入和删除操作
void test04() {
yl::list<int> lt;
lt.push_back(1);
lt.push_back(3);
lt.push_back(4);
auto it = lt.begin();
++it;
lt.insert(it, 2);
cout << "插入后: ";
print(lt);
it = lt.begin();
++it;
lt.erase(it);
cout << "删除后: ";
print(lt);
}
int main() {
cout << "=== 测试基本功能 ===" << endl;
test01();
cout << "\n=== 测试拷贝构造和初始化列表 ===" << endl;
test02();
cout << "\n=== 测试迭代器操作 ===" << endl;
test03();
cout << "\n=== 测试插入和删除操作 ===" << endl;
test04();
return 0;
}
这段代码新增了迭代器操作和插入删除功能的测试,以下是重点内容概括:
3. 测试函数3:迭代器操作测试
- 功能:验证迭代器自增自减操作
- 步骤 :
- 创建链表
lt
并插入元素10, 20, 30
- 测试前置
++
:移动到第二个元素(输出20
) - 测试后置
++
:移动到第二个元素(输出20
) - 测试前置
--
:从end()
移动到最后一个元素(输出30
) - 测试后置
--
:从end()
移动到最后一个元素(输出30
)
- 创建链表
- 验证点 :
- 双向迭代器的移动正确性
- 前置/后置操作符的语义差异
4. 测试函数4:插入删除测试
- 功能:验证插入删除接口
- 步骤 :
- 创建链表
lt
并插入元素1, 3, 4
- 在第二个位置插入
2
(链表变为1, 2, 3, 4
) - 删除第二个位置元素(链表变为
1, 3, 4
)
- 创建链表
- 验证点 :
insert
在指定位置前插入元素erase
正确删除元素并返回下一位置迭代器