用C ++编码。我需要一堆排序字符串的数据结构。我将一次性插入所有字符串而不更新它,但我会经常搜索字符串。我只需要查看结构中是否存在给定字符串。我期待这个列表大约有100个字符串。 什么是更快的结构?我最初想的是hashmap,但是我看到某个地方对于这么少的元素,对向量的二元搜索会更好(因为它们已经排序)。
答案 0 :(得分:17)
假设你在谈论"全尺寸" CPU 1 ,通过字符串进行二进制搜索,即使只有100个元素也可能相当慢,至少相对于其他解决方案而言。对于每次搜索,您可能会遇到几个分支错误预测,并且最终可能会多次检查输入字符串中的每个字符(因为您需要在二分查找中的每个节点重复strcmp
)。
正如有人已经指出的那样,唯一真正知道的方法是衡量 - 但要做到这一点,你仍然需要能够弄清楚候选人是什么!此外,并不总是可以在现实场景中进行测量,因为可能甚至不知道这样的场景(想象一下,例如,设计一个在许多不同情况下广泛使用的库函数) )。
最后,了解什么可能是快速的,让你们既可以消除你知道会表现不佳的候选人,也可以让你用自己的直觉仔细检查你的测试结果:如果某些事情比你想象的要慢得多,那么它就会消失。值得检查为什么(编译器做了一些愚蠢的事情),如果某些事情更快那么也许是时候更新你的直觉了。
所以我会尝试实际考虑快速的事情 - 假设速度真的很重要,你可以花一些时间来验证一个复杂的解决方案。作为基线,直接实现可能需要100 ns,而真正优化的实现可能需要10 ns。因此,如果您花费10个小时的工程时间,那么您必须将此功能称为 4000亿次才能获得10个小时的支持 5 。当您考虑到错误风险,维护复杂性和其他开销时,您将需要确保在尝试优化它之前多次调用此函数 trillions 。这些功能很少见,但它们肯定存在 4 。
也就是说,您缺少帮助设计快速解决方案所需的大量信息,例如:
std::string
还是const char *
还是别的?上面的答案可以帮助您按如下所述划分设计空间。
如果每(4),您可以接受(可控)数量的误报 2 ,或每(3) 您的大多数搜索都不会成功,那么您应该考虑Bloom Filter。例如,您可以使用1024位(128字节)过滤器,并使用字符串的60位散列来索引6个10位函数。这给出了<误报率为1%。
这具有以下优点:在散列计算之外,它独立于字符串的长度,并且它不依赖于匹配行为(例如,依赖于重复字符串比较的搜索将如果字符串倾向于具有长公共前缀,则会变慢。)
如果你可以接受误报,那么你就完成了 - 但是如果你需要它总是正确的但是期望大多数不成功的搜索,你可以将它用作过滤器:如果bloom过滤器返回 false (通常的情况)你完成了,但是如果它返回true,你需要仔细检查下面讨论的一个总是正确的结构。所以常见的情况很快,但总是会返回正确的答案。
如果在编译时知道~100个字符串的集合,或者你可以做一些一次性繁重的工作来预处理字符串,你可以考虑一个完美的哈希。如果你有一个编译时已知的搜索集,你可以将字符串打到gperf
,它会吐出一个哈希函数和查找表。
例如,我只是将100个随机英语单词 3 输入gperf
并生成一个哈希函数,只需要查看两个字符区分每个单词,如下:
static unsigned int hash (const char *str, unsigned int len)
{
static unsigned char asso_values[] =
{
115, 115, 115, 115, 115, 81, 48, 1, 77, 72,
115, 38, 81, 115, 115, 0, 73, 40, 44, 115,
32, 115, 41, 14, 3, 115, 115, 30, 115, 115,
115, 115, 115, 115, 115, 115, 115, 16, 18, 4,
31, 55, 13, 74, 51, 44, 32, 20, 4, 28,
45, 4, 19, 64, 34, 0, 21, 9, 40, 70,
16, 0, 115, 115, 115, 115, 115, 115, 115, 115,
/* most of the table omitted */
};
register int hval = len;
switch (hval)
{
default:
hval += asso_values[(unsigned char)str[3]+1];
/*FALLTHROUGH*/
case 3:
case 2:
case 1:
hval += asso_values[(unsigned char)str[0]];
break;
}
return hval;
}
现在你的哈希函数快速并且可能已经很好地预测了(如果你没有太多3或更少长度的字符串)。要查找字符串,您只需索引到哈希表(也由gperf
生成),并比较您获得的输入字符串。
根据一些合理的假设,这将尽可能快地得到 - clang
生成如下代码:
in_word_set: # @in_word_set
push rbx
lea eax, [rsi - 3]
xor ebx, ebx
cmp eax, 19
ja .LBB0_7
lea ecx, [rsi - 1]
mov eax, 3
cmp ecx, 3
jb .LBB0_3
movzx eax, byte ptr [rdi + 3]
movzx eax, byte ptr [rax + hash.asso_values+1]
add eax, esi
.LBB0_3:
movzx ecx, byte ptr [rdi]
movzx edx, byte ptr [rcx + hash.asso_values]
cdqe
add rax, rdx
cmp eax, 114
ja .LBB0_6
mov rbx, qword ptr [8*rax + in_word_set.wordlist]
cmp cl, byte ptr [rbx]
jne .LBB0_6
add rdi, 1
lea rsi, [rbx + 1]
call strcmp
test eax, eax
je .LBB0_7
.LBB0_6:
xor ebx, ebx
.LBB0_7:
mov rax, rbx
pop rbx
ret
这是一大堆代码,但具有合理数量的ILP。关键路径是通过3个相关的内存访问(在char
中查找str
值 - >在哈希函数表中查找char
的哈希值 - >查找字符串在实际的哈希表中,你可以预期这通常需要20个周期(当然加上strcmp
时间。)
"经典" complci解决这个问题的方法是trie。对于你的问题,trie可能是一种合理的方法,特别是许多不成功的匹配可以在前几个字符内快速拒绝(这在很大程度上取决于匹配集的内容和你正在检查的字符串)。
您需要一个快速的trie实现来完成这项工作。总的来说,我觉得这种方法会受到依赖于串行的存储器访问的限制 - 每个节点很可能以一种指针追逐的方式被访问,所以你会遭受很多L1访问延迟。
strcmp
几乎所有上述解决方案在某些时候都依赖于strcmp
- 例外是允许误报的布隆过滤器。因此,您希望确保代码的这一部分速度很快。
特别是编译器有时可能内联"内置"版本strcmp
而非调用库函数:在快速测试中icc
执行了内联,但clang
和gcc
选择调用库函数。没有一个简单的规则,哪个会更快,但一般来说,库例程通常是SIMD优化的,对于长字符串可能更快,而内联版本避免函数调用开销,对于短字符串可能更快。您可以测试这两种方法,并且主要强制编译器在您的情况下执行更快的操作。
更好的是,您可以利用对输入的控制来做得更好 - 如果您可以确保,例如,输入字符串将 null padded ,以便length是8的倍数,那么你可以对哈希表中的引用字符串(或任何其他结构)执行相同的操作,并且可以一次比较字符串8个字节。这不仅极大地加速了匹配,还大大减少了分支误预测,因为它基本上量化了循环行为(1-8个字符的所有字符串循环一次,等等)。
1 这里我的意思是台式机,服务器,笔记本电脑CPU,甚至是现代智能手机CPU,而不是嵌入式设备MCU或类似的东西。
2 允许误报表示如果您的"处于设置状态"即使输入字符串不在集合中,有时也会返回true。请注意,它反而永远不会出错:当 中的字符串 时,始终返回true - 没有 false negatives
3 具体来说,awk 'NR%990==0' /usr/share/dict/american-english > words
。
4 例如,您在计算历史中调用了多少次strcmp
?如果它甚至快1 ns,会节省多少时间?
5 这种方式将CPU时间与工程时间等同起来,工程时间可能超过1000倍:亚马逊AWS每小时收取0.02美元的CPU时间,一个优秀的工程师可以期望每小时50美元(在第一世界)。因此(非常粗略!)公制工程时间比CPU时间高出2500倍。所以也许你需要花费数十亿的电话才能获得10小时的工作才能获得回报......
答案 1 :(得分:15)
在某种情况下判断哪种结构最快的最佳(也是唯一)方法是使用不同的数据结构对其进行基准测量。然后选择最快的。
或换句话说:衡量代码比那些认为自己太聪明而无法衡量的人更有优势。 ;)
对于你在问题中提到的100个元素这样的相当小的列表,你使用的结构/算法并没有多大区别,因为获得的时间可能是微不足道的 - 除非你的程序经常执行搜索。
答案 2 :(得分:3)
这是一个有趣的问题,因为它非常接近JAVA字符串池的概念。 Java使用JNI调用由C ++实现的本机对应方法
字符串池是JVM对string interning概念的特定实现:
在计算机科学中,字符串实习是一种只存储每个不同字符串值的一个副本的方法,该字符串值必须是不可变的。实习字符串使得一些字符串处理任务更加节省时间或空间,代价是在创建或实现字符串时需要更多时间。不同的值存储在字符串实习池中。
让我们看看如何在Java 7中实现String池
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class <code>String</code>.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this <code>String</code> object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this <code>String</code> object is added to the
* pool and a reference to this <code>String</code> object is returned.
* <p>
* It follows that for any two strings <code>s</code> and <code>t</code>,
* <code>s.intern() == t.intern()</code> is <code>true</code>
* if and only if <code>s.equals(t)</code> is <code>true</code>.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
当调用intern方法时,如果池已经包含等于此String对象的字符串(由等于对象确定),则返回池中的字符串。否则,将此对象添加到池中,并返回对此字符串对象的引用。
Java使用JNI调用由C ++实现的本机StringTable.intern方法
\ openjdk7 \ JDK \ SRC \共享\天然\ java中\郎\ String.c
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
\ openjdk7 \热点\ SRC \共享\ VM \ prims \ jvm.h
/*
* java.lang.String
*/
JNIEXPORT jstring JNICALL
JVM_InternString(JNIEnv *env, jstring str);
\ openjdk7 \热点\ SRC \共享\ VM \ prims \ jvm.cpp
// String support ///////////////////////////////////////////////////////////////////////////
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollector oam;
if (str == NULL) return NULL;
oop string = JNIHandles::resolve_non_null(str);
oop result = StringTable::intern(string, CHECK_NULL);
return (jstring) JNIHandles::make_local(env, result);
JVM_END
\ openjdk7 \热点\ SRC \共享\ VM \类文件\ symbolTable.cpp
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
unsigned int hashValue = java_lang_String::hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop string = the_table()->lookup(index, name, len, hashValue);
// Found
if (string != NULL) return string;
// Otherwise, add to symbol to table
return the_table()->basic_add(index, string_or_null, name, len,
hashValue, CHECK_NULL);
}
\ openjdk7 \热点\ SRC \共享\ VM \类文件\ symbolTable.cpp
oop StringTable::lookup(int index, jchar* name,
int len, unsigned int hash) {
for (HashtableEntry<oop>* l = bucket(index); l != NULL; l = l->next()) {
if (l->hash() == hash) {
if (java_lang_String::equals(l->literal(), name, len)) {
return l->literal();
}
}
}
return NULL;
}
如果您想了解更多关于oracle工程师如何在Java 7中更改字符串池逻辑的知识,该链接将对您有用。 Bug Report: make the string table size configurable。字符串池实现为具有固定容量的映射,每个存储桶包含具有相同代码的字符串列表。默认池大小为1009。
对于您的问题,您可以编写一个测试程序来与此方法进行比较,以堆积数据结构并确定哪个更好。
答案 3 :(得分:2)
除非你每秒进行数亿次搜索,否则你无法区分它们。 如果您每秒进行数亿次搜索,请尝试使用基数树。它的内存非常昂贵,但这个小数据集并不重要。
编写完成后,对其进行分析。
答案 4 :(得分:2)
问题有点模糊,但最快的字符串匹配算法是有限状态机,即aho-corasick算法。它是e Knuth-Morris-Pratt匹配算法的推广。如果你只是想要一个简单的查找,你可以尝试三元组或者压缩的trie(基数树),如果空间很重要甚至二元搜索。
答案 5 :(得分:2)
这取决于你的琴弦有多么不同或它们具有什么特定的形状。
如果您愿意接受内存开销,我认为散列图是一个好主意。对于大约100个字符串,第一个字符就足够了:
String* myStrings[256];
您只需查看字符串的第一个字符,以确定它可以在哪个数组中。
如果你的字符串足够异质(即它们通常不是以相同的字母开头),那么增益在理论上是256倍速。丢失a是内存中额外的257个指针(257 * 64 = 16448位)。 您可以通过从实际存储的字符串中删除第一个字符来弥补损失。
如果您决定扩展到2个字符或更多,则优势和不便都是指数级的。
String* myStrings[256][256][256];
但是,如果您的字符串是特殊的,并且不能以任何字符开头或包含任何字符,那么您可以减少数组并将使用过的字符映射到插槽。
char charToSlot[256];
String* myStrings[3];
例如,在这种情况下,如果您的字符串只能以字符100,235和201开头,则charToSlot [100] = 0,charToSlot [235] = 1,charToSlot [201] = 2。
查找索引稍慢但内存影响很小。如果您操作的字符串只能包含小写字母,那么这可以帮助您。那么你对一个角色的理想结构将是:
char charToSlot[256];
String* myStrings[26];
它可以更容易扩大规模:
char charToSlot[256];
String* myStrings[26][26][26];
如果您不想对字符串做任何假设(即它们可以包含任何内容),那么您可以实现一些动态索引(索引会在需要时立即添加,并且数组需要实现不断)。
char charToSlot[256];
String**** myStrings;
另一个技巧,如果你的字符串长度变化并且非常小(5-30长度),你可以添加一个额外的索引,再次通过搜索具有相同长度的字符串来再次乘以速度。
String* myStrings[30][256][256]...
如果您认为这些解决方案太重,那么您可以采用更加统计的方法。您可以将相同的分支分配给多个字符。例如&#39; a&#39;,&#39;&#39;&#39; c&#39;并且&#39; d&#39;所有人都会以同样的方式走下去,你的分支会减少4倍。然后你到达列表并再次检查,如果一个字符串是相同的,则char,char,并且增加获得你想要的机会。
例如,如果您的字符串可以包含所有256个字符,但您不想要256个字符,而是需要8个分支,那么您将拥有:
String* myStrings[8];
对于任何角色,你只需将它除以32(非常快)来挑选分支。这可能是我为您的问题推荐的解决方案,因为您只有大约100个字符串,而您可能不想要一个巨大的数组。
此外,这个扩展得更好:
String* myStrings[8][8][8][8]...
但是,存储的数组可能有32倍以上的字符串,而且内容不具有确定性。
同样,所有这些都取决于字符串的特定属性,更重要的是取决于您拥有多少字符串。对于一个非常庞大的字符串数据库,如果用巨大的因子提高搜索速度并删除99.99%的迭代,没有人会关心甚至是太比特的映射开销。
答案 6 :(得分:1)
使用std::unordered_set<std::string>
,非常适合您的情况。如果您还需要按顺序迭代它们,则可以有一个std::set<std::string>
。
如果在分析后发现您花费了所有时间查询数据结构,那么现在是时候提出另一个问题(使用您将要使用的精确代码)。
答案 7 :(得分:1)
Trie是您的最佳解决方案。
我这样说是因为你没有太多字符串,所以走这条路会更好。
你可以在我的github链接上查看我的trie实现
https://github.com/prem-ktiw/Algorithmic-Codes/blob/master/Trie/char_trie.cpp
代码评论很好,允许您在线性时间插入字符串,并在线性时间内搜索。没有在散列中看到的冲突问题。
已经使用了动态分配,因此记忆不会成为问题
唯一的事情是你在我的实现中不能有多个相同字符串的重复副本,并且没有关于trie中有多少个字符串副本的记录。
如果需要任何帮助,我想听听您的意见。
答案 8 :(得分:0)
您可以尝试binary index array,它是c库索引结构成员字段。
教程博客在https://minikawoon.quora.com/How-to-search-data-faster-on-big-amount-of-data-in-C-C++
示例: -
步骤1.定义你的结构
typedef struct {
char book_name[30];
char book_description[61];
char book_categories[9];
int book_code;
} my_book_t;
// 160000 size, 10 index field slot
bin_array_t *all_books = bin_array_create(160000, 10);
步骤2.添加索引
if (bin_add_index(all_books, my_book_t, book_name, __def_cstr_sorted_cmp_func__)
&& bin_add_index(all_books, my_book_t, book_categories, __def_cstr_sorted_cmp_func__)
&& bin_add_index(all_books, my_book_t, book_code, __def_int_sorted_cmp_func__)
) {
步骤3.初始化您的数据
my_book_t *bk = malloc(sizeof(my_book_t));
strcpy(bk->book_name, "The Duck Story"));
....
...
bin_array_push(all_books, bk );
步骤4.搜索结果eq,lt(小于),gt(大于)
int data_search = 100;
bin_array_rs *bk_rs= (my_book_t*) ba_search_eq(all_books, my_book_t,
book_code, &data_search);
my_book_t **bks = (my_book_t**)bk_rs->ptrs; // Convert to pointer array
// Loop it
for (i = 0; i < bk_rs->size; i++) {
address_t *add = bks[i];
....
}
步骤5.多重搜索和内部联接或联合
// Join Solution
bin_array_rs *bk_rs=bin_intersect_rs(
bin_intersect_rs(ba_search_gt(...), ba_search_lt(...), true),
bin_intersect_rs(ba_search_gt(...), ba_search_lt(....), true),
true);
// Union Solution
bin_array_rs *bk_rs= bin_union_rs(
bin_union_rs(ba_search_gt(...), ba_search_lt(...), true),
bin_union_rs(ba_search_gt(...), ba_search_lt(....), true),
true);