【问题标题】:Efficient algorithm to step through all of linked list (in C++)遍历所有链表的高效算法(在 C++ 中)
【发布时间】:2013-10-14 15:46:22
【问题描述】:

假设我有一个双向链表,每个元素都有一个字节。维基百科具有良好的视觉效果;假装这些数字是十六进制数字:

现在,给定一个指向字符串中最后一个节点(在本例中为37 节点)的指针,从列表构建字符串的简单(“显而易见”)方法是:

using std::string;

string node::makeString()
{
  return this->prev->makeString() + this->data;
}

目标是字符串"\0x12\0x99\0x37"。然而,这个函数需要对正在构建的字符串进行大量的重新分配和大量的函数调用开销(它不能进行尾调用优化);毫无疑问,还有其他我不知道的低效率。

有没有更好的方法?当然,我不只是希望最小化理论时间复杂度;我真的在努力寻找一种实践中最快的方法

【问题讨论】:

  • 那个解决方案实际上很糟糕......每个字符调用一个函数会添加到构建字符串的实际成本中。
  • 是的,这就是我说它幼稚的原因;)
  • 不只是天真...天真的会在循环中迭代添加每个字符。这看起来是一种糟糕的功能方法(糟糕的是它不允许尾递归优化)
  • 尾调用优化会非常干净......有没有办法做到这一点?
  • @SigTerm: 除了不是tail-recursion...接近,但不一样

标签: c++ c++11 linked-list


【解决方案1】:

从空的std::string 开始,回到列表的前面,然后遍历节点和push_back 到字符串上。这需要线性时间,这对于这个问题来说是最佳的。

如果您事先知道列表有多长,则可以进行进一步优化。在这种情况下,您可以从一个适当长度的字符串开始,然后直接在其中插入字符。

【讨论】:

  • std::string 具有 push_back,具有相同的复杂性要求,并具有“说出你的意思”的额外好处。
  • 为什么std::vector 优于std::string? (编辑:啊,我被@BoBTFish 狙击了;似乎不是。)
  • @BoBTFish:好点。我很少使用std::string,因为我主要使用 C++ 进行数值工作。
【解决方案2】:

有没有更好的办法?

当然。

  1. 找到列表的开头。 在定位列表的开头时,计算节点总数(如果它尚不可用)并计算总字符串大小以获得最终字符串。
  2. Preallocate 使用std::string::reserve() 所需大小的字符串
  3. 从第一个节点到最后一个节点遍历列表,将数据添加到先前预分配字符串的末尾。为此,您可以使用 std::string::append()

【讨论】:

  • 这比@JerryCoffin 的回答快吗?他说要把弦倒过来,然后倒过来。当然,它们具有相同的大 O 复杂性,但在实践中,哪一个会更快?
  • @thirtythreeforty:我认为这可能取决于相关列表/字符串的长度。如果它足够小,可以全部放入缓存中,那么第二次通过列表将非常便宜,所以它很可能会得到回报。如果它足够长,以至于您从内存中读取了大部分列表两次,则很可能会输给(少数)可能的重新分配。
  • @thirtythreeforty:JerryCoffin 没有提到“保留”。如果没有保留,您将在附加新字符串时触发多次重新分配。根据您的数据,向后构建字符串(尽管这是合理的想法)可能并不容易。如果源数据不是字符串,需要转成字符串,那么c++格式化函数不会产生“反转”的字符串(也就是说,会有额外的操作)。
【解决方案3】:

考虑到手头的限制(你基本上被困在反向遍历列表),最好也反向构建字符串,然后在添加所有字符后,反转字符串。

你现在做事的方式是二次复杂度——每次你插入另一个字符时,把那个字符放入一个字符串中,将所有现有的字符复制到新的字符串中,所以每个插入是线性的,N 次插入大约是 O(N2)。[注意:实际上,我误读了代码——它很糟糕,但没那么糟糕] 事实上现在,我们可以期望每个字符至少被复制两次——一次到堆栈,一次到目标字符串。如果您从内存带宽的角度考虑,效率低下可能最为明显。至少,每次调用都必须读取一个指针,将当前字符写入堆栈并写入一个返回地址,所有这些都是为了将​​一个字节从链表复制到目标字符串。假设一个 64 位实现,我们在读取和写入指针(以及其他开销)与复制我们真正关心的数据方面的比率约为 18:1。

