⸝⋆ ━━━┓
- 个性标签 - :来于"云"的"羽球人"。 Talk is cheap. Show me the code
┗━━━━━━━ ➴ ⷯ
本人座右铭 : 欲达高峰,必忍其痛;欲戴王冠,必承其重。
👑💎💎👑💎💎👑
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑 希望在看完我的此篇博客后可以对你有帮助哟
👑👑💎💎💎👑👑 此外,希望各位大佬们在看完后,可以互相支持,蟹蟹!
👑👑👑💎👑👑👑
目录:
一:简单使用
1.push_front()
push_front():此函数功能就是头插一个数据(在第一个有效数据的前面进行插入)
此函数的参数:任意类型的数据(注意这里是使用引用,避免对象过大)
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
2.push_back()
**此函数功能:**尾插一个数据(就是在尾结点进行插入数据)
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的尾插
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
3.pop_front()
pop_front() 函数功能:把第一个有效数据对应的节点进行释放
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
l1.pop_front();//删除第一个数据
cout << "pop_front:";
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
4. pop_back()
pop_back()函数功能:把最后一个有效数据对应的节点进行释放
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
l1.pop_back();//删除最后一个数据
cout << "pop_back:";
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
5. insert()
insert()函数功能: 在指定位置之前进行数据插入
此函数支持:某个位置插入一个数据,也支持从当前位置插入n 个数据;还支持一段区间数据的插入
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = l1.begin();
l1.insert(++it, 22);
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
6. erase()
erase() 函数功能:对指定位置删除
此函数支持:对某个位置的删除;也支持一段区间的删除
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = l1.begin();
l1.erase(++it);
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
7. swap()
swap() 函数功能: 对多个list 类型对象进行数据交换
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>l2;
l2.push_back(11);
l2.push_back(22);
l2.push_back(33);
l2.push_back(44);
l2.push_back(55);
cout << " 交换之前对应的数据:" << endl;
list<int>::iterator it = l1.begin();
cout << "l1:";
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
list<int>::iterator it1 = l2.begin();
cout << "l2:";
while (it1 != l2.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
l1.swap(l2);
cout << "交换之后对应的数据:" << endl;
it = l1.begin();
cout << "l1:";
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it1 = l2.begin();
cout << "l2:";
while (it1 != l2.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
}
运行结果:
不知道一些老铁是否和我一样有个问题:为什么算法库里面有一个swap() 而list 容器还要自己实现swap(),这是为啥???
这就涉及到迭代器的分类了
迭代器按功能分:正向迭代器和反向迭代器
迭代器按性质分为:
单向迭代器:只支持 ++ 比如单链表,哈希表
双向迭代器:支持++ 和-- 比如list ,红黑树(map,set)
随机迭代器:++ , -- ,+ ,- 比如vector ,string......
而我们的list 迭代器是双向的,但是算法库里面的swap () 要求迭代器的类型是随机的,所以list 只能自己实现一个swap () 函数
list 迭代器:
8.clear()
clear() 函数功能:清除有效数据
使用示范:
cpp
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
l1.clear();//清除有效数据
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
二:模拟实现相关函数
在我们模拟list 相关函数之前对需要对list 这个容器有一定的了解。通过前面数据结构对链表结构的学习,再结合官方文档模拟实现应该是不成问题。
首先list 这一个容器是由3个大类进行封装实现的:一个是节点类,一个是迭代器类,一个是链表类。其中前节点类和迭代器类都是为链表类进行服务的
对list这一容器实现的灵魂在于迭代器的实现:通过对一些运算符进行重载从而实现对迭代器的封装。
准备工作:
注意我这里为了避免与库里面的list 发生冲突,就把自己写的相关代码放在自己定义的y这个命名空间了(各位在模拟实现的时候,随意,看自己)
定义一个节点类
对于双向循环链表的每一个节点而言无非就是由3部分构成的:数据域;前一个节点的地址;后一个节点的地址
cpp
template<class T>
struct list_node // 之所以使用struct 而不用class ,没有特殊情况下,前者默认属性是pubblic
{
T _val;
list_node<T>* _next;
list_node<T>* _pre;
list_node(const T& x = T()) //对于T 的类型并不明确,不能指定_val 就是int类型,就初始化为0,所以采用匿名对象的形式
:_val(x)
, _next(nullptr)
, _pre(nullptr)
{}
};
定义一个迭代器类
cpp
template <class T>
struct list_iterator// 迭代器类
{
typedef list_iterator<T> self;//对当前迭代器进行重命名
typedef list_node Node;
Node* node;//迭代器其实就是指向每一个的节点的
//迭代器涉及到的相关函数
list_iterator(Node* node) // 不需要实现析构函数
:_node(node)
{}
T& operator*()
{
return _node->_val;
}
T* operator->()
{
return &_node->_val;//返回当前数据对应的地址
}
self& operator++()
{
_node = _node->_next;
return *this;//前置++ 不涉及到临时对象,可以使引用返回
}
self operator++(int)//后置++
{
Node* tmp(_node);//创建一个临时对象,因为后置++ 返回的是++之前的对象
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_pre;
return *this;
}
self operator--(int) // 后置--
{
Node* tmp(_node);
_node = _node->_pre;
return tmp;
}
};
接下来思考一个问题是不是const 迭代器就是对普通迭代器加一个const 进行修饰即可???
各位老铁看看以下写法是否正确
普通迭代器: iterator
const 迭代器: const iterator
答案自然是错误的:相信有不少人在初学迭代器相关内容的时候,cosnt 迭代器无非就是对普通迭代器加一个cosnt 进行修饰嘛。OK 那思考以下问题
cosnt T*
T* const
是否有区别??? 若有区别,具体又是啥区别???
有区别的
cosnt T* : 是对 T* 进行修饰,换言之就是对当前指针指向的内容修饰(当前的数据不可以修改,只能读)
T* cosnt : 修饰的是当前指针
cosnt 迭代器和普通迭代器是两个不同的类型
对于list 迭代器的实现:在底层是没有区别的
所以说:对于一个程序员而言,相同的代码那就没有必要在撸一遍了,这种事情咱还是交给小弟来搞即可(编译器)
这就不得不提及模板参数了
编译器会根据当前参数的不同,自动生成对应的函数
普通迭代器对应的参数类型:T,T&,T*
const 迭代器对应参数类型:T ,const T&,const T*
cpp
template<class T,class Ref,class Ptr> // 此时的Ref可以表示T& 或者 coonst T& ;Ptr可以表示T* 或者const T*
struct _list_iterator //迭代器其实就是对当前节点进行一系列的封装
{
typedef _list_iterator<T,Ref,Ptr> self;// 进行重命名
typedef list_node<T> Node;
Node* _node;
_list_iterator(Node* node)// 迭代器的构造函数 不需要写析构函数(析构函数是把当前这个节点释放,但是在使用迭代器的时候并不需要进行节点释放在某些场景下)
:_node(node)
{}
//T& operator*()
//{
// return _node->_val;
//}
//T* operator->()// 意义:对自定义类型数据访问更加方便,原始访问:(*it).某个具体成员
//{
// return &(_node->_val);//返回当前数据的地址
//}
//self& operator++()
//{
// _node = _node->_next;//注意返回的是一个迭代器
// return *this;
//}
//self& operator++(int)
//{
// Node* tmp(_node);
// _node = _node->_next;
// return tmp;
//}
//self*& operator--()
//{
// _node = _node->_pre;
// return _node;
//}
//self*& operator--(int)
//{
// Node* tmp(_node);
// _node = _node->_pre;
// return tmp;
//}
Ref operator*()
{
return _node->_val;
}
Ptr operator->()// 意义:对自定义类型数据访问更加方便,原始访问:(*it).某个具体成员
{
return &(_node->_val);//返回当前数据的地址
}
self& operator++()
{
_node = _node->_next;//注意返回的是一个迭代器
return *this;
}
self& operator++(int)
{
Node* tmp(_node);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_pre;
return _node;
}
self& operator--(int)
{
Node* tmp(_node);
_node = _node->_pre;
return tmp;
}
bool operator==(const self& s)
{
return _node == s;
}
bool operator!=(const self& s)
{
//return _node != s;// 应该比较的是 _node
return _node != s._node;
}
};
定义一个链表类
cpp
struct list//链表类
{
typedef list_node<T> Node;
private:
size_t _size;//统计节点的个数
Node* _head;//哨兵位
pubblic:
void empty_init()
{
_head = new Node;
//构成环
_head->_next = _head->_pre = _head;
_size = 0;
}
typedef list_iterator<T> self;//对当前迭代器进行重命名
list()
{
empty_init();
}
};
1.push_back() 模拟实现
尾插也就是在在尾结点(_head-> _pre)后面进行数据插入,之后在进行指针连接
cpp
void push_back(const T& x) // 使用T:来表示任意类型数据
{
Node* tail = _head->_pre;
Node* newnode = new Node(x);
tail->_next = newnode;
newnode->_next = _head;
newnode->_pre = tail;
//注意不要忘记对_head->pre进行链接
_head->_pre = newnode;
}
2.push_front() 模拟实现
头插:在第一个数据前面插入(_head-> _next)
cpp
void pop_back()
{
Node* del = _head->_pre;
del->_pre->_next = _head;
_head->_pre = del->_pre;
delete del;
}
3. pop_back() 模拟实现
尾删:把最后一个数据对应的节点释放,同时让倒数第二个节点成为新的尾结点
cpp
void pop_back()
{
Node* del = _head->_pre;
del->_pre->_next = _head;
_head->_pre = del->_pre;
delete del;
}
4. pop_front() 模拟实现
cpp
void pop_front()
{
Node* del = _head->_next;
_head->_next = del->_next;
del->_next->_pre = _head;
delete del;
}
5. insert( ) 模拟实现
insert( )一般默认是在指定位置之前进行数据的插入
注意节点连接的先后顺序问题
cpp
iterator insert(iterator pos,const T& x)
{
//默认在pos 位置之前插入
Node* newnode = new Node(x);
Node* pre = pos._node->_pre;
newnode->_next = pos._node;
pos._node->_pre = newnode;
newnode->_pre = pre;
pre->_next = newnode;
++_size;
return newnode;//返回插数据对应的迭代器
}
6. erase ()模拟实现
erase( ) 默认是把指定位置的数据删除
相信初次模拟此函数的时候,有不少老铁是这样写的吧:
注意这样写是有问题的:迭代器失效当我们再访问pos 下一个位置的时候就会出现迭代器失效的问题
解决:返回下一个节点对应的迭代器
正确代码:
cpp
iterator erase(iterator pos)
{
//进行判空
//assert(!empty());
//删除pos位置数据
Node* pre = pos._node->_pre;
Node* next = pos._node->_next;
pre->_next = next;
next->_pre = pre;
delete pos._node;//此时迭代器失效
--_size;
return next;//返回下一个节点,解决迭代器失效问题
}
7.clear ( ) 模拟实现
一般写法:就是一个节点一个节点进行释放
其实完全可以借助erase( ) 函数进行编写:因为erase () 函数返回的就是下一个节点的迭代器
cpp
void clear()//清除有效数据
{
list<T>::iterator it = begin();// 其实是this 对象调用当前函数
//传统写法:就是保留下一个节点,删除当前节点
//现代写法:
while (it != end())
{
it = erase(it);//erase() 返回的就是下一个节点对应的迭代器
}
_size = 0;
}
8. 析构函数
此时还是借用clear ( ) 函数进行
只不过只需要把头结点进行释放即可
cpp
~list()
{
clear();
delete _head;
_head = nullptr;
}
9. swap()函数模拟实现
直接调用库里面的函数 swap(),交换链表对应的头结点以及_size的大小即可
cpp
void swap(list<int>& l)
{
std::swap(_head, l._head);
std::swap(_size, l._size);
}
10.operator=() 函数重载
传统写法:
cpp
// l2 = l1 :l2是对l1 的拷贝构造
//传统写法
iterator operator=(const list<int>& l)//l 指向对象 l1 this 指向当前对象l2
{
//第一次写拷贝构造函数的时候,编译错误:const 对象调用非const对象,权限扩大
//解决:需要const 迭代器
//const 与非const迭代器区别:无非就是对应函数返回值类型不一样==》 借用函数模板
clear();//对l2进行数据清理
list<int>::iterator it = l.begin();
while (it != l.end())
{
push_back(*it);
++it;
}
}
注意一些细节的实现:当我们没有实现对 const 迭代器的时候,此时编译是有问题的,因为此时对象的属性是cosnt ,编译器只能调用非const 对应的函数,自然造成权限放大的问题,导致编译不过
解决:写一个cosnt 迭代器
现代写法:
cpp
list<int>& operator=( list<int>& l)
{
swap(l);
return *this;
}
结语:以上就是我今日share 的内容。对于list 这个容器模拟实现的关键在于对迭代器的掌握(进行封装,屏蔽底层实现的细节,让使用者以统一的方式进行使用,但是迭代器类型不同在底层实现也是不一样的可以这么说,迭代器其实就是模拟指针的行为)