详解list容器(应用+模拟实现)

list容器

带头结点的双向循环链表

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);//解决失效问题//重新赋值itit = 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:5for (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。

  1. 我们插入数据,对于list而言也就是插入一个结点,所以我们要先创建一个结点
  2. 新结点的前一个结点指向插入位置的前一个结点
  3. 新结点下一个结点指向当前插入位置的结点
  4. 新界点前一个结点的下一个结点指向新结点
  5. 当前位置的结点的前一个结点指向新结点
  6. 返回迭代器类型的新结点

在这里插入图片描述

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那样,一个简单的指针就可以,如果是原生态指针,不能取到下一个结点。所以我们需将结点类型的指针重新封装

迭代器如果要当成指针的方式进行应用,我们必须在迭代器中提供如下的方法,让它具备类似于指针的特性

  1. 迭代器构造
  2. 移动迭代器(++/–)
  3. 两个迭代器之间要可以进行比较(!=/==)
  4. 重载*运算符和->运算符
// 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/383146.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vector和list容器有哪些区别

这个问题的本质还是在问顺序表和链表的区别 底层结构不同 vector容器list容器一段连续的空间带头结点的双向循环链表 元素访问方式 vector容器list容器支持随机访问—O(1)不支持随机访问—O(N)需要扩容不需要扩容任意位置插入元素----O(N)–搬移元素O(1) 迭代器不同 vector…

复习栈和队列,详解最小栈,栈的弹出压入序列,逆波兰表达式求值

栈和队列的概念 栈:吃进去吐出来 对列&#xff1a;吃进去拉出来 数据结构中的栈和内存中的区别 数据结构中的栈具有后进先出的特性&#xff0c;而内存中的栈是一个内存空间&#xff0c;只不过这个内存空间具与数据结构的栈具有相同的特性。 栈和队列操作 栈和队列基本操作…

详解优先级队列priority_queue(应用+模拟实现)

优先级队列的概念 优先队列是一种容器适配器&#xff0c;根据严格的弱排序标准&#xff0c;它的第一个元素总是它所包含的元素中最大的此上下文类似于堆&#xff0c;在堆中可以随时插入元素&#xff0c;并且只能检索最大堆元素(优先队列中位于顶部的元 素)。优先队列被实现为容…

私人博客定制

项目背景 可行性方面 需求分析&#xff1a; 详细设计&#xff1a; 数据库设计 博客管理API的设计 标签相关API 服务器端的实现 对数据库操作进行封装 对服务器操作进行封装 客户端实现 具体操作 使用markdown 具体实现 测试 项目效果展示 维护 完整代码 项目…

初识c++中的函数模板

函数模板 函数模板概念 函数模板:编译器生成代码的一个规则。函数模板代表了一个函数家族&#xff0c;该函数模板与类型无关&#xff0c;在使用时被参数化&#xff0c;根据实参类型产生函数的特定类型版本。 函数模板格式 //要让这个函数与类型无关 //Add函数模板 template…

深入理解c++中的函数模板

非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参&#xff1a;出现在模板参数列表中&#xff0c;跟在class或者typename之类的参数类型名称。 非类型形参&#xff0c;就是用一个常量作为类(函数)模板的一个参数&#xff0c;在类(函数)模板中可将该参数当成常量来使…

c++中的IO流

c语言中的IO操作 标准类型的输入输出: 输入------>数据来源是通过键盘进行输入输出------>程序中的数据输出到控制台 c语言中: scanf:输入 printf:输出 两个函数的相同点 1 —格式串 2 —不定参数 两个函数的缺陷 1 —用户要提供数据的格式—用户要记忆大量的格式串—…

201301 JAVA2~3级---走格子

请编写一个函数&#xff08;允许增加子函数&#xff09;&#xff0c;计算n x m的棋盘格子&#xff08;n为横向的格子数&#xff0c;m为竖向的格子数&#xff09;沿着各自边缘线从左上角走到右下角&#xff0c;总共有多少种走法&#xff0c;要求不能走回头路&#xff0c;即&…

复习Linux基本操作----常见指令

Linux基本操作 ls命令 ls(list):相当于windows上的文件资源管理器 语法&#xff1a; ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。 常用选项&#xff1a; -a 列出目…

复习Linux基础操作---权限操作

shell命令以及运行原理 Linux严格意义上说的是一个操作系统&#xff0c;我们称之为“核心&#xff08;kernel&#xff09;“ &#xff0c;但我们一般用户&#xff0c;不能直接使用kernel。而是通过kernel的“外壳”程序&#xff0c;也就是所谓的shell&#xff0c;来与kernel沟…

【剑指offer】_01 (二维数组中的查找)

题目描述 在一个二维数组中&#xff08;每个一维数组的长度相同&#xff09;&#xff0c;每一行都按照从左到右递增的顺序排序&#xff0c;每一列都按照从上到下递增的顺序排序。请完成一个函数&#xff0c;输入这样的一个二维数组和一个整数&#xff0c;判断数组中是否含有该…

再谈c++中的多态

何为多态 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会产生出不同的状态。 多态的实现 在继承的体系下 基类中必须有虚函数(被virtual关键字修饰的成员函数)&#xff0c;在派生类中必须…

再谈c++中的继承

继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类。继承呈现了面向对象程序设计的层次结构&#xff0c;体现了…

红黑树概念及其相关操作的实现

红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但它并不像AVL树一样&#xff0c;每个结点绑定一个平衡因子。但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或Black。 通过 对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c…

模拟实现STL中map和set容器

红黑树的迭代器 //红黑树的迭代器 template<class T> struct RBTreeIterator {typedef RBTreeNode<T>Node;typedef RBTreeIterator<T> Self; public:RBTreeIterator(Node* pNode nullptr):_pNode(pNode){}//具有指针操作T& operator*(){return _pNode-…

排序上---(排序概念,常见排序算法,直接插入,希尔排序,直接选择排序,堆排序)

排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序&…

排序下---(冒泡排序,快速排序,快速排序优化,快速排序非递归,归并排序,计数排序)

排序上 排序上 交换类排序 基本思想&#xff1a;所谓交换&#xff0c;就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置&#xff0c;交换排序的特点是&#xff1a;将键值较大的记录向序列的尾部移动&#xff0c;键值较小的记录向序列的前部移动。 冒泡…

哈希的概念及其操作

哈希概念 顺序结构以及平衡树中&#xff0c;元素关键码与其存储位置之间没有对应的关系&#xff0c;因此在查找一个元素时&#xff0c;必须要经过关键码的多次比较。顺序查找时间复杂度为O(N)&#xff0c;平衡树中为树的高度&#xff0c;即O( Log2N)&#xff0c;搜索的效率取决…

软件工程---1.概述

软件的特征 抽象&#xff1a; 不可触摸&#xff0c;逻辑实体&#xff0c;可记录&#xff0c;但看不到复制成本低&#xff1a;不受物质材料的限制&#xff0c;不受物理定律或加工过程的制约&#xff0c;与开发成本相比&#xff0c;复制成本很低无折旧、受硬件制约、未完全摆脱手…

软件工程---2.软件过程

三个模型 瀑布模型增量模型集成和配置模型 没有适用于所有不同类型软件开发的过程模型。 瀑布模型 需求定义系统和软件的设计实现与单元测试集成与系统测试运行与维护 瀑布模型的特征 从上一项活动中接受该项活动的工作成果&#xff08;工作产品&#xff09;&#xff0c;作…