【c++篇】:掌握list--实用的基本使用方法与模拟实现思路

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
个人主页:余辉zmh–****博客
文章所属专栏:c++篇–****博客

在这里插入图片描述

文章目录

  • 前言
  • 一.list的基本概念和使用
    • 基本概念
    • 基本操作使用
  • 二.模拟实现list
    • 1.节点类(_list_node)模拟实现
    • 2.迭代器类(_list_iterator)模拟实现
      • 基本框架
      • 成员函数
    • 3.链表类(list)模拟实现
      • 基本框架
      • 默认成员函数
      • 迭代器成员函数
      • 插入成员函数
      • 删除成员函数
  • 三.模拟实现完整代码
    • `list.h`文件
    • `test.cpp`文件

前言

在C++编程中,数据结构是理解和应用算法的基础。列表list作为线性数据结构的一种,它提供了有序的元素集合,支持高效的插入和删除操作。掌握list的基本使用和模拟实现,不仅有助于加深对数据结构的理解,也是提高编程能力的关键。
本文将深入探讨list基本概念、常见操作以及模拟实现方法。我们将从list的定义开始,逐步介绍如何使用标准库中的list类,并进一步探讨如何手动模拟实现一个简单的list数据结构。通过理论与实践的结合,帮助读者更好地理解和运用listt数据结构。

一.list的基本概念和使用

在C++中,list 是一种双向链表容器,属于标准模板库(STL)的一部分。

基本概念

  • 定义list 是一个序列容器,它允许非连续内存分配,并通过双向链表来管理元素。这意味着list 支持常数时间内的任意位置插入和删除操作,但随机访问元素的时间复杂度较高(因为要顺序查找)。

  • 结构list 由一系列节点组成,每个节点包含数据部分(_val)和指向前一个(_prev)及后一个(_next)节点的指针。这种结构使得list 在插入和删除操作时不需要移动大量数据,只需调整指针即可。

  • 迭代器list 提供了双向迭代器,支持向前和向后遍历元素。但需要注意的是,list 的迭代器不是随机访问迭代器(因为内存地址不是连续的),因此不支持通过加减整数来直接访问某个位置的元素,只能(++/–)。

基本操作使用

  1. 创建和初始化

    • 使用默认构造函数创建一个空list对象:

      std::list<int> mylist;
      
    • 使用初始化列表创建list

      std::list<int> mylist={1,2,3,4,5};
      
  2. 添加元素

    • 在末尾添加元素:

      //mylist:1,2,3,4,5
      //调用尾插函数push_back()
      mylist.push_back(6);
      
    • 在开头添加元素:

      //mylist:1,2,3,4,5,6
      //调用头插函数push_front()
      mylist.push_front(0);
      
    • 在指定位置插入元素:

      //mylist:0,1,2,3,4,5,6
      //获取begin位置的迭代器
      std::list<int>::iterator it=mylist.begin();
      //迭代器it++,指向下一个位置的迭代器
      it++;
      
      //在迭代器it位置前,也就是元素1前插入10
      //调用插入函数insert()
      it=mylist.insert(it.10);
      
  3. 删除元素

    • 删除指定位置的元素:

      //mylist:0,10,1,2,3,4,5,6
      //删除第二个元素10
      std::list<int>::iterator it=mylist.begin();
      it++;
      //调用删除函数erase()
      it=erase(it);
      
    • 删除第一个元素:

      //mylist:0,1,2,3,4,5,6
      //调用头删函数pop_front
      mylist.pop_front();
      
    • 删除最后一个元素:

      //mylist:1,2,3,4,5,6
      //调用尾删函数pop_back
      mylist.pop_back();
      
    • 清空整个list

      //调用清空函数clear()
      mylist.clear();
      
  4. 访问元素

    • 由于list不是随机访问容器,因此不能通过索引直接访问元素。但可以使用迭代器遍历list来访问元素:
      std::list<int>::iterator it=mylist.begin();
      while(it!=mylist.end()){
          cout<<*it<<" "
      }
      cout<<endl;
      
  5. 大小和容量

    • 获取list中元素的数量:

      //调用size()函数
      size_t sz=mylist.size();
      
    • 判断list是否为空:

      //调用empty()函数
      bool ret=mylist.empty();
      

    ​ 注意:list和string,vector这两个类不同的是,list的大小也就是存储的元素个数就是容量,不再区分,size和capacity

  6. 排序和查找

    • list进行排序:

      //list类中有自己的sort()函数,也可以使用算法库函数里面的
      mylist.sort();
      
    • 查找元素(返回迭代器):

      //list类中没有find()函数,所以只能有算法库函数里面的
      //查找值为4的迭代器位置
      auto it=mylist.find(mylist.begin(),mylist.end(),4);
      

二.模拟实现list

上面我们了解完了如何使用list,接下来,我们将自己模拟实现一个属于自己的list,模拟实现可以帮助我们更好地了解list的底层结构,让我们更好的使用和理解list这个数据结构,而list作为一个双向循环链表,实现一个基本的list需要分别设计三个不同的类,节点类(_list_node),用来封装节点结构;链表类(list),用来封装链表的成员函数和链表结构;迭代器类(_list_iterator),用来封装迭代器。

注意:list类和vector类,string类不同的是,list类需要对迭代器单独使用一个类进行封装,主要原因是因为它们的底层数据结构特性和访问方式的不同

  1. list的底层数据结构

    • list是基于链表实现的,链表的节点在内存中的存储位置是不连续的
    • 为了在链表中实现顺序访问(即,通过迭代器逐一访问链表中的元素),需要设计一个迭代器类来封装链表节点的指针(可以理解为,迭代器类中的成员变量表示的是一个节点的指针),并通过运算符重载(如++--*等)来模拟指针的行为。
    • 迭代器类内部维护一个指向当前节点的指针,并通过这些运算符重载来移动到下一个或上一个节点,或访问当前节点的数据。
  2. vector和string的底层数据结构

    • vectorstring是基于数组实现的,它们的元素在内存中是连续存储的
    • 由于数组元素的连续性,可以直接使用原生指针(即数组元素的地址)作为迭代器,而无需额外的封装。
    • 原生指针自然支持顺序访问(通过指针加减来访问相邻元素),因此vectorstring的迭代器本质上就是原生指针

模拟实现的整体框架:

namespace Mylist{
    template<class T>
    struct _list_node{
        //.....
    };
    
    //迭代器类封装
    template<class T,class Ref,class Ptr>
    class _list_iterator{
        //.....
    };
    
    //链表类封装
    template<class T>
    class list{
        //.....
    };
}

1.节点类(_list_node)模拟实现

  • 代码实现:

    template<class T>
    struct _list_node{
        //成员变量
        _list_node<T>* _prev;
        _list_node<T>* _next;
        T _val;
        
        //构造函数
        _list_node(const T& val=T())
            :_prev(nullptr)
            ,_next(nullptr)
            ,_val(val)
            {}
    };
    
  • 实现原理:

    • 定义三个成员变量表示一个节点的结构:指向前一个节点的指针_prev,指向后一个节点的指针_next,存储的数据_val
    • 构造函数初始化时,两个指针都设置为空指针,参数val=T()表示创建的匿名对象(在上一篇vector的模拟实现中有用到,不理解的可以看上一篇文章),自定义类型不如string类型调用对应的构造函数,内置类型也可以调用构造函数初始化。

在这里插入图片描述

2.迭代器类(_list_iterator)模拟实现

基本框架

  • 代码实现:

    template<class T,class Ref,class Ptr>
    struct _list_iterator{
        typedef _list_node<T> node;
        typedef _list_iterator<T,Ref,Ptr> self;
        
        //成员变量
        node* _node;
        
        //构造函数
        _list_iterator(node* node)
            :_node(node)
            {}
        
        //....其他成员函数
    };
    
  • 实现原理:

    • _list_iteartor是一个模板类,模版参数有三个,这里参数T比较好理解,表示的是存储的数据类型,而后面两个比较难理解,这里先不做解释,在后面讲到相关函数时在详细解释。
    • typedef对两个类类型名重定义(方便实用),注意,模板类和普通的类有一点不同,普通类,类名就是类型;而模版类,类名加上模版参数才是类型。
    • 成员变量是一个节点的指针,构造函数将参数节点指针初始化为迭代器(其实本质上还是指针,因为迭代器就是一种指针类型)。

成员函数

1.operator++函数:

  • 代码实现:

    //前置++
    self& operator++(){
        _node=_node->_next;
        return *this;
    }
    
    //后置++
    self operator++(int){
        self tmp(*this);
        _node=_node->_next;
        return tmp;
    }
    
  • 实现原理:

    • 因为list的迭代器不能直接加减常数,所以只能++或–,而++又分为前置++和后置++
    • 前置++,迭代器本身需要+1,并且返回的是+1后的迭代器,直接让当前节点指针指向下一个节点即可,返回类型是self&(_list_iterator<T,Ref,Ptr>&)
    • 后置++,拷贝构造一个新的迭代器用来保存+1前的迭代器,对原本的迭代器+1,返回保存的迭代器,返回类型时self(_list_iterator<T,Ref,Ptr>),因为保存的迭代器是一个局部变量,函数调用结束会销毁,所以不能用引用类型的返回。
    • 后置++参数中的int是用来和前置++构成函数重载,进行区分

