我很好奇,首先,为什么std::list
和std::forward_list
包含排序函数作为成员函数,与其他标准库容器不同。但对我来说更有意思的是,CPPReference和CPlusPlus都声称此排序是在O( n log n )时间内完成的。< / p>
我甚至无法想象如何在没有随机访问元素的情况下对容器进行排序。因此,我使用forward_list
将测试组合在一起,以使其尽可能困难。
#include <chrono>
#include <cstdint>
#include <deque>
#include <forward_list>
#include <iostream>
#include <random>
using std::endl;
using namespace std::chrono;
typedef nanoseconds::rep length_of_time;
constexpr int TEST_SIZE = 25000;
class Stopwatch
{
public:
void start_timing();
void end_timing();
length_of_time get_elapsed_time() const;
private:
time_point<high_resolution_clock> start;
time_point<high_resolution_clock> end;
length_of_time elapsed_time = 0;
};
void Stopwatch::start_timing()
{
start = high_resolution_clock::now();
}
void Stopwatch::end_timing()
{
end = high_resolution_clock::now();
auto elapsed = end - start;
auto elapsed_nanoseconds = duration_cast<nanoseconds>(elapsed);
elapsed_time = elapsed_nanoseconds.count();
}
length_of_time Stopwatch::get_elapsed_time() const
{
return elapsed_time;
}
std::mt19937_64 make_random_generator()
{
using namespace std::chrono;
auto random_generator = std::mt19937_64();
auto current_time = high_resolution_clock::now();
auto nanos = duration_cast<nanoseconds>(
current_time.time_since_epoch()).count();
random_generator.seed(nanos);
return random_generator;
}
int main()
{
Stopwatch timer;
std::deque<length_of_time> times;
auto generator = make_random_generator();
for (int i = 1; i <= TEST_SIZE; i++) {
std::forward_list<uint64_t> container;
for (int j = 1; j <= i; j++) {
container.push_front(generator());
}
timer.start_timing();
container.sort();
timer.end_timing();
times.push_back(timer.get_elapsed_time());
container.clear();
}
for (const auto& time: times) {
std::cout << time << endl;
}
}
该程序输出的数字给出了以下图表:
确实看起来像O( n log n )增长(尽管每三分之一的峰值都很有趣)。图书馆是如何做到这一点的?也许复制到一个支持排序,排序和复制的容器?
答案 0 :(得分:19)
可以使用Mergesort在 O(n log n)中对链接列表进行排序。
有趣的是,由于链接列表已经具有适当的结构,因此使用Mergesort对链接列表进行排序只需要 O(1)额外空间。
这需要专门针对列表结构调整的专门算法,这也是sort
是列表成员函数的原因,而不是单独的函数。
至于它是如何工作的 - 你需要的只是合并操作。合并操作有两个列表。您查看两个列表的头部,并删除最小的头并将其附加到结果列表中。你一直这样做,直到所有的头都被合并到大名单中 - 完成。
这是C ++中的示例合并操作:
struct Node {
Node* next;
int val;
};
Node* merge(Node* a, Node* b) {
Node fake_head(nullptr, 0);
Node* cur = &fake_head;
while (a && b) {
if (a->val < b->val) { cur->next = a; a = a->next; }
else { cur->next = b; b = b->next; }
cur = cur->next;
}
cur->next = a ? a : b;
return fake_head.next;
}
答案 1 :(得分:4)
自下而上合并排序的示例代码,使用指向列表的指针数组,其中array [i]指向大小为2 ^ i的列表(除了最后一个指针指向无限大小的列表)。这就是HP / Microsoft标准模板库实现std :: list :: sort的方式。
#define NUMLISTS 32 /* number of lists */
typedef struct NODE_{
struct NODE_ * next;
int data; /* could be any comparable type */
}NODE;
NODE * MergeLists(NODE *, NODE *);
NODE * SortList(NODE *pList)
{
NODE * aList[NUMLISTS]; /* array of lists */
NODE * pNode;
NODE * pNext;
int i;
if(pList == NULL) /* check for empty list */
return NULL;
for(i = 0; i < NUMLISTS; i++) /* zero array */
aList[i] = NULL;
pNode = pList; /* merge nodes into aList[] */
while(pNode != NULL){
pNext = pNode->next;
pNode->next = NULL;
for(i = 0; (i < NUMLISTS) && (aList[i] != NULL); i++){
pNode = MergeLists(aList[i], pNode);
aList[i] = NULL;
}
if(i == NUMLISTS)
i--;
aList[i] = pNode;
pNode = pNext;
}
pNode = NULL; /* merge array into one list */
for(i = 0; i < NUMLISTS; i++)
pNode = MergeLists(aList[i], pNode);
return pNode;
}
NODE * MergeLists(NODE *pSrc1, NODE *pSrc2)
{
NODE *pDst = NULL; /* destination head ptr */
NODE **ppDst = &pDst; /* ptr to head or prev->next */
while(1){
if(pSrc1 == NULL){
*ppDst = pSrc2;
break;
}
if(pSrc2 == NULL){
*ppDst = pSrc1;
break;
}
if(pSrc2->data < pSrc1->data){ /* if src2 < src1 */
*ppDst = pSrc2;
pSrc2 = *(ppDst = &(pSrc2->next));
continue;
} else { /* src1 <= src2 */
*ppDst = pSrc1;
pSrc1 = *(ppDst = &(pSrc1->next));
continue;
}
}
return pDst;
}
合并排序列表的另一种但较慢的方法类似于4磁带排序(所有顺序访问)。初始列表分为两个列表。每个列表都被认为是一个运行流,其中初始运行大小为1.在此示例中,计数器用于跟踪运行边界,因此它比指针数组方法更复杂和更慢。合并两个输入列表中的运行,在两个输出列表之间交替。在每次合并传递之后,运行大小加倍,合并的方向改变,因此输出列表变为输入列表,反之亦然。当所有运行仅在一个输出列表上结束时,排序完成。如果不需要稳定性,则可以将运行边界定义为任何节点,然后是无序节点,这将利用原始列表的自然排序。
NODE * SortList(NODE * pList)
{
NODE *pSrc0;
NODE *pSrc1;
NODE *pDst0;
NODE *pDst1;
NODE **ppDst0;
NODE **ppDst1;
int cnt;
if(pList == NULL) /* check for null ptr */
return NULL;
if(pList->next == NULL) /* if only one node return it */
return pList;
pDst0 = NULL; /* split list */
pDst1 = NULL;
ppDst0 = &pDst0;
ppDst1 = &pDst1;
while(1){
*ppDst0 = pList;
pList = *(ppDst0 = &pList->next);
if(pList == NULL)
break;
*ppDst1 = pList;
pList = *(ppDst1 = &pList->next);
if(pList == NULL)
break;
}
*ppDst0 = NULL;
*ppDst1 = NULL;
cnt = 1; /* init run size */
while(1){
pSrc0 = pDst0; /* swap merge direction */
pSrc1 = pDst1;
pDst0 = NULL;
pDst1 = NULL;
ppDst0 = &pDst0;
ppDst1 = &pDst1;
while(1){ /* merge a set of runs */
if(MergeRuns(&ppDst0, &pSrc0, &pSrc1, cnt))
break;
if(MergeRuns(&ppDst1, &pSrc0, &pSrc1, cnt))
break;
}
cnt <<= 1; /* bump run size */
if(pDst1 == NULL) /* break if done */
break;
}
return pDst0;
}
int MergeRuns(NODE ***pppDst, NODE **ppSrc0, NODE **ppSrc1, int cnt)
{
NODE **ppDst = *pppDst;
NODE *pSrc0 = *ppSrc0;
NODE *pSrc1 = *ppSrc1;
int cnt0, cnt1;
cnt0 = cnt;
cnt1 = cnt;
if(pSrc0 == NULL){ /* if end data src0 */
*ppDst = NULL;
*pppDst = ppDst;
return(1);
}
if(pSrc1 == NULL){ /* if end data src1 */
do{ /* copy rest of src0 */
*ppDst = pSrc0;
pSrc0 = *(ppDst = &(pSrc0->next));
}while(pSrc0);
*ppDst = NULL;
*pppDst = ppDst;
return(1);
}
while(1){
if(pSrc1->data < pSrc0->data){ /* if src1 < src0 */
*ppDst = pSrc1; /* move src1 */
pSrc1 = *(ppDst = &(pSrc1->next));
if(pSrc1 != NULL && --cnt1) /* if not end run1, continue */
continue;
do{ /* copy run0 */
*ppDst = pSrc0;
pSrc0 = *(ppDst = &(pSrc0->next));
}while(pSrc0 != NULL && --cnt0);
break;
} else { /* else src0 <= src1 */
*ppDst = pSrc0; /* move src0 */
pSrc0 = *(ppDst = &(pSrc0->next));
if(pSrc0 != NULL && --cnt0) /* if not end run0, continue */
continue;
do{ /* copy run1 */
*ppDst = pSrc1;
pSrc1 = *(ppDst = &(pSrc1->next));
}while(pSrc1 != NULL && --cnt1);
break;
}
}
*ppSrc0 = pSrc0; /* update ptrs, return */
*ppSrc1 = pSrc1;
*ppDst = NULL;
*pppDst = ppDst;
return(0);
}
答案 2 :(得分:2)
Mergesort是O(nlogn)。我不知道C ++的默认排序函数是什么,但我怀疑它是mergesort。
答案 3 :(得分:2)
我这里没有标准,但是{{3}}声明排序的复杂性是Nlog(N)比较。这意味着即使快速排序也是标准的符合实现,因为它将是Nlog(N)比较(但不是Nlog(N)时间)。
答案 4 :(得分:0)
您从未知长度的未排序列表开始。假设元素编号为0,1,2,3 ......
在第一遍中,您创建了两个链接列表,每个链接列表按排序顺序包含一对数字。列表0以排序顺序的元素0和1开始。列表1以排序顺序从元素2和3开始。元素4和5按排序顺序添加到列表0,6和7添加到列表1,依此类推。显然必须注意不要超过原始列表的末尾。
在第二遍中,您合并这两个列表以创建两个链接列表,每个链接列表按排序顺序组成4个数字。每次组合List 0中的两个元素和List 1中的两个元素时,下一个最小元素显然是每次在列表前面的元素。
在第二遍中,将这些列表合并为两个链接列表,每个列表包含8个已排序数字的集合,然后是16个,然后是32个,依此类推,直到结果列表包含n个或更多数字。如果n = 2 ^ k则存在k = log2(n)次通过,因此这需要O(n log n)。