C++深入學習:STL源碼剖析 (3) 從源碼深入剖析list
vector和list是最常用的容器,vector是連續的線性空間,list則是非連續的用指針串聯起的空間。因此list對於空間的利用是非常充分的,對於任意位置的插入刪除操作,list是常數時間完成。
list不僅是雙向鏈表,而且是環形雙向鏈表,因此只需要一個指針便可以實現對整個list的迭代器的標記。爲了符合STL的前閉後開的區間要求,list實現時將該指針節點指向一個置於尾端的空白節點,則該節點的前面是尾結點,該節點的後面是頭結點,根據此便可以實現諸多函數。
學習list,個人覺得最大的收穫是在於熟練運用指針完成相應的變換,list的部分函數也是非常經典的算法題,同時list的設計時也有諸多很巧妙的思想。
STL的list實現由三個模塊組成,節點__list_node,
迭代器__list_iterator以及list本身
list 的節點
list是一個雙向鏈表,因此每個節點不僅要存當前的值,還要存儲兩個指針用來尋找前一個節點和後一個節點。
template<class T>
struct __list_node{
typedef void* void_pointer;
void_pointer prev; //前向指針
void_pointer next; //後向指針
T data; //數據
}
list的迭代器
由於list 不是vector一樣節點是連續空間存儲,故不能以一個普通指針作爲迭代器。list的迭代器必須能夠指向list 的節點,同時,還能夠實現遞增(指向下一個節點)、遞減(指向上一個節點)、取值(取出節點的值)、成員取用(取節點的成員對象)等操作。
因此,由於list的迭代器只支持前移後移,而不支持隨機訪問,因此是 Bidirectional Iterators.
list和vector還有一個比較大的區別,list的插入和接合等都不會造成原有的list迭代器失效,而vector的插入操作當引起空間重配時原有的迭代器全部失效。甚者,list 的刪除操作也只有被刪除的那個迭代器失效,其他迭代器不受任何影響。
template<class T,class Ref,class Ptr>
struct __list_iterator{
typedef __list_iterator<T,T &,T*> iterator;
typedef __list_iterator<T,Ref,Ptr> self;
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef __list_node<T>* link_type;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
link_type node; //一個普通的指針,指向list的節點
//構造函數
__list_iterator(link_type x):node(x){}
__list_iterator(){}
__list_iterator()(const iterator& x):node(x.node){}
//一些迭代器操作
//重載等號和不等號
bool operator==(const self&x) const
{
return node==x.node;
}
bool operator!=(const self&x) const
{
return node!=x.node;
}
//對迭代器取值返回節點存儲的數據值
reference operator*()const{return (*node).data;}
//重載->,返回引用
pointer operator->()const {return &(operator*());}
//重載遞增遞減
self& operator++()
{
node=(link_type)((*node).next);
return *this;
}
self operator++(int) //後+,返回原值的拷貝,然後原值++
{
self tmp=*this;
++(*this);
return tmp;
}
self& operator--()
{
node=(link_type)((*node).prev);
return *this;
}
self operator--(int) //後-,返回原值的拷貝,然後原值--
{
self tmp=*this;
--(*this);
return tmp;
}
}
list的數據結構
list是一個環形雙向鏈表,因此只需要一個指針就可以完成整個鏈表的遍歷。
template<class T,class Alloc=alloc>
class list{
protected:
typedef __list_node<T> list_node;
public:
typedef list_node* link_type;
protected:
link_type node; //作爲標記整個環形雙向鏈表的指針
//... 之後都是一系列函數,之後逐一分析
}
將指針node指向刻意置於尾端的一個空白節點,則很容易實現一下函數:
//node的next指向表頭,prev就指向隊尾
iterator begin(){return (link_type)((*node).next);}
iterator end(){return node;} //根據前閉後開原則,end返回隊尾之後的一個元素
bool empty(){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());}
list的構造和內存管理
構造、銷燬、配置、釋放節點:
protected:
//配置一個節點
link_type get_node(){return list_node_allocator::allocate();}
//釋放一個節點
void put_node(link_type p){list_node_allocator::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);
}
list的default constructor函數,創建一個空list,只有一個節點,首尾均指向自己,該節點爲空節點
public:
list(){empty_initialize();}
protected:
void empty_initialize() //令該節點的頭和尾都指向自己,不設元素值
{
node=get_node();
node->next=node;
node->prev=node;
}
list的元素操作函數
insert函數:在指定位置插入值爲x的一個節點,該函數可以作爲push類函數的基礎
iterator insert(iterator position,const T& x)
{
link_type tmp=create_node(x); //產生一個節點
//將position的prev節點的next指向新節點,同時新節點的prev指向該節點
//將positon的prev節點指向新節點
tmp->next=position.node;
tmp->prev=position.node->prev;
(link_type(position.node->prev))->next=tmp;
position.node->prev=tmp;
return tmp;
}
push_back和push_front函數
//向尾部插入元素
void push_back(const T& x){insert(end(),x);}
//向首部插入元素
void push_front(const T&x){insert(begin(),x);}
erase函數:移除迭代器所指的節點,可以作爲pop類函數的基礎
//移除position所指節點
//將positon的prev節點的next指向position的next節點,將next節點的prev指針指向position的prev節點
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;
//將position銷燬
destroy_node(position.node);
return iterator(next_node);
}
相應的,pop_back()和pop_front函數:
void pop_back()
{
iterator tmp=end();
erase(--tmp);
}
void pop_front()
{
erase(begin());
}
clear函數:清除整個鏈表的節點
void clear()
{
link_type cur=(link_type) node->next; //找到頭節點
while(cur!=node) //依次遍歷每一個節點
{
link_type tmp=cur;
cur=(link_type) cur->next; //向後遍歷
destrpy_node(tmp); //析構並釋放該節點
}
//將node恢復爲空list的狀態
node->next=node;
node->prev=node;
}
remove函數:將數值爲value的所有元素消除
void remove(const T& value)
{
iterator first=begin();
iterator last=end();
while(first!=last) //遍歷每一個節點
{
iterator next=first;
++next;
if(*first==value) //如果first的值是value,則移除
erase(first);
first=next;
}
}
unique函數:移除數值相同的連續元素(連續且數值相同會倍移除剩一個,常用於排序後的list)
void unique()
{
iterator first=begin();
iterator last=end();
if(first==end) //空鏈表不需要操作
return;
iterator next=first;
while(++next!=last) //依次遍歷
{
if(*first==*next) //找到相同元素將後者移除
erase(next);
else //不重複的話將first調整爲next
first=next;
next=first; //將next修正爲first(遍歷時有前置++)
}
}
list的元素遷移函數
list實現元素的遷移時,即將連續範圍的元素遷移到指定位置,只需要處理指針之間的關係即可。該函數爲諸多更加複雜的操作提供了基礎,包括splice,sort,merge等函數。
這是將一個list的部分移到另一個list中,不僅要處理連接[first,end)的鏈表,也要處理移除[first,end)的鏈表
//將[first,last)範圍內的元素移到position之前
void transfer(iterator position,iterator first,iterator last)
{
if(position!=last) //
{
//將尾結點(last.node->prev)的next指針指向positon節點
(*(link_type((*last.node).prev))).next=position.node;
//將first的prev的next指針直接指向last
(*(link_type((*first.node).prev))).next=last.node;
//將position的prev的next節點指向first
//---------上述均只完成了單向,下面將每條邊的令一方向建立連接即可
(*(link_type((*position.node).prev))).next=first.node;
link_type tmp=link_type((*position.node).prev);
(*position.node).prev=(*last.node).prev;
(*last.node).prev=(*first.node).prev;
(*first.node).prev=tmp;
}
}
transfer是list的一個protected對象,list提供了splice函數完成接合操作;
一個簡單的程序說明splice和reverse以及sort
list<int> list1{1, 2, 5, 3, 6};
list<int> list2{10,30,20,50,40};
list<int>::iterator ite = find(list1.begin(),list1.end(),5);
//將list2放入value=5的節點之前
list1.splice(ite,list2);
for_each(list1.begin(), list1.end(), [](int &a) { cout << a << ' '; });
cout << endl;
//reverse函數 倒置
list1.reverse();
for_each(list1.begin(), list1.end(), [](int &a) { cout << a << ' '; });
cout << endl;
//sort函數,排序
list1.sort();
for_each(list1.begin(), list1.end(), [](int &a) { cout << a << ' '; });
//1 2 10 30 20 50 40 5 3 6
//6 3 5 40 50 20 30 10 2 1
//1 2 3 5 6 10 20 30 40 50
splice實現:
//將x接合到position之前
void splice(iterator position,list& x)
{
if(!x.empty())
transfer(position,x.begin(),x.end());
}
//將i所指元素接合於position所指位置之前,position和i可以指代同一元素
void splice(iterator position,list&,iterator i)
{
iterator j=i;
++j;
if(position==i||position==j)
return;
transfer(position,i,j);
}
//將[first,end)結合於position之前
//position和[first,end)可以是同一個list,但position不可以是[first,end)之間
void splice(iterator position,list&,iterator first,iterator last)
{
if(first!=last)
transfer(position,first,last);
}
merge,reverse,sort
這三個算法都是鏈表相關問題中非常常見的,可以學習一下STL如何實現的
merge函數:將兩個升序的list合併
template <class T,class Alloc>
void merge(list<T,Alloc> &x)
{
iterator first1=begin(); //標記當前鏈表的頭尾
iterator last1=end();
iterator first2=begin(); //標記另一個鏈表的頭尾
iterator last2=end();
while(first1!=last1&&first2!=last2) //遍歷至其中一個結束
{
if(*firs2<*first1)
{
iterator next=first2;
transfer(first1,first2,++next); //將該節點移動到first1之前
first2=next; //first2向後迭代
}
else //否則只需要first1向後迭代,因爲返回的是原鏈表
++first1;
}
if(first2!=last2) //如果第二個鏈表還沒有遍歷結束,則將剩餘全部移動至尾端即可
transfer(last1,first2,last2);
}
reverse函數:將鏈表倒置
template <class T,class Alloc>
void reverse()
{
//只有一個節點或者是空鏈表直接返回即可
if(node->next==node||linke_type(node->next)->next==node)
return;
iterator first=begin();
++first;
while(first!=end()) //每次將遍歷到的這個節點放到最前面
{
iterator old=first;
++first;
transfer(begin(),old,first);
}
}
sort函數:排序鏈表
由於STL的sort算法只接收RandomAccessIterator,故list需要定義自己的sort函數
list的sort實現應該是歸併排序,並且是一個非遞歸的歸併排序,實現思路非常巧妙,當然理解起來也是相當困難,STL源碼剖析中說這是快排應該有錯。
該博文對list的sort函數講解非常好:https://www.cnblogs.com/avota/archive/2016/04/13/5388865.html
template<class T,class Alloc>
void sort()
{
//只有一個節點或者是空鏈表直接返回即可
if(node->next==node||linke_type(node->next)->next==node)
return;
//中介數據存放
list<T,Alloc> carry; // 輔助鏈表,用於從a中提取元素以及臨時保存兩個鏈表的合併結果
list<T,Alloc> counter[64]; // 保存着當前每一個歸併層次的結果
int fill=0;
while(!empty())
{ // 將鏈表的第一個元素移動至carry開頭
carry.splice(carry.begin(),*this,begin());
int i=0;
// 從小往大不斷合併非空歸併層次直至遇到空層或者到達當前最大歸併層次
while(i<fill&&!counter[i].empty())
{
counter[i].merge(carry); // 鏈表合併,結果鏈表是有序的
carry.swap(counter[i++]); // 鏈表元素互換
}
carry.swap(counter[i]);
if(i==fill)
++fill;
}
for(int i=1;i<fill;++i) // 將所有歸併層次的結果合併得到最終結果counter[fill - 1]
counter[i].merge(counter[i-1]);
swap(counter[fill-1]);
}