【问题标题】:How do I find the closest match to a string key within a C++ map?如何在 C++ 映射中找到与字符串键最接近的匹配项?
【发布时间】:2023-01-11 04:28:19
【问题描述】:

我正在制作一个程序,我想实现的一个功能是错误检查类型的东西,如果 C++ 程序搜索整个程序并没有找到匹配项,它将返回最接近的匹配字符串。

我目前有这样的设置:

help_data["invite"] = "**How to use?**\n `/invite` - *No other parameters* \n **Purpose** \n To generate an invite for Beyond!";
help_data["help"] = "**How to use?** \n `/help [command]` \n **Purpose** \n To generate an embed on how a specific command works, and its' syntax.";
std::string command = std::get<std::string>(event.get_parameter("command"));
    std::transform(command.begin(), command.end(), command.begin(), ::tolower); //lowercase
    if (help_data.find(command) == help_data.end()) {
        // Not Found
        event.reply(dpp::ir_channel_message_with_source, dpp::message().set_content("Unable to find help data for command `" + command + "`"));
    }
    else {
        // Found
}

谢谢!

【问题讨论】:

  • 你不知道。 map 针对精确匹配进行了优化,没有任何接近匹配的功能。
  • 好吧,谢谢你让我知道,我还是个初学者,哈哈。
  • 请注意,如果您想要最接近的匹配项排序,您可以使用 equal_range(或其两半,lower_boundupper_bound - 如果匹配不准确,它们将相等,因此形成一个空范围)。如果你想检查前缀(但不限于它们),可以使用它。
  • @MarkRansom:map 确实进行了优化,但不是针对精确匹配,这是unordered_map 的工作。使用 map,您将获得 equal_range 以获得最接近的词典匹配。这是“最接近匹配”的一种形式,但不是典型 UX 场景的最接近匹配。尽管如此,优化错误场景的速度很少有用。您仍然可以遍历 map 的所有键并进行手动匹配。
  • @MSalters 好吧,我给你一个 - equal_range 如果你正在寻找仅在字符串末尾不同的紧密匹配,那么它可能很有用。但是在字符串的开头查找紧密匹配是没有用的;为此,您需要按照您所说的那样迭代整个容器。一旦你决定迭代整个容器,容器的选择几乎是无关紧要的——vector 可能是你最好的选择。

标签: c++


【解决方案1】:

第1部分(见下文第 2 和 3 部分)

作为 @BasileStarynkevitch 的 suggested,您可以实现 Levenstein distance,它测量两个字符串之间的编辑距离(插入、删除、替换的次数),或者换句话说,两个字符串有多相似,Levenstein 距离的值越接近 0相似的字符串越多。

刚才我用 C++ 从头开始​​实现了这个距离计算。并展示了使用此距离函数在给定字符串中查找与查询字符串最接近的字符串的示例。

功能Levenstein() 是根据维基百科(上面的链接)实现的,并没有优化只是为了使其易于阅读和理解,用于教育目的。在生产代码中使用 Memoization 技术(相同函数调用的缓存结果)使其更快,因为如您所见,对于较大的字符串,我的实现会非常慢,对于相同的两个字符串,它将执行很多冗余的相同功能电话。另一种加速计算的方法是使用Dynamic programming 方法来缓存和重用数组中以前的结果。

Try it online!

#include <string>
#include <string_view>
#include <algorithm>
#include <vector>
#include <tuple>
#include <iostream>

size_t Levenstein(std::string_view const & a, std::string_view const & b) {
    // https://en.wikipedia.org/wiki/Levenshtein_distance
    if (b.size() == 0)
        return a.size();
    if (a.size() == 0)
        return b.size();
    if (a[0] == b[0])
        return Levenstein(a.substr(1), b.substr(1));
    return 1 + std::min(
        std::min(
            Levenstein(a          , b.substr(1)),
            Levenstein(a.substr(1), b          )
        ),  Levenstein(a.substr(1), b.substr(1))
    );
}

std::tuple<size_t, size_t> FindClosest(
        std::vector<std::string> const & strs, std::string const & query) {
    size_t minv = size_t(-1), mini = size_t(-1);
    for (size_t i = 0; i < strs.size(); ++i) {
        size_t const dist = Levenstein(strs[i], query);
        if (dist < minv) {
            minv = dist;
            mini = i;
        }
    }
    return std::make_tuple(mini, minv);
}

int main() {
    std::vector<std::string> const strs = {"world", "worm", "work"};
    std::string const query = "word";
    auto const [idx, dist] = FindClosest(strs, query);
    std::cout << "Closest to '" << query << "' is '"
        << strs[idx] << "', distance " << dist << std::endl;
}

