哪个更快,哈希查找或二进制搜索?

时间:2008-12-11 16:48:12

标签: algorithm hash hashmap lookup binary-search

当给定一组静态对象(在某种意义上是静态的,一旦加载它很少会发生变化),需要重复的并发查找以及最佳性能,这是更好的,HashMap或带有二进制搜索使用一些自定义比较器?

答案是对象还是结构类型的函数?哈希和/或平等功能表现?哈希的独特性?清单大小? Hashset尺寸/尺寸?

我正在看的那套的大小可以是500k到10m之间的任何地方 - 这些信息很有用。

虽然我正在寻找一个C#答案,但我认为真正的数学答案不在于语言,所以我不包括那个标签。但是,如果需要了解C#特定的事情,那么需要该信息。

17 个答案:

答案 0 :(得分:48)

对于非常小的收藏品,差异可以忽略不计。在您的范围的低端(500k项目),如果您正在进行大量查找,您将开始看到差异。二进制搜索将是O(log n),而哈希查找将是O(1),amortized。这与真正不变的不一样,但你仍然需要有一个相当糟糕的哈希函数来获得比二分搜索更差的性能。

(当我说“糟糕的哈希”时,我的意思是:

hashCode()
{
    return 0;
}

是的,它本身就很快,但会导致你的哈希映射成为一个链表。)

ialiashkevich使用数组和字典来编写一些C#代码来比较这两种方法,但是它使用了Long值作为键。我想测试在查找期间实际执行散列函数的东西,所以我修改了那段代码。我将其更改为使用String值,并将populate和lookup部分重构为自己的方法,以便在分析器中更容易看到。我还留下了使用Long值的代码,作为比较点。最后,我摆脱了自定义二进制搜索功能,并使用了Array类中的那个。

这是代码:

class Program
{
    private const long capacity = 10_000_000;

    private static void Main(string[] args)
    {
        testLongValues();
        Console.WriteLine();
        testStringValues();

        Console.ReadLine();
    }

    private static void testStringValues()
    {
        Dictionary<String, String> dict = new Dictionary<String, String>();
        String[] arr = new String[capacity];
        Stopwatch stopwatch = new Stopwatch();

        Console.WriteLine("" + capacity + " String values...");

        stopwatch.Start();

        populateStringArray(arr);

        stopwatch.Stop();
        Console.WriteLine("Populate String Array:      " + stopwatch.ElapsedMilliseconds);

        stopwatch.Reset();
        stopwatch.Start();

        populateStringDictionary(dict, arr);

        stopwatch.Stop();
        Console.WriteLine("Populate String Dictionary: " + stopwatch.ElapsedMilliseconds);

        stopwatch.Reset();
        stopwatch.Start();

        Array.Sort(arr);

        stopwatch.Stop();
        Console.WriteLine("Sort String Array:          " + stopwatch.ElapsedMilliseconds);

        stopwatch.Reset();
        stopwatch.Start();

        searchStringDictionary(dict, arr);

        stopwatch.Stop();
        Console.WriteLine("Search String Dictionary:   " + stopwatch.ElapsedMilliseconds);

        stopwatch.Reset();
        stopwatch.Start();

        searchStringArray(arr);

        stopwatch.Stop();
        Console.WriteLine("Search String Array:        " + stopwatch.ElapsedMilliseconds);

    }

    /* Populate an array with random values. */
    private static void populateStringArray(String[] arr)
    {
        for (long i = 0; i < capacity; i++)
        {
            arr[i] = generateRandomString(20) + i; // concatenate i to guarantee uniqueness
        }
    }

    /* Populate a dictionary with values from an array. */
    private static void populateStringDictionary(Dictionary<String, String> dict, String[] arr)
    {
        for (long i = 0; i < capacity; i++)
        {
            dict.Add(arr[i], arr[i]);
        }
    }

    /* Search a Dictionary for each value in an array. */
    private static void searchStringDictionary(Dictionary<String, String> dict, String[] arr)
    {
        for (long i = 0; i < capacity; i++)
        {
            String value = dict[arr[i]];
        }
    }

    /* Do a binary search for each value in an array. */
    private static void searchStringArray(String[] arr)
    {
        for (long i = 0; i < capacity; i++)
        {
            int index = Array.BinarySearch(arr, arr[i]);
        }
    }

