【C++】list模拟实现

news2024/11/29 5:50:48

看源码观察结构

由源码可以得知,list的底层是带头双向循环链表

image-20220217154710856


结点类模拟实现

list实际上是一个带头双向循环链表,要实现list,则首先需要实现一个结点类,而一个结点需要存储的信息为:数据、前驱指针、后继指针

而对于该结点类的成员函数来说,我们只需实现一个构造函数即可,因为该结点类只需要根据数据来构造一个结点即可,而结点的释放则由list的析构函数来完成,

基本结构

//结点类 -> struct类型,内容公有
//结点类模板参数
template<class T>
struct ListNode
{
    //成员函数
    ListNode(const T& val = T()); //构造函数

    //成员变量
    T _val;                 //数据域
    ListNode<T>* _next;   //后继指针
    ListNode<T>* _prev;   //前驱指针
};

构造函数

构造函数直接根据所给数据构造一个结点即可,构造出来的结点的数据域存储的就是所给数据,而前驱指针和后继指针均初始化为空指针即可

//构造函数 err版本
ListNode(const T& x)
{
    _val = x;
    _next = nullptr;
    _prev = nullptr;
}

提供默认参数才合适

若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据,如果是string就是空串,如果是int就是0

//这样写更好  默认参数是一个匿名对象
ListNode(const T& val = T())	
    	:_val(val)
        ,_prev(nullptr)
        ,_next(nullptr)
{}

迭代器类模拟实现

关于迭代器的说明

迭代器有两种实现方式,具体应根据容器底层数据结构实现:

  1. 原生态指针,比如:vector和string ->物理空间是连续的
    • 因为string和vector对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针,

2.将原生态指针进行封装,因迭代器使用形式与指针完全相同,因此在自定义的类中必须实现以下
方法:

  • 指针可以解引用,迭代器的类中必须重载operator*()
  • 指针可以通过->访问其所指空间成员,迭代器类中必须重载oprator->()
  • 指针可以++向后移动,迭代器类中必须重载operator++()operator++(int)
    至于operator--()/operator--(int) 是否需要重载,根据具体的结构来抉择,双向链表可
    以向前 移动,所以需要重载,如果是forward_list就不需要重载–
  • 迭代器需要进行是否相等的比较,因此还需要重载operator==()operator!=()

但是对于list来说,其各个结点在内存当中的位置是随机的,并不是连续的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作,

迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问,

list的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符操作进行重载

总结: list的迭代器 实际上就是对结点指针进行了封装,对其各种运算符进行了重载,使得结点指针的各种行为看起来和普通指针一样,(例如,对结点指针自增就能指向下一个结点 p = p->next)


接口函数

list的迭代器是个结点(对象),里面封装了一个指向链表结点的指针

//迭代器的类模板参数
//如果是const迭代器:  Ref就是const T& Ptr就是const T*
//如果是普通迭代器:Ref就是T&   Ptr就是T*
template<class T, class Ref, class Ptr>
struct _list_iterator
{
    typedef ListNode<T> ListNode;//结点类型
    typedef _list_iterator<T, Ref, Ptr> self;//迭代器类型

    _list_iterator(ListNode* pnode);  //构造函数

    //运算符重载函数
    self operator++();
    self operator--();
    self operator++(int);
    self operator--(int);
    bool operator==(const self& s) const;
    bool operator!=(const self& s) const;
    Ref operator*();
    Ptr operator->();

    //成员变量
    ListNode* _pnode; //一个指向结点的指针
};

迭代器模板参数说明

template<class T, class Ref, class Ptr>
    
typedef _list_iterator<T, Ref, Ptr> self;//重命名迭代器类型为self

image-20220220204719504

迭代器类的模板参数列表当中的Ref和Ptr分别代表的是引用类型和指针类型

当我们使用普通迭代器时,编译器就会用类模板实例化出一个普通迭代器对象;当我们使用const迭代器时,编译器就会用类模板实例化出一个const迭代器对象

不设计三个模板参数,那么就不能很好的区分普通迭代器和const迭代器,因为普通对象的迭代器可读可写,而const对象的迭代器 只可读不可写!


构造函数

迭代器类实际上就是对结点指针进行了封装

其成员变量就是结点指针,所以其构造函数直接根据所给结点指针构造一个迭代器对象即可,

//构造函数
//直接根据所给结点指针构造一个迭代器对象
_list_iterator(ListNode* pnode)
   :_pnode(pnode)
{}

关于拷贝构造等函数的说明:

拷贝构造,operator,析构函数我们都不需要写,因为成员变量是内置类型(指针), 用编译器默认生成的就可以

list<int>:: iterator it = lt,begin();

注意:这里不是赋值,而是迭代器的拷贝构造. 使用浅拷贝足矣,成员变量是指针,是类型,编译器默认生成的就是浅拷贝

链表的结点不属于迭代器管,不需要写析构函数, 迭代器只是去访问修改即可.结点是属于链表的


注意:我们把当前迭代器对象的类型_list_iterator<T, Ref, Ptr> typedef为:self


注意: return *this返回的是当前对象的克隆或者本身(若返回类型为A, 则是克隆, 若返回类型为A&, 则是本身 ),return this返回当前对象的地址(指向当前对象的指针)


++运算符重载

前置++

前置++原本的作用是将数据自增,然后返回自增后的数据,

而对于结点迭代器的前置++:应该先让结点指针指向后一个结点.然后再返回“自增”后的结点迭代器即可

让结点迭代器的行为看起来更像普通指针

//前置++
self& operator++()
{
    _pnode = _pnode->_next;//当前迭代器结点中的指针指向下一个结点
    return *this;//返回自增后的迭代器
}

后置++

后置++,先拷贝构造当前迭代器结点, 然后让当前迭代器结点的指针自增指向下一个结点,最后返回“自增”前的结点迭代器即可,

//后置++
self operator++(int)
{
    self tmp(*this);//拷贝构造当前迭代器对象
    _pnode = _pnode->_next;//当前迭代器结点中的指针自增指向下一个结点
    return tmp;//返回自增前的迭代器对象
}

–运算符重载

前置–

前置- -:当前迭代器结点中的指针指向前一个结点,然后再返回“自减”后的结点迭代器即可,

//前置--
self& operator--()
{
    _pnode = _pnode->_prev;//当前迭代器结点中的指针指向前一个结点
    return *this;//返回自减后的迭代器
}

后置–

拷贝构造当前迭代器对象 -> 当前迭代器结点中的指针自减指向前一个结点 ->返回自减前的迭代器

//后置--
self operator--(int)
{
    self tmp(*this);//拷贝构造当前迭代器对象
    _pnode = _pnode->_prev;//当前迭代器结点中的指针自减指向前一个结点
    return tmp;//返回自减前的迭代器
}

