web-dev-qa-db-ja.com

ハッシュ検索とバイナリ検索のどちらが速いですか?

静的なオブジェクトのセット(一度ロードすると変更されることはめったにないという意味で静的)が与えられ、最適なパフォーマンスでより良い同時検索が必要な場合、HashMapまたはバイナリ検索の配列カスタムコンパレータを使用していますか?

答えはオブジェクトまたは構造体タイプの関数ですか?ハッシュおよび/または同等の機能パフォーマンス?ハッシュの一意性?リストサイズ? Hashset size/set size?

私が見ているセットのサイズは、500kから10mの範囲である可能性があります-その情報が役立つ場合。

私はC#の答えを探していますが、真の数学的な答えは言語にはないので、そのタグは含めません。ただし、C#固有の注意事項がある場合は、その情報が必要です。

64
TheSoftwareJedi

さて、私は短くしようとします。

C#の短い答え:

2つの異なるアプローチをテストします。

.NETは、コード行を使用してアプローチを変更するツールを提供します。そうでない場合は、System.Collections.Generic.Dictionaryを使用して、初期容量として大きな数で初期化してください。そうしないと、GCが古いバケット配列を収集するためにアイテムを挿入して残りの人生を過ごすことになります。

より長い答え:

ハッシュテーブルにはALMOST定数のルックアップ時間があり、現実世界のハッシュテーブルの項目に到達するためには、ハッシュを計算するだけではありません。

アイテムに到達するために、ハッシュテーブルは次のようなことをします。

  • キーのハッシュを取得します
  • そのハッシュのバケット番号を取得します(通常、マップ関数はこのバケット= hash%bucketsCountのようになります)
  • そのバケットで開始し、追加しようとしているアイテムの1つと各キーを比較するアイテムチェーン(基本的には同じバケットを共有するアイテムのリストです。ほとんどのハッシュテーブルはバケット/ハッシュの衝突を処理するこの方法を使用します)含まれている場合は削除/更新/確認します。

ルックアップ時間は、「良好」(スパースの出力)とハッシュ関数の速さ、使用しているバケットの数、キー比較器の速さに依存しますが、必ずしも最良のソリューションとは限りません。

より適切で詳細な説明: http://en.wikipedia.org/wiki/Hash_table

20
Maghis

非常に小さなコレクションの場合、違いはごくわずかです。範囲の下限(500,000アイテム)で、多くのルックアップを行っている場合、違いが見え始めます。バイナリ検索はO(log n)になりますが、ハッシュルックアップはO(1)、 amortized になります。これは真に一定ではありませんが、バイナリ検索よりもパフォーマンスが低下するためには、かなりひどいハッシュ関数が必要です。

(「ひどいハッシュ」と言うとき、私は次のようなものを意味します:

hashCode()
{
    return 0;
}

ええ、それ自体は非常に高速ですが、ハッシュマップがリンクリストになります。)

ialiashkevich は、配列と辞書を使用して2つのメソッドを比較する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長い値...
長い辞書を作成:26
長い配列に入力:2
長い辞書を検索:9
長い配列の検索:80

500000文字列値...
人口配列:1237
文字列辞書に入力:46
文字列配列の並べ替え:1755
検索文字列辞書:27
検索文字列配列:1569

1000000長い値...
長い辞書を作成:58
長配列の配置:5
長い辞書を検索:23
長い配列を検索:136

1000000文字列値...
人口配列:2070
Populate String Dictionary:121
文字列配列の並べ替え:3579
検索文字列辞書:58
検索文字列配列:3267

3000000長い値...
長い辞書を作成:207
長配列の配置:14
長い辞書を検索:75
長い配列を検索:435

3000000文字列値...
人口ストリング配列:5553
Populate String Dictionary:449
ソート文字列配列:11695
検索文字列辞書:194
検索文字列配列:10594

10000000長い値...
長い辞書を作成:521
長配列の配置:47
長い辞書を検索:202
長い配列を検索:1181

