Подтвердить что ты не робот

O (N) медленнее алгоритма O (N logN)

В массиве чисел каждое число появляется четное количество раз, и только одно число появляется нечетным числом раз. Нам нужно найти это число (вопрос ранее обсуждался при переполнении стека).

Вот решение, которое решает вопрос тремя различными методами:— два метода - O (N) (hash_set и hash_map), а один - O (NlogN) (сортировка). Однако профилирование для произвольно большого ввода показывает, что сортировка выполняется быстрее и становится все быстрее (по сравнению) при увеличении ввода.

Что не так с реализацией или анализом сложности, почему метод O (NlogN) быстрее?

#include <algorithm>
#include <chrono>
#include <cmath>
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <unordered_set>
#include <unordered_map>

using std::cout;
using std::chrono::high_resolution_clock;
using std::chrono::milliseconds;
using std::endl;
using std::string;
using std::vector;
using std::unordered_map;
using std::unordered_set;

class ScopedTimer {
public:
    ScopedTimer(const string& name)
    : name_(name), start_time_(high_resolution_clock::now()) {}

    ~ScopedTimer() {
        cout << name_ << " took "
        << std::chrono::duration_cast<milliseconds>(
                                                    high_resolution_clock::now() - start_time_).count()
        << " milliseconds" << endl;
    }

private:
    const string name_;
    const high_resolution_clock::time_point start_time_;
};

int find_using_hash(const vector<int>& input_data) {
    unordered_set<int> numbers(input_data.size());
    for(const auto& value : input_data) {
        auto res = numbers.insert(value);
        if(!res.second) {
            numbers.erase(res.first);
        }
    }
    return numbers.size() == 1 ? *numbers.begin() : -1;
}

int find_using_hashmap(const vector<int>& input_data) {
    unordered_map<int,int> counter_map;
    for(const auto& value : input_data) {
        ++counter_map[value];
    }
    for(const auto& map_entry : counter_map) {
        if(map_entry.second % 2 == 1) {
            return map_entry.first;
        }
    }
    return -1;
}

int find_using_sort_and_count(const vector<int>& input_data) {
    vector<int> local_copy(input_data);
    std::sort(local_copy.begin(), local_copy.end());
    int prev_value = local_copy.front();
    int counter = 0;
    for(const auto& value : local_copy) {
        if(prev_value == value) {
            ++counter;
            continue;
        }

        if(counter % 2 == 1) {
            return prev_value;
        }

        prev_value = value;
        counter = 1;
    }
    return counter == 1 ? prev_value : -1;
}

void execute_and_time(const string& method_name, std::function<int()> method) {
    ScopedTimer timer(method_name);
    cout << method_name << " returns " << method() << endl;
}

int main()
{
    vector<int> input_size_vec({1<<18,1<<20,1<<22,1<<24,1<<28});

    for(const auto& input_size : input_size_vec) {
        // Prepare input data
        std::vector<int> input_data;
        const int magic_number = 123454321;
        for(int i=0;i<input_size;++i) {
            input_data.push_back(i);
            input_data.push_back(i);
        }
        input_data.push_back(magic_number);
        std::random_shuffle(input_data.begin(), input_data.end());
        cout << "For input_size " << input_size << ":" << endl;

        execute_and_time("hash-set:",std::bind(find_using_hash, input_data));
        execute_and_time("sort-and-count:",std::bind(find_using_sort_and_count, input_data));
        execute_and_time("hash-map:",std::bind(find_using_hashmap, input_data));

        cout << "--------------------------" << endl;
    }
    return 0;
}

Результаты профилирования:

sh$ g++ -O3 -std=c++11 -o main *.cc
sh$ ./main 
For input_size 262144:
hash-set: returns 123454321
hash-set: took 107 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 37 milliseconds
hash-map: returns 123454321
hash-map: took 109 milliseconds
--------------------------
For input_size 1048576:
hash-set: returns 123454321
hash-set: took 641 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 173 milliseconds
hash-map: returns 123454321
hash-map: took 731 milliseconds
--------------------------
For input_size 4194304:
hash-set: returns 123454321
hash-set: took 3250 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 745 milliseconds
hash-map: returns 123454321
hash-map: took 3631 milliseconds
--------------------------
For input_size 16777216:
hash-set: returns 123454321
hash-set: took 14528 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 3238 milliseconds
hash-map: returns 123454321
hash-map: took 16483 milliseconds
--------------------------
For input_size 268435456:
hash-set: returns 123454321
hash-set: took 350305 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 60396 milliseconds
hash-map: returns 123454321
hash-map: took 427841 milliseconds
--------------------------

Добавление

Быстрое решение с xor, предложенное @Matt, конечно, вне конкурса; менее 1 сек для наихудшего случая в примере:

int find_using_xor(const vector<int>& input_data) {
    int output = 0;
    for(const int& value : input_data) {
        output = output^value;
    }
    return output;
}
For input_size 268435456:
xor: returns 123454321
xor: took 264 milliseconds

но вопрос все еще стоит; почему хэш настолько неэффективен по сравнению с сортировкой на практике, несмотря на теоретическое преимущество алгоритмической сложности?

4b9b3361

Ответ 1

Это действительно зависит от реализации hash_map/hash_set. Заменив libstdС++ unordered_{map,set} на Google dense_hash_{map,set}, он значительно быстрее, чем sort. Недостатком для dense_hash_xxx является то, что они требуют наличия двух значений для ключа, которые никогда не будут использоваться. Подробнее см. В их документе.

Еще одна вещь, которую следует помнить: hash_{map,set} обычно выполняет много динамического распределения/освобождения памяти, поэтому лучше использовать лучшую альтернативу libc default malloc/free, например. Google tcmalloc или Facebook jemalloc.

hidden $ g++ -O3 -std=c++11 xx.cpp /usr/lib/libtcmalloc_minimal.so.4
hidden $ ./a.out 
For input_size 262144:
unordered-set: returns 123454321
unordered-set: took 35 milliseconds
dense-hash-set: returns 123454321
dense-hash-set: took 18 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 34 milliseconds
unordered-map: returns 123454321
unordered-map: took 36 milliseconds
dense-hash-map: returns 123454321
dense-hash-map: took 13 milliseconds
--------------------------
For input_size 1048576:
unordered-set: returns 123454321
unordered-set: took 251 milliseconds
dense-hash-set: returns 123454321
dense-hash-set: took 77 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 153 milliseconds
unordered-map: returns 123454321
unordered-map: took 220 milliseconds
dense-hash-map: returns 123454321
dense-hash-map: took 60 milliseconds
--------------------------
For input_size 4194304:
unordered-set: returns 123454321
unordered-set: took 1453 milliseconds
dense-hash-set: returns 123454321
dense-hash-set: took 357 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 596 milliseconds
unordered-map: returns 123454321
unordered-map: took 1461 milliseconds
dense-hash-map: returns 123454321
dense-hash-map: took 296 milliseconds
--------------------------
For input_size 16777216:
unordered-set: returns 123454321
unordered-set: took 6664 milliseconds
dense-hash-set: returns 123454321
dense-hash-set: took 1751 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 2513 milliseconds
unordered-map: returns 123454321
unordered-map: took 7299 milliseconds
dense-hash-map: returns 123454321
dense-hash-map: took 1364 milliseconds
--------------------------
tcmalloc: large alloc 1073741824 bytes == 0x5f392000 @ 
tcmalloc: large alloc 2147483648 bytes == 0x9f392000 @ 
tcmalloc: large alloc 4294967296 bytes == 0x11f392000 @ 
For input_size 268435456:
tcmalloc: large alloc 4586348544 bytes == 0x21fb92000 @ 
unordered-set: returns 123454321
unordered-set: took 136271 milliseconds
tcmalloc: large alloc 8589934592 bytes == 0x331974000 @ 
tcmalloc: large alloc 2147483648 bytes == 0x21fb92000 @ 
dense-hash-set: returns 123454321
dense-hash-set: took 34641 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 47606 milliseconds
tcmalloc: large alloc 2443452416 bytes == 0x21fb92000 @ 
unordered-map: returns 123454321
unordered-map: took 176066 milliseconds
tcmalloc: large alloc 4294967296 bytes == 0x331974000 @ 
dense-hash-map: returns 123454321
dense-hash-map: took 26460 milliseconds
--------------------------

код:

#include <algorithm>
#include <chrono>
#include <cmath>
#include <iostream>
#include <functional>
#include <string>
#include <vector>
#include <unordered_set>
#include <unordered_map>

#include <google/dense_hash_map>
#include <google/dense_hash_set>

using std::cout;
using std::chrono::high_resolution_clock;
using std::chrono::milliseconds;
using std::endl;
using std::string;
using std::vector;
using std::unordered_map;
using std::unordered_set;
using google::dense_hash_map;
using google::dense_hash_set;