==运算符重载

想知道的是这两个迭代器是否是同一个位置的迭代器-> 判断这两个迭代器当中的结点指针的指向是否相同即可

//比较两个迭代器是否相同
bool operator==(const self& s) const
{
    return _pnode == s._pnode;//直接比较两个迭代器的指针是否一样即可
}

!=运算符重载

想知道的是这两个迭代器是否 不是同一个位置的迭代器-> 判断这两个迭代器当中的结点指针的指向是否相同即可

bool operator!=(const self& s) const
{
    return _pnode != s._pnode;//直接比较两个迭代器的指针是否不一样即可
}

*运算符重载

使用解引用操作符时,是想得到该指针指向的数据内容

因此,我们直接返回当前结点指针所指结点的数据即可,这里需要使用引用返回,因为解引用后可能需要对数据进行修改,

Ref operator*()
{
    return _pnode->_val;//返回迭代器的指针指向结点的数据
}

->运算符重载

->返回当前迭代器结点的指针所指结点的数据的地址

Ptr operator->()
{
    return &_pnode->_val; //返回迭代器的指针所指结点的数据的地址
}

应用场景

class Date
{
public:
	Date(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	int _year;
	int _month;
	int _day;
};
int main()
{
	Mango::list<Date> lt;
	Date d1(2022, 1, 1);
	Date d2(2022, 1, 2);
	lt.push_back(d1);
	lt.push_back(d2);
    auto it = lt.begin();
	while (it != lt.end())
	{
		/*cout << (*it)._year <<" "<<(*it)._month<< " "<< (*it)._day<<endl;*/
		cout << it->_year << " " << it->_month << " " << it->_day << endl;
		it++;
	}
}

*it 得到的是日期类对象, 然后用.访问成员

当然也可以用->直接访问成员

image-20220220210758742


list的模拟实现

接口函数

//模拟实现list
template<class T>
class list
{ 	
 public:
    typedef ListNode<T> ListNode;
    typedef _list_iterator<T, T&, T*> iterator;
    typedef _list_iterator<T, const T&, const T*> const_iterator;
	
    //默认成员函数
    list();
    list(size_t n, const T& val = T())
    list(int n, const T& val = T())
    template<class InputIterator>//取名为InputIterator说明可以用任意类型的迭代器构造
	list(InputIterator first, InputIterator last)
        
    list(const list<T>& lt);
    list<T>& operator=(const list<T>& lt);
    ~list();

    //迭代器相关函数
    //正向迭代器
    iterator begin();
    iterator end();
    const_iterator begin() const;
    const_iterator end() const;
    
    //访问容器相关函数
    T& front();
    T& back();
    const T& front() const;
    const T& back() const;

    //插入、删除函数
    void insert(iterator pos, const T& x);
    iterator erase(iterator pos);
    void push_back(const T& x);
    void pop_back();
    void push_front(const T& x);
    void pop_front();