10000000文字列値...
人口配列:18119
人口辞書:1088
ソート文字列配列:28174
検索文字列辞書:747
検索文字列配列:26503

比較のために、プログラムの最後の実行(1000万件のレコードとルックアップ)のプロファイラー出力を以下に示します。関連する機能を強調しました。彼らは、上記のストップウォッチのタイミングメトリックにかなり同意しています。

Profiler output for 10 million records and lookups

辞書検索はバイナリ検索よりもはるかに高速であり、(予想どおり)コレクションが大きいほど差が大きくなることがわかります。したがって、合理的なハッシュ関数(衝突がほとんどないかなり高速)を持っている場合、ハッシュルックアップはこの範囲のコレクションのバイナリ検索に勝るものです。

48
Bill the Lizard

ボビー、ビル、コービンの答えは間違っています。 O(1)は、固定/有界nに対してO(log n)より遅くありません:

log(n)は定数なので、一定時間に依存します。

そして、遅いハッシュ関数のために、md5のことを聞いたことがありますか?

デフォルトの文字列ハッシュアルゴリズムはおそらくすべての文字に影響し、長い文字列キーの平均比較よりも100倍遅くなる可能性があります。そこに行って、それをしました。

(部分的に)基数を使用できる場合があります。 256個のほぼ同じサイズのブロックに分割できる場合、2kから40kのバイナリ検索を見ています。これにより、パフォーマンスが大幅に向上する可能性があります。

[編集]理解できないことを投票する人が多すぎます。

バイナリ検索の文字列比較ソートセットには非常に興味深い特性があります。ターゲットに近づくほど遅くなります。最初に最初の文字で中断し、最後に最後の文字でのみ中断します。それらのために一定の時間を仮定することは間違っています。

37

この質問に対する唯一の合理的な答えは、次のとおりです。データのサイズ、データの形状、ハッシュの実装、バイナリ検索の実装、およびデータがどこに存在するかによって異なります(質問には記載されていませんが)。他にもいくつかの回答がありますので、これを削除するだけです。ただし、フィードバックから元の答えに学んだことを共有できたら嬉しいかもしれません。

  1. ハッシュアルゴリズムはO(1)で、バイナリ検索はO(log n)。)」と書きました-コメントで述べたように、Big O表記は速度ではなく複雑さを推定します。これは絶対に真実です。通常、アルゴリズムの時間とスペースの要件を把握するために複雑さを使用することに注意してください。速度と同じですが、心の奥にある時間やスペースなしで複雑さを推定することは珍しいです。
  2. nが無限に近づくにつれて...」と書いた-これは、答えに含めることができた最も愚かなことについてです。無限はあなたの問題とは何の関係もありません。上限は1,000万です。無限を無視します。コメント者が指摘しているように、非常に大きな数値はハッシュに関するあらゆる種類の問題を引き起こします。 (非常に大きな数でも、バイナリ検索は公園を散歩しません。)私の推奨事項:無限を意味する場合を除き、無限は言及しないでください。
  3. また、コメントから:デフォルトの文字列ハッシュに注意してください(文字列をハッシュしていますか?言及していません)。データベースインデックスはbツリー(思考の糧)です。私の推奨事項:すべてのオプションを検討してください。他のデータ構造とアプローチを検討してください...昔ながらの trie (文字列の保存と取得用)または R-tree (空間データ用)または- MA-FSA (最小非循環有限状態オートマトン-小さなストレージフットプリント)。

コメントを考えると、ハッシュテーブルを使用する人は混乱していると思われるかもしれません。ハッシュテーブルは無謀で危険ですか?これらの人々は非常識ですか?