class ScopedTimer {
public:
    ScopedTimer(const string& name)
    : name_(name), start_time_(high_resolution_clock::now()) {}

    ~ScopedTimer() {
        cout << name_ << " took "
        << std::chrono::duration_cast<milliseconds>(
                                                    high_resolution_clock::now() - start_time_).count()
        << " milliseconds" << endl;
    }

private:
    const string name_;
    const high_resolution_clock::time_point start_time_;
};

int find_using_unordered_set(const vector<int>& input_data) {
    unordered_set<int> numbers(input_data.size());
    for(const auto& value : input_data) {
        auto res = numbers.insert(value);
        if(!res.second) {
            numbers.erase(res.first);
        }
    }
    return numbers.size() == 1 ? *numbers.begin() : -1;
}

int find_using_unordered_map(const vector<int>& input_data) {
    unordered_map<int,int> counter_map;
    for(const auto& value : input_data) {
        ++counter_map[value];
    }
    for(const auto& map_entry : counter_map) {
        if(map_entry.second % 2 == 1) {
            return map_entry.first;
        }
    }
    return -1;
}

int find_using_dense_hash_set(const vector<int>& input_data) {
    dense_hash_set<int> numbers(input_data.size());
    numbers.set_deleted_key(-1);
    numbers.set_empty_key(-2);
    for(const auto& value : input_data) {
        auto res = numbers.insert(value);
        if(!res.second) {
            numbers.erase(res.first);
        }
    }
    return numbers.size() == 1 ? *numbers.begin() : -1;
}

int find_using_dense_hash_map(const vector<int>& input_data) {
    dense_hash_map<int,int> counter_map;
    counter_map.set_deleted_key(-1);
    counter_map.set_empty_key(-2);
    for(const auto& value : input_data) {
        ++counter_map[value];
    }
    for(const auto& map_entry : counter_map) {
        if(map_entry.second % 2 == 1) {
            return map_entry.first;
        }
    }
    return -1;
}

int find_using_sort_and_count(const vector<int>& input_data) {
    vector<int> local_copy(input_data);
    std::sort(local_copy.begin(), local_copy.end());
    int prev_value = local_copy.front();
    int counter = 0;
    for(const auto& value : local_copy) {
        if(prev_value == value) {
            ++counter;
            continue;
        }

        if(counter % 2 == 1) {
            return prev_value;
        }

        prev_value = value;
        counter = 1;
    }
    return counter == 1 ? prev_value : -1;
}

void execute_and_time(const string& method_name, std::function<int()> method) {
    ScopedTimer timer(method_name);
    cout << method_name << " returns " << method() << endl;
}

int main()
{
    vector<int> input_size_vec({1<<18,1<<20,1<<22,1<<24,1<<28});

    for(const auto& input_size : input_size_vec) {
        // Prepare input data
        std::vector<int> input_data;
        const int magic_number = 123454321;
        for(int i=0;i<input_size;++i) {
            input_data.push_back(i);
            input_data.push_back(i);
        }
        input_data.push_back(magic_number);
        std::random_shuffle(input_data.begin(), input_data.end());
        cout << "For input_size " << input_size << ":" << endl;

        execute_and_time("unordered-set:",std::bind(find_using_unordered_set, std::cref(input_data)));
        execute_and_time("dense-hash-set:",std::bind(find_using_dense_hash_set, std::cref(input_data)));
        execute_and_time("sort-and-count:",std::bind(find_using_sort_and_count, std::cref(input_data)));
        execute_and_time("unordered-map:",std::bind(find_using_unordered_map, std::cref(input_data)));
        execute_and_time("dense-hash-map:",std::bind(find_using_dense_hash_map, std::cref(input_data)));

        cout << "--------------------------" << endl;
    }
    return 0;
}

Ответ 2

Этот анализ практически такой же, как в user3386199, в его ответе . Это анализ, который я бы выполнил независимо от его ответа. но он попал туда первым.

Я запустил программу на своей машине (HP Z420 с производным Ubuntu 14.04 LTE) и добавил вывод для 1<<26, поэтому у меня есть другой набор чисел, но коэффициенты выглядят удивительно похожими на отношения от данных в исходном посте. Сырые времена я получил (файл on-vs-logn.raw.data):