    //其他函数
    size_t size() const;
    void resize(size_t n, const T& val = T());
    void clear();
    bool empty() const;
    void swap(list<T>& lt);
 private:
        ListNode* _head; //指向链表头结点的指针
};

默认成员函数

构造函数1-默认构造函数

list是一个带头双向循环链表,我们需要先构造一个头结点,并让其前驱指针和后继指针都指向自己

image-20220220213943037

//构造函数
list()
{
    _head = new ListNode;//先新开一个结点
    //哨兵位自己指向自己
    _head->_prev = _head;
    _head->_next = _head;
}

构造函数2-用n个值相同的值初始化

//注意:这里的val要给缺省值,不能给0之类的,因为T的类型未知
list(size_t n, const T& val = T())
{
    //新建一个哨兵位
    _head = new ListNode();
    _head->_next = _head;
    _head->_prev = _head;
    for (size_t i = 0; i < n; i++)
    {
        push_back(val);	//复用push_back接口
    }
}

构造函数3-迭代器区间初始化

//使用迭代器区间初始化
template<class InputIterator>//取名为InputIterator说明可以用任意类型的迭代器构造
list(InputIterator first, InputIterator last)
{
    _head = new ListNode();
    _head->_next = _head;
    _head->_prev = _head;
    while (first != last)
    {
        push_back(*first);//复用push_back
        first++;
    }
}

上述两个构造函数可能存在冲突

image-20220707174805996

存在冲突的例子:

void test_list0()
{
    
	Mango::list<int> lt(5, 2);//报错,非法的间接寻址
	for (auto e : lt)
	{
		cout << e << " ";
	}
    Mango::list<Date> lt2(5,Date(2022,1,1));//没问题
	cout << endl;
}

我们本意是想用5个2初始化lt,但是调用的是迭代器区间初始化,因为迭代器区间里存在解引用,所以会报错间接寻址.

同样的,vector也会存在这个问题!解决方法也一样,看下面讲解

但是用5个Date对象初始化lt2却没有问题,调用的是n个值初始化的构造函数


我们首先要知道为什么会有这个错误

image-20220707175711906

编译器的原则是有更匹配的就去找更匹配的,有现成的就不去推演模板


如何修改呢?为n个值初始化的构造函数增设一个int版本的

//使用n个相同的值初始化
list(int n, const T& val = T())
{
    //新建一个哨兵位
    _head = new ListNode();
    _head->_next = _head;
    _head->_prev = _head;
    for (int i = 0; i < n; i++)
    {
        push_back(val);		//复用push_back接口
    }
}

构造函数4-初始化列表初始化

方法1:迭代器遍历插入

//使用初始化列表初始化  list<int> lt{1,2,3,4};
//需要引用#include<initializer_list>函数
list(initializer_list<T> ilt)
{
    _head = new ListNode();
    _head->_next = _head;
    _head->_prev = _head;
	
    initializer_list<T>::iterator it = ilt.begin();
    while(it!=ilt.end())
    {
        push_back(*it);//复用push_back函数
        it++;
    }
}

方法2:范围for

//使用初始化列表初始化  list<int> lt{1,2,3,4};
//需要引用#include<initializer_list>函数
list(initializer_list<T> ilt)
{
    _head = new ListNode();
    _head->_next = _head;
    _head->_prev = _head;

    for (auto& e : ilt)
    {
        push_back(e);
    }
}

方法3:现代写法

注意:必须要构造哨兵位节点,否则就把随机值给了tmp, tmp出了作用域调用析构函数,析构函数里面会调用clear函数,会去访问迭代器释放节点,会导致崩溃

list(initializer_list<T> ilt)
{
    //构造哨兵位节点
    _head = new ListNode();
    _head->_next = _head;
    _head->_prev = _head;
	
	list<T> tmp(ilt.begin(),ilt.end());//复用迭代器初始化的函数,构造出临时对象
    std::swap(_head,tmp._head);//交换哨兵位节点指针
}

拷贝构造函数

根据所给list容器,直接拷贝构造出一个相同的list对象,

对于拷贝构造函数,我们也是要先申请一个头结点,并让其前驱指针和后继指针都指向自己

然后将所给容器当中的数据,通过遍历的方式一个个尾插到新构造的容器后面即可

传统写法:

//拷贝构造函数
//l2(l1)// l2.list(l1)
list(const list<T>& lt)
{
    _head = new ListNode;//申请一个哨兵位结点
    _head->_next = _head;
    _head->_prev = _head;

    //把lt的数据尾插到当前调用对象的容器
    for (const auto& e : lt)
    {
        push_back(e);//将容器lt当中的数据一个个尾插到新构造的容器后面
    }
}

现代写法:

// 拷贝构造 - 现代写法
// lt2(lt1)
list(const list<T>& lt)
{
    _head = new Node;
    _head->_prev = _head;
    _head->_next = _head;

    list<T> tmp(lt.begin(), lt.end());//迭代器区间构造
    ::swap(_head, tmp._head);//交换哨兵位指针
}

注意必须给一个哨兵位节点,否则_head是一个随机值,换给tmp后出作用域调用析构函数,clear时获取begin()要解引用 _head-> _next会崩溃


赋值运算符重载函数

传统写法

先调用clear函数将原容器清空,然后将容器lt当中的数据,通过遍历的方式尾插到清空后的容器当中即可,

为了减少拷贝,传参传引用

//赋值重载函数
//lt2 = lt;//lt2.operator=(lt)
list<T>& operator=(const list<T>& lt)
{
    //防止自己给自己赋值->地址比较
    if (this != &lt)
    {
        clear();//先清除原来有的内容,但是头结点 不会清除
        //把内容拷贝过去
        for (const auto& e : lt)
        {
            push_back(e);
        }
    }
    return *this;//为了支持连续赋值
}

现代写法

不使用引用接收参数

通过编译器自动调用list的拷贝构造函数构造出来一个list临时对象,然后调用swap函数将原容器与该临时的list对象进行交换即可

因为lt是临时对象,所以当该赋值运算符重载函数调用结束时,lt对象会自动销毁,并调用其析构函数进行清理,

//现代写法
//注意这里是传值
list<T>& operator=(list<T> lt)
{
    //两个list的内容直接交换
    swap(lt);//使用我们自己写的swap函数  ->::swap(_head, lt._head);//直接交换两个容器的哨兵位即可
    return *this;//为了支持连续赋值
}

析构函数

先调用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空即可,

//析构函数
~list()
{
    clear();//先清空原来的内容
    delete _head;//释放哨兵位
    _head = nullptr;//哨兵位指针置空
}

迭代器

begin()和end()

begin函数返回的是第一个数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器->即哨兵位的迭代器

直接把结点的地址传过去即可,通过结点的地址构造出一个迭代器

image-20220220214710785

//迭代器
iterator begin()
{
    //返回使用头结点的地址构造出来的普通迭代器
    /*	iterator tmp = iterator(_head->_next);
			return tmp;*/
    
    return iterator(_head->_next);
}
iterator end()
{
    //返回使用哨兵位结点的地址构造出来的普通迭代器
    return iterator(_head);
}
const_iterator begin() const
{
    //返回使用头结点的地址构造出来的const迭代器
    return const_iterator(_head->_next);
}
const_iterator end() const
{
    //返回使用哨兵位结点的地址构造出来的const迭代器
    return const_iterator(_head);
}

增删查改

front

获取第一个数据的内容

begin()迭代器返回的就是第一个数据的地址

T& front()
{
    return *begin();//返回第一个数据的引用
}

const T& front() const
{
	return *begin(); //返回第一个数据的const引用
}

back

获取最后一个数据的内容

end()迭代器返回的是哨兵位的地址 --end()返回的就是最后一个数据的地址

T& back()
{
    return *(--end());//返回尾结点数据的引用
}

const T& back() const
{
	return *(--end()); //返回最后一个有效数据的const引用
}

insert

insert函数可以在所给迭代器pos之前插入一个新结点,

1.先根据所给迭代器pos得到该位置处的结点指针cur 2.然后通过cur指针找到前一个位置的结点指针prev