输出:

Closest to 'word' is 'world', distance 1

第2部分

正如答案第 1 部分中所建议的,我决定使用 Memoization 技术实现列文斯坦距离的优化版本,以在数组中存储和重用相同的结果。

这个版本更难理解,阅读时间更长,但运行速度更快。

Try it online!

#include <string>
#include <string_view>
#include <algorithm>
#include <vector>
#include <tuple>
#include <iostream>
#include <functional>

size_t Levenstein(std::string_view const & a, std::string_view const & b) {
    // https://en.wikipedia.org/wiki/Levenshtein_distance
    std::vector<size_t> d_((a.size() + 1) * (b.size() + 1), size_t(-1));
    auto d = [&](size_t ia, size_t ib) -> size_t & {
        return d_[ia * (b.size() + 1) + ib];
    };
    std::function<size_t(size_t, size_t)> LevensteinInt =
        [&](size_t ia, size_t ib) -> size_t {
            if (d(ia, ib) != size_t(-1))
                return d(ia, ib);
            size_t dist = 0;
            if (ib >= b.size())
                dist = a.size() - ia;
            else if (ia >= a.size())
                dist = b.size() - ib;
            else if (a[ia] == b[ib])
                dist = LevensteinInt(ia + 1, ib + 1);
            else
                dist = 1 + std::min(
                    std::min(
                        LevensteinInt(ia,     ib + 1),
                        LevensteinInt(ia + 1, ib    )
                    ),  LevensteinInt(ia + 1, ib + 1)
                );
            d(ia, ib) = dist;
            return dist;
        };
    return LevensteinInt(0, 0);
}

std::tuple<size_t, size_t> FindClosest(
        std::vector<std::string> const & strs, std::string const & query) {
    size_t minv = size_t(-1), mini = size_t(-1);
    for (size_t i = 0; i < strs.size(); ++i) {
        size_t const dist = Levenstein(strs[i], query);
        if (dist < minv) {
            minv = dist;
            mini = i;
        }
    }
    return std::make_tuple(mini, minv);
}

int main() {
    std::vector<std::string> const strs = {"world", "worm", "work"};
    std::string const query = "word";
    auto const [idx, dist] = FindClosest(strs, query);
    std::cout << "Closest to '" << query << "' is '"
        << strs[idx] << "', distance " << dist << std::endl;
}

输出:

Closest to 'word' is 'world', distance 1

第 3 部分

我使用200 most common English words 比较了时间。

比较第 1 部分和第 2 部分中的慢速和快速(带记忆)Levenstein 实现。

对于 5 个字母的字符串,慢速版本比快速版本慢 8 倍,对于 10 个字母的字符串慢 5000 倍,这是非常非常慢的。这种缓慢的发生只是因为具有许多重复的纯递归性质。

所有时间都低于以微秒为单位的代码。

我还在这里提供了进行测量的完整代码。

Try it online!

#include <string>
#include <string_view>
#include <algorithm>
#include <vector>
#include <tuple>
#include <iostream>
#include <iomanip>
#include <functional>
#include <chrono>

size_t Levenstein(std::string_view const & a, std::string_view const & b) {
    // https://en.wikipedia.org/wiki/Levenshtein_distance
    if (b.size() == 0)
        return a.size();
    if (a.size() == 0)
        return b.size();
    if (a[0] == b[0])
        return Levenstein(a.substr(1), b.substr(1));
    return 1 + std::min(
        std::min(
            Levenstein(a          , b.substr(1)),
            Levenstein(a.substr(1), b          )
        ),  Levenstein(a.substr(1), b.substr(1))
    );
}

size_t LevensteinFast(std::string_view const & a, std::string_view const & b) {
    // https://en.wikipedia.org/wiki/Levenshtein_distance
    thread_local std::vector<size_t> d_;
    d_.clear();
    d_.resize((a.size() + 1) * (b.size() + 1), size_t(-1));
    auto d = [&](size_t ia, size_t ib) -> size_t & {
        return d_[ia * (b.size() + 1) + ib];
    };
    std::function<size_t(size_t, size_t)> LevensteinInt =
        [&](size_t ia, size_t ib) -> size_t {
            if (d(ia, ib) != size_t(-1))
                return d(ia, ib);
            size_t dist = 0;
            if (ib >= b.size())
                dist = a.size() - ia;
            else if (ia >= a.size())
                dist = b.size() - ib;
            else if (a[ia] == b[ib])
                dist = LevensteinInt(ia + 1, ib + 1);
            else
                dist = 1 + std::min(
                    std::min(
                        LevensteinInt(ia,     ib + 1),
                        LevensteinInt(ia + 1, ib    )
                    ),  LevensteinInt(ia + 1, ib + 1)
                );
            d(ia, ib) = dist;
            return dist;
        };
    return LevensteinInt(0, 0);
}

