STL源碼分析: list的sort函數

今天,來看看STL裏面的sort函數,這個排序函數真是讓人眼前一亮啊,有趣的很~·

很奇怪的是裏面居然硬編碼了一個counter[64]??? 一個很naive的懷疑就是,這玩意的長度是硬編碼進去的不會越界嘛????

其實現爲:

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];
  int fill = 0;
  while (!empty()) {
    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[i].merge(counter[i-1]);
  swap(counter[fill-1]);
}

其中,先看看排序過程中涉及到的函數的實現:

  • splice函數:
  void splice(iterator position, list&, iterator first, iterator last) {
    if (first != last) 
      transfer(position, first, last);
  }

而transfer(position,first,last)函數則是把list雙向循環鏈表裏面由迭代器fist,last確定的一個左閉右開區間[first,last)的元素都掛在position迭代器的前面。
例如一個list: [5,4,2,7,10],position指向4,first 指向2,last指向10。transfer調用後得到:[5,2,7,4,10]。

splice就是對transfer一個簡單的封裝。

  • merge
template <class T, class Alloc> template <class StrictWeakOrdering>
void list<T, Alloc>::merge(list<T, Alloc>& x, StrictWeakOrdering comp) {
  iterator first1 = begin();
  iterator last1 = end();
  iterator first2 = x.begin();
  iterator last2 = x.end();
  while (first1 != last1 && first2 != last2)
    if (comp(*first2, *first1)) {
      iterator next = first2;
      transfer(first1, first2, ++next);
      first2 = next;
    }
    else
      ++first1;
  if (first2 != last2) transfer(last1, first2, last2);
}

merge函數負責把當前的list和參數指定的list進行合併,這兩個list都得是排好序的。merge的結果是把參數 x 鏈表的元素有序的合併到當前的list上。

  • swap函數
 void swap(list<T, Alloc>& x) { __STD::swap(node, x.node); }

swap函數負責,把當前鏈表的頭節點和目標鏈表的頭節點進行互換。

  • STL list的結構
    在STL中list就是一個雙向的循環鏈表,每個list有一個表頭節點node ,這個節點的數據域不發揮左右,只用於跟蹤當前的鏈表。
    它的結構類似於下圖:反正就是個雙向的循環鏈表。
    在這裏插入圖片描述

OK.相關函數就是上面這些,我們分析這個sort函數。

侯捷的書上說它是個快排,然而實際上這個怎麼看也不是快排。它的行爲其實有點像歸併的。

我們先初始化一個list: [5,3,7,8,2,6,9,5],在內部是這麼一個雙向鏈表結構:
在這裏插入圖片描述

OK,我們給源代碼各行標個序,便於分析。我們把上面那個鏈表代入進入調用sort看會里面是怎麼一回事。

template <class T, class Alloc>
void list<T, Alloc>::sort() {
  1.if (node->next == node || link_type(node->next)->next == node) return;
  2.list<T, Alloc> carry;
  3.list<T, Alloc> counter[64];
  4.int fill = 0;
  5.while (!empty()) {
  6.  carry.splice(carry.begin(), *this, begin());
  7.  int i = 0;
  8.  while(i < fill && !counter[i].empty()) {
  9.    counter[i].merge(carry);
  10.    carry.swap(counter[i++]);
  11.  }
  12.  carry.swap(counter[i]);         
  13.  if (i == fill) ++fill;
  } 

  14.for (int i = 1; i < fill; ++i) counter[i].merge(counter[i-1]);
  15.swap(counter[fill-1]);
}

首先第1行,檢查鏈表是不是空或者只有一個有效的元素。如果空或只有一個元素的話,那就不用排序了,自然而然就是有序的。
第2,3行定義兩個局部變量carry,counter。他們都是同類型的list變量。而且counter還是個硬編碼長度的List數組。
第5-13行是個大循環。循環的進入條件是當前的list不爲空。我們的list包含8個元素,可以進入循環。
第6、7行,把list的第一個元素給carry.此時:

變量 內容 -
carry 5
*this [3,7,8,2,6,9,5]
counter[0] []
fill 0
i 0

第8行,i爲0而且counter[0]也爲空,於是不進去內層循環。
第12行,carry和counter[0]交換表頭節點。結果爲:

變量 內容 -
carry []
*this [3,7,8,2,6,9,5]
counter[0] [5]
fill 0
i 0

第13行,i等於fill,於是fill自增,表示counter數組裏面已經有一個元素被佔坑了。

變量 內容 -
carry []
*this [3,7,8,2,6,9,5]
counter[0] [5]
fill 1
i 0

又回到第5行,看當前鏈表是否爲空,顯然目前*this 還有7個元素,當然不爲空,進入循環。
第6行,carry得到*this的首元素。
第7行,i爲0.此時:

變量 內容 -
carry [3]
*this [7,8,2,6,9,5]
counter[0] [5]
fill 1
i 0

第8行,此時i<fill而且counter[i]是有元素的,於是進入內層循環。
第9行,把carry包含的元素合併到counter[i]裏面。

變量 內容 -
carry [0]
*this [7,8,2,6,9,5]
counter[i] [3,5]
fill 1
i 0

第10行,再把新合併的counter[i]給carry,同時讓i自增。

變量 內容 -
carry [3,5]
*this [7,8,2,6,9,5]
counter[0] []
counter[1] []
fill 1
i 1

在回第8行,發現此時內循環條件不成立,跳出內循環。
第12行,把carry給counter[i]。即:

變量 內容 -
carry []
*this [7,8,2,6,9,5]
counter[0] []
counter[1] [3,5]
fill 1
i 1

第13行,更新fill,表示counter數組裏面已經填過2個兩個元素了。

變量 內容 -
carry []
*this [7,8,2,6,9,5]
counter[0] []
counter[1] [3,5]
fill 2
i 1

又回到第5行,看當前鏈表是否爲空,顯然目前*this 還有6個元素,當然不爲空,進入循環。
第6行,carry得到*this的首元素。
第7行,i爲0.此時:

變量 內容 -
carry [7]
*this [8,2,6,9,5]
counter[0] []
counter[1] [3,5]
fill 2
i 0

第8行,此時i<fill,但是counter[i]是沒有元素的,於是不進入內層循環。

第12行,把carry給counter[i]。即:

變量 內容 -
carry []
*this [8,2,6,9,5]
counter[0] [7]
counter[1] [3,5]
fill 2
i 0

template <class T, class Alloc>
void list<T, Alloc>::sort() {
  1.if (node->next == node || link_type(node->next)->next == node) return;
  2.list<T, Alloc> carry;
  3.list<T, Alloc> counter[64];
  4.int fill = 0;
  5.while (!empty()) {
  6.  carry.splice(carry.begin(), *this, begin());
  7.  int i = 0;
  8.  while(i < fill && !counter[i].empty()) {
  9.    counter[i].merge(carry);
  10.    carry.swap(counter[i++]);
  11.  }
  12.  carry.swap(counter[i]);         
  13.  if (i == fill) ++fill;
  } 

  14.for (int i = 1; i < fill; ++i) counter[i].merge(counter[i-1]);
  15.swap(counter[fill-1]);
}

又回到第5行,此時當前鏈表還有5個元素,當然進入循環。
第6,7行,把當前鏈表第一個元素給carry。

變量 內容 -
carry [8]
*this [2,6,9,5]
counter[0] [7]
counter[1] [3,5]
fill 2
i 0

第8行,i<fill,而且counter[i]有一個元素,進入內循環。
第9行,把carry合併到counter[i]

變量 內容 -
carry []
*this [2,6,9,5]
counter[0] [7,8]
counter[1] [3,5]
fill 2
i 0

第10行,再把counter[i]給carry,同時讓i自增。

變量 內容 -
carry [7,8]
*this [2,6,9,5]
counter[0] []
counter[1] [3,5]
fill 2
i 1

回到第8行,發現此時i<fill,而且counter[i]有兩個元素,於是進入再次進入循環。
第9行,把carry和counter[i]合併,得到:

變量 內容 -
carry []
*this [2,6,9,5]
counter[0] []
counter[1] [3,5,7,8]
fill 2
i 1

第10行,再把counter[i]給carry,同時讓i自增。

變量 內容 -
carry [3,5,7,8]
*this [2,6,9,5]
counter[0] []
counter[1] []
fill 2
i 2

回到第8行,不滿足條件。
第12行,把carry給counter[2].

變量 內容 -
carry []
*this [2,6,9,5]
counter[0] []
counter[1] []
counter[2] [3,5,7,8]
fill 2
i 2

第13行,把fill增大:

變量 內容 -
carry []
*this [2,6,9,5]
counter[0] []
counter[1] []
counter[2] [3,5,7,8]
fill 3
i 2

OK,分析到這裏,後面就是一樣的套路了。
把首元素給carry:

變量 內容 -
carry [2]
*this [6,9,5]
counter[0] []
counter[1] []
counter[2] [3,5,7,8]
fill 3
i 0

把carry給counter[0]

變量 內容 -
carry []
*this [6,9,5]
counter[0] [2]
counter[1] []
counter[2] [3,5,7,8]
fill 3
i 0

把首元素給carry:

變量 內容 -
carry [6]
*this [9,5]
counter[0] [2]
counter[1] []
counter[2] [3,5,7,8]
fill 3
i 0

合併carry和counter[0],把合併結果掛到counter[1]去

變量 內容 -
carry []
*this [9,5]
counter[0] []
counter[1] [2,6]
counter[2] [3,5,7,8]
fill 3
i 0

把首元素給carry,

變量 內容 -
carry [9]
*this [5]
counter[0] []
counter[1] [2,6]
counter[2] [3,5,7,8]
fill 3
i 0

把carry掛到counter[0]

變量 內容 -
carry []
*this [5]
counter[0] [9]
counter[1] [2,6]
counter[2] [3,5,7,8]
fill 3
i 0

把首元素給carry

變量 內容 -
carry [5]
*this []
counter[0] [9]
counter[1] [2,6]
counter[2] [3,5,7,8]
fill 3
i 0

把carry和counter[0]合併,

變量 內容 -
carry [5,9]
*this []
counter[0] []
counter[1] [2,6]
counter[2] [3,5,7,8]
fill 3
i 1

把carry和counter[1]合併

變量 內容 -
carry [2,5,6,9]
*this []
counter[0] []
counter[1] []
counter[2] [3,5,7,8]
fill 3
i 2

把carry和counter[2]合併

變量 內容 -
carry [2,3,5,5,6,7,8,9]
*this []
counter[0] []
counter[1] []
counter[2] []
fill 3
i 3

把carry給counter[3],同時自增fill

變量 內容 -
carry []
*this []
counter[0] []
counter[1] []
counter[2] []
counter[3] [2,3,5,5,6,7,8,9]
fill 4
i 3

最後,第5行的條件不再滿足。不進入循環。

template <class T, class Alloc>
void list<T, Alloc>::sort() {
  1.if (node->next == node || link_type(node->next)->next == node) return;
  2.list<T, Alloc> carry;
  3.list<T, Alloc> counter[64];
  4.int fill = 0;
  5.while (!empty()) {
  6.  carry.splice(carry.begin(), *this, begin());
  7.  int i = 0;
  8.  while(i < fill && !counter[i].empty()) {
  9.    counter[i].merge(carry);
  10.    carry.swap(counter[i++]);
  11.  }
  12.  carry.swap(counter[i]);         
  13.  if (i == fill) ++fill;
  } 

  14.for (int i = 1; i < fill; ++i) counter[i].merge(counter[i-1]);
  15.swap(counter[fill-1]);
}

14行,把相鄰的兩個counter[i]合併。
最後,把counter[fiil-1]給*this,
於是得到:

變量 內容 -
carry []
*this [2,3,5,5,6,7,8,9]
counter[0] []
counter[1] []
counter[2] []
counter[3] []
fill 4

總結

根據上面的分析,我們可以知道,sort函數設計思路是:counter[i] 維護當前最近遇到的元素裏面 2i2^i 個元素有序。當counter[i]需要接收超過2i2^i 個元素的時候,它就向上合併。遇到新的元素,先把它丟到counter[0],如果發現counter[0]有一個元素,那麼就把整個新的元素和原來的counter[0]裏面的元素共兩個元素合併,插入到counter[1],如果發現counter[1]也滿了,就把counter[1]也一起拿出來合併,向上呈遞。
···
通過這種方式,層層合併的方式,最後可以實現排序。
均攤下來的時間複雜度爲:O(nlogn)O(nlogn),猜測。

另外值得一提的是,STL裏面把counter數組長度硬編碼爲64,通過上面的分析是是綽綽有餘的!!
counter[64]可以接收2632^{63}個元素的排序,遠遠足夠了·····

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