  1. 根据所给数据x构造一个新结点 4.cur prev 新节点三者链接

注意**:pos可以是哨兵位的迭代器 ->这样相当于尾插** 但是pos迭代器结点中的指针不能为空

//pos迭代器位置前插入
iterator insert(iterator pos, const T& x)
{
    //插入时:pos可以是哨兵位,相当于尾插
    //pos是对象, 访问成员/成员函数用.访问
    assert(pos._pnode);//迭代器结点中的指针不能为空
    ListNode* cur = pos._pnode;
    ListNode* prev = cur->_prev;
    ListNode* newnode = new ListNode(x);
    //prev newnode cur 链接
    prev->_next = newnode;
    newnode->_prev = prev;
    newnode->_next = cur;
    cur->_prev = newnode;
    
    //这里是构造一个匿名对象返回,用newnode这个节点,构造一个迭代器,list的迭代器是一个结构体
    //该结构体有一个指针指向这个newnode节点
    return iterator(newnode);//返回新插入位置的迭代器
}

erase

erase函数可以删除所给迭代器位置的结点,

注意**:pos不可以是哨兵位的迭代器,即不能删除哨兵位 pos迭代器结点中的指针不能为空**

1.根据所给迭代器得到该位置处的结点指针cur 2.通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next

3.紧接着释放cur结点,最后prev和next结点进行链接

//删除pos迭代器位置
iterator erase(iterator pos)
{
    assert(pos._pnode);//迭代器中的节点指针不能为空
    assert(pos != end());//不能删除哨兵位
    ListNode* cur = pos._pnode;
    ListNode* prev = cur->_prev;
    ListNode* next = cur->_next;

    delete cur;//释放cur结点  ->外部的pos迭代器的指针成为野指针
    //prev next链接
    prev->_next = next;
    next->_prev = prev;

    return iterator(next); //返回删除位置pos的下一个迭代器
}

关于insert和erase迭代器失效问题

问:insert之后, pos迭代器是否失效

insert不会导致迭代器失效,因为pos迭代器中的节点指针仍然指向原来的节点

问:erase之后, pos迭代器是否失效

一定失效,因为此时pos迭代器中的节点指针指向的节点已经被释放了,该指针相当于是野指针


push_back

可以复用insert函数,也可以自己写

复用: 尾插:即在哨兵位的前面插入 -> insert(end(), x)

自己写: 1.新建一个结点 2.记录原来的最后一个结点tail 3.新节点,tail ,哨兵位 三个结点进行链接

//尾插
//没有结点也适用
void push_back(const T& x)
{
    //ListNode* newnode = new ListNode(x);/a新建一个结点
    //ListNode* tail = _head->_prev;//哨兵位的_prev指向的就是尾结点
     _head tail newnode 三者链接
    //_head->_prev = newnode;	//新节点成为新的尾
    //tail->_next = newnode;
    //newnode->_prev = tail;
    //newnode->_next = _head;

    //insert:在pos位置前插入
    insert(end(), x);//在哨兵位前插入->相当于尾插
}

pop_back

尾删:即删除哨兵位的前一个结点 begin()返回的就是第一个结点的迭代器

//尾删
void pop_back()
{
    //end()返回的是哨兵位的迭代器   --end()就是 _head->_prev 尾结点的迭代器
    erase(--end());
}

push_front

复用:头插:即在第一个迭代器的位置前面插入

自己写:1.新建一个结点 2.记录原来的第一个结点phead 3.哨兵位 新节点 phead三个结点进行链接

//头插
//没有结点也适用
void push_front(const T& x)
{
    //ListNode* newnode = new ListNode(x);//新建一个结点
    //ListNode* phead = _head->_next;//哨兵位的_next指向的就是头结点
     _head newnode phead 
    //_head->_next = newnode;//新节点成为新的头节点
    //newnode->_prev = _head;
    //newnode->_next = phead;
    //phead->_prev = newnode;

    insert(begin(),x);
}

pop_front

头删:即删除第一个结点->begin()返回的就是第一个结点的迭代器

//头删
void pop_front()
{
    //begin()就是头结点 
    erase(begin());
}

其它函数

size

size函数用于获取当前容器当中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数,

//长度
size_t size()
{
    //遍历统计
    size_t sz = 0;
    iterator it = begin();
    while (it != end())
    {
        ++sz;
        ++it;
    }
    return sz;
}

第二种方式:给list对象多设置一个成员变量size,用于记录当前容器内的有效数据个数,

 private:
        ListNode* _head; //指向链表头结点的指针
		size_t size; //记录链表的长度

clear

clear函数用于清空容器

通过遍历的方式逐个删除结点,只保留哨兵位即可

注意:要将哨兵位重新直接自己,否则再次插入会导致错误,_head指向已经释放的节点

写法1:

void clear()
{

    iterator it = begin();
    while (it != end())
    {
        //相当于先干掉当前位置迭代器的节点,然后it++
        iterator del = it++;//返回++之前的迭代器,删了之后,it往后走
        delete del._node;  //也可以写成erase(del)
    }
    //哨兵位节点指向自己
    _head->_next = _head;
    _head->_prev = _head;
}

写法2:

//清空内容
void clear()
{
    iterator it = begin();
    while (it != end())
    {
        it = erase(it);//erase会返回删除位置的下一个迭代器
    }
    
    //哨兵位重新直接自己,否则再次插入会导致错误,_head指向已经释放的节点
    _head->_prev =_head;
    _head->_next = _head;
}

可以简写为:

//清空内容
void clear()
{
    iterator it = begin();
    while (it != end())
    {
       erase(it++);
    }
    //哨兵位重新直接自己,否则再次插入会导致错误,_head指向已经释放的节点
    _head->_prev =_head;
    _head->_next = _head;
}

empty

判断list是否为空

直接判断该容器的begin函数和end函数所返回的迭代器是否是同一个位置的迭代器即可,

(如果相等此时说明容器当中只有一个哨兵位结点)

bool empty() const
{
    return begin() == end();
}

swap

swap函数用于交换两个list,list容器当中的成员变量只有指向哨兵位结点的指针,我们将这两个容器当中的哨兵位指针交换即可,

void swap(list<T>& lt)
{
    ::swap(_head, lt._head);//直接交换两个容器的哨兵位即可,此处用的是全局域std命名空间里面的swap函数
}

resize

case1:若当前容器的size小于所给n,则尾插结点,直到list的size等于n为止,

case2:若当前容器的size大于所给n,则只保留前n个数据,

低效做法

调用size()函数得到此时list的有效数据个数 和n进行对比

如果是case1: 找到尾结点的迭代器位置, 然后在后面尾插数据

如果是case2:从第一个结点(_head->next)开始遍历list,找到第n结点的迭代器,然后用循环释放后面的结点

调用size()需要遍历链表,时间复杂度就是O(n),如果结果是size大于n,那么还需要遍历list,效率低


高效方法:

设置一个变量len,用于记录当前所遍历的数据个数,然后开始遍历容器list,在遍历过程中:

