Что быстрее, поиск по хэшу или бинарный поиск? - PullRequest
64 голосов
/ 11 декабря 2008

Когда задан статический набор объектов (статический в том смысле, что однажды его загрузили, он редко, если вообще меняется), в который требуется повторный параллельный поиск с оптимальной производительностью, что лучше, HashMap или массив с двоичным поиском используя какой-то пользовательский компаратор?

Является ли ответ функцией типа объекта или структуры? Хэш и / или Равная производительность функции? Уникальность хеша? Размер списка? Hashset размер / установленный размер?

Размер набора, на который я смотрю, может быть от 500 до 10 метров, если эта информация полезна.

Пока я ищу ответ C #, я думаю, что истинный математический ответ не в языке, поэтому я не включаю этот тег. Однако, если есть какие-то специфичные для C # вещи, о которых нужно знать, эта информация желательна.

Ответы [ 16 ]

48 голосов
/ 11 декабря 2008

Для очень маленьких коллекций разница будет незначительной. В нижней части вашего диапазона (500 тыс. Предметов) вы начнете видеть разницу, если вы делаете много поисков. Бинарный поиск будет O (log n), тогда как поиск по хешу будет O (1), амортизируется . Это не то же самое, что действительно константа, но вам все равно придется иметь довольно ужасную хеш-функцию, чтобы получить худшую производительность, чем бинарный поиск.

(Когда я говорю «ужасный хэш», я имею в виду что-то вроде:

hashCode()
{
    return 0;
}

Да, он сам по себе очень быстрый, но превращает вашу хэш-карту в связанный список.)

ialiashkevich написал некоторый код C #, используя массив и словарь для сравнения двух методов, но он использовал длинные значения для ключей. Я хотел протестировать что-то, что фактически выполняло бы хеш-функцию во время поиска, поэтому я изменил этот код. Я изменил его, чтобы использовать значения String, и я реорганизовал разделы заполнения и поиска в свои собственные методы, чтобы их было легче увидеть в профилировщике. Я также оставил в коде, который использовал значения 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
Заполните словарь строк: 121
Массив строк сортировки: 3579
Поиск строки словаря: 58
Массив строк поиска: 3267

3000000 Длинные значения ...
Заполните длинный словарь: 207
Заполните длинный массив: 14
Поиск длинного словаря: 75
Поиск длинного массива: 435

3000000 Строковые значения ...
Заполните массив строк: 5553
Заполните словарь строк: 449
Массив строк сортировки: 11695
Поиск строки словарь: 194
Массив строк поиска: 10594

10000000 Длинные значения ...
Заполните длинный словарь: 521
Заполните длинный массив: 47
Поиск длинного словаря: 202
Длинный массив поиска: 1181

10000000 Строковые значения ...
Заполнить строковый массив: 18119
Заполните словарь строк: 1088
Массив строк сортировки: 28174
Поиск строки словаря: 747
Массив строк поиска: 26503

И для сравнения, вот вывод профилировщика для последнего запуска программы (10 миллионов записей и поисков). Я выделил соответствующие функции. Они довольно близко согласуются с метриками хронометража секундомера выше.

Profiler output for 10 million records and lookups

Вы можете видеть, что поиск по словарю выполняется намного быстрее, чем бинарный поиск, и (как и ожидалось) различие тем сильнее, чем больше коллекция. Таким образом, если у вас есть разумная функция хеширования (довольно быстрая, с небольшим количеством коллизий), поиск по хешу должен превзойти двоичный поиск коллекций в этом диапазоне.

37 голосов
/ 11 декабря 2008

Ответы Бобби, Билла и Корбина неверны. O (1) не медленнее, чем O (log n) для фиксированного / ограниченного n:

log (n) постоянно, поэтому оно зависит от постоянного времени.

А для медленной хэш-функции когда-нибудь слышали о md5?

Алгоритм хеширования строк по умолчанию, вероятно, затрагивает все символы и может быть в 100 раз медленнее, чем среднее сравнение для длинных строковых ключей. Был там, сделал это.

Возможно, вы сможете (частично) использовать основание. Если вы можете разделить на 256 блоков примерно одинакового размера, вы ищете бинарный поиск от 2k до 40k. Это может обеспечить гораздо лучшую производительность.

