【问题标题】:Optimizing very often used anagram function优化非常常用的字谜函数
【发布时间】:2013-08-10 01:03:19
【问题描述】:

我写了一个函数来判断两个单词是否是字谜。单词 如果您可以通过重新排列从 A 中构建单词 B,则 A 是单词 B 的字谜 字母,例如:

lead is anagram of deal

这是我的功能:

bool is_anagram(std::string const & s1, std::string const & s2)
{
    auto check = [](std::string const & x)
    {
        std::map<char, unsigned> counter;
        for(auto const & c : x)
        {
            auto it = counter.find(c);
            if(it == counter.end())
                counter[c] = 1;
            else
                ++counter[c];
        }
        return counter;
    };

    return check(s1) == check(s2);
}

这很好用,但是随着单词数量的增加(并且使用了此功能 在我的应用程序中数百万次),它很快成为一个主要的 我的应用程序的瓶颈。

有人知道如何加快这个功能吗?

【问题讨论】:

  • 对它们进行排序并比较。
  • 制作地图的成本很高。如果最多只有 255 个字符,则改为创建一个 255 个数组。但是R的建议更好。
  • 您真正想要的优化是避免频繁调用函数的优化。例如,通过对相同长度的单词进行分组,并通过一些字母顺序不敏感的哈希对单词进行分组。一个示例哈希可能是每个字母的 ASCII(或任何您的编码方案)值的总和。

标签: c++ string algorithm optimization anagram


【解决方案1】:

地图创建和您在迭代中调用std::map::find, 相当昂贵。

在这种情况下, 您可以使用 std::string 在许多方面表现得像 std::vector&lt;char&gt;,表示可以使用std::sort排序:

bool is_anagram(std::string s1, std::string s2)
{
    std::sort(s1.begin(), s1.end());
    std::sort(s2.begin(), s2.end());
    return s1 == s2;
}

我将复制字符串而不是您创建的两个地图 (通过值而不是 const 引用传递它们)并对它们进行排序,所以

sort("lead") => "adel"
sort("deal") => "adel"

此更改应该已经大大加快了您的算法速度。多一个 如果您倾向于比较任意的东西,可能会极大地影响性能 话:

bool is_anagram(std::string s1, std::string s2)
{
    if(s1.length() != s2.length())
        return false;
    /* as above */
}

如果两个字符串的长度不同,显然不能是字谜。 std::string::length() 是一个非常快速的操作(它需要存储 无论如何字符串大小),所以我们为我们节省了O(N*log(N))的喧嚣 对两个字符串进行排序。

【讨论】:

  • N * log(N) 比 N 好吗?
  • @MatsPetersson 不,因为N &gt;= 1log 是二进制对数。
  • 我的意思是计算每个字符的出现次数是 O(N)。当然,如果我们在地图中查找,每个 O 可能会更慢,但这也许可以用另一种方式更好地解决。我正在研究几个解决方案,稍后会发布(我想将此处发布的解决方案与我的解决方案进行实际比较,以显示实际时间)
  • 请注意,这个解决方案目前还不是最快的(见下面的答案!)
  • 恕我直言,但 N 通常在 2 到 6 之间,考虑到 big-O 是荒谬的。如果k 很小,即使O(N^2) 也会飞起来。 njansen 的方法看起来非常明智。
【解决方案2】:

你有 26 个字符,创建一个大小为 26 的 one 数组,然后遍历两个单词,当你在单词中遇到字符时,为第一个单词中的字符增加一个相应的数组元素并减少第二个单词中字符的相应数组元素。如果单词是字谜,则所有数组元素最终都将为 0。 复杂度仅为 O(N),其中 N 是单词的长度。

【讨论】:

  • 在这种表示法中常量不算,O(1e10*N + 1e100) 仍然是 O(N)。
  • 您还应该首先检查字符串的长度是否相同,因为它是 O(1) 检查。
  • 不一定。检查字符串的长度意味着遍历它们(取决于字符串的表示方式)。这可以通过忽略长度检查来避免。如果字符串的长度不同,则某些数组元素将不为零。这必须在真实样品上进行实际测试。最后的数组检查也可以优化。如果数组类型是 int8[],则可以将数组放入,例如,与 int64 数组的联合。后者将针对零进行大约 8 倍的测试。
  • 没有理智的std::string 是“遍历字符串”。使用std::string 的动机之一是避免在 C 风格的字符串中使用相当频繁的代码来“查找字符串的结尾”操作。如果std::string 仍然迭代以知道它自己的长度,那么这个好处将会丢失。正如我的帖子所示,这种方法绝对是最快的。
