本篇將介紹 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的指針,該指針表示鏈表的頭結點。
當鏈表爲空時,它的next
和prev
都指向自己,下面代碼說明了這一點:
//...
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;
}
結合代碼和圖片看,它的插入操作:
- 用要插入的x創建一個新節點;
- 讓新的節點的next指向position,prev指向position的前一個節點
- 讓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);
}
我們還是通過畫個圖來解釋:
相比於插入來說,刪除更簡單一點,它是這樣做的:
- 讓前一個節點的next指向下一個節點;
- 讓下一個節點的prev指向前一個節點;
- 釋放被刪除的節點。
當然刪除也提供了多重接口:
- 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】