[Изменить] Слишком много людей голосуют за то, чего не понимают.

Строковые сравнения для бинарного поиска отсортированных наборов обладают очень интересным свойством: они становятся медленнее, чем ближе к цели. Сначала они разбиваются на первого персонажа, в конце только на последнем. Предполагать, что постоянное время для них неверно.

20 голосов
/ 11 декабря 2008

Хорошо, я постараюсь быть коротким.

C # краткий ответ:

Проверьте два разных подхода.

.NET предоставляет вам инструменты для изменения вашего подхода с помощью строки кода. В противном случае используйте System.Collections.Generic.Dictionary и обязательно инициализируйте его с большим числом в качестве начальной емкости, иначе вы оставите остаток своей жизни, вставляя элементы из-за работы, которую GC должен выполнить для сбора старых массивов сегментов. *

Более длинный ответ:

Хеш-таблица имеет почти постоянное время поиска, и для получения элемента в хеш-таблице в реальном мире не требуется просто вычислять хеш.

Чтобы получить элемент, ваша хеш-таблица будет делать что-то вроде этого:

  • Получить хэш ключа
  • Получить номер корзины для этого хэша (обычно функция карты выглядит так: bucket = hash% bucketsCount)
  • Пройдите через цепочку предметов (в основном это список предметов, которые разделяют то же самое ведро, большинство хеш-таблиц используют этот метод обработки bucket / hash столкновения), который начинается в этом ведро и сравнить каждый ключ с один из предметов, которые вы пытаетесь добавить / удалить / обновить / проверить, если содержал.

Время поиска зависит от того, насколько «хорошо» (насколько разреженно выводится) и насколько быстро ваша хеш-функция, количество сегментов, которые вы используете, и насколько быстро сравнивает ключи, это не всегда лучшее решение.

Лучшее и более глубокое объяснение: http://en.wikipedia.org/wiki/Hash_table

18 голосов
/ 11 декабря 2008

Единственный разумный ответ на этот вопрос: это зависит. Это зависит от размера ваших данных, формы ваших данных, вашей реализации хеш-функции, вашей реализации двоичного поиска и того, где живут ваши данные (даже если это не упоминается в вопросе). Несколько других ответов говорят так же, так что я могу просто удалить это. Тем не менее, было бы неплохо поделиться тем, что я узнал из обратной связи с моим первоначальным ответом.

  1. Я написал: « Алгоритмы хеширования - это O (1), а бинарный поиск - O (log n). » - как отмечено в комментариях, система обозначений Big O оценивает сложность, а не скорость. Это абсолютно верно. Стоит отметить, что мы обычно используем сложность, чтобы понять требования алгоритма к времени и пространству. Так что, хотя глупо полагать, что сложность строго совпадает со скоростью, оценка сложности без времени и пространства в глубине души является необычной. Моя рекомендация: избегайте обозначений Big O.
  2. Я написал: " Так что n приближается к бесконечности ..." - это самое глупое, что я мог бы включить в ответ. Бесконечность не имеет ничего общего с вашей проблемой. Вы упоминаете верхнюю границу в 10 миллионов. Игнорировать бесконечность. Как указывают комментаторы, очень большие числа создадут всевозможные проблемы с хэшем. (Очень большие числа не делают бинарный поиск прогулкой по парку.) Моя рекомендация: не упоминайте бесконечность, если вы не имеете в виду бесконечность.
  3. Также из комментариев: остерегайтесь строковых хэшей по умолчанию (Вы хэшируете строки? Вы не упоминаете.), Индексы базы данных часто являются b-деревьями (пища для размышлений). Моя рекомендация: рассмотрите все ваши варианты. Рассмотрим другие структуры данных и подходы ... как старомодный trie (для хранения и извлечения строк) или R-дерево (для пространственных данных) или MA -FSA (минимальный ациклический конечный автомат - небольшой объем памяти).

Учитывая комментарии, вы можете предположить, что люди, которые используют хеш-таблицы, являются ненормальными. Хэш-таблицы безрассудны и опасны? Эти люди безумны?