【解决方案3】:

以下是一些执行字谜分析的函数:

#include <iostream>
#include <iomanip>
#include <map>
#include <cctype>
#include <string>
#include <algorithm>

using namespace std;

bool is_anagram(const std::string & s1, const std::string & s2)
{
    auto check = [](const std::string& x)
    {
        std::map<char, unsigned> counter;
        for(auto c : x)
        {
            auto it = counter.find(c);
            if(it == counter.end())
                counter[c] = 1;
            else
                ++counter[c];
        }
        return counter;
    };

    return check(s1) == check(s2);
}


bool is_anagram1(const std::string & s1, const std::string & s2)
{
    auto check = [](const std::string& x)
    {
        std::map<char, unsigned> counter;
        for(auto c : x)
        {
        ++counter[c];
        }
        return counter;
    };

    return check(s1) == check(s2);
}


bool is_anagram2(std::string s1, std::string s2)
{
    std::sort(s1.begin(), s1.end());
    std::sort(s2.begin(), s2.end());
    return s1 == s2;
}


bool is_anagram3(std::string s1, std::string s2)
{
    if (s1.length() != s2.length()) return false;
    std::sort(s1.begin(), s1.end());
    std::sort(s2.begin(), s2.end());
    return s1 == s2;
}

bool is_anagram4(const std::string &s1, const std::string &s2)
{
    int arr[256] = {};
    if (s1.length() != s2.length()) return false;
    for(std::string::size_type i = 0; i < s1.length(); i++)
    {
    arr[(unsigned)s1[i]]++;
    arr[(unsigned)s2[i]]--;
    }
    for(auto i : arr)
    {
    if (i) return false;
    } 
    return true;
}

bool is_anagram5(const std::string &s1, const std::string &s2)
{
    int arr[26] = {};
    if (s1.length() != s2.length()) return false;

    for(std::string::size_type i = 0; i < s1.length(); i++)
    {
    arr[(unsigned)tolower(s1[i])-'a']++;
    arr[(unsigned)tolower(s2[i])-'a']--;
    }
    for(auto i : arr)
    {
    if (i) return false;
    } 
    return true;
}


static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


template<typename T>
void bm(const char *name, T func, 
    const std::string &s1, const std::string &s2)
{
    unsigned long long time = rdtsc();
    bool res = func(s1, s2);
    time = rdtsc()-time;
    cout << "Function" << left << setw(15) << name 
     << " time: " << right << setw(8) << time 
     << " Res: " << res << endl;
}


#define BM(x) bm(#x, x, s1, s2)

int main()
{
    const std::string short1 = "lead";
    const std::string short2 = "deal";
    const std::string long1 = "leaddealleaddealleaddealleaddealleaddealleaddealleaddealleaddealleaddealleaddeal";
    const std::string long2 = "dealleaddealleaddealleaddealleaddealleaddealleaddealleaddealleaddealleaddeallead";

    cout << "Testing with short strings:" << endl;
    std::string s1 = short1;
    std::string s2 = short2;

    BM(is_anagram);
    BM(is_anagram1);
    BM(is_anagram2);
    BM(is_anagram3);
    BM(is_anagram4);
    BM(is_anagram5);

    cout << "Testing with long strings:" << endl;
    s1 = long1;
    s2 = long2;

    BM(is_anagram);
    BM(is_anagram1);
    BM(is_anagram2);
    BM(is_anagram3);
    BM(is_anagram4);
    BM(is_anagram5);

    cout << "Testing with inequal short string:" << endl;
    s1 = short1;
    s2 = short2;
    s1[s1.length()-1]++;

    BM(is_anagram);
    BM(is_anagram1);
    BM(is_anagram2);
    BM(is_anagram3);
    BM(is_anagram4);
    BM(is_anagram5);

    cout << "Testing with inequal long string:" << endl;
    s1 = long1;
    s2 = long2;
    s1[s1.length()-1]++;

    BM(is_anagram);
    BM(is_anagram1);
    BM(is_anagram2);
    BM(is_anagram3);
    BM(is_anagram4);
    BM(is_anagram5);
}

结果:

Testing with short strings:
Functionis_anagram      time:    72938 Res: 1
Functionis_anagram1     time:    15694 Res: 1
Functionis_anagram2     time:    49780 Res: 1
Functionis_anagram3     time:    10424 Res: 1
Functionis_anagram4     time:     4272 Res: 1
Functionis_anagram5     time:    18653 Res: 1
Testing with long strings:
Functionis_anagram      time:    46067 Res: 1
Functionis_anagram1     time:    33597 Res: 1
Functionis_anagram2     time:    52721 Res: 1
Functionis_anagram3     time:    41570 Res: 1
Functionis_anagram4     time:     9027 Res: 1
Functionis_anagram5     time:    15062 Res: 1
Testing with inequal short string:
Functionis_anagram      time:    11096 Res: 0
Functionis_anagram1     time:    10115 Res: 0
Functionis_anagram2     time:    13022 Res: 0
Functionis_anagram3     time:     8066 Res: 0
Functionis_anagram4     time:     2963 Res: 0
Functionis_anagram5     time:     1916 Res: 0
Testing with inequal long string:
Functionis_anagram      time:    44246 Res: 0
Functionis_anagram1     time:    33961 Res: 0
Functionis_anagram2     time:    45004 Res: 0
Functionis_anagram3     time:    38896 Res: 0
Functionis_anagram4     time:     6455 Res: 0
Functionis_anagram5     time:    14603 Res: 0

很明显,根据每个字符的出现,使用数组向上/向下计数的简单检查是赢家。我采用了原始代码并删除了find,并使用了map::operator[] 将在is_anagram1 中没有条目时创建零值的事实,这也显示了一些不错的改进。

结果来自我的机器,这些可能会或可能不会反映其他机器上的结果,但我怀疑“赢家与输家”是否会有显着不同。

编辑:

考虑了“查找”操作,并决定以不同的方式使用它:

bool is_anagram0(const std::string & s1, const std::string & s2)
{
    auto check = [](const std::string& x)
    {
        std::map<char, unsigned> counter;
        for(auto const &c : x)
        {
            auto it = counter.find(c);
            if(it == counter.end())
                counter[c] = 1;
            else
                it->second++;
        }
        return counter;
    };

    return check(s1) == check(s2);
}

与原始函数相比略有改进,但不足以与提供最佳结果的数组解决方案竞争。

【讨论】:

  • 有趣的结果!对于anagram4anagram5 解决方案,我想到了一个想法:不手动循环数组可能会更快,而是使用第二个'static int zeroes[sizeof(arr)/sizeof(*arr)] = { 0 };` 数组,然后在最后执行一个 memcmp(arr, zeroes, sizeof(arr)) == 0; 调用。这个想法是memcmp 可能具有在单个操作中比较四个(或八个或更多)字节的优化。不过对于这么小的数组,也许不值得。
  • @FrerichRaabe:是的,这有一点帮助——“is anagram”比“isn't”更多,因为它会在“isn't”早期跳过。 (我只为 is_anagram4() 做了这个 - 我认为 5 变体不值得)。
  • @MatsPetersson 想知道为什么 26 数组长度函数在大多数情况下比 256 函数慢,有什么见解吗?对我来说似乎违反直觉,我的测试也显示了不同的结果......
  • is_anagram5 变体较慢,因为它调用 tolower - 这当然有点多余。如果我删除它,它会比is_anagram4 快一点。
  • 我想你应该对每个样本多次运行每个算法,然后使用平均值。
【解决方案4】:

除了所有其他建议之外,如果您尝试在一组中查找字谜对,您将在相同的参数上调用 is_anagram(尽管不是相同的 pairs 参数) 重复。

大多数解决方案包括两个步骤:

  1. 将字符串参数映射到其他对象(排序后的字符串、长度为 26 的数组、您自己的解决方案中的映射)
  2. 将这些对象相互比较

如果您有一组N 字符串,并且您想查找所有相互变位的对,您将调用is_anagram O(N^2) 次。

首先为每个N 字符串计算上面的步骤1,然后比较结果,可以节省很多。

