更新 - Visual Studio 2022 切换到了一种高效的自底向上归并排序的递归实现,并重新使用指针而非迭代器。合并逻辑在可能的情况下改进为一次性拼接多个节点。这些变化保留了为VS2015更改提供的无内存分配和异常安全修复的原因。
template <class _Pr2>
static _Nodeptr _Sort(_Nodeptr& _First, const size_type _Size, _Pr2 _Pred) {
switch (_Size) {
case 0:
return _First;
case 1:
return _First->_Next;
default:
break;
}
auto _Mid = _Sort(_First, _Size / 2, _Pred);
const auto _Last = _Sort(_Mid, _Size - _Size / 2, _Pred);
_First = _Merge_same(_First, _Mid, _Last, _Pred);
return _Last;
}
void sort() {
sort(less<>{});
}
template <class _Pr2>
void sort(_Pr2 _Pred) {
auto& _My_data = _Mypair._Myval2;
_Scary_val::_Sort(_My_data._Myhead->_Next, _My_data._Mysize, _Pass_fn(_Pred));
}
这可以稍微改进一下(大约快4%),通过在sort()中检查(_My_data._Mysize < 2),然后只需要在_Sort()中检查(_Size == 1)。
template <class _Pr2>
static _Nodeptr _Sort(_Nodeptr& _First, const size_type _Size, _Pr2 _Pred) {
if (_Size == 1)
return _First->_Next;
auto _Mid = _Sort(_First, _Size / 2, _Pred);
const auto _Last = _Sort(_Mid, _Size - _Size / 2, _Pred);
_First = _Merge_same(_First, _Mid, _Last, _Pred);
return _Last;
}
void sort() {
sort(less<>{});
}
template <class _Pr2>
void sort(_Pr2 _Pred) {
auto& _My_data = _Mypair._Myval2;
if (_My_data._Mysize < 2)
return;
_Scary_val::_Sort(_My_data._Myhead->_Next, _My_data._Mysize, _Pass_fn(_Pred));
}
这个答案的剩余部分是历史性的,主要是关于我使用迭代器实现的自底向上归并排序,以替代Visual Studio 2015至2019中的自顶向下归并排序。
起初我以为微软在切换到使用迭代器时不会采用效率较低的自上而下的归并排序,除非有必要,所以我在寻找替代方案。直到我试图分析这些问题(出于好奇),我才意识到原始的自下而上的归并排序可以修改为与迭代器一起使用。
在@sbi的评论中,他问自上而下方法的作者Stephan T. Lavavej为什么要改用迭代器。Stephan的回答是“为了避免内存分配和默认构造分配器”。VS2015引入了非默认可构造和有状态的分配器,这在使用先前版本的列表数组时会出现问题,因为每个列表实例都会分配一个虚拟节点,需要进行更改以处理没有默认分配器的情况。
Lavavej的解决方案是使用迭代器来跟踪原始列表中的运行边界,而不是使用内部列表数组。合并逻辑被改为使用3个迭代器参数,第一个参数是左运行的起始迭代器,第二个参数是左运行的结束迭代器(等于右运行的起始迭代器),第三个参数是右运行的结束迭代器。合并过程使用std::list::splice在合并操作期间在原始列表中移动节点。这样做的额外好处是可以确保异常安全。如果调用者的比较函数抛出异常,列表将被重新排序,但不会丢失数据(假设splice不会失败)。在之前的方案中,如果发生异常,一些(或大部分)数据将位于内部列表数组中,并且原始列表中的数据将丢失。
我将自底向上的归并排序改为使用迭代器数组而不是链表数组,其中array[i]是一个指向具有2^i个节点的已排序运行的起始位置的迭代器,或者它是空的(使用std::list::end表示空,因为迭代器不能为null)。与自顶向下的方法类似,迭代器数组仅用于在原始链表中跟踪已排序运行的边界,并使用与自顶向下相同的合并逻辑,使用std::list::splice在原始链表中移动节点。
对列表进行单次扫描,根据数组中的已排序运行边界,在当前scan.next位置的左侧构建已排序运行,直到所有节点都合并到已排序运行中。然后合并已排序运行,得到一个排序后的列表。
例如,对于一个包含7个节点的列表,在扫描之后:
2 1 0 array index
run0->run0->run0->run0->run1->run1->run2->end
然后通过merge(left,right)从右到左合并3个已排序的运行,以保证排序的稳定性。
如果链表很大且节点分散,会有很多缓存未命中,从上到下的排序速度会比从下到上慢40%到50%,具体取决于处理器。然而,如果有足够的内存,通常将链表转移到数组或向量中,对数组或向量进行排序,然后从排序后的数组或向量创建新的链表会更快。
示例C++代码:
template <typename T>
typename std::list<T>::iterator Merge(std::list<T> &ll,
typename std::list<T>::iterator li,
typename std::list<T>::iterator ri,
typename std::list<T>::iterator ei);
#define ASZ 32
template <typename T>
void SortList(std::list<T> &ll)
{
if (ll.size() < 2)
return;
typename std::list<T>::iterator ai[ASZ];
typename std::list<T>::iterator ri;
typename std::list<T>::iterator ei;
size_t i;
for (i = 0; i < ASZ; i++)
ai[i] = ll.end();
for (ei = ll.begin(); ei != ll.end();) {
ri = ei++;
for (i = 0; (i < ASZ) && ai[i] != ll.end(); i++) {
ri = Merge(ll, ai[i], ri, ei);
ai[i] = ll.end();
}
if(i == ASZ)
i--;
ai[i] = ri;
}
for(i = 0; (i < ASZ) && ai[i] == ei; i++);
ri = ai[i++];
while(1){
for( ; (i < ASZ) && ai[i] == ei; i++);
if (i == ASZ)
break;
ri = Merge(ll, ai[i++], ri, ei);
}
}
template <typename T>
typename std::list<T>::iterator Merge(std::list<T> &ll,
typename std::list<T>::iterator li,
typename std::list<T>::iterator ri,
typename std::list<T>::iterator ei)
{
typename std::list<T>::iterator ni;
(*ri < *li) ? ni = ri : ni = li;
while(1){
if(*ri < *li){
ll.splice(li, ll, ri++);
if(ri == ei)
return ni;
} else {
if(++li == ri)
return ni;
}
}
}
这是一个用于VS2019的std::list::sort()的示例替换代码,包含在list
的头文件中。合并逻辑已经被拆分为一个单独的内部函数,因为它现在在两个地方使用。在std::list::sort()中调用_Sort的代码是_Sort(begin(), end(), _Pred, this->_Mysize());,其中_Pred是指向比较函数的指针(默认为std::less())。
private:
template <class _Pr2>
iterator _Merge(_Pr2 _Pred, iterator _First, iterator _Mid, iterator _Last){
iterator _Newfirst = _First;
for (bool _Initial_loop = true;;
_Initial_loop = false) {
if (_DEBUG_LT_PRED(_Pred, *_Mid, *_First)) {
if (_Initial_loop) {
_Newfirst = _Mid;
}
splice(_First, *this, _Mid++);
if (_Mid == _Last) {
return _Newfirst;
}
}
else {
++_First;
if (_First == _Mid) {
return _Newfirst;
}
}
}
}
template <class _Pr2>
void _Sort(iterator _First, iterator _Last, _Pr2 _Pred,
size_type _Size) {
if (_Size < 2) {
return;
}
const size_t _ASZ = 32;
iterator _Ai[_ASZ];
iterator _Mi;
iterator _Li;
size_t _I;
for (_I = 0; _I < _ASZ; _I++)
_Ai[_I] = _Last;
for (_Li = _First; _Li != _Last;) {
_Mi = _Li++;
for (_I = 0; (_I < _ASZ) && _Ai[_I] != _Last; _I++) {
_Mi = _Merge(_Pass_fn(_Pred), _Ai[_I], _Mi, _Li);
_Ai[_I] = _Last;
}
if (_I == _ASZ)
_I--;
_Ai[_I] = _Mi;
}
for (_I = 0; _I < _ASZ && _Ai[_I] == _Last; _I++);
_Mi = _Ai[_I++];
while (1) {
for (; _I < _ASZ && _Ai[_I] == _Last; _I++);
if (_I == _ASZ)
break;
_Mi = _Merge(_Pass_fn(_Pred), _Ai[_I++], _Mi, _Last);
}
}
我注意到这个变化是在2016年7月,然后在2016年8月1日给P.J. Plauger发了一封邮件询问这个变化。他的回复摘录如下:
Interestingly enough, our change log doesn't reflect this change. That
probably means it was "suggested" by one of our larger customers and
got by me on the code review. All I know now is that the change came
in around the autumn of 2015. When I reviewed the code, the first
thing that struck me was the line:
iterator _Mid = _STD next(_First, _Size / 2);
which, of course, can take a very long time for a large list.
The code looks a bit more elegant than what I wrote in early 1995(!),
but definitely has worse time complexity. That version was modeled
after the approach by Stepanov, Lee, and Musser in the original STL.
They are seldom found to be wrong in their choice of algorithms.
I'm now reverting to our latest known good version of the original code.
size()
是 O(1)。 - T.C.size()
在这里并没有太大的区别。它只有在递归的最顶层才有用。仅有O(1)的size()
还不足以证明这个算法的优越性。我对此的主要问题是每个递归层级都需要O(n)的std::next
,这与顶层的O(1)size()
并没有真正的关系。 - AnT stands with RussiaO(n)
,比快速排序和其他算法实现的O(log(n))
更好。尽管如此,直接基数排序具有如此高的常数项,以至于在所有相关情况下都比快速排序慢,使直接基数排序无用。永远不要忘记常数! - cmaster - reinstate monica