  1. 当len大于或是等于n时遍历结束,此时说明该结点后的结点都应该被释放,将之后的结点都释放即可,
  2. 当容器遍历完毕时遍历结束,此时说明容器当中的有效数据个数小于n,则需要尾插结点,直到容器当中的有效数据个数为n时停止尾插即可,
//调整容器的大小为n
void resize(size_t n, const T& val = T())
{
    iterator it = begin();//第一个数据的迭代器
    size_t len = 0;//记录遍历到的数据个数
    while (len < n && it != end())
    {
        ++len;
        ++it;
    }
    //跳出循环 -有两种情况:case1:len == n  或者 case2 :已经到达了end()位置
    //case1:len == n -> 说明是减少数据个数
    if (len == n)
    {
        //后面位置的数据都删除掉
        while (it != end())
        {
            it = erase(it);
        }
    }
    //case2:len < n ->说明是扩容的情况
    else
    {
        //从len位置开始往后扩容,扩容为n个
        while (len < n)
        {
            push_back(val);
            len++;
        }
    }
}

list.h

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace Mango
{
	//结点类 -> struct类型,内容公有
	template<class T>
	struct ListNode
	{
		T _val;//数据域
		ListNode<T>* _next;//后继指针
		ListNode<T>* _prev;//前驱指针

		//构造函数
		//ListNode(const T& x)
		//{
		//	_val = x;
		//	_next = nullptr;
		//	_prev = nullptr;
		//}
		
		//这样写更好  默认参数是一个匿名对象
		ListNode(const T& val = T())
			:_val(val)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

	//迭代器类
	template<class T,class Ref,class Ptr>
	struct  _list_iterator
	{
		typedef ListNode<T> ListNode;//重命名结点类型为ListNode

		typedef _list_iterator<T, Ref, Ptr> self;//重命名迭代器类型为self
		//传const对象和普通对象会分别实例化出下面两种迭代器:
		//typedef _list_iterator<T, T&, T*> self;//此时self普通迭代器类型
		//typedef _list_iterator<T, const T&, const T*> self;//此时self是const迭代器类型

		 //构造函数
		_list_iterator(ListNode* pnode)
			:_pnode(pnode)
		{}

		//前置++
		self& operator++()
		{
			_pnode = _pnode->_next;//当前迭代器的指针指向下一个结点
			return *this;//返回自增后的迭代器
		}

		//后置++
		self operator++(int)
		{
			self tmp(*this);//拷贝构造当前迭代器对象
			_pnode = _pnode->_next;//当前迭代器指针自增指向下一个结点
			return tmp;//返回自增前的迭代器
		}
		//前置--
		self& operator--()
		{
			_pnode = _pnode->_prev;//当前迭代器指针指向前一个结点
			return *this;//返回自减后的迭代器
		}
		//后置--
		self operator--(int)
		{
			self tmp(*this);//拷贝构造当前迭代器对象
			_pnode = _pnode->_prev;//当前迭代器指针自减指向前一个结点
			return tmp;//返回自减前的迭代器
		}
		//比较两个迭代器是否相同
		bool operator==(const self& s) const
		{
			return _pnode == s._pnode;//直接比较两个迭代器的指针是否一样即可
		}

		bool operator!=(const self& s) const
		{
			return _pnode != s._pnode;//直接比较两个迭代器的指针是否不一样即可
		}
		//
		Ref operator*()
		{
			return _pnode->_val;//返回迭代器的指针指向结点的数据
		}
		Ptr operator->()
		{
			return &_pnode->_val; //返回迭代器的指针所指结点的数据的地址
		}

		//成员变量
		ListNode* _pnode; //指向结点的指针

	};

	//list类
	template<class T>
	class list
	{
		typedef ListNode<T> ListNode;//把结点类型重命名为ListNode
		typedef _list_iterator<T, T&, T*> iterator;//普通迭代器类型
		typedef _list_iterator<T, const T&, const T*> const_iterator;//const迭代器类型
	public:
		//构造函数
		list()
		{
			_head = new ListNode;//先新开一个结点
			//哨兵位自己指向自己
			_head->_prev = _head;
			_head->_next = _head;
		}
		//拷贝构造函数
		//l2(l1)
		list(const list<T>& lt)
		{
			_head = new ListNode;//申请一个哨兵位
			_head->_next = _head;
			_head->_prev = _head;

			//把lt的数据尾插到当前调用对象的容器
			for (const auto& e : lt)
			{
				push_back(e);//将容器lt当中的数据一个个尾插到新构造的容器后面
			}
		}
		//赋值重载函数
		//lt2 = lt;
		//list<T>& operator=(const list<T>& lt)
		//{
		//	//防止自己给自己赋值->地址比较
		//	if (this != &lt)
		//	{
		//		clear();//先清除原来有的内容,但是头结点 不会清除
		//		//把内容拷贝过去
		//		for (const auto& e : lt)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;//为了支持连续赋值
		//}
		// 

		void swap(list<T>& lt)
		{
			::swap(_head, lt._head);//直接交换两个容器的哨兵位即可
		}
		//现代写法
		//lt是拷贝构造出来的临时对象
		list<T>& operator=(list<T> lt)
		{
			//两个list的内容直接交换
			swap(lt);
			return *this;//为了支持连续赋值
		}
		//迭代器
		iterator begin()
		{
			//返回使用头结点构造出来的普通迭代器
		/*	iterator tmp = iterator(_head->_next);
			return tmp;*/
			return iterator(_head->_next);
		}
		iterator end()
		{
			//返回使用哨兵位结点构造出来的普通迭代器
			return iterator(_head);
		}
		const_iterator begin() const
		{
			//返回使用头结点构造出来的const迭代器
			return const_iterator(_head->_next);
		}
		const_iterator end() const
		{
			//返回使用哨兵位结点构造出来的const迭代器
			return const_iterator(_head);
		}
		T& front()
		{
			return *begin();//返回第一个数据的引用
		}
		T& back()
		{
			return *(--end());//返回尾结点数据的引用
		}
		//尾插
		//没有结点也适用
		void push_back(const T& x)
		{
			//ListNode* newnode = new ListNode(x);//新建一个结点
			//ListNode* tail = _head->_prev;//哨兵位的_prev指向的就是尾结点
			 _head tail newnode 三者链接
			//_head->_prev = newnode;	//新节点成为新的尾
			//tail->_next = newnode;
			//newnode->_prev = tail;
			//newnode->_next = _head;

			//insert:在pos位置前插入
			insert(end(), x);//在哨兵位前插入->相当于尾插
		}
		//头插
		//没有结点也适用
		void push_front(const T& x)
		{
			//ListNode* newnode = new ListNode(x);//新建一个结点
			//ListNode* phead = _head->_next;//哨兵位的_next指向的就是头结点
			 _head newnode phead 
			//_head->_next = newnode;//新节点成为新的头节点
			//newnode->_prev = _head;
			//newnode->_next = phead;
			//phead->_prev = newnode;

			insert(begin(),x);
		}
		//pos迭代器位置前插入
		void insert(iterator pos, const T& x)
		{
			//插入时:pos可以是哨兵位,相当于尾插
			//pos是对象,访问成员/成员函数用.访问
			assert(pos._pnode);//迭代器的指针不能为空
			ListNode* cur = pos._pnode;
			ListNode* prev = cur->_prev;
			ListNode* newnode = new ListNode(x);
			//prev newnode cur 链接
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
		}
		//删除pos迭代器位置
		iterator erase(iterator pos)
		{
			assert(pos._pnode);//迭代器的指针不能为空
			assert(pos != end());//不能删除哨兵位
			ListNode* cur = pos._pnode;
			ListNode* prev = cur->_prev;
			ListNode* next = cur->_next;

			delete cur;//释放cur结点  ->外部的pos迭代器的指针成为野指针
			//prev next链接
			prev->_next = next;
			next->_prev = prev;

			return iterator(next); //返回删除位置pos的下一个迭代器
		}
		//尾删
		void pop_back()
		{
			//end()是哨兵位 --end()就是 _head->_prev 尾结点
			erase(--end());
		}
		//头删
		void pop_front()
		{
			//begin()就是头结点 
			erase(begin());
		}
		bool empty() const
		{
			return begin() == end();
		}
		//长度
		size_t size()
		{
			//遍历统计
			size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++sz;
				++it;
			}
			return sz;
		}
		//清空内容
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}
		//调整容器的大小为n
		void resize(size_t n, const T& val = T())
		{
			iterator it = begin();//第一个数据的迭代器
			size_t len = 0;//记录遍历到的数据个数
			while (len < n && it != end())
			{
				++len;
				++it;
			}
			//跳出循环 -有两种情况:case1:len == n  或者 case2 :已经到达了end()位置
			//case1:len == n -> 说明是减少数据个数
			if (len == n)
			{
				//后面位置的数据都删除掉
				while (it != end())
				{
					it = erase(it);
				}
			}
			//case2:len < n ->说明是扩容的情况
			else
			{
				//从len位置开始往后扩容,扩容为n个
				while (len < n)
				{
					push_back(val);
					len++;
				}
			}
		}
		//析构函数
		~list()
		{
			clear();//先清空原来的内容
			delete _head;//释放哨兵位
			_head = nullptr;//哨兵位指针置空
		}
	private:
		ListNode* _head; //指向哨兵位的指针
	};
    struct Date
    {
        int _year;
        int _month;
        int _day;