std::tuple<size_t, size_t> FindClosest(std::vector<std::string> const & strs,
        std::string const & query, bool fast = true) {
    size_t minv = size_t(-1), mini = size_t(-1);
    for (size_t i = 0; i < strs.size(); ++i) {
        size_t const dist = (fast ? LevensteinFast : Levenstein)(strs[i], query);
        if (dist < minv) {
            minv = dist;
            mini = i;
        }
    }
    return std::make_tuple(mini, minv);
}

double Time() {
    static auto const gtb = std::chrono::high_resolution_clock::now();
    return std::chrono::duration_cast<std::chrono::duration<double>>(
        std::chrono::high_resolution_clock::now() - gtb).count();
}

int main() {
    // https://1000mostcommonwords.com/1000-most-common-english-words/
    // 600 most common English words
    std::vector<std::string> const strs = {
        "as", "I", "his", "that", "he", "was", "for", "on", "are", "with", "they", "be", "at", "one", "have",
        "this", "from", "by", "hot", "word", "but", "what", "some", "is", "it", "you", "or", "had", "the", "of",
        "to", "and", "a", "in", "we", "can", "out", "other", "were", "which", "do", "their", "time", "if", "will",
        "how", "said", "an", "each", "tell", "does", "set", "three", "want", "air", "well", "also", "play", "small", "end",
        "put", "home", "read", "hand", "port", "large", "spell", "add", "even", "land", "here", "must", "big", "high", "such",
        "follow", "act", "why", "ask", "men", "change", "went", "light", "kind", "off", "need", "house", "picture", "try", "us",
        "again", "animal", "point", "mother", "world", "near", "build", "self", "earth", "father", "any", "new", "work", "part", "take",
        "get", "place", "made", "live", "where", "after", "back", "little", "only", "round", "man", "year", "came", "show", "every",
        "good", "me", "give", "our", "under", "name", "very", "through", "just", "form", "sentence", "great", "think", "say", "help",
        "low", "line", "differ", "turn", "cause", "much", "mean", "before", "move", "right", "boy", "old", "too", "same", "she",
        "all", "there", "when", "up", "use", "your", "way", "about", "many", "then", "them", "write", "would", "like", "so",
        "these", "her", "long", "make", "thing", "see", "him", "two", "has", "look", "more", "day", "could", "go", "come",
        "did", "number", "sound", "no", "most", "people", "my", "over", "know", "water", "than", "call", "first", "who", "may",
        "down", "side", "been", "now", "find", "head", "stand", "own", "page", "should", "country", "found", "answer", "school", "grow",
        "study", "still", "learn", "plant", "cover", "food", "sun", "four", "between", "state", "keep", "eye", "never", "last", "let",
        "thought", "city", "tree", "cross", "farm", "hard", "start", "might", "story", "saw", "far", "sea", "draw", "left", "late",
        "run", "don’t", "while", "press", "close", "night", "real", "life", "few", "north", "book", "carry", "took", "science", "eat",
        "room", "friend", "began", "idea", "fish", "mountain", "stop", "once", "base", "hear", "horse", "cut", "sure", "watch", "color",
        "face", "wood", "main", "open", "seem", "together", "next", "white", "children", "begin", "got", "walk", "example", "ease", "paper",
        "group", "always", "music", "those", "both", "mark", "often", "letter", "until", "mile", "river", "car", "feet", "care", "second",
        "enough", "plain", "girl", "usual", "young", "ready", "above", "ever", "red", "list", "though", "feel", "talk", "bird", "soon",
        "body", "dog", "family", "direct", "pose", "leave", "song", "measure", "door", "product", "black", "short", "numeral", "class", "wind",
        "question", "happen", "complete", "ship", "area", "half", "rock", "order", "fire", "south", "problem", "piece", "told", "knew", "pass",
        "since", "top", "whole", "king", "street", "inch", "multiply", "nothing", "course", "stay", "wheel", "full", "force", "blue", "object",
        "decide", "surface", "deep", "moon", "island", "foot", "system", "busy", "test", "record", "boat", "common", "gold", "possible", "plane",
        "stead", "dry", "wonder", "laugh", "thousand", "ago", "ran", "check", "game", "shape", "equate", "hot", "miss", "brought", "heat",
        "snow", "tire", "bring", "yes", "distant", "fill", "east", "paint", "language", "among", "unit", "power", "town", "fine", "certain",
        "fly", "fall", "lead", "cry", "dark", "machine", "note", "wait", "plan", "figure", "star", "box", "noun", "field", "rest",
        "correct", "able", "pound", "done", "beauty", "drive", "stood", "contain", "front", "teach", "week", "final", "gave", "green", "oh",
        "quick", "develop", "ocean", "warm", "free", "minute", "strong", "special", "mind", "behind", "clear", "tail", "produce", "fact", "space",
        "heard", "best", "hour", "better", "true", "during", "hundred", "five", "remember", "step", "early", "hold", "west", "ground", "interest",
        "reach", "fast", "verb", "sing", "listen", "six", "table", "travel", "less", "morning", "ten", "simple", "several", "vowel", "toward",
        "war", "lay", "against", "pattern", "slow", "center", "love", "person", "money", "serve", "appear", "road", "map", "rain", "rule",
        "govern", "pull", "cold", "notice", "voice", "energy", "hunt", "probable", "bed", "brother", "egg", "ride", "cell", "believe", "perhaps",
        "pick", "sudden", "count", "square", "reason", "length", "represent", "art", "subject", "region", "size", "vary", "settle", "speak", "weight",
        "general", "ice", "matter", "circle", "pair", "include", "divide", "syllable", "felt", "grand", "ball", "yet", "wave", "drop", "heart",
        "am", "present", "heavy", "dance", "engine", "position", "arm", "wide", "sail", "material", "fraction", "forest", "sit", "race", "window",
        "store", "summer", "train", "sleep", "prove", "lone", "leg", "exercise", "wall", "catch", "mount", "wish", "sky", "board", "joy",
        "winter", "sat", "written", "wild", "instrument", "kept", "glass", "grass", "cow", "job", "edge", "sign", "visit", "past", "soft",
        "fun", "bright", "gas", "weather", "month", "million", "bear", "finish", "happy", "hope", "flower", "clothe", "strange", "gone", "trade",
    };
    for (size_t K: {1, 2, 3, 5, 10, 20}) {
        size_t const query_str_cnt = 10, total_str_cnt = 20;
        double avg_len = 0;
        std::vector<std::string> strK;
        for (size_t i = 0; (i + 1) * K <= strs.size(); ++i) {
            std::string s;
            for (size_t j = 0; j < K; ++j)
                s += strs[i * K + j] + " ";
            strK.push_back(s);
            avg_len += s.size();
        }
        avg_len /= strK.size();
        std::vector<std::string> strs_search(strK.begin(),
            strK.begin() + std::min<size_t>(total_str_cnt, strK.size()));
        
        for (size_t ifast = K <= 2 ? 0 : 1; ifast < 2; ++ifast) {
            double tim = 1000;
            for (size_t itest = 0; itest < (1 << 0); ++itest) {
                auto tb = Time();
                for (size_t i = 0; i < query_str_cnt; ++i) {
                    auto volatile t = FindClosest(strs_search, strK.at(strK.size() - 1 - i), ifast);
                }
                tb = Time() - tb;
                tim = std::min<double>(tim, tb / query_str_cnt / strs_search.size());
            }
            std::cout << std::fixed << "Avg time " << std::setprecision(2) << std::setw(9) << tim * 1'000'000
                << " mc-sec per " << (ifast ? "Fast" : "Slow") << " Levenstein distance of " << std::setprecision(1)
                << std::setw(5) << avg_len << " symbol strings" << std::endl;
        }
        std::cout << std::endl;
    }
}

计时的控制台输出:

Avg time     10.41 mc-sec per Slow Levenstein distance of   4.8 symbol strings
Avg time      1.58 mc-sec per Fast Levenstein distance of   4.8 symbol strings

Avg time  30444.71 mc-sec per Slow Levenstein distance of   9.6 symbol strings
Avg time      5.54 mc-sec per Fast Levenstein distance of   9.6 symbol strings

Avg time     12.56 mc-sec per Fast Levenstein distance of  14.4 symbol strings

Avg time     38.44 mc-sec per Fast Levenstein distance of  24.1 symbol strings

Avg time    154.76 mc-sec per Fast Levenstein distance of  48.1 symbol strings

Avg time    659.87 mc-sec per Fast Levenstein distance of 110.6 symbol strings

【讨论】:

  • 使用字符串视图而不是字符串可能会更好。
  • @Thomas,好点!更新我的代码以使用std::string_view
  • 太感谢了!我对如何在代码中实现这一点感到困惑,这个指南帮助了很多人!感谢您的惊人回答!
  • 为什么这个不错的(而且非常有效!)答案没有得到一个 Thumbsup,我永远不会知道......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多