跨步所有链表的高效算法(在C ++中)

时间:2013-10-14 15:46:22

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

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

https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Doubly-linked-list.svg/500px-Doubly-linked-list.svg.png

现在,在给定指向字符串中最后一个节点(在本例中为37节点)的指针的情况下,从列表构建字符串的天真(“直接明显”)方法是:

using std::string;

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

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

有更好的方法吗?当然,我不只是想减少理论时间复杂度;我真的想找到一种在实践中最快的方法

6 个答案:

答案 0 :(得分:3)

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

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

答案 1 :(得分:2)

  

有更好的方法吗?

不确定。

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

答案 2 :(得分:1)

考虑到手头的约束(你基本上是在反向移动列表),最好也是反向构建字符串,然后当添加所有字符时,反转字符串。

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

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

如果您使用std::vector<char>,则可以明确指出在字符串末尾添加字符是分摊常量复杂度。使用std::string我们不会(我记得)获得复杂性保证,但它需要一个非常可怕的实现,因为它只是复制一个字符的递归调用。

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

答案 3 :(得分:0)

解决方案效率低下是由于递归造成的。对于链表,请设置一个字符串并使用简单的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;
}

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

答案 4 :(得分:0)

就个人而言,我会创建一个字符串链接列表,或者更确切地说是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;
}

答案 5 :(得分:0)

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

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 );