簡體   English   中英

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

[英]O(N) algorithm slower than O(N logN) algorithm

在數字數組中,每個數字出現偶數次,並且只有一個數字出現奇數次。 我們需要找到這個數字(之前在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

但問題仍然存在 - 盡管理論算法的復雜性優勢,為什么散列與實際排序相比效率低?

它實際上取決於hash_map / hash_set實現。 通過用Google的dense_hash_{map,set}替換libstdc ++的unordered_{map,set} dense_hash_{map,set} ,它明顯快於sort dense_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;
}

該分析與用戶3386199在其答案中所做的分析基本相同。 無論他的回答是什么,我都會進行分析 - 但他確實首先到達那里。

我在我的機器上運行程序(運行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

顯然,為哈希映射保留空間是有益的。

哈希集代碼是相當不同的; 它添加了大約一半時間(整體)的項目,並且“添加”然后在另一半時間刪除項目。 這比哈希映射代碼必須做的工作更多,所以它更慢。 這也意味着保留空間大於實際需要的空間,並且可能考慮到保留空間的性能下降。

讓我們從查看排序解決方案的數字開始。 在下表中,第一列是尺寸比率。 它是通過計算給定測試的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

請注意,時間比率位於范圍的低端,表示散列函數運行良好。

我通過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,這是你正在嘗試的極限。 我懷疑在使用那么多內存時其他系統因素正在發揮作用,這使得難以在實際的牆壁時間內進行評估。

事實上, O(N)似乎比O(N logN)慢,這讓我發瘋,所以我決定深入研究這個問題。

我在Windows中使用Visual Studio進行了此分析,但我敢打賭,在Linux上使用g ++時結果會非常相似。

首先,我使用Very Sleepy來查找在find_using_hash() for循環期間執行最多的代碼片段。 這就是我所看到的:

在此輸入圖像描述

如您所見,頂部條目都與列表相關(從列表代碼調用RtlAllocateHeap )。 顯然,問題在於,對於unordered_set每個插入,並且由於存儲桶是作為列表實現的,因此對節點進行分配,並且這會使算法的持續時間發生火花,而不是不進行分配的排序。

為了確定這是問題,我寫了一個非常簡單的哈希表實現,沒有分配,結果更合理:

在此輸入圖像描述

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

這里有許多很好的答案,但這是一種特殊的問題,自然會產生許多有效的答案。

我正在編寫以提供數學視角的答案(沒有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)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM