[英]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.