STL源碼分析--algorithm

更多精彩內容,請關注微信公衆號:後端技術小屋c++

STL中實現了一些跟容器相關的一些算法。這裏介紹algorithm頭文件中一些有意思的算法實現。算法

1 相關頭文件

stl_algo.h
stl_algobase.h
stl_numeric.h

2 find

algorithm頭文件中定義的find函數可適用於全部定義了迭代器的STL容器。可是一些經常使用的容器如map/unordered_map/set/unordered_set也定義了類內方法find。當須要搜索元素時,咱們應當選擇類內find方法仍是類外find函數呢?後端

當須要搜索容器內某個元素時,應當優先使用類內find方法, 由於其性能不低於類外函數find。舉例來在unordered_map中類內find的時間複雜度爲O(1),而類外函數find的時間複雜度爲O(n)微信

如下爲類外find的實現。首先經過iterator_traits獲取迭代器的類型(可參考STL源碼分析--iterator)。判斷其爲通常的input iterator仍是random access iterator。而後針對這兩種狀況對find進行了重載。dom

  • 若是是通常的input iterator:順序遍歷迭代器的區間,返回與目標值相等的第一個迭代器.
  • 若是是random access iterator:首先計算區間長度,而後在遍歷時對for循環作展開,展開係數爲4。這樣作的目的有二,一是爲了減小循環次數,減小CPU分支預測的開銷;二是提升循環內代碼並行化執行的可能性,充分利用現代CPU的SIMD提升吞吐量。
template <class _InputIter, class _Tp>
inline _InputIter find(_InputIter __first, _InputIter __last,
                       const _Tp& __val)
{
  __STL_REQUIRES(_InputIter, _InputIterator);
  __STL_REQUIRES_BINARY_OP(_OP_EQUAL, bool, 
            typename iterator_traits<_InputIter>::value_type, _Tp);
  return find(__first, __last, __val, __ITERATOR_CATEGORY(__first));
}
  
template <class _InputIter, class _Tp>
inline _InputIter find(_InputIter __first, _InputIter __last,
                       const _Tp& __val,
                       input_iterator_tag)
{
  while (__first != __last && !(*__first == __val))
    ++__first;
  return __first;
}

template <class _RandomAccessIter, class _Tp>
_RandomAccessIter find(_RandomAccessIter __first, _RandomAccessIter __last,
                       const _Tp& __val,
                       random_access_iterator_tag)
{
  typename iterator_traits<_RandomAccessIter>::difference_type __trip_count
    = (__last - __first) >> 2;

  for ( ; __trip_count > 0 ; --__trip_count) {
    if (*__first == __val) return __first;
    ++__first;

    if (*__first == __val) return __first;
    ++__first;

    if (*__first == __val) return __first;
    ++__first;

    if (*__first == __val) return __first;
    ++__first;
  }

  switch(__last - __first) {
  case 3:
    if (*__first == __val) return __first;
    ++__first;
  case 2:
    if (*__first == __val) return __first;
    ++__first;
  case 1:
    if (*__first == __val) return __first;
    ++__first;
  case 0:
  default:
    return __last;
  }
}

3 find_if

find_if與find類似,只不過遍歷區間時,判斷條件變了,判斷元素值與目標值相等 --> 判斷元素值知足給定的判斷條件ide

template <class _InputIter, class _Predicate>
inline _InputIter find_if(_InputIter __first, _InputIter __last,
                          _Predicate __pred,
                          input_iterator_tag)
{
  while (__first != __last && !__pred(*__first))
    ++__first;
  return __first;
}

4 power

power用於計算一個數的指數,在實現中使用了快速冪算法,時間複雜度爲O(log n),其中n爲power的冪次。函數

舉例說明快速冪算法:
計算: 3^7 = 3^(1+2+4) = (3^1) * (3^2) * (3*4) = 3 * (3^2) * (3^2)^2源碼分析

很容易看出規律,用僞代碼表示:性能

result = 1
curr = x
while (x)
{
  if (x%2) {
    result *= curr
  }
  curr *= curr
  x >>= 1
}
return result

在STL中實現:__opr爲函數對象,用它可定義兩個_Tp對象的乘法操做,缺省爲multiplies<_Tp>,對應的identity_element結果爲1,由於任何數字乘1都等於其自己。指針

template <class _Tp, class _Integer>
inline _Tp __power(_Tp __x, _Integer __n)
{
  return __power(__x, __n, multiplies<_Tp>());
}

template <class _Tp> inline _Tp identity_element(multiplies<_Tp>) {
  return _Tp(1);
}
  
template <class _Tp, class _Integer, class _MonoidOperation>
_Tp __power(_Tp __x, _Integer __n, _MonoidOperation __opr)
{
  if (__n == 0)
    return identity_element(__opr);
  else {
    while ((__n & 1) == 0) {
      __n >>= 1;
      __x = __opr(__x, __x);
    }

    _Tp __result = __x;
    __n >>= 1;
    while (__n != 0) {
      __x = __opr(__x, __x);
      if ((__n & 1) != 0)
        __result = __opr(__result, __x);
      __n >>= 1;
    }
    return __result;
  }
}

