泛型
2019.08
Xun
C++標准算法庫中的各種函數都有很強的適用性。比如其中的std::sort
函數,它即可以對std::vector
中的元素進行排序,也能對std::deque
中的元素進行排序,對於數組中的元素,它也可以正常運行。同時,std::sort
函數還可以接受一個函數指針(謂詞),用來指定排序規則。在這篇文章中,我們將模擬標准庫中的std::sort
函數,寫一個與其接口相同的排序函數。
這里排序的方法為歸並排序,其主要思想是將兩個已排序的短序列合並為一個更長的已排序序列。一般來說,子序列也是無序的,這就需要在函數中對兩個短序列進行歸並排序。若序列只有一個元素,則它肯定是有序的,遞歸結束。其動圖演示如下:(動圖來自互聯網)
下面參考一下std::sort
函數的接口。std::sort
函數無返回值(或返回void
),接收兩個迭代器指示序列的開始位置和尾后位置,同時還有一個可選參數,用來指定排序規則,其默認使用<
來進行比較。
於是我們排序函數的接口可能長成這樣:
template<typename RandIter>
void merge_sort
(
RandIter iterBeg, RandIter iterEnd, //開始與尾后迭代器
TYPE comp = ...//排序規則
)
這里的第一行代碼說明下面的RandIter
是一個類名,在函數調用時會將其替換為相應的類型,如int*
,std::vector<int>::iterator
等。comp
是一個二元謂詞,一旦RandIter
確定,comp
的類型也就隨之確定了。比如RandIter
是std::deque<int>::iterator
,其核心數據成員是int
類型,謂詞就是為bool (*comp) (int a, int b)
,即接收兩個int
型變量,返回bool
型的一個函數指針。然而對於各種不同的迭代器類型,該如何確定其中的TYPE
?
對於標准庫中的各種順序容器的迭代器類,都封裝了一個value_type
用來說明其指向序列中元素的類型,比如存儲着double元素的std::vector
的迭代器的value_type
,即std::vector<double>::iterator::value_type
其實就是double
類型。所以,comp
的類型TYPE
可以寫作typename RandIter::value_type
。
但是,對於普通的數組,傳入的迭代器是兩個指針,而指針類型是沒有value_type
的!因此這樣寫的話是不能處理數組的。
在頭文件type_traits
中,標准庫提供了很多的類型轉換函數。對於類型int*
,其解引用類型為int
。對於更一般的類型T
,若U*
與T
是相同的類型,則可以使用type_traits
中的std::remove_pointer<T>::type
來得到類型U
。
同樣的romove_pointer
只能處理指針,而對於順序容器就束手無策了。
C++中還提供了auto
、decltype
關鍵字來進行類型推斷。在函數接口中,是不能使用auto
的。於是,我們的希望就落在了decltype
上。
我們需要的是*iterBeg
的類型,但是decltype
一個解引用會得到引用類型,所以我們仍需借助標准庫頭文件type_traits
中的一個remove_reference
函數來去除引用性質。於是,函數的接口如下:
template<typename RandIter>
void merge_sort
(
RandIter iterBeg, RandIter iterEnd, //開始與尾后迭代器
bool (*comp)(typename std::remove_reference<decltype(*iterBeg)>::type, typename std::remove_reference<decltype(*iterBeg)>::type) = ...//排序規則
)
類型名解決了,下面就是默認參數了。這里,我們將一個lambda表達式傳入comp
。
bool (*comp)(...) = [](typename std::remove_reference<decltype(*iterBeg)>::type a, typename std::remove_reference<decltype(*iterBeg)>::type b){return a < b;}
將整個接口寫出來,是這樣的:
template<typename RandIter>
void merge_sort
(
RandIter iterBeg, RandIter iterEnd, //開始與尾后迭代器
bool (*comp)(typename std::remove_reference<decltype(*iterBeg)>::type,
typename std::remove_reference<decltype(*iterBeg)>::type) = []
(typename std::remove_reference<decltype(*iterBeg)>::type a,
typename std::remove_reference<decltype(*iterBeg)>::type b)
{return a < b; }
)
我之前寫這個函數的時候,參考了Visual Studio所采用的實現版本,發現它使用的是重載而不是默認參數來實現,其接口看起來要清爽不少:
template<typename RandIter>
void merge_sort//這里使用 '<' 來排序
(
RandIter iterBeg, RandIter iterEnd //開始與尾后迭代器
);
template<typename RandIter, typename Pred>
void merge_sort//這里使用 comp 來排序
(
RandIter iterBeg, RandIter iterEnd, //開始與尾后迭代器
Pred comp//排序規則
);
下面是函數的實現部分,與主題的關系不大,可以略過。
template<typename RandIter>
void merge_sort
(
RandIter iterBeg, RandIter iterEnd, //開始與尾后迭代器
bool (*comp)(typename std::remove_reference<decltype(*iterBeg)>::type,
typename std::remove_reference<decltype(*iterBeg)>::type) = []
(typename std::remove_reference<decltype(*iterBeg)>::type a,
typename std::remove_reference<decltype(*iterBeg)>::type b) ->bool
{return a < b; }
)
{
//遞歸終止條件為子序列只有一個元素,一個元素一定是有序的
//第二個判斷條件是為了防止輸入空序列
if (iterBeg + 1 == iterEnd || iterBeg == iterEnd)
return;
RandIter iterMid = iterBeg + (iterEnd - iterBeg) / 2;//指向序列中間位置
merge_sort(iterBeg, iterMid, comp);//遞歸
merge_sort(iterMid, iterEnd, comp);//只有子序列有序才進行合並
std::vector<std::remove_reference<decltype(*iterBeg)>::type>temp;//用來存儲合並的有序序列
temp.reserve(iterEnd - iterBeg);//保留空間用於存諸元素,防止空間擴張帶來的額外花銷
auto it1 = iterBeg, it2 = iterMid;//分別指向兩個子序列的開始位置
while (it1 != iterMid || it2 != iterEnd)//還有元素沒處理
{
if (it1 == iterMid)//序列1的元素已放完
temp.emplace_back(*it2++);//只能放序列2的元素,后加的優先級高於解引用 (解引用與前加同級)
else if (it2 == iterEnd)
temp.emplace_back(*it1++);
else
temp.emplace_back(comp(*it1, *it2) ? *it1++ : *it2++);//按謂詞規定的順序放入
}
//將排好序的序列復制到原序列中的位置
typename std::vector<std::remove_reference<decltype(*iterBeg)>::type>::iterator itTemp = temp.begin();//第一個typename說明iterator是一個類型名
for (RandIter it2 = iterBeg; it2 != iterEnd; ++itTemp, ++it2)
* it2 = *itTemp;
}
歸並排序不能原位操作,這里使用一個vector
來存儲臨時數據。
2019.10新增
- 從一個指針類型得到其基本類型的算法大致如下(
type_traits
)
template <class _Ty>
struct remove_pointer //如果_Ty不是指針類型,則直接使用原型
{
using type = _Ty;
};
template <class _Ty>
struct remove_pointer<_Ty*> //如果_Ty是指針類型,則使用指針
{
using type = _Ty;
};
- 使用默認參數的版本沒能實現對仿函數的接收
string s1("12345");
struct cmp
{
bool operator()(char a, char b) { return a > b; }
};
merge_sort(s1.begin(), s1.end(), cmp());//不成功
std::sort(s1.begin(), s1.end(), cmp());//成功
完