2.operator--函数:

  • 代码实现:

    //前置--
    self& operator--(){
        _node=_node->_prev;
        return *this;
    }
    
    //后置--
    self operator--(int){
        self tmp(*this);
        _node=_node->_prev;
        return tmp;
    }
    
  • 实现原理:

    • –又分为前置–和后置–
    • 前置–,迭代器本身需要-1,并且返回的是-1后的迭代器,直接让当前节点指针指向前一个节点即可,返回类型是self&(_list_iterator<T,Ref,Ptr>&)
    • 后置–,拷贝构造一个新的迭代器用来保存-1前的迭代器,对原本的迭代器-1,返回保存的迭代器,返回类型时self(_list_iterator<T,Ref,Ptr>),因为保存的迭代器是一个局部变量,函数调用结束会销毁,所以不能用引用类型的返回。
    • 后置–参数中的int是用来和前置–构成函数重载,进行区分

3.operator!=函数:

  • 代码实现:

    bool operator!=(const selt& it){
        return _node!=it._node;
    }
    
  • 实现原理:

    • 左参数迭代器和右参数迭代器判断是否不等,左参数迭代器是this,右参数迭代器是it
    • 直接返回两个迭代器是否是同一个节点的指针

4.operator*函数:

  • 代码实现:

    T& operator*(){
        return _node->_val;
    }
    const T& operator*(){
        return _node->_val;
    }
    
    //使用模版参数合二为一
    Ref operator*(){
        return _node->_val;
    }
    
  • 实现原理:

    • 首先迭代器是一个指向节点的指针,对其解引用*,表示获取节点中的数据域(_val)存储的数据
    • 返回节点中的数据域(_val),返回类型是T&
    • 这里有一个注意点,如果是对const类型的迭代器解引用*,直接将返回类型写成T&就会导致const类型的无法调用该函数,写成const T&才可以调用,所以为了解决这个问题,引入了第二个模版参数Ref,用来自动替换这两个不同的类型。

5.operator->函数:

  • 代码实现:

    T* operator->(){
        return &_node->_val;
    }
    const T* operator->(){
        return &_node->_val;
    }
    
    //使用模版参数合二为一
    Ptr operator->(){
        return &-node->_val;
    }
    
  • 实现原理:

    • ->运算符重载和*不同的是,operator*,返回的是节点中存储的数据_val的地址,返回类型是T*类型
    • operator*一样,如果有const类型的迭代器使用->,返回类型直接写成T*会导致const类型的无法调用该函数,写成const T* 类型才可以调用,为了解决这个问题,引入了第三个模版参数Ptr,用来自动替换这两个不同的类型。

3.链表类(list)模拟实现

基本框架

  • 代码实现:
template<class T>
class list{
    //节点类类型重定义,因为只在类内部使用,所以设置成私有
    typedef _list_node<T> node;
public:
    //迭代器类类型重定义
    typedef _list_iterator<T,T&,T*> iterator;
    typedef _list_iterator<T,const T&,const T*> const_iterator;
    
    //...成员函数
    
    private:
    //成员变量
    node* _head; 
    size_t _size;
};
  • 实现原理:
    • 将节点类类型重定义,设置成私有,迭代器类类型重定义,设置成公有
    • 成员变量声明一个哨兵节点,声明一个大小用来记录链表的节点个数

默认成员函数

1.构造函数代码实现:

void empty_init(){
    _head=new node;
    _head->_next=_head;
    _head->_prev=_head;
    _size=0;
}
//构造函数
list(){
    empty_init();
}
  • 实现原理:
    • 用一个函数将初始化的内容分开存放,这样方便拷贝构造函数直接调用
    • 定义一个哨兵节点,该节点的前节点指针和后节点指针都指向该节点,将大小初始化为0

在这里插入图片描述

2.拷贝构造函数代码实现:

list(const list<T>& lt){
    empty_init();
    for(auto& e:lt){
        push_back(e);
    }
}
  • 实现原理:
    • 先用函数empty_init()函数初始化一个空链表,也就是被拷贝的链表
    • 用范围for循环,将被拷贝的链表中的节点数据依次插入到拷贝的链表中完成拷贝

