list容器
带头结点的双向循环链表
list操作
构造和销毁
list<int>L1;
list<int>L2(10, 5);
vector<int>v{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
list<int>L3(v.begin(), v.end());
list<int>L4(L3);
元素访问
cout << L3.front() << endl;
cout << L2.back() << endl;
容量
元素修改
list<int>L;
L.push_back(1);
L.push_back(2);
L.push_back(3);
L.push_back(4);
cout << L.size() << endl;
L.push_front(0);
cout << L.front() << endl;
L.pop_front();
cout << L.front() << endl;
L.pop_back();
cout << L.back() << endl;
//查找元素
auto it = find(L.begin(), L.end(), 2);
//插入元素1,2,3
if (it != L.end())
L.insert(it, 5);
L.erase(it);
迭代器
auto it = L2.begin();
while (it != L2.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto& e : L3)
{
cout << e << " ";
}
cout << endl;
cout << L3.front() << endl;
cout << L2.back() << endl;
auto rit = L4.rbegin();
while (rit != L4.rend())
{
cout << *rit << " ";
++rit;
}
cout << endl;
一般容器遍历的时候都是左闭右开的区间
一般begin()都在要访问元素的第一个位置,而end()则是最后一个元素向后一个的位置,对于list而言也就是头结点。而逆向打印时,迭代器位置正好反过来,在当前位置时,打印前一个结点。
list 特殊操作
list<int>L{ 9, 1, 2, 2, 3, 4, 2, 6, 8 };
L.sort();
//相邻重复的元素才会被删除
//所以使用时必须保证list有序
L.unique();
L.reverse();
迭代器失效
//List中迭代器失效的问题---迭代器指向的结点不存在
void TestListIterator()
{
list<int>L{ 1, 2, 3, 4 };
auto it = L.begin();
//删除之后it已经不存在了
//所以使用迭代器时,一但有删除元素要小心
L.erase(it);
//解决失效问题
//重新赋值it
it = L.begin();
while (it!=L.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
以上的应用只给出了一部分,具体上面有个链接,专门讲解list的应用,本文重点在模拟实现list的一些相关操作
模拟实现list
我们要模拟实现list应用中的几个模块
list结构定义
list是一个带头结点的双向循环链表,所以我们要有一个头结点的指针指向这个链表,因为在构造中,我们不管是什么构造都需要先创建头结点,所以我们将创建头结点封装成一个成员函数,提高代码复用性。
双向循环链表初始化就是头结点的下个结点指向自己,上一个结点也指向自己
至于迭代器问题,我们稍后再来进行模拟
定义结点类型
// list: 带头结点双向循环链表
template<class T>
struct ListNode
{
ListNode(const T& data = T())
: _pNext(nullptr)
, _pPre(nullptr)
, _data(data)
{}
ListNode<T>* _pNext;
ListNode<T>* _pPre;
T _data;
};
class list
{
typedef ListNode<T> Node;
public:
typedef list_iterator<T> iterator;
typedef list_reverse_iterator<iterator, T> reverse_iterator;
private:
void CreateHead()
{
_pHead = new Node;
_pHead->_pNext = _pHead;
_pHead->_pPre = _pHead;
}
protected:
Node* _pHead;
};
构造与析构
首先是默认构造,默认构造只需要创建出头结点,即可。
list()
{
CreateHead();
}
有参构造,有两个参数,一个是创建多少个结点,第二个是每个结点的值。随后先创建头结点,然后进行尾插,具体实现将放在push_back
函数中,提高代码复用性
list(int n, const T& data )
{
CreateHead();
for (int i = 0; i < n; ++i)
{
push_back(data);
}
}
区间构造,我们需要用迭代器,因为给出的区间不一定就是list容器的区间,有可能是其他容器的区间,所以我们要使用类模板,然后先创建头结点,然后当first
没有走到末尾也就是last
时,不断的尾插*first
里的元素,然后first++
,直到给所有元素赋完值
template<class Iterator>
list(Iterator first, Iterator last)
{
CreateHead();
while (first != last)
{
push_back(*first);
++first;
}
}
拷贝构造,传进行类类型对象的引用,先创建头结点,头结点不能动,用临时变量pCur
指上来,当pCur
没有再次指向头结点时,也就是没有走完一圈,我们不断的尾插给进来对象的数据,每插一个pCur
往后走
list(const list<T>& L)
{
CreateHead();
Node* pCur = L._pHead->_pNext; //从第一个元素走
while (pCur != L._pHead)
{
push_back(pCur->_data);
pCur = pCur->_pNext;
}
}
赋值操作符重载,还是传进来类类型对象的引用,首先看是不是自己给自己赋值,如果不是,我们先用clear()
函数清除所有元素(后面会实现clear()函数),然后再重复拷贝构造里面的操作,最后返回*this
即可
//L1=L2;
list<T>& operator=(const list<T>& s)
{
if (this != &s)
{
clear(); //先清空
Node* pCur = L._pHead;
while (pCur != _pHead)
{
push_back(pCur->_data);
pCur = pCur->_pNext;
}
}
return *this;
}
析构函数,我们先用clear()
函数释放掉所有元素,但是头结点还在,所以我们再把头结点释放即可。
~list()
{
clear();
delete[]_pHead;
}
元素访问
这里的访问操作,思想很简单,返回第一个元素值,就是头结点的下一个结点的值,最后一个元素的值,就是头结点上一个元素的值。
T& front()
{
return _pHead->_pNext->_data;
}
const T& front()const
{
return _pHead->_pNext->_data;
}
T& back()
{
return _pHead->_pPre->_data;
}
const T& back()const
{
return _pHead->_pPre->_data;
}
容量
返回当前容器中有效元素个数,我们定义一个临时变量来记录有效元素个数,遍历一遍整个链表,每遍历一个元素,个数就+1。
size_t size()const
{
size_t count = 0;
Node* pCur = _pHead->_pNext;
while (pCur != _pHead)
{
++count;
pCur = pCur->_pNext;
}
return count;
}
判空操作就是当头结点的下一个结点指向自己就为空
size_t empty()const
{
return _pHead->_pNext == _pHead;
}
改变容量的操作,我们要传进来新容量,还有如果新容量大于旧容量,我们要对扩容的元素就是值填充,填充元素的值我们也要给出来,如果没有给我们就给出一个默认值。
当新容量大于旧容量时,就让变量i从旧容量的末尾开始走,走到新容量的末尾,然后不断的尾插我们给进来的data
当新容量小于旧容量时,就让变量i从新容量开始走,走到旧容量的位置,不断的pop_back()
尾删元素即可。
void resize(size_t newsize, const T& data = T())
{
size_t oldsize = size();
if (newsize > oldsize)
{
// 节点增多
for (size_t i = oldsize; i < newsize; ++i)
push_back(data);
}
else
{
// 节点减少
// oldsize:10 newsize:5
for (size_t i = newsize; i < oldsize; ++i)
pop_back();
}
}
元素修改
我们刚才不断的使用到了push_back
这个函数,我们的尾插函数只需要不断的往尾部insert
数据就行
insert()
我们稍后实现
void push_back(const T& data)
{
insert(end(), data);
}
pop_back()
我们只需要把尾部元素删除掉,就是end()
的前一个位置
void pop_back()
{
erase(--end());
}
头插,在begin()
的位置insert
数据data
就行
void push_front(const T& data)
{
insert(begin(), data);
}
头删,就是删除begin()位置的元素就行
void pop_front()
{
erase(begin());
}
我们之前相当于一直在推卸责任,不断的把具体实现功能往后推,现在终于要实现具体的功能
insert函数返回迭代器,在pos位置,插入T类型的数据data。
- 我们插入数据,对于list而言也就是插入一个结点,所以我们要先创建一个结点
- 新结点的前一个结点指向插入位置的前一个结点
- 新结点下一个结点指向当前插入位置的结点
- 新界点前一个结点的下一个结点指向新结点
- 当前位置的结点的前一个结点指向新结点
- 返回迭代器类型的新结点
iterator insert(iterator pos, const T& data)
{
Node* pNewNode = new Node(data);
//链表为空删不了
Node* pCur = pos._pCur;
pNewNode->_pPre = pCur->_pPre;
pNewNode->_pNext = pCur;
pNewNode->_pPre->_pNext = pNewNode;
pCur->_pPre = pNewNode;
return iterator(pNewNode);
}
iterator erase(iterator pos)
{
Node* pDelNode = pos._pCur;
if (pDelNode == _pHead)
return end();
Node* pRet = pDelNode->_pNext;//保存删除结点的下一个,用于返回
pDelNode->_pPre->_pNext = pDelNode->_pNext;
pDelNode->_pNext->_pPre = pDelNode->_pPre;
delete pDelNode;
return iterator(pRet);
}
清除,就是一个一个把有效元素删除,用头删法就行,当pCur没有指向头结点时,代表还没有结束
不断的让头结点指向pCur的下一个结点,然后删除pCur,再让pCur指向头结点的下一个结点
void clear()
{
Node* pCur = _pHead->_pNext;
// 头删法
// []-->1-->2-->3...
while (pCur != _pHead)
{
_pHead->_pNext = pCur->_pNext;
delete pCur;
pCur = _pHead->_pNext;
}
_pHead->_pNext = _pHead;
_pHead->_pPre = _pHead;
}
交换函数,此我们要交换两个链表,我们只需要调用库函数swap,将两个链表的头指针传进去即可。
void Swap(list<T>& L)
{
swap(_pHead, L._pHead);
}
迭代器
迭代器的本身就是一个指针
我们可以看到我们最重要的insert()
函数和erase()
函数都需要用到迭代器来进行实现,而list的迭代器并非像vector
那样,一个简单的指针就可以,如果是原生态指针,不能取到下一个结点。所以我们需将结点类型的指针重新封装
迭代器如果要当成指针的方式进行应用,我们必须在迭代器中提供如下的方法,让它具备类似于指针的特性
- 迭代器构造
- 移动迭代器(++/–)
- 两个迭代器之间要可以进行比较(!=/==)
- 重载
*
运算符和->
运算符
// list迭代器:将节点类型的指针重新封装
template<class T>
struct list_iterator
{
typedef ListNode<T> Node;
typedef list_iterator<T> Self;
public:
list_iterator(Node* pCur)
: _pCur(pCur)
{}
// 按照指针的方式进行应用
T& operator*()
{
//返回数据本身
return _pCur->_data;
}
T* operator->()
{
//返回数据的地址
return &(_pCur->_data);
}
// 3. 移动
Self& operator++()
{
_pCur = _pCur->_pNext;
return *this;
}
Self operator++(int)
{
Self temp(*this);
_pCur = _pCur->_pNext;
return temp;
}
Self& operator--()
{
_pCur = _pCur->_pPre;
return *this;
}
Self operator--(int)
{
Self temp(*this);
_pCur = _pCur->_pPre;
return temp;
}
// 4. 比较
bool operator!=(const Self& s)
{
return _pCur != s._pCur;
}
bool operator==(const Self& s)
{
return _pCur == s._pCur;
}
Node* _pCur;
};
总结:
如何给一个类定义迭代器:
分析:该类的迭代器是原生态的指针还是需要对指针进行封装
取决于当前的数据结构(看是顺序的结构还是其他的 结构,顺序结构可以是原生态的指针,链式或者树形的迭代器需要进行封装)
1.结合该类的数据结构,定义迭代器类(封装指针)
2.将迭代器与该类进行结合:在容器类中--->typedef 迭代器类型 iterator
3.容器类中提供:begin()和end()
反向迭代器(了解)
template<class Iterator, class T>
struct list_reverse_iterator
{
typedef list_reverse_iterator<Iterator, T> Self;
public:
list_reverse_iterator(Iterator it)
: _it(it)
{}
T& operator*()
{
Iterator temp = _it;
--temp;
return *temp;
}
T* operator->()
{
return &(operator*());
}
Self& operator++()
{
--_it;
return *this;
}
Self operator++(int)
{
Self temp(*this);
_it--;
return temp;
}
Self& operator--()
{
++_it;
return *this;
}
Self operator--(int)
{
Self temp(*this);
_it++;
return temp;
}
bool operator!=(const Self& s)
{
return _it != s._it;
}
bool operator==(const Self& s)
{
return _it == s._it;
}
Iterator _it;
};
测试
#include<vector>
void TestList1()
{
bite::list<int>L1;
bite::list<int>L2(10, 5);
vector<int>v{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
bite::list<int>L3(v.begin(),v.end());
bite::list<int>L4(L3);
auto it = L2.begin();
while (it != L2.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}