For input_size 262144:
hash-set: returns 123454321
hash-set: took 45 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 34 milliseconds
hash-map: returns 123454321
hash-map: took 61 milliseconds
--------------------------
For input_size 1048576:
hash-set: returns 123454321
hash-set: took 372 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 154 milliseconds
hash-map: returns 123454321
hash-map: took 390 milliseconds
--------------------------
For input_size 4194304:
hash-set: returns 123454321
hash-set: took 1921 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 680 milliseconds
hash-map: returns 123454321
hash-map: took 1834 milliseconds
--------------------------
For input_size 16777216:
hash-set: returns 123454321
hash-set: took 8356 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 2970 milliseconds
hash-map: returns 123454321
hash-map: took 9045 milliseconds
--------------------------
For input_size 67108864:
hash-set: returns 123454321
hash-set: took 37582 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 12842 milliseconds
hash-map: returns 123454321
hash-map: took 46480 milliseconds
--------------------------
For input_size 268435456:
hash-set: returns 123454321
hash-set: took 172329 milliseconds
sort-and-count: returns 123454321
sort-and-count: took 53856 milliseconds
hash-map: returns 123454321
hash-map: took 211191 milliseconds
--------------------------

real    11m32.852s
user    11m24.687s
sys     0m8.035s

Я создал script, awk.analysis.sh, чтобы проанализировать данные:

#!/bin/sh

awk '
BEGIN { printf("%9s  %8s  %8s  %8s  %8s  %8s  %8s  %9s  %9s  %9s  %9s\n",
               "Size", "Sort Cnt", "R:Sort-C", "Hash Set", "R:Hash-S", "Hash Map",
               "R:Hash-M", "O(N)", "O(NlogN)", "O(N^3/2)", "O(N^2)")
}
/input_size/           { if (old_size   == 0) old_size   = $3; size       = $3 }
/hash-set: took/       { if (o_hash_set == 0) o_hash_set = $3; t_hash_set = $3 }
/sort-and-count: took/ { if (o_sort_cnt == 0) o_sort_cnt = $3; t_sort_cnt = $3 }
/hash-map: took/       { if (o_hash_map == 0) o_hash_map = $3; t_hash_map = $3 }
/^----/ {
    o_n = size / old_size
    o_nlogn = (size * log(size)) / (old_size * log(old_size))
    o_n2    = (size * size) / (old_size * old_size)
    o_n32   = (size * sqrt(size)) / (old_size * sqrt(old_size))
    r_sort_cnt = t_sort_cnt / o_sort_cnt
    r_hash_map = t_hash_map / o_hash_map
    r_hash_set = t_hash_set / o_hash_set
    printf("%9d  %8d  %8.2f  %8d  %8.2f  %8d  %8.2f  %9.0f  %9.2f  %9.2f  %9.0f\n",
           size, t_sort_cnt, r_sort_cnt, t_hash_set, r_hash_set,
           t_hash_map, r_hash_map, o_n, o_nlogn, o_n32, o_n2)
}' < on-vs-logn.raw.data

Вывод из программы довольно широк, но дает:

     Size  Sort Cnt  R:Sort-C  Hash Set  R:Hash-S  Hash Map  R:Hash-M       O(N)   O(NlogN)   O(N^3/2)     O(N^2)
   262144        34      1.00        45      1.00        61      1.00          1       1.00       1.00          1
  1048576       154      4.53       372      8.27       390      6.39          4       4.44       8.00         16
  4194304       680     20.00      1921     42.69      1834     30.07         16      19.56      64.00        256
 16777216      2970     87.35      8356    185.69      9045    148.28         64      85.33     512.00       4096
 67108864     12842    377.71     37582    835.16     46480    761.97        256     369.78    4096.00      65536
268435456     53856   1584.00    172329   3829.53    211191   3462.15       1024    1592.89   32768.00    1048576

Достаточно ясно, что на этой платформе алгоритмы хеш-набора и хэш-карты не являются O (N), и они не хуже O (N.logN), но они лучше, чем O (N 3/2), не говоря уже о (N 2). С другой стороны, алгоритм сортировки очень близок к O (N.logN).

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

И, как раз для записи, здесь вывод из анализа script по исходным данным:

     Size  Sort Cnt  R:Sort-C  Hash Set  R:Hash-S  Hash Map  R:Hash-M       O(N)   O(NlogN)   O(N^3/2)     O(N^2)
   262144        37      1.00       107      1.00       109      1.00          1       1.00       1.00          1
  1048576       173      4.68       641      5.99       731      6.71          4       4.44       8.00         16
  4194304       745     20.14      3250     30.37      3631     33.31         16      19.56      64.00        256
 16777216      3238     87.51     14528    135.78     16483    151.22         64      85.33     512.00       4096