Оказывается, это не так. Подобно тому, как двоичные деревья хороши в определенных вещах (обход данных по порядку, эффективность хранения), у хэш-таблиц тоже есть свой момент. В частности, они могут быть очень хороши при сокращении количества операций чтения, необходимых для получения ваших данных. Алгоритм хеширования может генерировать местоположение и переходить прямо к нему в памяти или на диске, в то время как бинарный поиск считывает данные во время каждого сравнения, чтобы решить, что читать дальше. Каждое чтение может привести к потере кэша, которая на порядок (или более) медленнее, чем инструкция процессора.

Это не значит, что хеш-таблицы лучше, чем бинарный поиск. Они не. Также нельзя утверждать, что все реализации хеширования и двоичного поиска одинаковы. Они не. Если у меня есть точка зрения, это так: оба подхода существуют по причине. Вам решать, что лучше для ваших нужд.

Оригинальный ответ:


Хеш-алгоритмы O (1), а бинарный поиск O (log n). Так как п приближается к бесконечности, производительность хэша улучшается относительно двоичного поиск. Ваш пробег будет варьироваться в зависимости от n вашего хэша реализация, и ваша реализация двоичного поиска.

Интересное обсуждение O (1) . Перефразировано:

O (1) не означает мгновенное. Это означает, что производительность не изменить как размер п растет. Вы можете разработать алгоритм хеширования это так медленно, что никто бы никогда не использовал его, и все равно это будет O (1). Я уверен, что .NET / C # не страдает от чрезмерно дорогостоящего хеширования, однако;)

7 голосов
/ 12 декабря 2008

Если ваш набор объектов действительно статичен и неизменен, вы можете использовать совершенный хеш , чтобы получить гарантированную производительность O (1). Я видел gperf , упомянутых несколько раз, хотя мне никогда не приходилось использовать его самому.

6 голосов
/ 12 декабря 2008

Удивительно, что никто не упомянул хеширование Cuckoo, которое обеспечивает гарантированный O (1) и, в отличие от идеального хеширования, способно использовать всю выделяемую память, в то время как идеальное хеширование может закончиться гарантированным O (1), но тратит больше часть его выделения. Предостережение? Время вставки может быть очень медленным, особенно с увеличением количества элементов, поскольку вся оптимизация выполняется на этапе вставки.

Я полагаю, что некоторая версия этого используется в аппаратном обеспечении маршрутизатора для поиска ip.

См. текст ссылки

6 голосов
/ 11 декабря 2008

Хеши обычно быстрее, хотя бинарный поиск имеет лучшие характеристики в худшем случае. Доступ к хешу обычно является вычислением, чтобы получить значение хеша, чтобы определить, в каком «сегменте» будет запись, и, таким образом, производительность, как правило, зависит от того, насколько равномерно распределены записи, и от метода, используемого для поиска в сегменте. Плохая хеш-функция (оставляя несколько сегментов с большим количеством записей) с линейным поиском по сегментам приведет к медленному поиску. (С другой стороны, если вы читаете диск, а не память, хэш-блоки, вероятно, будут смежными, в то время как двоичное дерево в значительной степени гарантирует нелокальный доступ.)

Если вы хотите вообще быстро, используйте хеш. Если вы действительно хотите гарантированно ограниченную производительность, вы можете использовать двоичное дерево.

4 голосов
/ 17 февраля 2015

Dictionary / Hashtable использует больше памяти и занимает больше времени для заполнения по сравнению с массивом. Но поиск выполняется быстрее с помощью словаря, а не двоичного поиска в массиве.

Вот числа для 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;
        }
    }
}
3 голосов
/ 11 декабря 2008

Я сильно подозреваю, что в проблемном наборе размером ~ 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 запоминается под капотом

2 голосов
/ 12 декабря 2008

Интересно, почему никто не упомянул идеальное хеширование .

Это актуально, только если ваш набор данных зафиксирован в течение длительного времени, но для чего он анализирует данные и создает идеальную хеш-функцию, которая гарантирует отсутствие коллизий.

Довольно аккуратно, если ваш набор данных постоянен и время для вычисления функции мало по сравнению со временем выполнения приложения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...