3.赋值运算符重载:

swap(list<T>& lt){
    std::swap(_head,lt._head);
    std::swap(_size,lt._size);
}
list<T>& operator=(list<T> lt){
    swap(lt);
    return *this;
}
  • 实现原理:
    • 用一个自己实现的交换函数,交换两个链表的哨兵节点和大小,就能达到赋值的效果
    • 最后将被赋值的链表返回,返回类型是链表类类型list<T>,返回要引用返回

4.析构函数:

void clear(){
    auto it=begin();
    while(it!=end()){
        it=erase(it);
    }
    _size=0;
}
~list(){
    clear();
    delete _head;
    _head=nullptr;
}
  • 实现原理:
    • 用一个自己实现的清空函数,通过迭代器来依次删除链表中的每个节点,再讲大小置为0
    • 析构函数调用清空函数后,在通过delete函数,释放哨兵节点,最后置为空指针

迭代器成员函数

1.begin()函数代码实现:

//普通对象类型的迭代器
iterator begin(){
    return _head->_next;
}
//const对象类型的迭代器
const_iterator begin()const {
    return _head->_next;
}
  • 实现原理:

    直接返回哨兵节点的下一个节点即可

2.end()函数代码实现:

//普通对象类型的迭代器
iterator end(){
    return _head;
}
//const对象类型的迭代器
const_iterator end()const {
    return _head;
}
  • 实现原理:

    直接返回哨兵节点即可

在这里插入图片描述

插入成员函数

1.insert()函数代码实现:

 iterator insert(iterator pos const T& x){
     node* newnode=new node(x);
     node* cur=pos._node;
     node* prev=cut->_prev;
     
     prev->_next=newnode;
     newnode->_next=cur;
     cur->_prev=newnode;
     newnode->_prev=prev;
     _size++;
     return pos
    
 }
  • 实现原理:
    • 先创建需要插入的新节点newnode,然后找pos迭代器指向的当前节点cur,再找到pos当前指向的节点的前节点prev
    • 将新节点插入到节点cur和prev中间即可

在这里插入图片描述

2.push_back()尾插函数代码实现:

void push_back(const T& x){
    //相当于在哨兵节点前插入
    insert(end(),x);
}

3.push_front()头插函数代码实现:

void push_front(const T& x){
    //相当于在第一个节点前插入,也就是哨兵节点的后一个节点
    insert(begin(),x);
}

删除成员函数

1.erase()删除函数代码实现:

iteraotr erase(iterator pos){
    assert(pos!=end());
    node* cur=pos._node;
    node* prev=cur->_prev;
    node* next=cur->_next;
    
    prev->_next=next;
    next->_prev=prev;
    return next;
}
  • 实现原理:
    • 首先断言判断pos是否是指向哨兵节点的迭代器,因为不能将哨兵节点删除
    • 找到当前迭代器pos指向的节点,再找到前节点prev,后节点next
    • 将pos指向的节点删除,返回next指向的节点,也就是删除节点的下一个节点

在这里插入图片描述

2**pop_back()尾删函数代码实现:**

void pop_back(){
    //相当于删除最后一个节点,也就是哨兵节点的前一个节点
    erase(--end);
}

3.pop_front()头删函数代码实现:

void pop_front(){
    //相当于删除第一个节点,也就是哨兵节点的后一个节点
    erase(begin());
}

三.模拟实现完整代码

list.h文件

#include<iostream>
#include<algorithm>
#include<assert.h>
using namespace std;

namespace Mylist
{
    //节点类封装
    template<class T>
    struct list_node
    {
        //成员变量,前节点指针,后节点指针,存储的数据val
        list_node<T>* _next;
        list_node<T>* _prev;
        T _val;

        //构造函数  创造节点
        list_node(const T& val=T())
        :_next(nullptr)
        ,_prev(nullptr)
        ,_val(val)
        {}
    };

    //迭代器类封装
    template<class 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->_val;
        }

        Ptr operator->(){
            return &_node->_val;
        }

        bool operator!=(const self& it){
            return _node!=it._node;
        }

        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;
        }


    };

    //链表类封装
    template<class T>
    class list
    {
        typedef list_node<T> node;

        void empty_init(){
             _head=new node;
            _head->_next=_head;
            _head->_prev=_head;
            _size=
上一篇:RapidrepairController


下一篇:PHP MySQLi: A Comprehensive Guide