list

本篇將介紹 STL 中 list 的使用及實現原理

這裏寫圖片描述

list是一種序列式容器,它採用鏈式結構,其中的元素採用節點方式儲存,每個節點獨佔“一塊”內存,它對於空間的運用有絕對的精準,一點也不會浪費。爲什麼呢?如下:

list概述

我們知道,vector中的數據是連續的儲存在一塊靜態內存上,而此處的list與之不同,它使用“節點”的方式儲存數據,節點之間採用指針鏈接。每一個數據獨佔一個節點,每當插入數據時,它申請一個節點的內存,在刪除數據時,它釋放一個節點內存。

基於上述原因,它不會浪費任何空間,對空間的運用有着絕對的精準;這樣做帶來的另一個好處是在任何位置插入或者刪除數據時,它的時間複雜度永遠是常數;但也因爲這樣,list不能像vector那樣通過下標隨機訪問元素,它只能使用迭代器依序巡防其中元素。

內部結構

  • 節點結構
    這裏寫圖片描述

  • 節點代碼

template <class T>
struct __list_node {//list節點
  typedef void* void_pointer;
  void_pointer next;//指向下一個節點的指針
  void_pointer prev;//指向前一個節點的指針
  T data;//數據元素
};
  • list結構代碼
template <class T, class Alloc = alloc>
class list {//list結構,省略的內部很多代碼
//...
protected:
  typedef __list_node<T> list_node;
//...
public:
  typedef list_node* link_type;//link_type就是節點的指針類型
//...
protected:
  link_type node;//重點是這個,它是鏈表的頭結點
//...
};

我們可以看到,list不僅是一個雙向鏈表,而且是一個循環鏈表。在它的內部只是簡單的維護了一個指向list_node的指針,該指針表示鏈表的頭結點。

當鏈表爲空時,它的nextprev都指向自己,下面代碼說明了這一點:

//...
protected:
  void empty_initialize() { 
    node = get_node();//獲取一個節點
    node->next = node;//指向自己
    node->prev = node;//指向自己
  }
//...
public:
  list() { empty_initialize(); }//無參數的構造函數

基本操作

對於list,在接口上它與vector相似,但在內部實現上千差萬別。
我們先看它包括(但不限於)哪些接口:

push_back();//尾插
pop_back();//尾刪
push_front();//頭插
pop_front();//頭刪
insert();//任意位置插入
erase();//刪除某個節點
remove();//移除某個值
unique();//去重複

與vector相比,list多了頭插、頭刪、去重等幾個接口,這些接口在特定的場合下有很大的作用。

下面通過源碼窺探它的內部奧祕。

  • insert()
    這裏寫圖片描述

如上圖所示,我們在鏈表的尾部插入了幾個數據。
那麼它的內部是怎樣做的呢?

  iterator insert(iterator position, const T& x) {
    link_type tmp = create_node(x);//創建一個節點
    tmp->next = position.node;//改變指針
    tmp->prev = position.node->prev;
    (link_type(position.node->prev))->next = tmp;
    position.node->prev = tmp;
    return tmp;
  }

這裏寫圖片描述

結合代碼和圖片看,它的插入操作:

  1. 用要插入的x創建一個新節點;
  2. 讓新的節點的next指向position,prev指向position的前一個節點
  3. 讓posotion的前一個節點的next指向新節點,position的prev指向新節點

通過上面三步就在list中插入了一個新節點。也可以看出,在某個位置插入一個節點,實際上是把新節點插到該位置的前面。

上面是STL最底層的插入算法,對於insert(),它有多種插入接口,這些插入接口在底層都調用了上面的。其它接口如下:

  • iterator insert (iterator position, const value_type& val);
    //將value插入到position位置處;

  • void insert (iterator position, size_type n, const value_type& val);
    //在position的前面插入n個value,下圖證明之:

這裏寫圖片描述

  • template < class InputIterator>
    void insert (iterator position, InputIterator first, InputIterator last);
    //將first至last之間(左閉右開)的數據插入到position之前。
    下圖證之:

這裏寫圖片描述

  • erase()
    這裏寫圖片描述

代碼:

  iterator erase(iterator position) {
    link_type next_node = link_type(position.node->next);
    link_type prev_node = link_type(position.node->prev);
    prev_node->next = next_node;
    next_node->prev = prev_node;
    destroy_node(position.node);
    return iterator(next_node);
  }