【讨论】:

    【解决方案5】:

    我提出了一个解决方案,它只对两个字符串中的一个进行排序:

     bool is_anagram(std::string const & s1, std::string s2)
     {
         if (s1.length() != s2.length()) return false;
         for (int i=0; i<s1.length(); ++i)
         {
             size_t pos = s2.find(s1[i], i);
             if (pos == std::string::npos)
             {
                 return false;
             }
             std::swap(s2[i], s2[pos]);
         }
         return true;
     }
    

    这种解决方案在一个字符串中有一个字符而另一个字符串中没有的情况下可能是有利的 - 因为如果它没有在另一个字符串中找到该字符,它可以缩短评估 (与此处显示的所有其他解决方案相反,其中始终评估两个完整字符串)。特别是如果一侧的这个独占字符出现在一个长字符串的早期......

    排序解决方案的一个优势还在于需要存储空间 - 排序解决方案需要复制两个字符串,而在我的解决方案中只创建一个副本。对于较短的字符串长度(取决于用于计数的 int 类型,但至少最多 256 个字符),它也比“向上/向下计数”解决方案需要更少的内存。

    另一方面,对于 字谜的较长字符串,它开始落后于“向上/向下计数”解决方案。

    分析

    请参阅下面的代码和结果。可以很容易看出,我的解决方案 (is_anagram3) 对于短字谜非常快(仅被实际上功能不完全等效的 26 个字符向上/向下计数方法击败),并且对于具有非字谜的长非字谜情况也是最快的匹配字符;但对于较长的字符串(即字谜)或长的非字谜(仅因字符数不同),往往比向上/向下计数方法慢。

    总结

    最后,理想的解决方案是什么,实际上取决于预期的输入数据:

    • 对于单次调用,“向上/向下计数”解决方案通常会在许多情况下提供最佳性能。
    • 根据具体情况(例如,如上所述,字符串包含不同字符的概率),我的解决方案可能会更快
    • 尚未测试,但似乎可以确定:对于需要执行许多字谜检查以及为已排序字符串实现缓存的情况,排序和比较解决方案将变得最快。

    代码:

    #include <string>
    #include <map>
    #include <algorithm>
    
    #include <sys/time.h>
    #include <iostream>
    #include <iomanip>
    
    bool is_anagram(std::string const & s1, std::string const & s2)
    {
        auto check = [](std::string const & x)
        {
            std::map<char, unsigned> counter;
            for(auto const & c : x)
            {
                auto it = counter.find(c);
                if(it == counter.end())
                    counter[c] = 1;
                else
                    ++counter[c];
            }
            return counter;
        };
    
        return check(s1) == check(s2);
    }
    
    bool is_anagram2(std::string s1, std::string s2)
    {
        std::sort(s1.begin(), s1.end());
        std::sort(s2.begin(), s2.end());
        return s1 == s2;
    }
    
    bool is_anagram3(std::string const & s1, std::string s2)
    {
        if (s1.length() != s2.length()) return false;
    
        for (int i=0; i<s1.length(); ++i)
        {
            size_t pos = s2.find(s1[i], i);
            if (pos == std::string::npos)
            {
                return false;
            }
            std::swap(s2[i], s2[pos]);
        }
        return true;
    }
    
    bool is_anagram4(std::string const & s1, std::string const & s2)
    {
        if (s1.length() != s2.length()) return false;
    
        int count[26] = {0};
        for (int i=0; i<s1.length(); ++i)
        {
            count[s1[i]-'a']++;
            count[s2[i]-'a']--;
        }
        for (int i=0; i<26; ++i)
        {
            if (count[i] != 0) return false;
        }
        return true;
    }
    
    bool is_anagram5(std::string const & s1, std::string const & s2)
    {
        if (s1.length() != s2.length()) return false;
    
        int count[256] = {0};
        for (int i=0; i<s1.length(); ++i)
        {
            count[s1[i]]++;
            count[s2[i]]--;
        }
        for (int i=0; i<256; ++i)
        {
            if (count[i] != 0) return false;
        }
        return true;
    }
    
    template<typename T>
    bool test(const char* name, T func)
    {
        if (!func("deal", "lead") || !func("lead", "deal") || !func("dealbreaker", "breakdealer") ||
            !func("dealbreakerdealbreakerdealbreakerdealbreaker", "breakdealerbreakdealerbreakdealerbreakdealer") ||
            func("dealbreakerdealbreakerdealbreakerdealbreakera", "breakdealerbreakdealerbreakdealerbreakdealerb") ||
            func("dealxbreakerdealbreakerdealbreakerdealbreaker", "breakdealerbreakdealerbreakdealerbreakdealerb") ||
            func("abcdefg", "tuvwxyz") ||
            func("lot", "bloat") || func("lead", "deala") ||
            func("lot", "bloat") || func("deala", "lead") ||
            func("abc", "abcd"))
        {
            std::cout << name << ": impl doesn't work" << std::endl;
            return false;
        }
        return true;
    }
    
    template<typename T>
    void measure(const char* name, T func,
        std::string const & s1, std::string const & s2)
    {
        timeval start,end;
        unsigned long secDiff;
        long usecDiff;
    
        gettimeofday(&start, 0);
        for (int i=0; i<100000; ++i)
        {
            func(s1, s2);
        }
        gettimeofday(&end, 0);
        secDiff = end.tv_sec - start.tv_sec;
        usecDiff = end.tv_usec - start.tv_usec;
        if (usecDiff < 0)
        {
            secDiff--;
            usecDiff += 1000000;
        }
     std::cout << name << ": " << secDiff << "."<< std::setw(3) << std::setfill('0') << (usecDiff/1000) << " s" << std::endl;
    }
    
    template <typename T>
    void run(const char * funcName, T func)
    {
        std::cout << funcName << std::endl;
        if (!test(funcName, func)) {
            return;
        }
        measure("short", func, "dealbreaker", "breakdealer");
        measure("short no anagram", func, "abcdefg", "tuvwxyz");
        measure("long", func, "dealbreakerdealbreakerdealbreakerdealbreaker", "breakdealerbreakdealerbreakdealerbreakdealer");
        measure("long no anagram", func, "dealbreakerdealbreakerdealbreakerdealbreakera", "breakdealerbreakdealerbreakdealerbreakdealerb");
        measure("long no anagram nonmatching char", func, "dealxbreakerdealbreakerdealbreakerdealbreaker", "breakdealerbreakdealerbreakdealerbreakdealerb");
    }
    
    int main(int argv, char**argc)
    {
        run("is_anagram", is_anagram);
        run("is_anagram2", is_anagram2);
        run("is_anagram3", is_anagram3);
        run("is_anagram4", is_anagram4);
        run("is_anagram5", is_anagram5);
    }
    

    输出

    在我的慢速 Atom 机器上测量,不同系统上的结果可能会有所不同,但总体上应该可以很好地了解相对性能:

    is_anagram
    short: 2.960 s
    short no anagram: 2.154 s
    long: 8.063 s
    long no anagram: 8.092 s
    long no anagram nonmatching char: 8.267 s
    is_anagram2
    short: 0.685 s
    short no anagram: 0.333 s
    long: 3.332 s
    long no anagram: 3.557 s
    long no anagram nonmatching char: 3.740 s
    is_anagram3
    short: 0.280 s
    short no anagram: 0.022 s
    long: 0.984 s
    long no anagram: 0.994 s
    long no anagram nonmatching char: 0.140 s
    is_anagram4
    short: 0.123 s
    short no anagram: 0.071 s
    long: 0.399 s
    long no anagram: 0.389 s
    long no anagram nonmatching char: 0.390 s
    is_anagram5
    short: 0.283 s
    short no anagram: 0.145 s
    long: 0.551 s
    long no anagram: 0.454 s
    long no anagram nonmatching char: 0.455 s
    

    【讨论】:

    • @nightcracker 你的意思是 is_anagram 函数需要更频繁地运行?我不同意。相对性能非常明显,运行时间变化不大......我可以显示第二次运行的结果 - 我在另一次运行中看到的最大偏差是 14 毫秒(在 is_anagram - long no anagram不匹配的字符)。如果你问我,那是相当准确的......
    【解决方案6】:

    假设大多数词对不是字谜,那么你最需要优化的情况就是失败情况。

    显然,如果长度不同,则字符串不能是字谜,并且长度是在创建字符串时计算一次的属性,因此作为快速输出进行比较是非常有效的事情。

    如果您更改字符串类以包含更多这些属性,则可以提高快速输出案例的准确性。而不是以以下方式开始测试功能:

    if (s1.length() != s2.length()) return false;
    

    你可以使用:

    if (s1.hash != s2.hash) return false;
    

    当您创建字符串时(或修改后),您将为hash 计算一个值,该值对于具有任意顺序的该组字母的所有字符串是唯一的(或几乎是唯一的)。

    您仅在字符串更改时计算此哈希值;并非针对您所做的每一次比较。

    计算哈希的一个非常简单的方法是:

    long sum = 0;
    for (int i = 0; i < s.length(); i++)
        sum += s[i];
    s.hash = sum;
    

    计算速度很快,但容易发生冲突。

    更高级的方法:

    static const unsigned int primetable[256] = { 1, 3, 5, 7, /* ... */ };
    
    unsigned long prod = 1;
    for (int i = 0; i < s.length(); i++)
        prod *= primetable[s[i]];
    s.hash = prod;
    

    请注意,素数列表中省略了两个。这确保了prod 始终与unsigned long 的可能范围互质。这在面对数值溢出时最大化了结果的分布。

    如果表格被安排在频繁的字母位置放置小素数,则可以最大限度地减少发生溢出(可能导致哈希冲突)的情况。如果没有溢出,那么您就有了一个完美的哈希值(用于这些目的),并且您可以通过比较 hash 来绝对确定两种方式(立即返回 truefalse)。

    因此,像这样计算哈希值会更好:

    static const unsigned int primetable[256] = {
        1, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619,
        821, 823, 827, 829, 839, 853, 857, 107, 859, 863, 877, 881, 883, 109, 887, 907, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 911, 919, 929, 937, 941, 947,
        601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811,
        359, 11, 61, 31, 37, 3, 71, 43, 59, 7, 101, 79, 29, 53, 13, 23, 47, 103, 17, 5, 19, 41, 73, 83, 89, 67, 97, 367, 373, 379, 383, 389,
        1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427,
        953, 967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171,
        397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599,
        173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353
    };
    
    unsigned long prod = 1;
    boolean overflow = false;
    for (int i = 0; i < s.length(); i++)
    {
        overflow |= (prod > ULONG_MAX / primetable[s[i]]);
        prod *= primetable[s[i]];
    }
    if (overflow)
        prod ^= 1;
    s.hash = prod;
    

    有快出:

    if (s1.hash != s2.hash) return false;
    if ((s1.hash & 1) != 0) return true;
    if (s1.length() != s2.length()) return false;
    

    只有在字符编码不是多字节的情况下才能安全使用中间行。如果您使用的是多字节编码方案,那么哈希仍然会消除大多数非字谜,但它会产生很多误报,因为某些字节顺序不能被忽略。

    Hacking Mats Petersson 的从字典中读取的测试代码,并在真实的英语字典输入上尝试这个和其他算法,我们发现比下一个最佳算法大约提高了四倍(它是十倍,但是我调整了其他代码):

    Functionis_anagram      time:    218.9s hits: 93
    Functionis_anagram1     time:      200s hits: 93
    Functionis_anagram2     time:       40s hits: 93
    Functionis_anagram3     time:     7.74s hits: 93
    Functionis_anagram4     time:     2.65s hits: 93
    Functionis_anagram5     time:      2.3s hits: 166
    Functionis_anagram6     time:     0.56s hits: 166  <- This method
    

    (命中数不同,因为最后两种算法都不区分大小写,而且我的字典可能包含与自然词冲突的首字母缩略词)


    更新:虽然这不是被问到的,但我没有指出这一点是我的疏忽。我不知道我是否没有发现它,或者我只是厌倦了打字,或者我不想对调用代码做出假设......

    对所有单词进行散列处理后,将测试次数降至最低的一种简单方法是按该散列对列表进行排序。然后,您可以轻松地将比较限制为可能匹配的列表部分(根据哈希)。这也可以让branch prediction 更有效率。

    我刚刚尝试更改我最初测试的 N^2 次迭代(我确信 是故意低效的)以迭代排序列表中的邻居。 sort() 调用占主导地位,但比最快的 N^2 测试快 200 倍,并且比较算法的选择不再对性能产生任何有意义的影响。

    或者您可以在收到单词时通过哈希对单词进行分类。

    【讨论】:

      【解决方案7】:

      这个解决方案怎么样?如果您不介意,它在 C# 中。

      public bool isAnagram(string first, string second) {
          if(first == null || second == null)
              return false;
          if(first.Length != second.Length)
              return false;
          string characters = "abcd...zABCD...Z";
          int firstResult = 0;
          int secondResult = 0;
          foreach(char x in second) {
              if(first.IndexOf(x) == -1)
                  return false;
              if(char == " ")
                  secondResult += 0;
              else
                  secondResult += characters.IndexOf(x);
          }
          foreach(char x in first) {
              if(char == " ")
                  firstResult += 0;
              else
                  firstResult += characters.IndexOf(x);
          }
          return firstResult == secondResult;
      

      }

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-02-08
        • 1970-01-01
        • 2015-03-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-16
        相关资源
        最近更新 更多