面试笔记---list

128 阅读7分钟

1 类介绍

list容器是双向循环链表,同样是序列式容器(sequence_container),list动态增删是每插入和删除一个元素就配置和删除一个元素的空间,因此不像vector一样有备用的空间。根据链表的特点,list应该有三个属性,自身的值、上一个节点的位置、下一个节点的位置。

template<class T>
  struct list_node {
    T data;
    struct list_node<T>* next;//节点的下一个节点的地址
    struct list_node<T>* prev;//节点的上一个节点的地址

    list_node(const T date)
      :
      data(date),
      next(nullptr)
      , prev(nullptr)
    {
    }
  };

List容器实现为双链接链表;双链接链表可以将它们包含的每个元素存储在不同的、不相关的存储位置。在内部,顺序是通过与每个元素的关联来保持的,这些元素是指向它前面的元素的链接和指向它后面的元素的链接。

list节点的关联是通过其前后节点的指针来描述的,与别的元素无关。所以插入插入/删除操作都只需建立/销毁与前序节点和后序节点的联系即可。因此,对于list来说,只需要一个指针,便可以完整表示整个链表,其源码如下所示:

  template<class T>
  class list {
    typedef list_node<T> Node;
  private:
    Node* head;
  public:
    typedef list_Iterator<T, T*, T&> iterator;
    typedef list_Iterator<T, const T*, const T&> const_iterator;
    };

为了遵循STL容器的前闭后开原则,list容器用一个头结点(尾结点)来作为链表的开始或终止,该节点是空白节点,不存储元素;我们只需要让指针node指向该空白结点,list便可视为“前闭后开”。

2 list构造函数介绍

2.1 构造函数

default (1)explicit list (const allocator_type& alloc = allocator_type());
fill (2)explicit list (size_type n, const value_type& val = value_type(),const allocator_type& alloc = allocator_type());
range (3)template
list (InputIterator first, InputIterator last, const allocator_type& alloc = allocator_type());
copy (4)list (const list& x);

默认构造函数:构造list空对象

带参构造函数:构造的list中包含n个值为val的元素

拷贝构造函数:拷贝构造函数

迭代器构造函数:区间中的元素构造list

2.2 构造函数的模拟实现

首先是list节点类的构造函数

list_node(const T date)
      :
      data(date),
      next(nullptr)
      , prev(nullptr)
    { }

默认构造函数:list默认是一个带头双向链表,默认构造函数是构造一个空对象,但是还是有一个头节点,所以将头节点的两个指针都指向自己

list() {
      head = new Node(T());
      head->next = head;
      head->prev = head;
    }

迭代器构造函数:该构造函数和vector迭代器构造函数一样都可以将其他容的元素拷贝到list容器中

    template<class IteraterFirst, class IteraterFirstEnd >

    list(IteraterFirst first,IteraterFirstEnd en) {
      head = new Node(T());
      head->next = head;
      head->prev = head;
    
      while (first!=en) {
        push_back(*first);
        first++;
      }
    }

拷贝构造函数:构造一个临时对象调用迭代器构造函数将t中元素拷贝到临时对象中,接着将this和临时对象调换地址,使this指针指向临时对象开辟的空间

list(const list<T>& t) {
      head = new Node(T());  
      head->next = head;
      head->prev = head;
      list<T> temp(t.begin(),t.end());
      swap(head, temp.head);
    }

3 list的迭代器及常用函数模拟实现

3.1 list的迭代器介绍

由于list的节点除了要存储数据和关联节点,还需要适配容器常用的动作,例如递增、递减等;因此普通的节点指针无法满足需求,所以list将迭代器封装成一个类。

  //迭代器结构体
  template<class T, class Ref, class Ptr>
  struct list_Iterator {
    typedef list_node<T> Node;
    typedef list_Iterator<T, Ref, Ptr>  Self;
    Node* node;
  };

node为list的节点类型为自定义类型包含了节点的前后指针和数值

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

迭代器的模拟实现

首先是++的模拟实现:因为前置++和后置++在功能上是不一样的所以在实现上参数加了一个int成了运算符重载 ++就是访问下一个结点的地址直接将下一个结点的地址赋给当前节点的 然后返回就行

  //前置++
    Self& operator ++()
    {
      node = node->next;
      return *this;
    }
    //后置++和前置++构成重载

    Self& operator ++(int)
    {
      node = node->next;
      return *this;
    }