        Date(int year = 1, int month = 1, int day = 1)
            :_year(year)
                , _month(month)
                , _day(day)
            {}
    };


    //测试构造+拷贝构造 + 赋值重载
    void test_list1()
    {
        Mango::list<int> lt1;
        lt1.push_back(1);
        lt1.push_back(2);
        lt1.PrintList();

        Mango::list<int> lt2 = lt1;//拷贝构造
        lt2.PrintList();

        Mango::list<int> lt3;
        lt3 = lt1;//赋值重载
        lt3.PrintList();
    }

    //测试正向迭代器的operator* 和operator->
    void test_list2()
    {
        Mango::list<Date> lt;
        lt.push_back(Date(2022, 3, 12));
        lt.push_back(Date(2022, 3, 13));
        lt.push_back(Date(2022, 3, 14));

        Mango::list<Date>::iterator it = lt.begin();
        while (it != lt.end())
        {
            //cout << (*it)._year << "/" << (*it)._month << "/" << (*it)._day << endl;
            //cout << it->_year << "/" << it->_month << "/" << it->_day << endl;

            ++it;
        }
        cout << endl;
    }
    //测试正向迭代器
    void test_list3()
    {
        Mango::list<int> lt1;
        lt1.push_back(1);
        lt1.push_back(2);
        lt1.push_back(3);

        //lt1.clear();

        Mango::list<int> lt2(lt1);
        for (auto& e : lt2)
        {
            cout << e << " ";
        }
        cout << endl;

        Mango::list<int> lt3;
        lt3.push_back(10);
        lt3.push_back(10);
        lt3.push_back(10);
        lt3.push_back(10);

        lt1 = lt3;
        for (auto e : lt1)
        {
            cout << e << " ";
        }
        cout << endl;
    }


    //测试n个值初始化+迭代器区间初始化是否能准确匹配
    void test_list4()
    {
        //没有问题
        Mango::list<Date> lt1(5, Date(2022, 3, 15));//用5个日期类对象初始化
        for (auto& e : lt1)
        {
            cout << e._year << "/" << e._month << "/" << e._day << endl;
        }
        cout << endl;

        Mango::list<int> lt2(5, 1);//用5个1初始化  ->但是调用的是迭代器区间初始化->err.报错:非法寻址
        for (auto e : lt2)
        {
            cout << e << " ";
        }
        cout << endl;
    }

    //测试构造函数时的列表初始化
    void test_list5()
    {
        Mango::list<int> lt{ 1,2,3,4,5 };
        lt.PrintList();
    }
    //测试反向迭代器
    void test_list6()
    {
        Mango::list<int> lt{ 1,2,3,4,5 };
        lt.PrintList();
        auto it = lt.rbegin();
        while (it != lt.rend())
        {
            cout << *it << " ";
            it++;
        }
        cout << endl;
    }

}

理解类型的力量

image-20220707171947954


反向迭代器

反向迭代器就是对正向迭代器的封装,这样它可以是任意容器的反向迭代器

  • 它们的不同就在于++调的是正向迭代器的–;–调的是正向迭代器的++
  • 注意:源码中为了使正向迭代器和反向迭代器的开始和结束保持对称,解引用*取的是前一个位置

image-20220707185429573


反向迭代器和正向迭代器的区别就是 ++ – 的方向是相反的,所以反向迭代器封装正向迭代器即可,重载控制++ --的方向


实现版本1:只有一个模板参数(源码版本)

此时List和正向迭代器要增加的内容:

list结构体中增加的:

//list.h增加的
typedef Reverse_iterator<const_iterator> const_reverse_iterator; //const版本的反向迭代器,模板参数const版本的正向迭代器传过去
typedef Reverse_iterator<iterator> reverse_iterator;

//注意:正向迭代器的end()位置就是反向迭代器的rbegin()位置
//正向迭代器的begin()位置就是反向迭代器的rend()位置
reverse_iterator rbegin()
{
    return reverse_iterator(end());//end()返回正向迭代器,然后构造一个反向迭代器返回
}

reverse_iterator rend()
{
    return reverse_iterator(begin());
}

const_reverse_iterator rbegin() const
{
    return const_reverse_iterator(end());
}
const_reverse_iterator rend() const
{
    return const_reverse_iterator(begin());
}

正向迭代器中新增代码


