記錄一個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。避免快排時比較兩個數大小越界的問題。