1. 程式人生 > >STL原始碼剖析(四)序列式容器--list

STL原始碼剖析(四)序列式容器--list

文章目錄

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
  1. 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也就迎刃而解
  1. 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個…元素,實現一個歸併排序