違います。バイナリツリーが特定のもの(順序どおりのデータトラバーサル、ストレージ効率)に優れているのと同様に、ハッシュテーブルも同様に輝きを放ちます。特に、データをフェッチするために必要な読み取り回数を減らすのに非常に役立ちます。ハッシュアルゴリズムは場所を生成し、メモリ内またはディスク上の場所に直接ジャンプしますが、バイナリ検索は各比較中にデータを読み取り、次に読み取る内容を決定します。各読み取りには、CPU命令よりも1桁(またはそれ以上)遅いキャッシュミスの可能性があります。

ハッシュテーブルがバイナリ検索よりも優れていると言っているわけではありません。そうではありません。また、すべてのハッシュとバイナリ検索の実装が同じであることを示唆するものでもありません。そうではありません。私がポイントを持っている場合、それはこれです:理由は両方のアプローチが存在します。どちらがあなたのニーズに最適かを決めるのはあなた次第です。

元の回答:


ハッシュアルゴリズムはO(1)で、バイナリ検索はO(log n)です。nが無限に近づくにつれて、ハッシュ検索のパフォーマンスはバイナリ検索に比べて向上します。燃費はn、ハッシュによって異なります実装、およびバイナリ検索の実装。

O(1)に関する興味深い議論 。言い換え:

O(1) doesn't mean instantaneous. It means that the performance doesn't change as the size of n grows. You can design a hashing algorithm that's so slow no one would ever use it and it would still be O(1). I'm fairly sure .NET/C# doesn't suffer from cost-prohibitive hashing, however ;)

18
Corbin March

オブジェクトのセットが本当に静的で変化しない場合、 完全なハッシュ を使用してO(1)のパフォーマンスを保証します。 gperf 何度か言及しましたが、自分で使用する機会は一度もありませんでした。

7
Mark Ransom

驚いた人はカッコウハッシュについて言及していませんでした。これは、保証されたO(1)を提供し、完全なハッシュとは異なり、割り当てられたすべてのメモリを使用できます。完全なハッシュは保証されたO(1)しかし、割り当ての大部分を無駄にします。警告?最適化はすべて挿入フェーズで実行されるため、特に要素の数が増えると、挿入時間が非常に遅くなる可能性があります。

これのいくつかのバージョンは、IPルックアップ用のルーターハードウェアで使用されると思います。

リンクテキスト を参照してください

6
ApplePieIsGood

通常、ハッシュは高速ですが、バイナリ検索の方がワーストケースの特性が優れています。ハッシュアクセスは通常、ハッシュ値を取得してレコードがどの「バケット」に入るかを決定するための計算であるため、パフォーマンスは通常、レコードがどの程度均等に分散されるか、およびバケットの検索に使用される方法に依存します。バケット内の線形検索での不正なハッシュ関数(多数のレコードを含むいくつかのバケットを残す)は、検索が遅くなります。 (3番目に、メモリではなくディスクを読み取る場合、ハッシュバケットは連続している可能性が高く、バイナリツリーは非ローカルアクセスをほぼ保証します。)

一般的に高速にしたい場合は、ハッシュを使用します。保証された制限付きパフォーマンスが本当に必要な場合は、バイナリツリーを使用できます。

6
David Thornley

辞書/ハッシュテーブルはより多くのメモリを使用しており、配列と比較してデータを取り込むのにより多くの時間がかかります。ただし、配列内のバイナリ検索ではなく、ディクショナリによって検索が高速に行われます。

1ミリオンのInt64アイテムの数を以下に示します。さらに、自分で実行できるサンプルコード。

辞書メモリ:462,836

アレイメモリ:88,376

辞書を作成:402

配列に入力:2

検索辞書:176

検索配列:68

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;
        }
    }
}
4
ialiashkevich

サイズが〜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が内部でメモされているかどうかさえわかりません

3
Jimmy

主にハッシュと比較メソッドのパフォーマンスに依存すると思います。たとえば、非常に長いがランダムな文字列キーを使用する場合、比較は常に非常に迅速な結果をもたらしますが、デフォルトのハッシュ関数は文字列全体を処理します。

ただし、ほとんどの場合、ハッシュマップの方が高速です。

2