268435456     60396   1632.32    350305   3273.88    427841   3925.15       1024    1592.89   32768.00    1048576

Дальнейшее тестирование показывает, что изменение хеш-функций показано:

int find_using_hash(const vector<int>& input_data) {
    unordered_set<int> numbers;
    numbers.reserve(input_data.size());

и

int find_using_hashmap(const vector<int>& input_data) {
    unordered_map<int,int> counter_map;
    counter_map.reserve(input_data.size());

производит такой анализ:

     Size  Sort Cnt  R:Sort-C  Hash Set  R:Hash-S  Hash Map  R:Hash-M       O(N)   O(NlogN)   O(N^3/2)     O(N^2)
   262144        34      1.00        42      1.00        80      1.00          1       1.00       1.00          1
  1048576       155      4.56       398      9.48       321      4.01          4       4.44       8.00         16
  4194304       685     20.15      1936     46.10      1177     14.71         16      19.56      64.00        256
 16777216      2996     88.12      8539    203.31      5985     74.81         64      85.33     512.00       4096
 67108864     12564    369.53     37612    895.52     28808    360.10        256     369.78    4096.00      65536
268435456     53291   1567.38    172808   4114.48    124593   1557.41       1024    1592.89   32768.00    1048576

Ясно, что резервирование пространства для хэш-карты полезно.

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

Ответ 3

Давайте начнем с просмотра чисел для решения сортировки. В приведенной ниже таблице первый столбец - это отношение размера. Он вычисляется путем вычисления NlogN для данного теста и деления на NlogN для первого теста. Второй столбец - это отношение времени между данным тестом и первым тестом.

 NlogN size ratio      time ratio
   4*20/18 =  4.4     173 / 37 =  4.7
  16*22/18 = 19.6     745 / 37 = 20.1
  64*24/18 = 85.3    3238 / 37 = 87.5
1024*28/18 = 1590   60396 / 37 = 1630

Вы можете видеть, что между двумя отношениями существует очень хорошее совпадение, что указывает на то, что процедура сортировки действительно O (NlogN).

Итак, почему хеш-подпрограммы не работают так, как ожидалось. Простое представление о том, что извлечение элемента из хэш-таблицы - это O (1), - это чистая фантазия. Фактическое время извлечения зависит от качества функции хэширования и количества бункеров в хеш-таблице. Фактическое время извлечения составляет от O (1) до O (N), где наихудший случай возникает, когда все записи в хэш-таблице заканчиваются в одном и том же ящике. Поэтому, используя хэш-таблицу, вы должны ожидать, что ваша производительность будет находиться где-то между O (N) и O (N ^ 2), которые, по-видимому, соответствуют вашим данным, как показано ниже

 O(N)  O(NlogN)  O(N^2)  time
   4     4.4       16       6
  16      20      256      30
  64      85     4096     136
1024    1590     10^6    3274

Обратите внимание, что отношение времени находится на нижнем конце диапазона, что указывает на то, что хеш-функция работает достаточно хорошо.

Ответ 4

Я запускал программу через valgrind с разными размерами ввода, и я получил эти результаты для подсчета циклов:

with 1<<16 values:
  find_using_hash: 27 560 872
  find_using_sort: 17 089 994
  sort/hash: 62.0%

with 1<<17 values:
  find_using_hash: 55 105 370
  find_using_sort: 35 325 606
  sort/hash: 64.1%

with 1<<18 values:
  find_using_hash: 110 235 327
  find_using_sort:  75 695 062
  sort/hash: 68.6%

with 1<<19 values:
  find_using_hash: 220 248 209
  find_using_sort: 157 934 801
  sort/hash: 71.7%

with 1<<20 values:
  find_using_hash: 440 551 113
  find_using_sort: 326 027 778
  sort/hash: 74.0%

with 1<<21 values:
  find_using_hash: 881 086 601
  find_using_sort: 680 868 836
  sort/hash: 77.2%

with 1<<22 values:
  find_using_hash: 1 762 482 400
  find_using_sort: 1 420 801 591
  sort/hash: 80.6%

with 1<<23 values:
  find_using_hash: 3 525 860 455
  find_using_sort: 2 956 962 786
  sort/hash: 83.8%

Это означает, что время сортировки медленно обгоняет хэш-время, по крайней мере теоретически. С моим конкретным компилятором/библиотекой (gcc 4.8.2/libsddС++) и оптимизацией (-O2) методы сортировки и хэша будут иметь одинаковую скорость со значениями около 2 ^ 28, что находится на пределе того, что вы пытаетесь. Я подозреваю, что другие системные факторы вступают в игру при использовании большого количества памяти, что затрудняет оценку в реальном времени стены.

Ответ 5

Тот факт, что O(N) был, по-видимому, медленнее, чем O(N logN), сводил меня с ума, поэтому я решил глубоко погрузиться в проблему.

Я сделал этот анализ в Windows с Visual Studio, но я уверен, что результаты будут очень похожими на Linux с g++.

Прежде всего, я использовал Very Sleepy, чтобы найти фрагменты кода, которые наиболее полно выполняются в цикле for в find_using_hash(). Вот что я увидел:

enter image description here

Как вы можете видеть, верхние записи связаны со списками (RtlAllocateHeap вызывается из списка кодов). По-видимому, проблема заключается в том, что для каждой вставки в unordered_set, и поскольку ведра реализованы в виде списков, выполняется выделение для node, и это sky-ракеты продолжительность алгоритма, в отличие от сортировки, которая не делает распределение.

Чтобы быть уверенным, что это была проблема, я написал ОЧЕНЬ простую реализацию хеш-таблицы без выделения, и результаты были гораздо более разумными:

enter image description here

Таким образом, коэффициент log N, умножающий N, который в вашем самом большом примере (т.е. 1<<28) равен 28, по-прежнему меньше, чем "постоянный" объем работы, необходимый для распределения.

Ответ 6

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

И я пишу, чтобы дать ответ с математической точки зрения (что трудно обойтись без LaTeX), потому что важно исправить неподтвержденное заблуждение, что решение данной проблемы с хэшами представляет собой проблему, которая является "теоретически" , O(n), но как-то "практически" хуже, чем O(n). Такая вещь была бы математической невозможностью!

Для тех, кто хочет продолжить эту тему более подробно, я рекомендую это книгу, которую я сохранил и купил как очень бедную среднюю школу студент, и который вызвал мой интерес к прикладной математике для многих грядущие годы, существенно меняя исход моей жизни: http://www.amazon.com/Analysis-Algorithms-Monographs-Computer-Science/dp/0387976876

Чтобы понять, почему проблема не является "теоретически" O(n), необходимо отметить, что базовое предположение также неверно: неверно, что хеши являются "теоретически" структурой данных O(1).

Противоположное на самом деле верно. Хэши в чистом виде являются "практически" структурой данных O(1), но теоретически все еще являются структурой данных O(n). (Примечание: в гибридной форме они могут достичь теоретической производительности O(log n).)

Следовательно, решение в лучшем случае по-прежнему остается проблемой O(n log n), так как n приближается к бесконечности.

Вы можете начать отвечать, но всем известно, что хэши O (1)!

Итак, теперь позвольте мне объяснить, как это утверждение верно, но в практическом, а не теоретическом смысле.

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

Но когда вы позволяете n приближаться к бесконечности, как того требует определение Big- O, тогда ведра начинают заполняться (что должно происходить по принципу голубины), а всякая чистая структура хэша распадается на Алгоритм O(n) (нота Big-O здесь игнорирует постоянный множитель, который зависит от количества ведер).

Вау! Там много в этом предложении.

И поэтому в этот момент, а не в уравнениях, более подходящая аналогия была бы более полезной:

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

  • "Хэш-функция" - это операция O(1), смотрящая на первую письмо.

  • Хранение - это операция O(1): размещение файла внутри ящика для этой буквы.

  • И пока в каждом ящике не более одного файла, поиск - это операция O(1): открытие ящика для этой буквы.

В рамках этих конструктивных ограничений эта структура хэша O(1).

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

По сравнению с бросанием всех файлов в одну огромную кучу, средняя производительность в целом примерно в 1/26-м раза больше времени. Но помните, математически, нельзя сказать O(n/26), потому что обозначение O(n) по определению не учитывает постоянные факторы, которые влияют на производительность, а только алгоритмическую сложность как функцию n. Поэтому, когда ограничения дизайна превышены, структура данных O(n).