    private static void testLongValues()
    {
        Dictionary<long, long> dict = new Dictionary<long, long>(Int16.MaxValue);
        long[] arr = new long[capacity];
        Stopwatch stopwatch = new Stopwatch();

        Console.WriteLine("" + capacity + " Long values...");

        stopwatch.Start();

        populateLongDictionary(dict);

        stopwatch.Stop();
        Console.WriteLine("Populate Long Dictionary: " + stopwatch.ElapsedMilliseconds);

        stopwatch.Reset();
        stopwatch.Start();

        populateLongArray(arr);

        stopwatch.Stop();
        Console.WriteLine("Populate Long Array:      " + stopwatch.ElapsedMilliseconds);

        stopwatch.Reset();
        stopwatch.Start();

        searchLongDictionary(dict);

        stopwatch.Stop();
        Console.WriteLine("Search Long Dictionary:   " + stopwatch.ElapsedMilliseconds);

        stopwatch.Reset();
        stopwatch.Start();

        searchLongArray(arr);

        stopwatch.Stop();
        Console.WriteLine("Search Long Array:        " + stopwatch.ElapsedMilliseconds);
    }

    /* Populate an array with long values. */
    private static void populateLongArray(long[] arr)
    {
        for (long i = 0; i < capacity; i++)
        {
            arr[i] = i;
        }
    }

    /* Populate a dictionary with long key/value pairs. */
    private static void populateLongDictionary(Dictionary<long, long> dict)
    {
        for (long i = 0; i < capacity; i++)
        {
            dict.Add(i, i);
        }
    }

    /* Search a Dictionary for each value in a range. */
    private static void searchLongDictionary(Dictionary<long, long> dict)
    {
        for (long i = 0; i < capacity; i++)
        {
            long value = dict[i];
        }
    }

    /* Do a binary search for each value in an array. */
    private static void searchLongArray(long[] arr)
    {
        for (long i = 0; i < capacity; i++)
        {
            int index = Array.BinarySearch(arr, arr[i]);
        }
    }

    /**
     * Generate a random string of a given length.
     * Implementation from https://stackoverflow.com/a/1344258/1288
     */
    private static String generateRandomString(int length)
    {
        var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        var stringChars = new char[length];
        var random = new Random();

        for (int i = 0; i < stringChars.Length; i++)
        {
            stringChars[i] = chars[random.Next(chars.Length)];
        }

        return new String(stringChars);
    }
}

以下是几个不同大小的集合的结果。 (时间以毫秒为单位。)

  

500000长值...
  Populate Long Dictionary:26
  填充长阵列:2
  搜索长词典:9
  搜索长阵列:80

     

500000字符串值...
  填充字符串数组:1237
  填充字符串字典:46
  排序字符串数组:1755
  搜索字符串字典:27
  搜索字符串数组:1569

     

1000000长值...
  Populate Long Dictionary:58
  填充长阵列:5
  搜索长词典:23
  搜索长阵列:136

     

1000000字符串值...
  填充字符串数组:2070
  填充字符串字典:121
  排序字符串数组:3579
  搜索字符串词典:58
  搜索字符串数组:3267

     

3000000长值...
  Populate Long Dictionary:207
  填充长阵列:14
  搜索长词典:75
  搜索长阵列:435

     

3000000字符串值...
  填充字符串数组:5553
  填充字符串字典:449
  排序字符串数组:11695
  搜索字符串字典:194
  搜索字符串数组:10594

     

10000000长值...
  Populate Long Dictionary:521
  填充长阵列:47
  搜索长词典:202
  搜索长阵列:1181

     

10000000字符串值...
  填充字符串数组:18119
  填充字符串字典:1088
  排序字符串数组:28174
  搜索字符串字典:747
  搜索字符串数组:26503

为了进行比较,这是程序最后一次运行的分析器输出(1000万条记录和查找)。我强调了相关的功能。他们非常赞同上面的秒表计时指标。

Profiler output for 10 million records and lookups

您可以看到字典查找比二进制搜索快得多,并且(如预期的那样)收集越大,差异越明显。因此,如果你有一个合理的散列函数(相当快的几次冲突),哈希查找应该胜过这个范围内的集合的二进制搜索。

答案 1 :(得分:37)

Bobby,Bill和Corbin的回答是错误的。对于固定/有界n,O(1)不比O(log n)慢:

log(n)是常量,因此它取决于常数时间。

对于慢速哈希函数,有没有听说过md5?

默认的字符串哈希算法可能会触及所有字符,并且比长字符串键的平均比较速度慢100倍。去过也做过。

您可能(部分)使用基数。如果您可以拆分256个大致相同的块,那么您可以查看2k到40k的二进制搜索。这可能会提供更好的表现。

[编辑] 太多人投票决定他们不理解的事情。

二进制搜索的字符串比较有序集具有一个非常有趣的属性:它们越接近目标就越慢。首先,他们将打破第一个角色,最后只打破最后一个角色。假设它们的恒定时间是不正确的。

答案 2 :(得分:20)

好的,我会尽量做空。

C#简答:

测试两种不同的方法。

.NET为您提供了使用一行代码更改方法的工具。 否则使用System.Collections.Generic.Dictionary并确保使用较大的数字初始化它作为初始容量,或者由于GC必须采取的工作来收集旧的存储桶阵列,因此您将终生插入项目。

更长的答案:

哈希表具有ALMOST常量查找时间,并且在现实世界中获取哈希表中的项目不仅需要计算哈希值。

要获取某个项目,您的哈希表将执行以下操作:

  • 获取密钥的哈希值
  • 获取该哈希的桶号(通常地图函数看起来像这个桶=哈希%bucketsCount)
  • 遍历项目链(基本上它是共享项目的列表 同一个桶,大多数哈希表使用 这种处理桶/哈希的方法 碰撞)从那开始 桶和比较每个键 你想要的其中一个项目 添加/删除/更新/检查是否 含有。

查找时间取决于“好”(输出的稀疏程度)和快速是您的哈希函数,您使用的桶数以及密钥比较器的速度,它并不总是最佳解决方案。

更好更深入的解释:http://en.wikipedia.org/wiki/Hash_table

答案 3 :(得分:18)

这个问题唯一合理的答案是:这取决于。这取决于数据的大小,数据的形状,哈希实现,二进制搜索实现以及数据的存在位置(即使问题中未提及)。还有其他几个答案,所以我可以删除它。但是,将我从反馈中学到的东西分享到原来的答案可能会很好。

  1. 我写道,“哈希算法是O(1)而二进制搜索是O(log n)。” - 如评论中所述,Big O表示法估计复杂性,而不是速度。这绝对是真的。值得注意的是,我们通常使用复杂性来了解算法的时间和空间要求。因此,虽然假设复杂性与速度完全相同是愚蠢的,但是在脑海中没有时间或空间的情况下估计复杂性是不寻常的。我的建议:避免使用Big O表示法。
  2. 我写道,“所以,当n接近无限 ......” - 这是关于我能在回答中包含的最蠢的事情。无限与你的问题无关。你提到1000万的上限。忽略无限。正如评论者指出的那样,非常大的数字会产生哈希的各种问题。 (非常大的数字也不能让二元搜索在公园散步。)我的建议:除非你的意思是无穷大,否则不要提及无穷大。
  3. 同样来自评论:当心默认字符串哈希(你是否哈希字符串?你没有提及。),数据库索引往往是b-trees(值得深思)。我的建议:考虑所有选择。考虑其他数据结构和方法......如旧式trie(用于存储和检索字符串)或R-tree(用于空间数据)或MA-FSA(最小非循环有限状态)自动机 - 占用空间小。)
  4. 鉴于这些评论,您可能会认为使用哈希表的人会被混乱。哈希表是鲁莽和危险的吗?这些人疯了吗?

    原来他们不是。正如二叉树在某些事物上的优点(有序数据遍历,存储效率)一样,哈希表也有其发光的时刻。特别是,它们可以非常好地减少获取数据所需的读取次数。哈希算法可以生成一个位置并在内存或磁盘上直接跳转到它,而二进制搜索在每次比较期间读取数据以决定接下来要读取什么。每次读取都有可能发生缓存未命中,这比CPU指令慢一个数量级(或更多)。

    这并不是说哈希表比二进制搜索更好。他们不是。它也不是建议所有哈希和二进制搜索实现都是相同的。他们不是。如果我有一个观点,就是这样:两种方法都存在是有原因的。由您来决定哪种方法最适合您的需求。

    原始答案:


      

    哈希算法是O(1),而二进制搜索是O(log n)。所以n   接近无穷大,哈希性能相对于二进制提高   搜索。您的里程数将根据您的哈希值而变化   实现,以及您的二进制搜索实现。

         

    Interesting discussion on O(1)。转述:

         

    O(1)并不意味着瞬间。这意味着性能没有   随着n的大小变化而变化。您可以设计散列算法   这是如此之慢,没有人会使用它,它仍然是O(1)。   我很确定.NET / C#不会遭受成本过高的散列,   然而;)

