首页 文章

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

提问于
浏览
21

在数字数组中,每个数字出现偶数次,并且只有一个数字出现奇数次 . 我们需要找到这个数字(之前讨论的问题是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
--------------------------

Addition

@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 回答

  • 6

    它实际上取决于hash_map / hash_set实现 . 通过用Google的 dense_hash_{map,set} 替换libstdc的 unordered_{map,set} ,它明显快于 sort . dense_hash_xxx 的缺点是它们需要两个永远不会使用的键值 . 请参阅其文档了解详细信息

    另一件需要记住的事情是: hash_{map,set} 通常会进行大量的动态内存分配/解除分配,因此最好使用更好的替代libc的默认值 malloc/free ,例如谷歌的 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

    这个分析与他的answer中的user3386199基本相同 . 无论他的回答是什么,我都会进行分析 - 但他确实首先到达那里 .

    我在我的机器上运行程序(运行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(N3 / 2),更不用说O (N2) . 另一方面,排序算法确实非常接近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
    

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

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

  • 0

    让我们从查看排序解决方案的数字开始 . 在下表中,第一列是尺寸比率 . 它是通过计算给定测试的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
    

    请注意,时间比率处于低位范围的结尾,表明哈希函数运行得相当好 .

  • 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,这正是你所尝试的极限 . 我怀疑在使用那么多内存时其他系统因素正在发挥作用,这使得难以在实际的墙壁时间内进行评估 .

  • 13

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

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

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

    enter image description here

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

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

    enter image description here

    因此,在最大的例子(即 1<<28 )中,因子 log N 乘以 N 仍然小于分配所需的"constant"工作量 .

  • 2

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

    我正在编写以提供数学视角的答案(没有LaTeX很难做到),因为纠正未解决的误解是很重要的,即用哈希解决给定问题代表了一个问题,但不知何故"practically"比 O(n) 更糟糕 . 这样的事情在数学上是不可能的!

    对于那些希望更深入地探讨这个话题的人,我推荐这本书,这本书是我为一个非常贫穷的高中生而购买的,这引起了我对未来多年应用数学的兴趣,从根本上改变了我的成果 . 生活:http://www.amazon.com/Analysis-Algorithms-Monographs-Computer-Science/dp/0387976876

    要理解为什么问题不是"theoretically" O(n) ,有必要注意基础假设也是错误的:哈希是"theoretically" O(1) 数据结构不是真的 .

    事实恰恰相反 . 哈希以其纯粹的形式只是"practically"数据结构,但理论上仍然是 O(n) 数据结构 . (注意:在混合形式中,它们可以实现理论上的性能 . )

    因此,在最好的情况下,解决方案仍然是 O(n log n) 问题,因为 n 接近无穷大 .

    你可能会开始回应,但每个人都知道哈希是O(1)!

    所以现在让我解释一下这种说法是否属实,但是在实践中,而不是理论上 .

    对于任何应用程序(无论 n ,只要 n 提前知道 - 他们在数学证明中称为"fixed"而不是"arbitrary"),您可以设计哈希表以匹配应用程序,并在约束条件下获得 O(1) 性能环境 . 每个纯散列结构旨在在先验的数据集大小范围内良好地执行,并且假定密钥相对于散列函数具有独立性 .

    但是当你按照Big- O 符号的定义要求接近无穷大时,那么桶开始填充(这必须通过鸽子原理发生),并且任何纯哈希结构都会分解成一个 O(n) 算法(大 - O 符号在这里忽略了取决于有多少桶的常数因子 .

    哇!那句话里有很多东西 .

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

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

    • "hash function"是一个 O(1) 操作,查看第一个字母 .

    • 存储是 O(1) 操作:将文件放在该字母的抽屉内 .

    • 只要每个抽屉内没有多个文件,检索就是 O(1) 操作:打开该字母的抽屉 .

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

    现在假设您超出了这个“文件柜”散列结构的设计约束,并且存储了数百个文件 . 存储现在需要尽可能多的操作来在每个抽屉中找到空的空间,并且检索采取与每个抽屉内的项目数一样多的操作 .

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

相关问题