STL原始碼剖析(四)序列式容器--list
阿新 • • 發佈:2018-12-03
文章目錄
1. 關於list
1.1 首先在介紹list之前先來觀察一下list的節點結構:
template <class T>
struct __list_node {
typedef void* void_pointer;
void_pointer prev;
void_pointer next;
T data;
};
- 從上述節點結構很清楚list是一個雙向連結串列
1.2 與vector的區別
- 相比於vector的連續線性空間,list顯得更為複雜;但list每次插入或刪除一個元素時,就將對應的空間釋放掉,因此,對於任何位置的插入或元素刪除,list永遠是常數時間
2. list的迭代器
- list中的元素由於都是節點,因此其迭代器遞增時取用的是下一個節點,遞減時取用上一個節點,取值時取的是節點的資料值,成員取用時取用的是節點的成員
- 由於list是雙向連結串列,迭代器必須具備前移、後移的能力,因此,list提供的是Bidirectional Iterators
- 關於list的迭代器的設計,大抵就是遞增、遞減運算子、取值以及比較運算子的過載,在這就不細說
3. list的資料結構
- list是一個雙向連結串列,而且是一個環狀雙向連結串列:
//取首元素,node是尾端的一個空節點
iterator begin() { return (link_type) ((*node).next); }
//取尾元素的下一個,即node
iterator end() { return node; }
//為空,說明只有node
bool empty() const { return node->next == node; }
size_type size() const {
size_type result = 0;
distance(begin(), end(), result);
return result;
}
reference front() { return *begin(); }
reference back() { return *(--end()); }
- 可見,STL將node作為一個空節點放在尾端,與首元素、尾元素相連,形成一個環
4. list記憶體構造
- 還是老樣子,以例項說明:
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;
int main(int argc, char** argv) {
list<int> ilist;
cout << "size = " << ilist.size() << endl; //size = 0
ilist.push_back(0);
ilist.push_back(1);
ilist.push_back(2);
ilist.push_back(3);
ilist.push_back(4);
cout << "size = " << ilist.size() << endl; //size = 5
list<int>::iterator ite;
for (ite = ilist.begin(); ite != ilist.end(); ++ite)
cout << *ite << ' '; //0 1 2 3 4
cout << endl;
return 0;
}
- 當建立一個list時,呼叫list的預設建構函式構造一個空list:
public:
list() { empty_initialize(); } //預設建構函式
protected:
void empty_initialize() {
node = get_node(); //配置一個節點空間
node->next = node;
node->prev = node;
}
//既然說到配置空間,就將配置、釋放、構造、銷燬一併提了吧
//配置一個節點
link_type get_node() { return list_node_allocator::allocate(); }
//釋放一個節點
void put_node(link_type p) { list_node_deallocator::deallocate(p); }
//產生一個節點,帶有元素值
link_type create_node(const T& x) {
link_type p = get_node();
construct(&p->data, x);
return p;
}
//銷燬一個節點
void destroy_node(link_type p) {
destroy(&p->data);
put_node(p);
}
- 其中push_back一個元素,實則呼叫insert(與指標插入節點一樣)
5. list的元素操作:
操作 | 功能 |
---|---|
push_front | 插入一個節點作為頭節點 |
push_back | 插入一個節點作為尾節點 |
erase | 刪除迭代器所指的節點 |
pop_front | 移除頭節點 |
pop_back | 移除尾節點 |
clear | 清除整個連結串列 |
remove | 移除某個元素 |
unique | 刪除連續相同的元素的重複項 |
splice | 將迭代器所指元素或連結串列接合於目的迭代器之前 |
merge | 將兩個連結串列合併 |
reverse | 將連結串列逆置 |
sort | 排序 |
- 以下重點理解下transfer以及sort:
- transfer:
// 將 [first,last) 內的所有元素搬移到 position 之前
void transfer(iterator position, iterator first, iterator last)
{
if (position != last) // 如果last == position, 則相當於連結串列不變化, 不進行操作
{
(*(link_type((*last.node).prev))).next = position.node; //(1)
(*(link_type((*first.node).prev))).next = last.node; //(2)
(*(link_type((*position.node).prev))).next = first.node; //(3)
link_type tmp = link_type((*position.node).prev); //(4)
(*position.node).prev = (*last.node).prev; //(5)
(*last.node).prev = (*first.node).prev; //(6)
(*first.node).prev = tmp; //(7)
}
}
- 理解了上述transfer的操作,splice、merge、reverse也就迎刃而解
- sort:這個sort原始碼雖然不長,但理解起來卻是最為費勁的
廢話不多說,讓我們來一起解析它:
//侯捷老師在本書中講解本函式使用quick sort
//但看過該段原始碼的都知道不是使用quick sort,而是merge sort,這應該是一處錯誤吧
template <class T, class Alloc>
void list<T, Alloc>::sort() {
//如果是空連結串列或僅有一個元素,不進行任何操作
if (node->next == node || link_type(node_next)->next == node)
return;
list<T, Alloc> carry;
list<T, Alloc> counter[64]; //維護陣列,最大一層可儲存2 ^64 - 1個元素
int fill = 0;
while (!empty())
{
//每次取出list中的一個元素
carry.splice(carry.begin(), *this, begin());
int i = 0;
while (i < fill && !counter[i].empty()) {
counter[i].merge(carry); //將當前carry與count[i]中的資料歸併
carry.swap(counter[i++]); //交換carry與count[i]中的資料
}
carry.swap(counter[i]); //將count[i]中的資料存入count[i+1]中
if (i == fill)
++fill;
}
for (int i = 1; i < fill; ++i)
counter[i].merge(counter[i - 1]);
swap(counter[fill - 1]);
}
具體分析:假定list中元素為45,21,1,35,28,3,6,75
則此時呼叫sort,逐步進行分析:
fill = 0,carry = 45,此時i<fill,執行swap,則此時counter[0]中存有元素45 i = fill,fill = 1
fill = 1,carry = 21,此時i被重置為0,進入while迴圈,執行merge語句得到counter[0]{21,45},進行swap,carry{21,45},出迴圈,再swap得counter[1]{21,45},i= fill,fill=2
fill = 2,carry = 1,此時i被重置為0,但此時counter[0]為空,則跳過while執行外層swap得counter[0]{1}
fill = 2,i = 0,carry = 35,i < fill並且counter[0]不為空,進入while,執行merge得counter[0]{1,35},swap得到carry{1,35},此時i<fill繼續,i= 2,merge得counter[1]{1,21,35,45},swap得到carry{1,21,35,45},i= fill,出while得counter[2]{1,21,35,45},i = fill, fill = 3
....經過上面的分析,我們發現每次counter[i]達到2^(i+1)個元素時,會轉給counter[i+1],因此可以得出當counter[2]達到8個元素時,會轉給counter[3]得到counter[3]{1,3,6,21,28,35,45,75}
1.其實我們觀察,不難發現counter[64]陣列每層最多維護2^(n) 個元素(n從0開始),則不難得出count[64]最多一次能處理2^64 -1個元素
2.看完上述大概就能理解為何是一個歸併排序,即每次兩個元素merge完放入下一層中進行merge,然後4個再進入下一層merge,如此得到merge好的8個、16個…元素,實現一個歸併排序