答案 4 :(得分:7)

如果您的对象集是真正静态且不变的,则可以使用perfect hash来保证O(1)性能。我已经看过几次gperf,虽然我从来没有机会亲自使用它。

答案 5 :(得分:6)

哈希值通常更快,但二进制搜索具有更好的最坏情况特征。散列访问通常是计算以获取散列值以确定记录将在哪个“桶”中,因此性能通常取决于记录分布的均匀程度以及用于搜索存储桶的方法。通过桶进行线性搜索的错误哈希函数(留下一些包含大量记录的桶)将导致搜索速度变慢。 (第三方面,如果您正在读取磁盘而不是内存,则散列桶可能是连续的,而二叉树几乎可以保证非本地访问。)

如果您想要快速,请使用哈希。如果你真的想要保证有限的性能,你可以使用二叉树。

答案 6 :(得分:6)

惊讶没有人提到Cuckoo哈希,它提供了保证的O(1),并且与完美哈希不同,它能够使用它分配的所有内存,其中完美哈希最终可以保证O(1)但浪费更大部分分配。警告?插入时间可能非常慢,特别是随着元素数量的增加,因为所有优化都是在插入阶段执行的。

我相信它的某些版本在路由器硬件中用于ip查找。

请参阅link text

答案 7 :(得分:4)

Dictionary / Hashtable使用更多内存,并且需要更多时间来填充数组。 但是,通过Dictionary而不是数组内的二进制搜索,搜索速度更快。

以下是要搜索和填充的 10 百万 Int64 项目的数字。 加上您可以自己运行的示例代码。

字典记忆: 462,836

阵列记忆: 88,376

填充词典: 402

填充数组: 23

搜索词典: 176

搜索数组: 680

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace BinaryVsDictionary
{
    internal class Program
    {
        private const long Capacity = 10000000;

        private static readonly Dictionary<long, long> Dict = new Dictionary<long, long>(Int16.MaxValue);
        private static readonly long[] Arr = new long[Capacity];

        private static void Main(string[] args)
        {
            Stopwatch stopwatch = new Stopwatch();

            stopwatch.Start();

            for (long i = 0; i < Capacity; i++)
            {
                Dict.Add(i, i);
            }

            stopwatch.Stop();

            Console.WriteLine("Populate Dictionary: " + stopwatch.ElapsedMilliseconds);

            stopwatch.Reset();

            stopwatch.Start();

            for (long i = 0; i < Capacity; i++)
            {
                Arr[i] = i;
            }

            stopwatch.Stop();

            Console.WriteLine("Populate Array:      " + stopwatch.ElapsedMilliseconds);

            stopwatch.Reset();

            stopwatch.Start();

            for (long i = 0; i < Capacity; i++)
            {
                long value = Dict[i];
//                Console.WriteLine(value + " : " + RandomNumbers[i]);
            }

            stopwatch.Stop();

            Console.WriteLine("Search Dictionary:   " + stopwatch.ElapsedMilliseconds);

            stopwatch.Reset();

            stopwatch.Start();

            for (long i = 0; i < Capacity; i++)
            {
                long value = BinarySearch(Arr, 0, Capacity, i);
//                Console.WriteLine(value + " : " + RandomNumbers[i]);
            }

            stopwatch.Stop();

            Console.WriteLine("Search Array:        " + stopwatch.ElapsedMilliseconds);

            Console.ReadLine();
        }

        private static long BinarySearch(long[] arr, long low, long hi, long value)
        {
            while (low <= hi)
            {
                long median = low + ((hi - low) >> 1);

                if (arr[median] == value)
                {
                    return median;
                }

                if (arr[median] < value)
                {
                    low = median + 1;
                }
                else
                {
                    hi = median - 1;
                }
            }

            return ~low;
        }
    }
}

答案 8 :(得分:3)

我强烈怀疑在大小为〜1M的问题集中,散列会更快。

仅供参考:

二进制搜索需要~20比较(2 ^ 20 == 1M)

哈希查找需要对搜索关键字进行1次哈希计算,之后可能会进行少量比较以解决可能的冲突

编辑:数字:

    for (int i = 0; i < 1000 * 1000; i++) {
        c.GetHashCode();
    }
    for (int i = 0; i < 1000 * 1000; i++) {
        for (int j = 0; j < 20; j++)
            c.CompareTo(d);
    }

次:c =“abcde”,d =“rwerij”哈希码:0.0012秒。比较:2.4秒。

免责声明:实际上,对哈希查找和二进制查找进行基准测试可能比这个不完全相关的测试更好。我甚至不确定GetHashCode是否在引擎盖下被记忆

答案 9 :(得分:2)

我会说它主要取决于散列和比较方法的性能。例如,当使用非常长但随机的字符串键时,比较将始终产生非常快速的结果,但默认的哈希函数将处理整个字符串。

但在大多数情况下,哈希映射应该更快。

答案 10 :(得分:2)

我想知道为什么没有人提到perfect hashing

只有你的数据集长时间固定才有意义,但它的作用是分析数据并构建一个完美的哈希函数,确保不会发生冲突。

非常简洁,如果您的数据集是常量,并且与应用程序运行时间相比,计算函数的时间很短。

答案 11 :(得分:1)

这取决于您如何处理哈希表的重复项(如果有的话)。如果你确实想要允许散列键重复(没有散列函数是完美的),它仍然是主键查找的O(1),但搜索“正确”值可能是昂贵的。那么,理论上,大多数情况下,哈希更快。 YMMV取决于你放在那里的数据......

答案 12 :(得分:1)

Here 它描述了如何构建哈希,并且因为密钥的Universe相当大并且哈希函数被构建为“非常单射”,因此冲突很少发生,哈希表的访问时间不是O( 1)实际上...它是基于某些概率的东西。 但是,可以合理地说哈希的访问时间几乎总是小于时间O(log_2(n))

答案 13 :(得分:0)

当然,对于如此庞大的数据集,哈希是最快的。

由于数据很少发生变化,一种加快速度的方法是以编程方式生成ad-hoc代码,将第一层搜索作为一个巨大的switch语句(如果你的编译器可以处理它),然后分支关闭以搜索生成的存储桶。

答案 14 :(得分:0)

答案取决于。让我们认为元素'n'的数量非常大。如果你擅长编写一个较小冲突的更好的哈希函数,那么哈希就是最好的。 请注意 哈希函数在搜索时仅执行一次,并且它指向相应的桶。因此,如果n很高,这不是一个很大的开销    Hashtable中的问题: 但是哈希表中的问题是如果哈希函数不好(发生更多冲突),那么搜索不是O(1)。它倾向于O(n),因为在桶中搜索是线性搜索。可能比二叉树更糟糕。   二进制树中的问题: 在二叉树中,如果树不平衡,它也倾向于O(n)。例如,如果您将1,2,3,4,5插入到更可能是列表的二叉树中。   的所以, 如果您可以看到良好的哈希方法,请使用哈希表 如果没有,最好使用二叉树。

答案 15 :(得分:0)

这更多是对比尔的回答的评论,因为即使他的回答有错,他的回答也是如此多。所以我不得不发布这个。

我看到很多讨论,涉及哈希表中查找的最坏情况的复杂性是什么,什么才算是摊销分析,什么不是。 请检查下面的链接

Hash table runtime complexity (insert, search and delete)

与Bill所说的相反,最坏的情况是O(n)而不是O(1)。因此,他的O(1)复杂度不会摊销,因为该分析只能用于最坏的情况(他自己的Wikipedia链接也是如此)

https://en.wikipedia.org/wiki/Hash_table

https://en.wikipedia.org/wiki/Amortized_analysis

答案 16 :(得分:0)

这个问题比纯算法性能的范围还复杂。如果除去二进制搜索算法对缓存更友好的因素,则从一般意义上讲,哈希查找将更快。找出最佳方法是构建程序并禁用编译器优化选项,并且由于一般算法时间效率为O(1),我们可以发现哈希查找的速度更快。

但是,当您启用编译器优化并尝试使用较少数量的样本(例如少于10,000个)进行相同的测试时,二进制搜索会利用其缓存友好的数据结构胜过哈希查找。