O(N)算法比O(N logN)算法慢

时间:2015-01-13 22:41:22

标签: c++ performance algorithm c++11 hash

在数字数组中,每个数字出现偶数次,并且只有一个数字出现奇数次。我们需要找到这个数字(之前已经讨论过问题on Stack Overflow)。

这是一个用3种不同方法解决问题的解决方案 - 两种方法是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
--------------------------

加成

@Matt建议使用xor的快速解决方案当然不会参加比赛 - 例如,在最差情况下不到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

但问题仍然存在 - 尽管理论算法的复杂性优势,但为什么散列与实际排序相比效率低?

6 个答案:

答案 0 :(得分:13)

这实际上取决于hash_map / hash_set实现。将libstdc ++的unordered_{map,set}替换为Google的dense_hash_{map,set},速度明显快于sortdense_hash_xxx的缺点是它们需要有两个永远不会使用的键值。有关详细信息,请参阅其文档。

要记住的另一件事是:hash_{map,set}通常会进行大量动态内存分配/释放,因此最好使用更好的替代libc的默认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;
}

答案 1 :(得分:6)

此分析与user3386199 answer中的{{3}}基本相同。无论他的回答如何,我都会进行分析 - 但他确实是先到达那里。

我在我的机器上运行程序(运行Ubuntu 14.04 LTE衍生产品的HP Z420),并添加了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

我创建了一个脚本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 )更不用说O(N 2 )。另一方面,排序算法确实非常接近O(N.logN)。

您只能将其归结为哈希集和哈希映射代码中的理论缺陷,或者哈希表的大小调整不足以使它们使用次优哈希表大小。值得研究一下有哪些机制可以预先调整哈希集和哈希映射的大小,以确定使用它是否会影响性能。 (另请参阅下面的额外信息。)

而且,仅为了记录,这里是分析脚本对原始数据的输出:

     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

显然,为哈希映射保留空间是有益的。

哈希集代码相当不同;它添加一个项目大约一半的时间(整体),并添加&#39;然后在另一半时间删除一个项目。这比哈希映射代码必须做的工作更多,所以它更慢。这也意味着保留空间大于实际需要的空间,并且可能考虑到保留空间的性能下降。

答案 2 :(得分:2)

让我们从查看排序解决方案的数字开始。在下表中,第一列是尺寸比率。它是通过计算给定测试的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)是纯粹的幻想。实际提取时间取决于散列函数的质量以及散列表中的bin数。实际提取时间范围从 O(1) O(N),其中最坏的情况发生在哈希表中的所有条目最终都在同一个bin中。因此,使用哈希表时,您应该期望您的性能介于 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

请注意,时间比率位于范围的低端,表示散列函数运行良好。

答案 3 :(得分:2)

我通过不同输入大小的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 / libsddc ++)和优化(-O2),sort和hash方法的速度大约相同,大约为2 ^ 28,这是你正在尝试的极限。我怀疑在使用那么多内存时其他系统因素正在发挥作用,这使得难以在实际的时间内进行评估。

答案 4 :(得分:2)

O(N)似乎比O(N logN)慢似的事实让我发疯,所以我决定深入研究这个问题。

我在Windows中使用Visual Studio进行了此分析,但我敢打赌,在Linux上使用g ++,结果会非常相似。

首先,我使用Very Sleepy查找forfind_using_hash()循环中执行次数最多的代码片段。这就是我所看到的:

enter image description here

如您所见,顶部条目都与列表相关(从列表代码调用RtlAllocateHeap)。显然,问题在于,对于unordered_set中的每个插入,并且由于存储桶是作为列表实现的,因此会对节点进行分配,并且这会使算法的持续时间发生火花,而不是分配。

为了确定这是问题,我写了一个非常简单的哈希表实现而没有分配,结果更合理:

enter image description here

因此,在你的最大例子(即log N)中,因子N乘以1<<28的因子仍小于所需的“常量”工作量。分配。

答案 5 :(得分:0)

这里有许多很好的答案,但这是一种特殊的问题,自然会产生许多有效的答案。

我正在编写以提供数学视角的答案(没有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)性能。每个纯散列结构都旨在在 a priori 范围的数据集大小范围内表现良好,并假设密钥与散列函数有关。

但是当你让n接近无穷大时,正如Big - O符号的定义所要求的那样,那些桶开始填充(这必须通过鸽子原理发生),以及任何纯哈希结构分解为O(n)算法(此处的大 - O符号忽略了取决于有多少桶的常数因子。

  

哇!那句话中有很多。

所以在这一点上,而不是方程,一个适当的类比会更有帮助:

通过想象一个包含26个抽屉的文件柜,每个字母的一个字母,可以获得对哈希表的非常准确的数学理解。每个文件都存储在抽屉中,该抽屉对应于文件名中的第一个字母。

  • “哈希函数”是O(1)操作,查看第一个 信。

  • 存储是O(1)操作:将文件放在抽屉内 那封信。

  • 只要每个抽屉里面只有一个文件, 检索是O(1)操作:打开该信件的抽屉。

在这些设计约束中,此哈希结构为O(1)

现在假设您超出了这个“文件柜”散列结构的设计约束,并且存储了数百个文件。存储现在需要尽可能多的操作来查找每个抽屉中的空白区域,并且检索所需的操作与每个抽屉中的项目数量相同。

与将所有文件投入一个大堆相比,整体平均性能大约好于时间的1/26。但请记住,在数学上,不能说O(n/26),因为O(n)符号的定义并未考虑影响性能的常数因素,而只考虑作为n函数的算法复杂度。因此,当超出设计约束时,数据结构为O(n)