—的实现:前置—后置—也是将当前节点的地址向前移动,所以让当前指针指向前一个节点的地址返回就行,这也是加了一个参数成了运算符重载

    Self& operator--() {
      node = node->prev;
      return *this;
    }
    Self& operator--(int) {
      node = node->prev;
      return *this;
    }

/->运算符重载

     // 重载 "*"操作符,用于通过迭代器取出节点元素  
    Ptr operator*()const {
      return node->data;
    }
 // 重载"->"操作符,用于通过迭代器操作元素的成员
    Ptr operator->() {

      return &node->data;
    }

判断是不是相等,直接判断节点地址是不是相等

bool operator  !=(const Self& it) const {

      return node != it->node;
    }
    bool operator  ==( const Self& it) const {

      return node == it.node;
    }

list的元素操作不会引起其他的元素空间重新分配,因此迭代器不会失效

3.2 insert函数介绍及模拟实现

函数原型:

single element (1)iterator insert (iterator position, const value_type& val);
fill (2)void insert (iterator position, size_type n, const value_type& val);
range (3)template
void insert (iterator position, InputIterator first, InputIterator last);

函数模拟实现

iterator insert(iterator pos, const T& x) {
      Node* cue = pos.node;
      Node* prev = cue->prev;
      Node* newnode = new Node(x);
      prev->next = newnode;
      newnode->prev = prev;
      newnode->next = cue;
      cue->prev = newnode;

      return iterator(newnode);

    }

函数功能:在pos点插入一个x,因为list空间是不连续的之间是不连续的使用指针互相连接,所以在pos之前插入节点,直接申请新节点断开节点之间的连接,将新节点插入到两个节点之间。

可以看到,插入时,由于前后均为双指针的关系,在调整时需要调整四个指针指向,注意顺序。

3.3 erase函数介绍及模拟实现

函数原型:第一个函数删除pos点的节点,函数二:删除区间first last区间的节点

iterator erase (iterator position); iterator erase (iterator first, iterator last);

函数实现

iterator erase(iterator iter) {
      assert(iter != end());

      Node* cur = iter.node;
      Node* prev = cur->prev;
      Node* Next = cur->next;
      prev->next = Next;
      Next->prev = prev;
      delete cur;
      return iterator(Next);

    }

删除iter点的节点,就是将iter前边的节点的位置后指针和iter后边的节点的前指针连接在一起,最后将iter点的节点释放掉

和insert类似的,erase在删除元素时需要获取该节点的前、后结点,并将前节点的next指向后节点,即将被删除的点从链表中断开

3.4 begin()函数和end()函数

end就是最后一个节点后边的位置也就是头节点的位置,begin是第一个节点的位置返回的是头节点的下一个节点

//头节点的下一个节点
    iterator begin()const {

      return iterator(head->next);

    }
    //end()头结点的位置
    iterator end() const{
      return iterator(head);

    }

3.5 其他函数实现

其他函数的实现可以借助insert函数和erase函数直接实现

void push_fort(const T& x) {
      insert(begin(), x);
    }
      void pop_back() {

      erase(iterator(head->prev));

    }
    void pop_forn() {
      erase(begin());
    }
      void clear(){
        iterator it = begin();
        while (it!=end()) {
          it=erase(it);
        }
      }
      void push_back(const T& t) {
      /*    Node* l = head->prev;
          Node* newnode = new Node(t);
          newnode->prev = l;
          l->next = newnode;
          head->prev = newnode;
          newnode->next = head;*/
      insert(end(), t);

    }
    void push_fort(const T& x) {
      insert(begin(), x);
    }

4 面试热点

4.1 vector和list的区别

  • 1.vector和数组类似,拥有一段连续的内存空间。当插入新的元素时,vector当前拥有的内存空间不够插入元素,通常以两倍重新申请一块更大的内存,并将原来的元素拷贝过去。由于vector的元素是连续存储的,因此随机访问非常高效,时间复杂度为O(1);但插入或删除元素时,会引起其他元素整体移动,时间复杂度为O(n);
  • 2.list是双向循环链表,各元素的内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,要遍历才能做到,时间复杂度为O(n);但可以高效地进行插入和删除,(不需要拷贝和移动数据,只需要改变指针的指向就可以了),时间复杂度为O(1)。

需要高效的随机存取,而不在乎插入和删除的效率,选vector。 需要大量的插入和删除,而不关心随机存取,则使用list。