通过反向构建字符串,然后将其反转,您可以在字符串的末尾添加新字符,您可以预期它会很快发生。然后你只做一次所有额外的移动,而不是为你添加的每个角色做一次。

如果您使用std::vector<char>,您可以明确指出在字符串末尾添加一个字符是摊销常数复杂度。使用std::string,我们(我记得)没有得到复杂性保证,但它需要一个非常糟糕的实现才能让它像复制一个字符的递归调用一样糟糕。

另一种可能性是使用std::deque<char> 而不是string。使用双端队列,您可以在前面插入字符,而无需移动所有其他字符以腾出空间。这确实有两个缺点:结果(通常)不是连续的内存块,并且您通常会获得额外的间接级别,因此在构建后访问数据会稍微慢一些。

【讨论】:

  • 最终,如果可以的话,我将对其进行模板化;所以我可能无法选择使用哪个容器。哪种方法与最多数量的容器兼容? (如果它对速度有惊人的帮助,我可以专攻。)
  • @thirtythreeforty:第一个(反向构建,然后反向构建)是容器独立性的明确选择。后者完全依赖于前端快速插入的容器,因此无法与stringvector 一起工作,这可能是两个最有可能/最常见的目标。
  • 我还想指出,这同样适用于从叶子开始的树。 (往上走,然后往下走适用于列表,但不适用于树,除非你使用递归。)我认为我最喜欢这种方法。
【解决方案4】:

您的解决方案效率低下是由于递归。对于链表,设置一个字符串并使用一个简单的 while 循环。这将带来更好的性能,因为每个字符串不会有一个函数调用的开销。

string makeString() {
  Node* p = l.end(); //l is the linked list. end is its tail node
  string s = "";
  while(p != NULL) {
    s = p.value() + s; //append the value to the string
    p = p.prev(); //advance p to the prev node
  }
  return s;
}

当然,为了获得更好的性能,我会考虑不使用链接数据结构,因为它们会导致处理内存中的局部性的效率低下。

【讨论】:

  • 没有包含指向头节点的指针的“容器”。
  • 抱歉,已修复。这些是近似的调用。您必须修改您的代码,因为我们无法访问您的整个代码。
  • 另外,您需要将 makeString 移出节点类以更有效地实现这一点。
  • 为什么把它移到课外很重要?
  • 您可能不需要,但考虑到上下文,它可能更有意义。两种方法都可以。
【解决方案5】:

就我个人而言,我会创建一个字符串链接列表,或者更确切地说,char 数组,然后将每个节点向后填充。

struct StringNode
{
  char buffer [20];
  struct StringNode *next;
};

StringNode *node = new StringNode;
node->buffer [19] = '\0';
node->next = 0;
size_t output = 18;
size_t count = 1;

for (ptr = last item ; ptr ; ptr = ptr->prev)
{
  node->buffer [output] = ptr->character;
  ++count;
  if (output)
  {
    --output;
  }
  else
  {
    StringNode *newnode = new StringNode;
    newnode->buffer [19] = '\0';
    newnode->next = node;
    output = 18;
    node = newnode;
  }
}

string output (count); // preallocate enough storage for whole string and initialise to an empty string

while(node)
{
  output += &node->buffer [output+1];
  // or: cout << &node->buffer [output+1];
  StringNode *nextnode = node->next;
  delete node;
  node = nextnode;
  output = -1;
}

【讨论】:

    【解决方案6】:

    瓶颈正在重新分配字符串。所以我会首先计算节点的数量,然后我会构建字符串。例如

    std::string::size_type n = 1;
    for ( ; node->prev; node = node->prev ) ++n;
    std::string s;
    s.reserve( n );
    for ( ; node->next; node = node->next ) s.push_back( node->data );
    

    【讨论】:

    • 这就是为什么 C++ std::list 类有一个 O(1) .size() 成员。
    • 然而,作业中的这个列表只有三个成员:prev、next 和 data。它甚至没有头也没有尾。:)
    • @MSalters 嗯,添加 height 成员可能会为我解决问题。
    猜你喜欢
    • 1970-01-01
    • 2018-11-23
    • 2015-09-24
    • 1970-01-01
    • 2021-09-04
    • 1970-01-01
    • 1970-01-01
    • 2012-08-21
    • 2014-11-29
    相关资源
    最近更新 更多