なぜ誰も言及していないのだろうか 完全なハッシュ

データセットが長期間固定されている場合にのみ関係がありますが、それが行うことは、データを分析し、衝突がないことを保証する完全なハッシュ関数を構築します。

データセットが一定で、関数の計算時間がアプリケーションの実行時間に比べて短い場合は、かなりきれいです。

2
orip

ここ ハッシュの構築方法と、キーのユニバースが適度に大きく、ハッシュ関数が「非常に単射的」であるように構築されているため、ハッシュテーブルのアクセス時間がO(1)実際には...これはいくつかの確率に基づいています。しかし、ハッシュのアクセス時間はほとんどの場合、O(log_2(n))の時間よりも短いと言うのが妥当です

1
xxxxxxx

ハッシュテーブルの重複を処理する方法(ある場合)によって異なります。ハッシュキーの複製を許可したい場合(完全なハッシュ関数はありません)、プライマリキールックアップの場合はO(1)のままですが、「正しい」値の背後での検索にはコストがかかる場合があります。理論的にはほとんどの場合、ハッシュは高速になりますYMMVはそこに置くデータによって異なります...

1
Keltia

この質問は、純粋なアルゴリズムのパフォーマンスの範囲よりも複雑です。バイナリ検索アルゴリズムがよりキャッシュフレンドリーである要因を削除すると、一般的な意味でハッシュルックアップが高速になります。プログラムを構築してコンパイラ最適化オプションを無効にするのが最善の方法です。アルゴリズムの時間効率が一般的な意味でO(1)である場合、ハッシュ検索はより高速であることがわかります。

しかし、コンパイラの最適化を有効にして、10,000個未満のサンプル数で同じテストを試みると、キャッシュに優しいデータ構造を利用して、バイナリ検索はハッシュ検索よりも優れていました。

0
Richard Lin

もちろん、このような大きなデータセットの場合、ハッシュは最速です。

データを変更することはほとんどないため、さらに高速化する1つの方法は、プログラムでアドホックコードを生成し、検索の最初のレイヤーを巨大なswitchステートメント(コンパイラーで処理できる場合)として実行し、検索に分岐することです。結果のバケット。

0
Mike Dunlavey

答えは異なります。要素の数「n」が非常に大きいと考えてみましょう。衝突の少ない、より良いハッシュ関数を書くのが得意であれば、ハッシュが最適です。 ハッシュ関数は検索時に一度だけ実行され、対応するバケットに向けられます。したがって、nが大きい場合、大きなオーバーヘッドではありません。
Hashtableの問題:しかし、ハッシュテーブルの問題は、ハッシュ関数が適切でない場合(さらに衝突が発生する場合)、検索がO(1)ではないことです。 O(n)である傾向があります。バケット内の検索は線形検索であるためです。バイナリツリーよりも最悪の場合があります。バイナリツリーの問題:バイナリツリーでは、ツリーのバランスが取れていない場合、O(n)の傾向もあります(たとえば、1,2,3,4,5をリストである可能性が高いバイナリツリーに挿入した場合)So、適切なハッシュ方法論が見られる場合は、ハッシュテーブルを使用します。そうでない場合は、バイナリツリーを使用する方がよいでしょう。

0
Lahiru

これは、ビルの答えに対するコメントです。なぜなら、彼の答えは間違っていても非常に多くの賛成票を持っているからです。だから私はこれを投稿しなければなりませんでした。

ハッシュテーブルでのルックアップの最悪のケースの複雑さ、および償却分析と見なされるもの/そうでないものについて、多くの議論があります。以下のリンクを確認してください

ハッシュテーブルランタイムの複雑さ(挿入、検索、削除)

最悪の場合の複雑さは、O(n)であり、O(1)ではなく、ビルの言うこととは異なります。したがって、彼のO(1)この分析は最悪の場合にしか使用できないため、複雑さは償却されません(彼自身のウィキペディアのリンクもそう言っています)

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

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

0
homeOfTheWizard