5 swap

交換兩個同類型對象的值。

注意,當對兩個容器進行交換時,應當儘可能使用容器類內swap方法,由於algorithm中的swap函數會生成臨時變量,執行額外的複製構造函數和析構函數,開銷較大。而類內swap方法會盡可能避免這些開銷。兩者之因此會有這種差異是由於類外swap函數沒法訪問容器的private/protect成員,沒法以最小代價實現交換。

template <class _Tp>
inline void swap(_Tp& __a, _Tp& __b) {
  __STL_REQUIRES(_Tp, _Assignable);
  _Tp __tmp = __a;
  __a = __b;
  __b = __tmp;
}

vector爲例,其類內swap以下:

void swap(vector<_Tp, _Alloc>& __x) {
    __STD::swap(_M_start, __x._M_start);
    __STD::swap(_M_finish, __x._M_finish);
    __STD::swap(_M_end_of_storage, __x._M_end_of_storage);
  }

若是想交換兩個vector的值,使用algorithm中的swap函數意味着生成一個臨時vector,以及由此帶來的內存分配釋放、容器內元素構造/析構的開銷。而使用vector類內swap方法只須要交換三個指針便可,開銷相比類外swap可忽略不計。

6 rotate

rotate操做:對於容器區間[first, last), 給定旋轉點mid, rotate以後,區間將會變成[mid, last)+[first, mid)。

相信刷過leetcode這道題的同窗對roate必定不會陌生:https://leetcode.com/problems/rotate-array/

STL中針對正向迭代器(forward iterator),雙向迭代器(bidirectional iterator)和隨機迭代器(random access iterator)的特性重載了rotate算法。雖然實現不一樣,可是對用戶暴露了一樣的接口,用戶不用care迭代器類型,由於STL中會經過萃取技術作類型推導。所以在性能和通用性獲得了很好的均衡。

6.1 正向迭代器版本

假設容器區間左右兩個部分爲A B, 且len(A) < len(B)。又假設B可拆分紅B1 B2 B3 B4, 且len(B1) = len(B2) = len(B3) = len(A), len(B4) < len(A)
所以A B = A B1 B2 B3 B4, rotate以後的結果: B A = B1 B2 B3 B4 A

算法描述:
第一步:
從作到右不斷交換相鄰的相同長度區間的值
A B1 B2 B3 B4 -->
B1 A B2 B3 B4 -->
B1 B2 A B3 B4 -->
B1 B2 B3 A B4

第二步:
注意,第一步的最終結果只須要對A B4 進行roate操做, 便可變成B1 B2 B3 B4 A,也就是rotate最終要達到的效果。

  1. 由於len(A) > len(B4), 設A = A1 A2, len(A1) = len(B4), 所以A B4 = A1 A2 B4, B4 A = B4 A1 A2。交換A1和B4, 獲得B4 A2 A1, 此時問題轉化爲交換A1和A2
  2. 不斷重複1.中的過程,直到參與交換的兩個區間分界線處於右邊界位置

算法時間複雜度:n次swap操做,也就是3n次賦值操做

template <class _ForwardIter, class _Distance>
_ForwardIter __rotate(_ForwardIter __first,
                      _ForwardIter __middle,
                      _ForwardIter __last,
                      _Distance*,
                      forward_iterator_tag) {
  if (__first == __middle)
    return __last;
  if (__last  == __middle)
    return __first;

  _ForwardIter __first2 = __middle;
  do {
    swap(*__first++, *__first2++);
    if (__first == __middle)
      __middle = __first2;
  } while (__first2 != __last);

  _ForwardIter __new_middle = __first;

  __first2 = __middle;

  while (__first2 != __last) {
    swap (*__first++, *__first2++);
    if (__first == __middle)
      __middle = __first2;
    else if (__first2 == __last)
      __first2 = __middle;
  }

  return __new_middle;
}

6.2 雙向迭代器版本

原理:
由於雙向迭代器既可前進,有可後退,所以rotate的實現比正向迭代器簡單的多。設A B, roate以後爲B A

第一輪:
翻轉A => reverse(A) B

第二輪
翻轉B => reverse(A) reverse(B)

第三輪
翻轉整個區間 => reverse(reverse(A) reverse(B)) => B A

時間複雜度:交換n次,也就是賦值3n次

template <class _BidirectionalIter, class _Distance>
_BidirectionalIter __rotate(_BidirectionalIter __first,
                            _BidirectionalIter __middle,
                            _BidirectionalIter __last,
                            _Distance*,
                            bidirectional_iterator_tag) {
  __STL_REQUIRES(_BidirectionalIter, _Mutable_BidirectionalIterator);
  if (__first == __middle)
    return __last;
  if (__last  == __middle)
    return __first;

  __reverse(__first,  __middle, bidirectional_iterator_tag());
  __reverse(__middle, __last,   bidirectional_iterator_tag());

  while (__first != __middle && __middle != __last)
    swap (*__first++, *--__last);

  if (__first == __middle) {
    __reverse(__middle, __last,   bidirectional_iterator_tag());
    return __last;
  }
  else {
    __reverse(__first,  __middle, bidirectional_iterator_tag());
    return __first;
  }
}

