我比C ++更熟悉C#,所以我必须就此问题寻求建议。我不得不将一些代码片段重写为C ++,然后(令人惊讶地)遇到了性能问题。
我已将问题缩小到这些片段:
C#
public class SuffixTree
{
public class Node
{
public int Index = -1;
public Dictionary<char, Node> Children = new Dictionary<char, Node>();
}
public Node Root = new Node();
public String Text;
public SuffixTree(string s)
{
Text = s;
for (var i = s.Length - 1; i >= 0; --i)
InsertSuffix(s, i);
}
public void InsertSuffix(string s, int from)
{
var cur = Root;
for (int i = from; i < s.Length; ++i)
{
var c = s[i];
if (!cur.Children.ContainsKey(c))
{
var n = new Node() { Index = from };
cur.Children.Add(c, n);
return;
}
cur = cur.Children[c];
}
}
public bool Contains(string s)
{
return FindNode(s) != null;
}
private Node FindNode(string s)
{
var cur = Root;
for (int i = 0; i < s.Length; ++i)
{
var c = s[i];
if (!cur.Children.ContainsKey(c))
{
for (var j = i; j < s.Length; ++j)
if (Text[cur.Index + j] != s[j])
return null;
return cur;
}
cur = cur.Children[c];
}
return cur;
}
}
}
C ++
struct node
{
int index;
std::unordered_map<char, node*> children;
node() { this->index = -1; }
node(int idx) { this->index = idx; }
};
struct suffixTree
{
node* root;
char* text;
suffixTree(char* str)
{
int len = strlen(str) + 1;
this->text = new char[len];
strncpy(this->text, str, len);
root = new node();
for (int i = len - 2; i >= 0; --i)
insertSuffix(str, i);
}
void insertSuffix(char* str, int from)
{
node* current = root;
for (int i = from; i < strlen(str); ++i)
{
char key = str[i];
if (current->children.find(key) == current->children.end())
{
current->children[key] = new node(from);
return;
}
current = current->children[key];
}
}
bool contains(char* str)
{
node* current = this->root;
for (int i = 0; i < strlen(str); ++i)
{
char key = str[i];
if (current->children.find(key) == current->children.end())
{
for (int j = i; j < strlen(str); ++j)
if (this->text[current->index + j] != str[j])
return false;
return true;
}
current = current->children[key];
}
}
}
在这两种情况下,我都会创建一个后缀树,然后在一个更大的函数中使用它,这个函数与post无关(让我们称之为F())。我已经测试了两个随机生成的长度为100000的字符串.C#版本构造了我的后缀树并在F()中使用它的总执行时间为: 480 ms 而代码是我已经&#34;翻译成C ++&#34;执行 48秒
我已经深入研究了这一点,似乎在我的C ++代码中,构造函数需要 47秒,而使用F()中的树运行 48 ms 比C#快10倍。
结论
似乎主要问题在于 insertSuffix(),也许是我对 unordered_map 结构缺乏了解和理解。任何人都可以对此有所了解吗?我是否在C ++变体中犯了一些新错误导致对象构造花了这么长时间?
有条件的信息
我已经编译了C#和C ++程序以获得最大速度/ O2(发布)
答案 0 :(得分:6)
在C#中,System.String包含其Length,因此您可以在恒定时间内获得长度。在C ++中,std::string
还包含其size,因此它也可以在固定时间内使用。
但是,您没有使用C ++ std::string
(对于算法的良好翻译,您应该使用它);你正在使用C-style null-terminated char
array。 char*
字面意思是“指向char
的指针”,只是告诉你字符串的第一个字符在哪里。 strlen
函数查看指向前向的每个char
,直到找到空字符'\0'
(不要与null pointer混淆);这很昂贵,你可以在insertSuffix
的循环的每次迭代中完成。这可能至少占你减速的一小部分。
在进行C ++时,如果你发现自己使用原始指针(任何涉及*
的类型),你应该总是想知道是否有更简单的方法。有时答案是“不”,但通常是“是”(随着语言的发展,这种情况越来越普遍)。例如,考虑一下您的struct node
和node* root
。两者都使用node
指针,但在这两种情况下你都应该直接使用node
,因为不需要那个间接(在node
的情况下,一些间接量是必要的,因此您没有每个节点包含另一个节点 ad infinitum ,但这是由std::unordered_map
提供的。
其他一些提示:
insertSuffix
以std::string
作为第一个参数,而是std::string const&
;同样,contains
应该采用std::string const&
。更好的是,由于insertSuffix
可以看到text
成员,因此根本不需要使用第一个参数,只需使用from
。for
循环。std::string_view
代替std::string
查看一个字符串,不需要更改它或保持对它的引用。这对contains
很有用,因为你想在text
成员中创建一个本地副本,即使对于构造函数也是如此;它在text
成员本身中没用,因为被查看的对象可能是临时的。但是,在C ++中,有时候生命周期很棘手,直到你掌握它为止,你可能只想使用std::string
来保证安全。node
在suffixTree
的概念之外没有用处,所以它应该在它内部,就像在C#版本中一样。作为对C#版本的偏离,您可能希望将类型node
和数据成员root
和text
变为private
而不是public
成员。