为字符串中的每个字符(不是字符位置!)分配唯一的索引

Assigning a unique index to each character (not character position!) within a string

本文关键字:字符 位置 分配 索引 唯一 字符串      更新时间:2023-10-16

我正在尝试为 UTF32 字符串中的字符分配一个介于 0 和 N 之间的唯一索引(其中 n 是字符串中唯一字符的数量)。

例如,如果我有字符串"hello",则该函数的输出将是:

'h' = 0
'e' = 1
'l' = 2
'o' = 3

字符串"hello"中有 4 个唯一字符,因此输出需要介于 0 和 3 之间。

我知道这可以很容易地使用哈希表来完成,甚至是最小的完美哈希。我很好奇是否有更有效的方法来处理此任务,因为我只需要将单个字符映射到单个输出值(例如,我不需要散列整个字符串)。正因为如此,使用 std::map 之类的东西似乎有点矫枉过正,但是我无法找到任何可以更快地初始化或评估的替代方案(尽管我想你可以将字符推到排序数组中并使用二进制搜索查找它们)。

我可能会使用哈希表(以std::unordered_set的形式)来存储唯一的字母,然后在需要输出时使用一个简单的计数器。

类似的东西

std::string str = "hello";
std::unordered_set<char> chars(begin(str), end(str));
std::size_t counter = 0;
for (char c : chars)
std::cout << ''' << c << "' = " << counter++ << 'n';
任何

初始化或评估速度更快的替代方案

你不会比std::unordered_map<char, size_t>更快,因为你必须检查你是否已经看到了一个char,然后你才知道你是否需要存储一个新的char->size_t地图。

当然,除非你写了一个更好的无序图。 正如@MaxLanghof指出的那样:这可以通过像std::array<char, 256>一样的东西来完成,该被告知未找到的值。

如果您使用 8 位字符,您可以使用从char到唯一索引std::array<char, 256>映射(这显然也适合char):

constexpr unsigned char UNASSIGNED = 255; // Could be another character but then the loop logic gets harder.
std::array<unsigned char, 256> indices;
std::fill(indices.begin(), indices.end(), UNASSIGNED);
std::string input = ...;
unsigned char nextUniqueIndex = 0;
for (unsigned char c : input)
if (indices[c] == UNASSIGNED)
{
indices[c] = nextUniqueIndex;
++nextUniqueIndex;
}
// indices now contains a mapping of each char in the input to a unique index.

这当然要求输入字符串不使用char的整个值范围(或者更确切地说,输入中没有 256 个不同的字符)。

现在,您说您正在使用 UTF32,这并不能使该解决方案立即可行。事实上,对于 32 位字符,映射将需要 16 GB 的内存(在任何情况下都不会很好地执行)。但是,如果您实际上以随机顺序接收了 2个 32个不同的 UTF32 字符,那么您已经是 16 GB 的输入数据,因此此时的问题是"您可以对输入数据做出哪些假设,可以利用这些假设来改进查找"(大概是以良好的哈希函数的形式)以及哪种哈希表为您提供最佳性能。我敢打赌,std::unordered_map每个键值对的单独分配和查找时的链表遍历不会带来峰值性能。

您提到的排序方法就是这样一种选择,但是如果例如整个输入是两个字符的混合,则与其他方法相比,这也不会"有效"。我还将在此处删除关键字 Bloom Filter,因为对于大量数据,这可能是快速处理常见字符的好方法(即为常用键与不常用键具有单独的数据结构)。

当您使用 UTF32 字符串时,我假设这是有充分理由的,即您希望支持来自世界各地的大量不同字符和符号。如果你完全不能假设你最有可能处理哪些角色,我认为某个程序员的答案是你最好的选择。

但是,众所周知,std::unordered_set比Max Langhof提出的简单数组查找要慢得多。所以,如果你做出一些假设,你也许能够将这两个想法结合起来。

例如,如果您可以合理地假设大部分输入将是 ASCII 字符,则可以使用如下内容:

constexpr char32_t ExpectedStart = U' '; // space == 32
constexpr char32_t ExpectedEnd = 128;
int main()
{
std::basic_string<char32_t> input = U"Hello €";
std::array<bool, ExpectedEnd - ExpectedStart> fastLookup;
std::fill(fastLookup.begin(), fastLookup.end(), false);
std::unordered_set<char32_t> slowLookup;
for (auto c : input)
{
if (ExpectedStart <= c && c < ExpectedEnd)
fastLookup[c - ExpectedStart] = true;
else
slowLookup.insert(c);
}
size_t unique_id = 0;
for (char32_t c = ExpectedStart; c < ExpectedEnd; ++c)
if (fastLookup[c - ExpectedStart])
std::wcout << ''' << (wchar_t)c << "' = " << unique_id++ << 'n';
for (auto c : slowLookup)
std::wcout << ''' << (wchar_t)c << "' = " << unique_id++ << 'n';
}

现场演示。

请注意,出于打印目的,我将字符转换为wchar_t,因为显然很难正确打印char32_t。但我假设你的最终目标无论如何都不是打印,所以我希望这无关紧要。