6.3 隨機迭代器版本

這個實現其實涉及到一個羣論中定理:
設區間由A B 組成,len(A) = m, len(B) = n, gcd(m, m+n)= s(gcd表示兩個整數的最大公約數)

那麼分別以0, 1, 2, ..., s-1位置爲初始點,經過x(n) = [x(n-1) + m] % (m+n)的規則迭代(終止條件:再次回到初始點),正好能夠惟一遍歷區間上的全部位置

證實:

  1. 首先證實以不一樣初始點迭代時,遍歷的點中沒有交集,使用反證法:
    假設0<= a < s, 0<= b < s, a < b, 以a, b爲初始點迭代時,遍歷的點中有交集。
    => 存在k1, k2, 使得 (a + k1 * m) % (m+n) = (b + k2 * m) % (m+n)
    => (b - a) % (m+n) = [(k2 - k1) * m] % (m+n)
    => b-a = (m * k3) % (m+n)
    由於s = gcd(m, m+n) => b-a = k4 * s, k4 * s < m + n
    由於0 < b-a < s => k4 = 0 => b = a
    獲得矛盾,所以得證。

  2. 再證實以a, 0 <= a <s爲起始點迭代,可以剛好遍歷(m+n)/s個點
    => 若是不考慮終止條件,迭代必然陷入死循環,死循環的週期(單位:迭代次數)爲(m+n)/s
    => 對於任意a, 存在0 <= k1 < k2, 使得(a + k1 * m) % (m+n) = (a + k2 * m) % (m+n)
    => (k2 - k1)m = k3 * (m+n)
    => k2 - k1 = k3 * (m+n) / m
    => k2 - k1 = k3 * x / y, 其中x與y互質
    => 爲使k2-k1最小,k3 = y = m/s
    => k2 - k1 = m/s * (m+n)/m = (m+n)/s
    => 迭代週期爲(m+n)/s
    => 得證
    綜合第一和第二結論,上述定理得證。

在證實了上述結論以後,再回到rotate話題:

  • 首先計算s = gcd(m, m+n)
  • 以[0, s)區間內的元素爲起始點,以x(n) = [x(n-1) + m] % (m+n)的規則計算下一個點,直到回到起始點,這些點組成一個環,對這個環內全部位置的元素逆時針移動
  • 最終獲得rotate以後的容器區間。由於第2步中全部元素都向逆時針方向移動了m個位置(把區間想象成順時針方向索引遞增的環形),這正好符合rotate的定義。

時間複雜度:賦值n次,說明在容器可隨機訪問的狀況下,時間複雜度是其餘狀況的1/3

template <class _RandomAccessIter, class _Distance, class _Tp>
_RandomAccessIter __rotate(_RandomAccessIter __first,
                           _RandomAccessIter __middle,
                           _RandomAccessIter __last,
                           _Distance *, _Tp *) {
  __STL_REQUIRES(_RandomAccessIter, _Mutable_RandomAccessIterator);
  _Distance __n = __last   - __first;
  _Distance __k = __middle - __first;
  _Distance __l = __n - __k;
  _RandomAccessIter __result = __first + (__last - __middle);

  if (__k == 0)
    return __last;

  else if (__k == __l) {
    swap_ranges(__first, __middle, __middle);
    return __result;
  }

  _Distance __d = __gcd(__n, __k);

  for (_Distance __i = 0; __i < __d; __i++) {
    _Tp __tmp = *__first;
    _RandomAccessIter __p = __first;

    if (__k < __l) {
      for (_Distance __j = 0; __j < __l/__d; __j++) {
        if (__p > __first + __l) {
          *__p = *(__p - __l);
          __p -= __l;
        }

        *__p = *(__p + __k);
        __p += __k;
      }
    }

    else {
      for (_Distance __j = 0; __j < __k/__d - 1; __j ++) {
        if (__p < __last - __k) {
          *__p = *(__p + __k);
          __p += __k;
        }

        *__p = * (__p - __l);
        __p -= __l;
      }
    }

    *__p = __tmp;
    ++__first;
  }

  return __result;
}

7 for_each

對於區間內每一個元素執行__f。注意,這裏__f既能夠是函數對象,也能夠是函數指針或std::function類型,__first__last指定了執行__f的容器區間。

// for_each.  Apply a function to every element of a range.
template <class _InputIter, class _Function>
_Function for_each(_InputIter __first, _InputIter __last, _Function __f) {
  __STL_REQUIRES(_InputIter, _InputIterator);
  for ( ; __first != __last; ++__first)
    __f(*__first);
  return __f;
}

推薦閱讀

更多精彩內容,請掃碼關注微信公衆號:後端技術小屋。若是以爲文章對你有幫助的話,請多多分享、轉發、在看。
二維碼

相關文章
相關標籤/搜索