一直以来学习排序算法, 都没有在链表排序上下太多功夫,因为用得不多。最近看STL源码,才发现,原来即使是链表,也能有时间复杂度为O(nlogn)的算法,
大大出乎我的意料之外,一般就能想到个插入排序。
下面的代码就是按照源码写出的(去掉了模板增加可读性),注意forward_list是C++11新加的单向链表,这里选这个是因为它更接近我们自己实现链表时的做法。
void sort_list(forward_list<int>& l){ auto it = l.begin(); if (l.empty() || ++it == l.end()) return; forward_list<int> carry; forward_list<int> counter[64];//总共只设立了64个桶,因为第i个桶被填充的时候,里面至多2^i个元素,2^64,远超一般元素数目 int fill = 0; //fill记录的是当前填到哪个桶了 while (!l.empty()){ carry.splice_after(carry.cbefore_begin(), l, l.cbefore_begin());//每次从原链表上取下一个元素 int i = 0; while (i < fill && !counter[i].empty()){ counter[i].merge(carry); carry.swap(counter[i++]); } carry.swap(counter[i]); if (i == fill) ++fill; } for (int i = 1; i < fill; ++i) counter[i].merge(counter[i - 1]); l.swap(counter[fill - 1]); }
关于此算法的分析:网上很多争论这个到底是快速排序还是归并排序的(侯捷的书上说是快排)。
为了理解这个代码,可以手动运行一下。
fill,记录用到了几个桶,最外层循环保证这样一个事实:从第0个桶到第fill - 1个桶里面,要么为空,要么存储有2^fill个元素,而且是有序链表。
每次与新取下的元素(存储在carry中)合并的桶都是counter[0]。如果第0个桶里没有元素,那就直接放进去carry.swap(counter[i])。下一轮循环的
时候,因为counter[0]里有元素了,就会进入内层循环,这时候,counter[i].merge(carry)使得第0个桶变为一个含两个元素的链表,然后内层循环第二步,又将这个链表
转移到carry中,下一轮内层循环的时候,带有两个元素的carry就会争取与第1个桶合并(如果第1个桶不空的话),如果第一个桶为空,它将退出内层循环并将元素放进第一个桶,(此时发现i == fill,于是fill变为2)然后下一轮外层循环继续从链表中取下一个元素,(此时counter[0]为空,counter[1]中含有两个元素),如前面一样,先是发现counter[0]是空的,进不去内层循环,于是直接把元素填到counter[0]中,下一次外层循环又取一个元素,进入内层循环,与counter[0]合并,交换,然后下一轮内层循环中,带有两个元素的carry就会与带有两个元素的counter[1]合并,交换, i == fiil == 2退出内层循环,四个元素变到第2个桶中,fill++。
走完这一遍之后,感觉清晰了,但是除了外层循环的循环不变条件以外,实在是很难从基本的快排或归并排序的思想中想到这样去做。