我們還是通過畫個圖來解釋:
這裏寫圖片描述
相比於插入來說,刪除更簡單一點,它是這樣做的:

  1. 讓前一個節點的next指向下一個節點;
  2. 讓下一個節點的prev指向前一個節點;
  3. 釋放被刪除的節點。

當然刪除也提供了多重接口:

  • iterator erase (iterator position);
    //刪除position處的節點;
  • iterator erase (iterator first, iterator last);
    //刪除fisrt至last間的節點
  • push_back()
    下面我們看看push_back()內部做了什麼。
 void push_back(const T& x) { insert(end(), x); }

是不是很吃驚,沒錯! push_back()就是這麼簡單,它只是簡單調用了insert()接口

  • pop_back()
  void pop_back() { 
    iterator tmp = end();
    erase(--tmp);
  }

pop_back()稍有不同,它先取得尾節點,在減減之,之後調用erase(),爲什麼要這樣呢?原因在於,STL中,幾乎所有的區間表示,都是左閉右開,所以end()標記的是最後一個節點的下一個節點,要刪除最後一個節點,當然得如上面那樣做了。

  • remove()
    這裏寫圖片描述
template <class T, class Alloc>
void list<T, Alloc>::remove(const T& value) {
  iterator first = begin();
  iterator last = end();
  while (first != last) {
    iterator next = first;
    ++next;
    if (*first == value) erase(first);
    first = next;
  }
}

關於remove(),它用來刪除list中所有值爲value的節點,注意是“所有”。

它的邏輯再簡單不過了,將list遍歷一遍,並調用erase()擦掉值域與value相等的節點。不過,注意到在刪除的時候,需要保存被刪節點的下一個節點,因爲這會牽扯到迭代器失效的問題。

迭代器失效簡單來說就是:當對(某些)容器進行增(刪、改)操作之後,(可能會)導致其內存重新分配,而原來的迭代器將(可能)指向非法的位置。
如此一來,如不對原迭代器處理而直接使用之,將發生不可預料的錯誤。

所以對上述問題的處理方法之一是:在刪之前保存該迭代器之前的位置,在刪除之後,再將之前的位置節點賦給該迭代器。

  • uniqie()
    這裏寫圖片描述

如上圖所示,unique()將鏈表重連續的重複元素刪掉,

template <class T, class Alloc>
void list<T, Alloc>::unique() {
  iterator first = begin();
  iterator last = end();
  if (first == last) return;
  iterator next = first;
  while (++next != last) {
    if (*first == *next)
      erase(next);
    else
      first = next;
    next = first;
  }
}

上面是它的代碼。先用兩個迭代器分別指向第一個和第二個元素,然後通過循環比較兩個迭代器的值,若相等則刪掉第二個值,否則兩個迭代器都後移,這樣將鏈表遍歷一遍就可以去掉鏈表中所有“連續的”重複元素了。

總結

在上面我們分析了list的結構以及部分的操作,其實關於list常用的也就是上面那幾個了。

下面來對比總結一下vector和list的異同。

vector和list都是序列式容器,這裏的序列式指的是元素可序的,說通俗一點就是,它們內部元素之間的結構就像用繩子串起來一樣,都支持在尾部插入、刪除數據,都不用手動管理空間,它們內部算法都會幫它們動態的增長空間,內部都儲存同類型的數據元素。但是不同的是:

vector:

  • 內部管理了一塊靜態的空間,在需要增大時要通過一些繁瑣的操作(開闢新空間、搬移元素、釋放舊空間)完成;
  • 在非尾端插入刪除數據的效率極低(需要大量的搬移元素);
  • 迭代器是普通類型的指針;
  • 支持通過下標隨機訪問元素。

list

  • 內部管理了一個雙向循環鏈表,每個數據獨自佔有“一塊空間”,在插入、刪除時只需處理單個節點即可,不過所整體也佔用了更大的空間(除數據之外還要儲存兩個指針);
  • 在任何位置插入數據的時間複雜度都是常數級的,相對於vector,在非尾位置增刪數據效率更高;
  • 迭代器需要單獨維護一個list的節點;
  • 不支持隨機訪問元素。比如,欲訪問第6個元素,vector只需v[5]即可訪問到,而list必須從頭節點開始遍歷至第六個節點。

——謝謝!


參考資料

  • 模擬實現的list

【作者:果凍 http://blog.csdn.net/jelly_9

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章