//正向迭代器
template<class T, class Ref, class Ptr>
    struct __list_iterator
    {
        typedef ListNode<T> ListNode;//节点类型重命名为ListNode
        typedef __list_iterator<T, Ref, Ptr> self;//迭代器类型重命名为self
        
		//新增这两条代码
        /*
            这里要typedef出来,我们不能取一个类的模板参数,
            模板参数不是这个类的成员,我们只能取一个类的成员变量 和 它的内嵌类型
            此时reference和pointer就是内嵌类型
        */
        typedef Ref reference;
        typedef Ptr pointer;

namespace Mango
{
template <class Iterator>
class Reverse_iterator
{
public:
    // Iterator是哪个容器的迭代器->传模板参数Iterator过来,reverse_iterator<Iterator>就可以
	// 适配出各种容器的反向迭代器,复用的体现
	typedef Reverse_iterator<Iterator> self; //self是反向迭代器的类型
    /*
    	Iterator是模板参数,要取它的内嵌类型Ref和Ptr  
		这里就需要使用typename,凡是要取模板里面的内嵌类型,就需要加typename
		告诉编译器后面这个Ref是个类型,让他编译通过,等这个类模板Iterator实例化再去找这个具体的类型
    */
	typedef typename Iterator::reference Ref;   
	typedef typename Iterator::pointer Ptr;
    
	Reverse_iterator(Iterator it)//使用正向迭代器初始化
		:_it(it)
	{}
    
    //当然也可以写成:
    //typename Iterator::reference operator*()
	Ref operator*()//如果是普通反向迭代器 : Ref就是T&, 如果是const反向迭代器, Ref就是const T&
	{
		//复用正向迭代器的--,取得是其前一个位置的数据
		Iterator prev = _it;
		return *--prev;
	}
    
	//typename Iterator::pointer operator->()
	Ptr operator->()//如果是普通反向迭代器:Ptr就是T*,如果是const反向迭代器,Ptr就是const T*
	{
		return &operator*();//返回的是operator*()函数的返回数据的地址
	}

	//下面的函数的实现和写法1一样
    //self是反向迭代器的类型
    
	self& operator++()	//前置++
	{
		--_it; //复用正向迭代器的前置--
		return *this;
	}

	self operator++(int)	//后置++
	{
		self tmp(*this);
		_it--; //复用正向迭代器的后置--
		return tmp;
	}

	self& operator--()//前置--
	{
		++_it;//复用正向迭代器的前置++
		return *this;
	}
	self operator--(int)//后置--
	{
		self tmp(*this);
		++_it;//复用正向迭代器的++
		return tmp;
	}
	bool operator!= (const self& rit) const
	{
		return rit._it != _it;
	}
	bool operator== (const self& rit) const
	{
		return rit._it == _it;
	}
private:
	Iterator _it;
};
}

但是上述反向迭代器的代码针对vector类型就不适用了,为什么呢?

因为vector的迭代器是原生指针,无法取内嵌类型,那么上面的方式就完蛋了,源码中是通过迭代器萃取技术解决这个问题的


实现方法2:带三个模板参数

为了方便做operator* 和operator->的返回值(为了获取数据类型T),我们还可以加两个类模板参数RefPtr,.,但是带了这两个参数更容易理解

list结构体中增加的代码:、

image-20220707195308829

编译器找东西都是就近原则,先在局部找,然后再去全局找!


不管const迭代器先声明还剩普通迭代器先声明,都要指明域

image-20220707195425843

typedef reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
typedef Mango::reverse_iterator<iterator, T&, T*>  reverse_iterator;//指定域

reverse_iterator rbegin()
{
    return reverse_iterator(end());
}
reverse_iterator rend()
{
    return reverse_iterator(begin());
}

const_reverse_iterator rbegin() const
{
    return const_reverse_iterator(end());
}
const_reverse_iterator rend() const
{
    return const_reverse_iterator(begin());
}

正向迭代器中无增加的代码:


反向迭代器的实现:

namespace Mango
{ 
template <class Iterator, class Ref, class Ptr>
 // Iterator是哪个容器的迭代器,reverse_iterator<Iterator>就可以适配哪个容器的反向迭代器(复用)
class reverse_iterator
{
public:
	typedef reverse_iterator<Iterator, Ref, Ptr> self;; //self是反向迭代器的类型
    
	reverse_iterator(Iterator it)//构造函数,用一个正向迭代器构造
		:_it(it)
	{}
    
	Ref operator*() //如果是普通反向迭代器 : Ref就是T&, 如果是const反向迭代器, Ref就是const T&
	{
		//取的是正向迭代器的前一个位置的数据
		Iterator prev = _it;
		return *--prev;
	}
	Ptr operator->()//如果是普通反向迭代器:Ptr就是T*,如果是const反向迭代器,Ptr就是const T*
	{
		
		//取的是正向迭代器的前一个位置的数据的地址
		Iterator prev = _it;
		return &--prev;
		//return &operator*();//返回的是operator*()函数的返回数据的地址
	}
	self& operator++()	//前置++
	{
		--_it; //复用正向迭代器的--
		return *this;
	}

	self operator++(int)	//后置++
	{
		self tmp(*this);
		--_it; //复用正向迭代器的--
		return tmp;
	}

	self& operator--()//前置--
	{
		++_it;
		return *this;
	}
	self operator--(int)//后置--
	{
		self tmp(*this);
		++_it;//复用正向迭代器的++
		return tmp;
	}
	bool operator!= (const self& rit) const
	{
		return rit._it != _it;//比较两个正向迭代器中是否相同 -> 复用正向迭代器的!= 
	}
	bool operator== (const self& rit) const
	{
		return rit._it == _it;//比较两个正向迭代器中是否相同 -> 复用正向迭代器的== 
	}
private:
	Iterator _it;//反向迭代器里面的成员是一个正向迭代器
};

image-20220707202858551


为vector增加反向迭代器

image-20220707200535975

  • Iterator是哪个容器的迭代器,reverse_iterator就可以适配哪个容器的反向迭代器(复用)*

list和vector的对比:

vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及应用场景不
同,其主要不同如下:

vectorlist
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素效率O(1)不支持随机访问,访问某个元素效率O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空<间,拷贝元素,释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针对原生态指针(节点指针)进行封装
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

vector

连续的物理空间,既是优势,也是劣势

优势:支持高效的随机访问

劣势:

1.空间不够需要增容,增容代价比较大

2.可能存在一定的空间浪费,会导致频繁的增容,所以一般都会2倍左右增容

3.头部或者中间插入删除需要挪动数据,效率低下

list很好的解决vector的以上问题

1.按需申请释放空间

2.list任意位置支持O(1)插入删除

注意:STL的所有容器都不保证线程安全


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

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

相关文章

Linux磁盘根目录扩容

Linux磁盘根目录扩容 1.输入命令:df -hl 红色框标记的呢就是服务器的主目录&#xff0c;我们能看到总容量17G &#xff0c;已使用2.1G 可用15G 我们要扩张磁盘空间的就是挂载点为:/ (的这个) 2.查询磁盘分区 命令&#xff1a;fdisk -l 我们找到/dev/sdb 这个磁盘名称就是…

Linux常用命令——nethogs命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) nethogs 终端下的网络流量监控工具 补充说明 有很多适用于Linux系统的开源网络监视工具。比如说&#xff0c;你可以用命令iftop来检查带宽使用情况。netstat用来查看接口统计报告&#xff0c;还有top监控系统当…

CMake快速入门

介绍 之前讲的Makefile的配置跟你当前的系统非常强的相关&#xff0c;例如在Linux或苹果下配置完Makefile&#xff0c;放到Windows下就会有问题&#xff0c;因为编译器会不同、路径会不同等。 如果要做跨平台的软件&#xff0c;要给不同的操作系统&#xff0c;不同的编译环境…

靶机测试Connect-the-dots笔记

靶机描述DescriptionBack to the TopLevel: Beginner-IntermediateUser flag: user.txtRoot flag: root.txtDescription: The machine is VirtualBox compatible but can be used in VMWare as well (not tested but it should work). The DHCP will assign an IP automaticall…

一款兼容双系统、为代码而生的机械键盘--Keychron K3

&#x1f525;前言 从去年的9月份记得就有小伙伴发私信问我有没有值得推荐的键盘&#xff0c;前段时间又有几个小伙伴在发私信询问。于是我写下这篇文章去给大家推荐一款十分好用的矮轴机械键盘 > keychron K3蓝牙无线矮轴超薄机械键盘,从而让大家更好的去敲代码&#xff0c…

【PWA学习】4. 使用 Push API 实现消息推送

引言 在接下来的内容里&#xff0c;我们会探究 PWA 中的另一个重要功能——消息推送与提醒(Push & Notification)。这个能力让我们可以从服务端向用户推送各类消息并引导用户触发相应交互 Web Push 效果Push API 和 Notification API 其实是两个独立的技术&#xff0c;完全…

很好用的URL工具类

&#x1f4e2; &#x1f4e2; &#x1f4e2; &#x1f4e3; &#x1f4e3; &#x1f4e3;哈喽&#xff01;大家好&#xff0c;我是「奇点」&#xff0c;江湖人称 singularity。刚工作几年&#xff0c;想和大家一同进步 &#x1f91d; &#x1f91d;一位上进心十足的【Java ToB端…

【C++】deque双端队列

目录deque的原理介绍deque的优点和缺陷deque的原理介绍 1.deque(双端队列)&#xff1a;是一种双开口的“连续”空间的数据结构&#xff0c;双开口的含义是&#xff1a;在头尾两端都可以进行插入和删除操作&#xff0c;且时间复杂度为O(1)。 需要注意的是deque并不是真正连续的…

制作一个微信小程序步骤_分享个人微信小程序开发步骤

微信小程序的功能不断提高&#xff0c;以及用户对小程序的使用的增加&#xff0c;使得新一批的流量融入小程序中&#xff0c;越开越多的企业开始开发小程序&#xff0c;想要从中分一碗羹&#xff0c;今天内容就从如何制作一个微信小程序说起&#xff0c;希望对你有帮助。微信小…

Vue鼠标移入移出事件(冒泡问题)

一、定义1、mouseenter&#xff1a;在鼠标光标从元素外部首次移动到元素范围之内时触发&#xff0c;这个事件不冒泡。2、mouseleave&#xff1a;在位于元素上方的鼠标光标移动到元素范围之外时触发&#xff0c;这个事件不冒泡。3、mouseover&#xff1a;在鼠标指针位于一个元素…

Flink学习31:Table API SQL之注册表

table API tableAPI & flink SQL flink SQL是最上层的封装&#xff0c;封装了流处理的DataStream API 和 批处理的DataSet API。 但是DataStream API 、DataSet API、table API、flink SQL 这几层并没有分到不同层中&#xff0c;所以应用程序可以同时使用这几层。 Table A…

合合信息——用智能文字识别技术赋能古彝文原籍数字化

文章目录1. 背景介绍&#xff1a;古彝文是什么&#xff1f;为什么要保护它&#xff1f;如何保护它&#xff1f;2. 传统方法保护古彝文&#xff1a;原籍难获、翻译困难2.1. 古彝文原籍的破损与古法保存2.2 古彝文原籍的保护与翻译2.2.1 获取古彝文原籍2.2.2 修复古彝文原籍2.2.3…

ansible作业一

配置ansible学习环境实现以下要求 1.控制主机和受控主机通过root用户通过免密验证方式远程控住受控主机实施对应&#xff08;普通命令&#xff0c;特权命令&#xff09;任务 2.控制主机连接受控主机通过普通用户以免密验证远程控住受控主机实施指定&#xff08;普通命令&#x…

Makefile快速入门

介绍 这里以一个例子来演示利用Makfile进行多文件编译 一共有四个源程序&#xff1a;main.cpp&#xff0c;printhello.cpp&#xff0c;factorial.cpp&#xff0c;functions.h 首先是main.cpp内容 #include<iostream> #include"functionals.h"using namespa…

Apache与Nginx虚拟机的三种访问+非简单请求+跨域知识点整理

Apache 在D:\project\web\index.html中写入 <h1>welcome useing apache!</h1>基于ip访问 打开phpstudy_pro\Extensions\Apache2.4.39\conf\extra\httpd-vhosts.conf写入 <VirtualHost 192.168.1.4:80>ServerAdmin 88888888163.com #管理员邮箱DocumentRoo…

【BP靶场portswigger-服务端7】访问控制漏洞和权限提升-11个实验(全)

前言&#xff1a; 介绍&#xff1a; 博主&#xff1a;网络安全领域狂热爱好者&#xff08;承诺在CSDN永久无偿分享文章&#xff09;。 殊荣&#xff1a;CSDN网络安全领域优质创作者&#xff0c;2022年双十一业务安全保卫战-某厂第一名&#xff0c;某厂特邀数字业务安全研究员&…

【手写 Vue2.x 源码】第十五篇 - 生成 ast 语法树 - 构造树形结构

一&#xff0c;前言 上篇&#xff0c;主要介绍了生成 ast 语法树 - 模板解析部分 使用正则对 html 模板进行解析和处理&#xff0c;匹配到模板中的标签和属性 本篇&#xff0c;生成 ast 语法树 - 构造树形结构 二&#xff0c;构建树形结构 1&#xff0c;需要描述什么 前面…

文本相似度

传统方法 基于TF-IDF、BM25、Jaccord、SimHash、LDA等算法抽取两个文本的词汇、主题等层面的特征&#xff0c;然后使用机器学习模型&#xff08;LR, xgboost&#xff09;训练分类模型优点&#xff1a;可解释性较好缺点&#xff1a;依赖人工寻找特征&#xff0c;泛化能力一般&a…

linux有用技巧:使用ntfs-3g挂载ntfs设备

1.几种文件系统的比较 (1)在linux系统中支持以下文件系统&#xff1a; Ext2 第二扩展文件系统&#xff08;简称 ext2&#xff09;很多年前就已经成为 GNU/Linux 默认的文件系统了。ext2 取代了扩展文件系统(这是 “第二代” 的前身)。它纠正了它前身的一些错误并突破了…

【工具篇】41 # 常用可视化工具集整理(完结)

说明 【跟月影学可视化】学习笔记。 可视化场景主体需求 主体需求示例绘制基本图形根据数据绘制一些二维、三维的几何图形&#xff0c;它们不一定是完整的图表&#xff0c;通常是具有灵活性和视觉冲击力的小图形&#xff0c;例如粒子效果绘制基础图表通常是指绘制常见的饼图…