解析調用sort導致死循環問題的原因

記錄一個sort問題

問題復現

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

bool cmp(const int &a, const int &b)
{
    return true;
}

int main()
{
    vector<int> v;
    for (int i = 0; i < 33; i++)
        v.push_back(1);

    sort(v.begin(), v.end(), cmp);
    for (int i = 0; i < v.size(); i++)
        cout << v[i] << endl;

    return 0;
}

這段代碼很簡單,首先向vector裏插入了33個1(只要是重複元素就會出現這個問題),然後用sort函數,利用自定義比較函數對其進行排序,最後將排序的結果輸出。按道理來說,自定義排序函數如果一直返回true的結果是,不對vector內元素進行排序,但是編譯執行後的結果是程序一直處於運行狀態,遲遲沒有結果輸出。經測試,只要數組元素大於16,就會出現這種情況。百思不得其解,後來查看stl源碼得到了答案。

查找問題

從stl-sort源碼開始分析。源碼版本5.1.5。

template <class _RandomAccessIter, class _Compare>
// 帶自定義比較函數的sort函數
void sort(_RandomAccessIter __first, _RandomAccessIter __last, _Compare __comp) {
  _STLP_DEBUG_CHECK(_STLP_PRIV __check_range(__first, __last))
  // 這裏判斷 如果元素數量不爲0則進行排序
  if (__first != __last) {
    // 先執行introsort(自省)排序
    _STLP_PRIV __introsort_loop(__first, __last,
                                _STLP_VALUE_TYPE(__first, _RandomAccessIter),
                                _STLP_PRIV __lg(__last - __first) * 2, __comp);
    // 之後用簡單的插入排序做合併
    _STLP_PRIV __final_insertion_sort(__first, __last, __comp);
  }
}

自省排序:是一種混合排序方式,大部分情況下與median-of-3 Quick Sort排序算法完全相同,但是當分割行爲有惡化爲二次行爲傾向時,能能夠自我偵測,轉而改用Heap Sort,使效率維持在O(NlogN)。注:二次行爲傾向,看過代碼後感覺就是快排的次數超過了設置的閾值,這個閾值由__lg()函數計算出,代碼下面有)。

用來控制分割閾值的情況,找出2^k <= n的最大值k返回

template <class _Size>
inline _Size __lg(_Size __n) {
  _Size __k;
  for (__k = 0; __n != 1; __n >>= 1) ++__k;
  return __k;
}
template <class _RandomAccessIter, class _Tp, class _Size, class _Compare>
// 自省排序主流程
void __introsort_loop(_RandomAccessIter __first,
                      _RandomAccessIter __last, _Tp*,
                      _Size __depth_limit, _Compare __comp) {
  // 如果元素數量少於__stl_threshold,則直接返回
  // __stl_threshold是一個全局常數,const int 16
  while (__last - __first > __stl_threshold) {
    if (__depth_limit == 0) {
      // 至此,分割惡化,改用堆排序
      partial_sort(__first, __last, __last, __comp);
      return;
    }
    --__depth_limit;
    // 利用快排進行排序,並返回中樞節點
    _RandomAccessIter __cut =
      __unguarded_partition(__first, __last,
                            _Tp(__median(*__first,
                                         *(__first + (__last - __first)/2),
                                         *(__last - 1), __comp)),
       __comp);
    // 對右半段進行sort
    __introsort_loop(__cut, __last, (_Tp*) 0, __depth_limit, __comp);
    __last = __cut;
    // 現在回到while循環,對左半段進行sort
  }
}

先簡單說一下快排算法。分割方法不只一種,以下敘述既簡單又有良好成效的做法。令頭端迭代器first向尾部移動, 尾端迭代器last向頭部移動。當*first大於或等千樞軸時就停下來, 當*last小於或等於樞軸時也停下來,然後檢驗兩個迭代器是否交錯。如果first仍然在左而last仍然在右, 就將兩者元素互換, 然後各自調整一個位置(向中央逼近),再繼續進行相同的行爲。如果發現兩個迭代器交錯了(亦即!(first<last)),表示整個序列已經調整完畢,以此時的first爲軸,將序列分爲左右兩半,左半部所有元素值都小於或等於樞軸,右半部所有元素值都大於或等於樞軸。

template <class _RandomAccessIter, class _Tp, class _Compare>
// 快排算法,也是書裏所說的分割算法
_RandomAccessIter __unguarded_partition(_RandomAccessIter __first,
                                        _RandomAccessIter __last,
                                        _Tp __pivot, _Compare __comp) {
  for (;;) {
    // 這裏調用自定義的比較函數,等到first >= pivot 元素就停下來
    // 問題就出在了這,一直返回true會讓fitst指針一直++,而這個函數並沒有邊界檢查,所以就會死在這
    // 返回false的話就沒問題 
    while (__comp(*__first, __pivot)) {
      _STLP_VERBOSE_ASSERT(!__comp(__pivot, *__first), _StlMsg_INVALID_STRICT_WEAK_PREDICATE)
      ++__first;
    }
    --__last;
    // last找到 <= pivot 的元素就停下來
    while (__comp(__pivot, *__last)) {
      _STLP_VERBOSE_ASSERT(!__comp(*__last, __pivot), _StlMsg_INVALID_STRICT_WEAK_PREDICATE)
      --__last;
    }
    // 交錯,結束循環
    if (!(__first < __last))
      return __first;
    // 大小值交換
    iter_swap(__first, __last);
    ++__first;
  }
}

接着走完整個流程。當待排序數據變爲局部有序之後,就可以執行最後一步,調用插入排序來完成整個排序過程。

template <class _RandomAccessIter, class _Compare>
void __final_insertion_sort(_RandomAccessIter __first,
                            _RandomAccessIter __last, _Compare __comp) {
  // 判斷元素個數是否大於__stl_threshold
  if (__last - __first > __stl_threshold) {
    // 將前16調用這個函數排序
    __insertion_sort(__first, __first + __stl_threshold, _STLP_VALUE_TYPE(__first,_RandomAccessIter), __comp);
    // 餘下調用這個函數排序
    __unguarded_insertion_sort(__first + __stl_threshold, __last, __comp);
  }
  else
    __insertion_sort(__first, __last, _STLP_VALUE_TYPE(__first,_RandomAccessIter), __comp);
}

問題原因

從頭說一下流程。當我們調用sort進行排序的時候,首先會判斷元素個數,如果大於16,則先進行快排(分割算法),等數據呈有序小塊的時候再調用插入排序進行合併。

在快排那裏,它首先要找到調用自定義比較函數返回false的那個數據(cmp(first,pivot)),如果返回true就一直向後找,注意這裏是沒有進行邊界檢查了,也就是說如果我們讓cmp函數一直返回true,程序就會一直讓first++,死在這裏。

結論

sort函數自定義排序函數中,當兩個數相等時返回false。避免快排時比較兩個數大小越界的問題。

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