跳过列表,它们真的像 Pugh 论文声称的那样好吗?

2023-12-24

我正在尝试使用最小的额外内存开销来实现一个与 BST 一样好的跳过列表,目前即使不考虑任何内存限制,我的 SkipList 实现的性能也与非常幼稚的平衡 BST 实现相去甚远 -可以这么说,手工制作的 BTS :) -

作为参考,我使用 William Pugh 的原始论文PUG89 http://epaperpress.com/sortsearch/download/skiplist.pdf以及我在 Sedgewick -13.5- 的 C 算法中找到的实现。我的代码是递归实现,这是插入和查找操作的线索:

sl_node* create_node()
{
    short lvl {1};
    while((dist2(en)<p)&&(lvl<max_level))
        ++lvl;
    return new sl_node(lvl);
}

void insert_impl(sl_node* cur_node,
        sl_node* new_node,
        short lvl)
{
    if(cur_node->next_node[lvl]==nullptr || cur_node->next_node[lvl]->value > new_node->value){
        if(lvl<new_node->lvl){
            new_node->next_node[lvl] = cur_node->next_node[lvl];
            cur_node->next_node[lvl] = new_node;
        }
        if(lvl==0) return;
        insert_impl(cur_node,new_node,lvl-1);
        return;
    }
    insert_impl(cur_node->next_node[lvl],new_node,lvl);
}
sl_node* insert(long p_val)
{
    sl_node* new_node = create_node();
    new_node->value = p_val;
    insert_impl(head, new_node,max_level-1);
    return new_node;
}

这是查找操作的代码:

sl_node* find_impl(sl_node* cur_node,
        long p_val,
        int lvl)
{
    if(cur_node==nullptr) return nullptr;
    if(cur_node->value==p_val) return cur_node;
    if(cur_node->next_node[lvl] == nullptr || cur_node->next_node[lvl]->value>p_val){
        if(lvl==0) return nullptr;
        return find_impl(cur_node,p_val,lvl-1);
    }
    return find_impl(cur_node->next_node[lvl],p_val,lvl);
}

sl_node* find(long p_val)
{
    return find_impl(head,p_val,max_level-1);
}

sl_node -跳过列表节点 - 看起来像这样:

struct sl_node
{
    long  value;
    short lvl;
    sl_node** next_node;

    sl_node(int l) : lvl(l)
    {
        next_node = new sl_node*[l];
        for(short i{0};i<l;i++)
            next_node[i]=nullptr;
    }
    ~sl_node()
    {
        delete[] next_node;
    }
};

正如你所看到的,与本书的实现相比,这个实现没有什么特别的或先进的,我不会分享平衡 BTS 代码,因为我认为这里不需要,但它是一个非常基本的 BTS,在当新节点高度大于 16*lg(n) 时插入,其中 n 是节点数。

也就是说,只有当最大高度比最佳理论高度高 16 倍时,我才会重新平衡这三个,正如我所说,这种简单的自制 BST 的性能比自制的 Skip List 好得多。

但首先,让我们看一些数据,使用 p=.5 和 n=262144,SkipList 中的节点级别具有以下分布:

1:141439, 53.9547%
2:65153, 24.8539%
3:30119, 11.4895%
4:13703, 5.22728%
5:6363, 2.42729%
6:2895, 1.10435%
7:1374, 0.524139%
8:581, 0.221634%
9:283, 0.107956%
10:117, 0.044632%
11:64, 0.0244141%
12:31, 0.0118256%
13:11, 0.00419617%
14:5, 0.00190735%
15:1, 0.00038147%
16:5, 0.00190735%

这几乎完全 - 哦,这是一个大问题! - 与文章中的理论相符,即:50% 1 级,25% 2 级,依此类推。输入数据来自我最好的可用伪随机数生成器,又名 std::random_device ,带有 std::default_random_engine 和统一的 int 分布。输入对我来说看起来很随机:)!

在我的机器上,以随机顺序搜索 SkipList 中的“所有”262144 个元素所需的时间为 315 毫秒,而对于朴素 BTS 上的相同搜索操作,所需时间为 134 毫秒,因此 BTS 几乎比跳过列表。这并不完全是我所期望的“跳过列表算法与平衡树具有相同的渐近预期时间界限,并且简单、更快且使用更少的空间”PUG89 http://epaperpress.com/sortsearch/download/skiplist.pdf.

“插入”节点所需的时间对于 SkipList 是 387 毫秒,对于 BTS 是 143 毫秒,同样,朴素的 BST 性能更好。

如果我不使用输入数字的随机序列而是使用排序序列,事情会变得更有趣,在这里我可怜的自制 BST 变得很慢,并且插入 262144 个排序 int 需要 2866ms,而 SkipList 仅需要 168ms。

但是,当谈到搜索时间时,BST 仍然更快!对于排序后的输入,我们需要 234 毫秒而不是 77 毫秒,这个 BST 快了 3 倍。

使用不同的 p 因子值,我得到的性能结果略有不同:

最后但并非最不重要的一点是内存使用情况图,正如您所期望的那样,它证实了如果我们增加每个节点的级别数量,我们会显着影响内存指纹。内存使用量的计算方式为存储所有节点的附加指针所需的空间总和。

毕竟,你们中的任何人都可以向我提供有关如何实现与 BTS 一样好的 SkipList 的评论吗(不包括额外的内存开销)?

我知道 DrDobbs 的文章LINK http://www.drdobbs.com/cpp/skip-lists-in-c/184403579关于 SkipList,我浏览了所有论文,搜索和插入操作的代码与原始实现完全匹配PUG89 http://epaperpress.com/sortsearch/download/skiplist.pdf所以应该和我的一样好,而且这篇文章无论如何都没有提供任何性能分析,我没有找到任何其他来源。你能帮助我吗?

任何评论都将受到高度赞赏!

谢谢! :)


History

自从威廉·皮尤(William Pugh)写出他的原始论文以来,时代已经发生了一些变化。我们在他的论文中没有提到 CPU 和操作系统的内存层次结构,这已成为当今普遍关注的焦点(现在通常与算法复杂性同等重要)。

他的基准测试输入案例只有区区 2^16 个元素,而当时的硬件通常最多只能提供 32 位扩展内存寻址。这使得指针的大小比我们今天在 64 位机器上使用的指针大小减半或更小。同时,例如,字符串字段可能一样大,使得存储在跳跃列表中的元素与跳跃节点所需的指针之间的比率可能小很多,特别是考虑到我们通常每个跳跃节点需要多个指针。

当时,C 编译器在寄存器分配和指令选择等方面的优化并不那么积极。即使是普通的手写程序集通常也可以提供显着的性能优势。编译器提示如下register and inline在那段时间实际上做了一件大事。虽然这看起来有点没有实际意义,因为平衡 BST 和跳跃列表实现在这里是平等的,但即使是基本循环的优化也是一个更加手动的过程。当优化越来越成为一个手动过程时,更容易实施的事情通常也更容易优化。跳跃列表通常被认为比平衡树更容易实现。

因此,所有这些因素可能都影响了皮尤当时的结论。然而时代已经变了:硬件变了,操作系统变了,编译器变了,对这些主题进行了更多的研究,等等。

执行

除此之外,让我们享受一些乐趣并实现一个基本的跳过列表。我最终调整了可用的实现here http://en.literateprograms.org/Skip_list_%28C_Plus_Plus%29出于懒惰。这是一种普通的实现方式,与当今大量易于访问的示例性跳过列表实现方式几乎没有什么不同。

我们将比较我们的实现的性能std::set它几乎总是被实现为红黑树*。

* Some might wonder why I use 0 instead of nullptr and things of that sort. It's a habit. At my workplace, we still have to write open libraries that target a wide range of compilers including those that only support C++03, so I'm still used to writing lower/mid-level implementation code that way, and sometimes even in C, so please forgive the old style in which I wrote this code.

#include <iostream>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <vector>
#include <cassert>
#include <cstring>
#include <set>

using namespace std;

static const int max_level = 32;
static const float probability = 0.5;

static double sys_time()
{
    return static_cast<double>(clock()) / CLOCKS_PER_SEC;
}

static int random_level() 
{
    int lvl = 1;
    while ((static_cast<float>(rand()) / RAND_MAX) < probability && lvl < max_level)
        ++lvl;
    return lvl;
}

template <class T>
class SkipSet
{
public:
    SkipSet(): head(0)
    {
        head = create_node(max_level, T());
        level = 0;
    }
    
    ~SkipSet()
    {
        while (head)
        {
            Node* to_destroy = head;
            head = head->next[0];
            destroy_node(to_destroy);
        }
    }

    bool contains(const T& value) const
    {
        const Node* node = head;
        for (int i=level; i >= 0; --i)
        {
            while (node->next[i] && node->next[i]->value < value)
                node = node->next[i];
        }
        node = node->next[0];
        return node && node->value == value;
    }

    void insert(const T& value)
    {
        Node* node = head;  
        Node* update[max_level + 1];
        memset(update, 0, sizeof(Node*)*(max_level + 1));

        for (int i = level; i >= 0; i--) 
        {
            while (node->next[i] && node->next[i]->value < value)
                node = node->next[i];
            update[i] = node; 
        }
        node = node->next[0];

        if (!node || node->value != value)
        {
            int lvl = random_level();
            assert(lvl >= 0);
            if (lvl > level) 
            {
                for (int i = level + 1; i <= lvl; i++) {
                    update[i] = head;
                }
                level = lvl;
            }
            node = create_node(lvl, value);
            for (int i = 0; i <= lvl; i++) {
                node->next[i] = update[i]->next[i];
                update[i]->next[i] = node;
            }            
        }
    }

    bool erase(const T& value)
    {
        Node* node = head;  
        Node* update[max_level + 1];
        memset(update, 0, sizeof(Node*)*(max_level + 1));

        for (int i = level; i >= 0; i--) 
        {
            while (node->next[i] && node->next[i]->value < value)
                node = node->next[i];
            update[i] = node; 
        }
        node = node->next[0];

        if (node->value == value)
        {
            for (int i = 0; i <= level; i++) {
                if (update[i]->next[i] != node)
                    break;
                update[i]->next[i] = node->next[i];
            }
            destroy_node(node);
            while (level > 0 && !head->next[level])
                --level;
            return true;
        }
        return false;
    }

private:
    struct Node
    {
        T value;
        struct Node** next;
    };

    Node* create_node(int level, const T& new_value)
    {
        void* node_mem = malloc(sizeof(Node));
        Node* new_node = static_cast<Node*>(node_mem);
        new (&new_node->value) T(new_value);

        void* next_mem = calloc(level+1, sizeof(Node*));
        new_node->next = static_cast<Node**>(next_mem);
        return new_node;
    }

    void destroy_node(Node* node)
    {
        node->value.~T();
        free(node->next);
        free(node);
    }

    Node* head;
    int level;
};

template <class T>
bool contains(const std::set<T>& cont, const T& val)
{
    return cont.find(val) != cont.end();
}

template <class T>
bool contains(const SkipSet<T>& cont, const T& val)
{
    return cont.contains(val);
}

template <class Set, class T>
void benchmark(int num, const T* elements, const T* search_elements)
{
    const double start_insert = sys_time();
    Set element_set;
    for (int j=0; j < num; ++j)
        element_set.insert(elements[j]);
    cout << "-- Inserted " << num << " elements in " << (sys_time() - start_insert) << " secs" << endl;

    const double start_search = sys_time();
    int num_found = 0;
    for (int j=0; j < num; ++j)
    {
        if (contains(element_set, search_elements[j]))
            ++num_found;
    }
    cout << "-- Found " << num_found << " elements in " << (sys_time() - start_search) << " secs" << endl;

    const double start_erase = sys_time();
    int num_erased = 0;
    for (int j=0; j < num; ++j)
    {
        if (element_set.erase(search_elements[j]))
            ++num_erased;
    }
    cout << "-- Erased " << num_found << " elements in " << (sys_time() - start_erase) << " secs" << endl;
}

int main()
{
    const int num_elements = 200000;
    vector<int> elements(num_elements);
    for (int j=0; j < num_elements; ++j)
        elements[j] = j;
    random_shuffle(elements.begin(), elements.end());

    vector<int> search_elements = elements;
    random_shuffle(search_elements.begin(), search_elements.end());

    typedef std::set<int> Set1;
    typedef SkipSet<int> Set2;

    for (int j=0; j < 3; ++j)
    {
        cout << "std::set" << endl;
        benchmark<Set1>(num_elements, &elements[0], &search_elements[0]);
        cout << endl;

        cout << "SkipSet" << endl;
        benchmark<Set2>(num_elements, &elements[0], &search_elements[0]);
        cout << endl;
    }
}

在 GCC 5.2,-O2 上,我得到这个:

std::set
-- Inserted 200000 elements in 0.104869 secs
-- Found 200000 elements in 0.078351 secs
-- Erased 200000 elements in 0.098208 secs

SkipSet
-- Inserted 200000 elements in 0.188765 secs
-- Found 200000 elements in 0.160895 secs
-- Erased 200000 elements in 0.162444 secs

...这太糟糕了。我们的整体速度大约慢了一倍。

优化

然而我们可以做出明显的优化。如果我们看一下Node,其当前字段如下所示:

struct Node
{
    T value;
    struct Node** next;
};

这意味着 Node 字段的内存及其下一个指针列表是两个单独的块,它们之间可能有非常远的步幅,如下所示:

    [Node fields]-------------------->[next0,next1,...,null]

这对于参考位置来说效果很差。如果我们想改进这里的东西,我们应该将这些内存块合并成一个连续的结构,如下所示:

    [Node fields,next0,next1,...,null]

我们可以通过 C 中常见的变长结构体习惯来实现这一点。在 C++ 中实现有点尴尬,因为 C++ 不直接支持它,但我们可以像这样模拟效果:

struct Node
{
    T value;
    struct Node* next[1];
};

Node* create_node(int level, const T& new_value)
{
    void* node_mem = malloc(sizeof(Node) + level * sizeof(Node*));
    Node* new_node = static_cast<Node*>(node_mem);
    new (&new_node->value) T(new_value);
    for (int j=0; j < level+1; ++j)
        new_node->next[j] = 0;
    return new_node;
}

void destroy_node(Node* node)
{
    node->value.~T();
    free(node);
}

通过这个适度的调整,我们现在有这些时间:

SkipSet (Before)
-- Inserted 200000 elements in 0.188765 secs
-- Found 200000 elements in 0.160895 secs
-- Erased 200000 elements in 0.162444 secs

SkipSet (After)
-- Inserted 200000 elements in 0.132322 secs
-- Found 200000 elements in 0.127989 secs
-- Erased 200000 elements in 0.130889 secs

...这使我们更接近于与std::set.

随机数生成器

真正高效的跳跃列表实现通常需要非常快的 RNG。尽管如此,我在快速分析会话中发现,只有极少的时间用于生成随机级别/高度,几乎不足以将其视为热点。它也只会影响插入时间,除非它提供更均匀的分布,所以我决定跳过此优化。

内存分配器

至此,我想说我们对跳跃列表实现与 BST 的期望有了一个相当合理的概述:

Insertion
-- std::set: 0.104869 secs
-- SkipList: 0.132322 secs

Search:
-- std::set: 0.078351 secs
-- SkipList: 0.127989 secs

Removal:
-- std::set: 0.098208 secs
-- SkipList: 0.130889 secs

然而,如果我们想进一步努力,我们可以使用固定分配器。在这一点上,我们有点作弊std::set旨在与任何符合标准分配器接口要求的通用分配器一起使用。但是让我们看一下使用固定分配器:

#include <iostream>
#include <iomanip>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <vector>
#include <cassert>
#include <set>

using namespace std;

static const int max_level = 32;

class FixedAlloc
{
public:
    FixedAlloc(): root_block(0), free_element(0), type_size(0), block_size(0), block_num(0)
    {
    }

    FixedAlloc(int itype_size, int iblock_size): root_block(0), free_element(0), type_size(0), block_size(0), block_num(0)
    {
        init(itype_size, iblock_size);
    }

    ~FixedAlloc()
    {
        purge();
    }

    void init(int new_type_size, int new_block_size)
    {
        purge();
        block_size = max(new_block_size, type_size);
        type_size = max(new_type_size, static_cast<int>(sizeof(FreeElement)));
        block_num = block_size / type_size;
    }

    void purge()
    {
        while (root_block)
        {
            Block* block = root_block;
            root_block = root_block->next;
            free(block);
        }
        free_element = 0;
    }

    void* allocate()
    {
        assert(type_size > 0);
        if (free_element)
        {
            void* mem = free_element;
            free_element = free_element->next_element;
            return mem;
        }

        // Create new block.
        void* new_block_mem = malloc(sizeof(Block) - 1 + type_size * block_num);
        Block* new_block = static_cast<Block*>(new_block_mem);
        new_block->next = root_block;
        root_block = new_block;

        // Push all but one of the new block's elements to the free pool.
        char* mem = new_block->mem;
        for (int j=1; j < block_num; ++j)
        {
            FreeElement* element = reinterpret_cast<FreeElement*>(mem + j * type_size);
            element->next_element = free_element;
            free_element = element;
        }
        return mem;
    }

    void deallocate(void* mem)
    {
        FreeElement* element = static_cast<FreeElement*>(mem);
        element->next_element = free_element;
        free_element = element;
    }

    void swap(FixedAlloc& other)
    {
        std::swap(free_element, other.free_element);
        std::swap(root_block, other.root_block);
        std::swap(type_size, other.type_size);
        std::swap(block_size, other.block_size);
        std::swap(block_num, other.block_num);
    }

private:
    struct Block
    {
        Block* next;
        char mem[1];
    };
    struct FreeElement
    {
        struct FreeElement* next_element;
    };

    // Disable copying.
    FixedAlloc(const FixedAlloc&);
    FixedAlloc& operator=(const FixedAlloc&);

    struct Block* root_block;
    struct FreeElement* free_element;
    int type_size;
    int block_size;
    int block_num;
};

static double sys_time()
{
    return static_cast<double>(clock()) / CLOCKS_PER_SEC;
}

static int random_level()
{
    int lvl = 1;
    while (rand()%2 == 0 && lvl < max_level)
        ++lvl;
    return lvl;
}

template <class T>
class SkipSet
{
public:
    SkipSet(): head(0)
    {
        for (int j=0; j < max_level; ++j)
            allocs[j].init(sizeof(Node) + (j+1)*sizeof(Node*), 4096);
        head = create_node(max_level, T());
        level = 0;
    }

    ~SkipSet()
    {
        while (head)
        {
            Node* to_destroy = head;
            head = head->next[0];
            destroy_node(to_destroy);
        }
    }

    bool contains(const T& value) const
    {
        const Node* node = head;
        for (int i=level; i >= 0; --i) 
        {
            while (node->next[i] && node->next[i]->value < value)
                node = node->next[i];
        }
        node = node->next[0];
        return node && node->value == value;
    }

    void insert(const T& value)
    {
        Node* node = head;  
        Node* update[max_level + 1] = {0};
        for (int i=level; i >= 0; --i) 
        {
            while (node->next[i] && node->next[i]->value < value)
                node = node->next[i];
            update[i] = node; 
        }
        node = node->next[0];

        if (!node || node->value != value)
        {
            const int lvl = random_level();
            assert(lvl >= 0);
            if (lvl > level) 
            {
                for (int i = level + 1; i <= lvl; ++i)
                    update[i] = head;
                level = lvl;
            }
            node = create_node(lvl, value);
            for (int i = 0; i <= lvl; ++i) 
            {
                node->next[i] = update[i]->next[i];
                update[i]->next[i] = node;
            }            
        }
    }

    bool erase(const T& value)
    {
        Node* node = head;  
        Node* update[max_level + 1] = {0};
        for (int i=level; i >= 0; --i) 
        {
            while (node->next[i] && node->next[i]->value < value)
                node = node->next[i];
            update[i] = node; 
        }
        node = node->next[0];

        if (node->value == value)
        {
            for (int i=0; i <= level; ++i) {
                if (update[i]->next[i] != node)
                    break;
                update[i]->next[i] = node->next[i];
            }
            destroy_node(node);
            while (level > 0 && !head->next[level])
                --level;
            return true;
        }
        return false;
    }

    void swap(SkipSet<T>& other)
    {
        for (int j=0; j < max_level; ++j)
            allocs[j].swap(other.allocs[j]);
        std::swap(head, other.head);
        std::swap(level, other.level);
    }

private:
    struct Node
    {
        T value;
        int num;
        struct Node* next[1];
    };

    Node* create_node(int level, const T& new_value)
    {
        void* node_mem = allocs[level-1].allocate();
        Node* new_node = static_cast<Node*>(node_mem);
        new (&new_node->value) T(new_value);
        new_node->num = level;
        for (int j=0; j < level+1; ++j)
            new_node->next[j] = 0;
        return new_node;
    }

    void destroy_node(Node* node)
    {
        node->value.~T();
        allocs[node->num-1].deallocate(node);
    }

    FixedAlloc allocs[max_level];
    Node* head;
    int level;
};

template <class T>
bool contains(const std::set<T>& cont, const T& val)
{
    return cont.find(val) != cont.end();
}

template <class T>
bool contains(const SkipSet<T>& cont, const T& val)
{
    return cont.contains(val);
}

template <class Set, class T>
void benchmark(int num, const T* elements, const T* search_elements)
{
    const double start_insert = sys_time();
    Set element_set;
    for (int j=0; j < num; ++j)
        element_set.insert(elements[j]);
    cout << "-- Inserted " << num << " elements in " << (sys_time() - start_insert) << " secs" << endl;

    const double start_search = sys_time();
    int num_found = 0;
    for (int j=0; j < num; ++j)
    {
        if (contains(element_set, search_elements[j]))
            ++num_found;
    }
    cout << "-- Found " << num_found << " elements in " << (sys_time() - start_search) << " secs" << endl;

    const double start_erase = sys_time();
    int num_erased = 0;
    for (int j=0; j < num; ++j)
    {
        if (element_set.erase(search_elements[j]))
            ++num_erased;
    }
    cout << "-- Erased " << num_found << " elements in " << (sys_time() - start_erase) << " secs" << endl;
}

int main()
{
    const int num_elements = 200000;
    vector<int> elements(num_elements);
    for (int j=0; j < num_elements; ++j)
        elements[j] = j;
    random_shuffle(elements.begin(), elements.end());

    vector<int> search_elements = elements;
    random_shuffle(search_elements.begin(), search_elements.end());

    typedef std::set<int> Set1;
    typedef SkipSet<int> Set2;

    cout << fixed << setprecision(3);
    for (int j=0; j < 2; ++j)
    {
        cout << "std::set" << endl;
        benchmark<Set1>(num_elements, &elements[0], &search_elements[0]);
        cout << endl;

        cout << "SkipSet" << endl;
        benchmark<Set2>(num_elements, &elements[0], &search_elements[0]);
        cout << endl;
    }
}

* I also made a minor tweak to random_level to make it simply assume a probability of 50% after discovering that this seems to work quite well.

通过使用固定分配器,我们可以使用非常简单的常量时间策略快速分配和释放元素,更重要的是,以某种方式分配节点,使得具有相同高度的节点(最常一起访问的节点)被更连续地分配相互尊重(尽管不是按照理想的顺序)。这将时间缩短为:

Insertion
-- std::set: 0.104869 secs
-- SkipList: 0.103632 secs

Search:
-- std::set: 0.078351 secs
-- SkipList: 0.089910 secs

Removal:
-- std::set: 0.098208 secs
-- SkipList: 0.089224 secs

...这对于大约 40 分钟的对抗来说还不错std::set整个行业的专家都对这个问题进行了探讨和调整。我们的清除速度也比std::set(插入时间在我的机器上略有波动,我想说它们大致相同)。

当然,我们在应用最后一步时作了作弊。使用自定义分配器会使事情对我们有利,并且还会改变容器的特性,使其内存在被清除、销毁或压缩之前无法释放。它可以将内存标记为未使用,并在后续插入时回收它,但它不能使其使用的内存可供跳跃列表之外的内存使用。我也懒得去注意所有可能类型的正确对齐T这对于真正概括它是必要的。

排序输入

让我们尝试对排序输入使用它。为此,只需注释掉这两个random_shuffle声明。就我而言,我现在得到这些时间:

std::set
-- Inserted 200000 elements in 0.044 secs
-- Found 200000 elements in 0.023 secs
-- Erased 200000 elements in 0.019 secs

SkipSet
-- Inserted 200000 elements in 0.027 secs
-- Found 200000 elements in 0.023 secs
-- Erased 200000 elements in 0.016 secs

...现在我们的SkipSet表现优于std::set在所有领域,尽管只是针对这种特殊情况。

结论

所以我不知道这对跳过列表到底意味着什么。几乎没有花费任何时间和精力,我们就已经非常接近竞争对手了std::set*。然而我们并没有击败它,我们必须使用内存分配器来作弊才能真正接近它。

* Note that mileage may vary across machines, vendor implementations of std::set, etc.

自 Pugh 1989 年发表论文以来,时代已经发生了很大变化。

如今,引用局部性的好处在很大程度上占据了主导地位,即使是线性复杂度算法也可以胜过线性算法,前提是前者对缓存或页面友好得多。密切关注系统从内存层次结构的上层(例如:二级阶段)获取内存块的方式,内存速度较慢但更大,一直到小型 L1 高速缓存线和小型寄存器,这比以往任何时候都更重要,并且如果你问我什么时候带来的好处可以与算法改进相媲美,那么不再是“微观”。

考虑到节点的大小相当大,而且同样重要的是,节点的大小可变(这使得它们很难非常有效地分配),因此跳跃列表可能会被削弱。

我们没有考虑的一件事是跳跃列表在插入时更新的本地化性质。这往往会影响比平衡树通过旋转父节点重新平衡所需的区域少得多的区域。因此,跳跃列表可以提供并发集或映射的潜在更有效的实现。

跳跃列表的另一个有前途的特性是它只是一个最低级别的链接列表。因此,它提供了非常简单的线性时间顺序遍历,而不必以线性复杂度沿着树的分支下降,因此如果我们想要对包含的所有元素进行大量线性时间迭代,那么它可能非常好。

但永远记住:

一项技术只有能够应用到实施者手中才算好。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

跳过列表,它们真的像 Pugh 论文声称的那样好吗? 的相关文章

  • 如何将不记名令牌发送到 ASP NET MVC 5 中的视图?

    我有一个 NET MVC and WEB API项目 我想打电话给WEB API controllers来自 javascript 但我没有找到将令牌发送到我的视图的方法 我想添加bearer token in Viewbag变量 使用以下
  • 动态选择和更新 LINQ 结果集中的列值

    我有一个场景 其中存在 LINQ 结果集 我使用了以下查询 var stockDetails from d in db BloodBanks where d bbUserName Session username ToString sele
  • 绘图:仅保留最相关的数据

    为了节省带宽并且不用自己生成图片 图表 我计划使用 Google 的图表 API http code google com apis chart http code google com apis chart 它的工作原理是简单地发出一个
  • & 运算符的含义是什么?

    在下面的代码中 Expression
  • std::tr1::function 和 std::tr1::bind

    我在使用时遇到问题veryC 类中的复杂 C 函数 重写 C 函数是not一个选项 C函数 typedef void integrand unsigned ndim const double x void fdata unsigned fd
  • 通过用于 Symbol 条码扫描仪 DS4208 的 SNAPI.dll API 捕获图像

    我想通过 SNAPI API 从 Symbol 目前为 Zebra 条形码扫描仪 DS4208 型号 我们还使用 Zebra 的另一个但兼容的型号 捕获图像 条形码捕获 识别效果很好 但看起来像SnapiDLL SNAPI SnapShot
  • numpy 数组最快的保存和加载选项

    我有一个生成二维的脚本numpy数组与dtype float和形状的顺序 1e3 1e6 现在我正在使用np save and np load对数组执行 IO 操作 然而 这些函数对于每个数组都需要几秒钟的时间 是否有更快的方法来保存和加载
  • 我的 WPF 应用程序未触发 MainWindow_Loaded

    我目前正在关注Pluralsight C Fundamentals Part 1并在Classes and Objects视频部分指导我在 Visual Studio 中创建一个新的 WPF 应用程序并填写代码 这导致以下结果 namesp
  • 函数指针上的未知类型 F TYPE

    include
  • 修改正在运行的可执行文件的资源内容

    All 我将应用程序设置存储在资源中 当我的程序首次加载时 我使用 WinAPI 读取指定的资源 然后我解析检索到的字节数据 这对我来说完美无缺 现在假设用户更改了我的应用程序中的设置 他 她检查复选框控件 我想将更新的设置保存到我的资源中
  • 为什么我的 CAOpenGLLayer 更新速度比之前的 NSOpenGLView 慢?

    我有一个在 Mac OS X 上渲染 OpenGL 内容的应用程序 最初它渲染到 NSOpenGLView 然后我将其更改为渲染到 CAOpenGLLayer 子类 当我这样做时 我看到了巨大的性能损失 帧速率减半 鼠标响应能力降低 卡顿
  • 如何使用 ProtoGen 从 proto 文件生成结构

    我们一直在使用 protobuf net ProtoGen 从 proto 文件生成 C cs 文件 我们希望代替类来生成结构 例如 DataContract public struct Entity1 ProtoMember 1 publ
  • 从视图模型调用方法的命令

    好吧 我倾向于避免使用命令 因为它们总是让我感到困惑 但我正在进行一个新项目 并且正在尝试正确构建它 并且在我看来没有任何代码隐藏 基本上我现在想做的就是连接一个按钮来触发一个命令 在我的视图模型上执行一些操作 但不知何故 如此简单的事情仍
  • 生成范围 [min,max] 内的随机数 [重复]

    这个问题在这里已经有答案了 我正在使用 C 生成范围 min max 内的整数随机数 我在用 int random int int min int max return min rand max min 但我认为上面的代码适用于范围 min
  • 使用 _Alignas 进行结构成员对齐

    我想知道以下问题 是新的吗 Alignas结盟 C11 中的说明符适用于结构成员吗 我一直假设这么多 但彻底阅读了 N1570 公开草案似乎表明对齐说明符不能 出现在一个说明符限定符列表 这就是我所期望的 如果得到支持的话 我已经读过几遍语
  • 返回 ICollection 而不是 List 的真正优势是什么? [复制]

    这个问题在这里已经有答案了 我读过几篇博客文章 提到对于公共 API 我们应该始终返回 ICollection 或 IEnumerable 而不是 List 返回 ICollection 而不是 List 的真正优势是什么 Thanks 复
  • C# 3.0 中自动属性和公共字段的区别

    我无法理解为什么 C 3 0 中存在自动实现的属性语言功能 当你说的时候有什么区别 public string FirstName than public string FirstName get set 因为它们在生成的 IL 代码 和机
  • 在Framework 4.6项目中使用.net core DLL

    我已经在 net core 2 0 中构建了一个 DLL 现在我想在使用 net 4 6 1 框架的 WinForms 项目中使用它 我可以引用该 dll 但收到 System IO FileLoadException 表示找不到 Syst
  • char[length]初始化并处理

    我定义了一个字符数组 char d 6 如果我在以下方面有误 请纠正我 此时没有为变量分配内存d 现在我要初始化它 d aaaaa 这种初始化之后 就不需要释放内存了 它将自动完成 我怎么知道是否char 被初始化了吗 我正在寻找类似的模式
  • asp.net mvc GET 请求上的 formcollection 应该为空

    我正在发布一个简单的操作 public void Login FormCollection formCollection 即使查询字符串值很少 formcollection Count is 0 是靠行为吗 FormCollection 使

随机推荐

  • 多个声明和定义

    内容X c int i main fun 内容Y c int i fun 为什么这两个文件编译没有错误 使用海湾合作委员会 但如果我使用int i 10 它打印多重定义错误 您可能对这个问题及其答案感兴趣 关键词 暂定定义 C99 中的暂定
  • Linq Time(7) 和时间跨度映射

    我正在尝试将一条记录插入到我的表中 名为Test 我正在使用 LINQ 技术 问题是我有一个time我的表中的列类型为Time 7 但是当我尝试将数据插入表时出现此错误 Operand type clash bigint is incomp
  • 在 JavaDoc 中使用 Maven 属性

    是否可以使用 Maven Javadoc 插件扩展 javadoc 上的 Maven 属性范围 例如 My Awesome Class version project version 我想你可以这样尝试 这是两步过程 首先是将pom属性加载
  • Hive 来自 JSON 错误

    我无法以某种方式将此 json 放入配置单元表中 要么变成所有空数据 要么无法被选择 我只需要与我的 DDL 相同的字段 如果它在其中结构化 我想让它作为字符串而不是尝试解析它 唯一一个几乎只能通过以下方式实现 hive hcatalog
  • 即使 Xms = Xmx,G1GC 也会将内存释放给操作系统吗?

    阅读了一些答案后 例如this https stackoverflow com a 30464183 1432247 and JEP 346 https openjdk java net jeps 346 我已经意识到 G1 确实将内存释放
  • 可以在 iOS 中处理您自己的 http URL 方案吗?

    iOS 上的 iTunes App Store 和 YouTube 明确注册了 http URL 方案来打开其应用程序 任何人都可以做到这一点 而不仅仅是您自己的协议吗 我想这样做的原因是我正在为一个节日开发一个应用程序 我想 拦截 网站上
  • 如何以编程方式修改 Firebase 中的安全规则?

    Firebase 文档中的示例假设手动更新 Firebase 安全规则 如何以编程方式修改安全规则以支持现实世界的协作应用程序 在我正在考虑的用例中 要求用户能够邀请其他用户协作 共享所选数据 并能够授予 撤销对协作者的访问权限 如何使用
  • 什么是 .sln.ide 文件?

    我有一个 Visual Studio 2012 解决方案 Foobar sln 它包含一个类库项目和一个单元测试项目 几天前我注意到一个新文件 Foobar sln ide graph Foobar sln ide 内容
  • 如何使用react.js中的WHERE查询计算Firebase Firestore中集合中的文档数量

    我想要获取我的 Firebase Firestore 集合中的用户总数 我可以通过编写以下代码轻松获得它 const totalUsers setTotalUsers useState 0 const newUsers setNewUser
  • 为什么 Promise 构造函数需要一个在完成时调用“resolve”的函数,但“then”则不需要——它返回一个值?

    当我投入学习的时候Promises 我的理解停止在以下问题上 我没有找到讨论该问题 我发现的只是对Promise构造函数 以及Promise then 功能 但不是比较其设计模式的讨论 1 The Promise构造函数 来自 MDN 文档
  • Twine 上传类型错误:预期字符串或类似字节的对象

    当您尝试上传包时 有人遇到过这样的错误吗 twine upload dist Uploading distributions to https upload pypi org legacy Enter your username MyUse
  • AliasMatch 和 RegEx

    我正在开发服务器上使用集中式 CMS 该 CMS 位于 var www central cms 该网站 我有很多网站 可以通过以下网址访问 http web localdomain dev site1 如何访问 cms 只需输入以下网址 h
  • 如何在 JPA 下解锁一个实体

    all 我正在使用纯 JPA 编写一个程序 其提供程序是 hibernate 底层数据库是 Azure SQL DB 该程序将在分布式环境下运行 它的许多副本将运行在不同的服务器上 我需要执行类似的程序 例如 1 锁定指定的学生实体 2 检
  • 我可以从 WindowsPrincipal 获取 Active Directory 属性吗?

    我想获取当前登录用户的员工 ID 这在某些 Net 类中是否很容易获得 或者我是否需要进行某种 LDAP 查询 欢迎任何提示 更简单 使用新的 NET 3 5System DirectoryServices AccountManagemen
  • 接收 Android Google Play 音乐广播两次

    我希望你今天过得愉快 让我们进入正题 在我的清单文件中 我添加了一个像这样的接收器
  • 为什么在定义结构时使用不同的标识符?

    考虑这段代码 typedef struct Node Node struct Node struct Node node or this typedef struct Node struct Node node Node 有什么理由不将它们
  • netcdf4 提取经纬度子集

    我想提取一个相当大的 netcdf 文件的空间子集 从循环遍历 netcdf 文件并运行计算 Python 或 R https stackoverflow com questions 18665078 loop through netcdf
  • 如何将 StrLn 放入 Data.ByteString.Internal.ByteString?

    我正在学习 Haskell 并决定尝试编写一些小型测试程序来习惯 Haskell 代码和使用模块 目前 我正在尝试使用第一个参数来使用 Cypto PasswordStore 创建密码哈希 为了测试我的程序 我尝试从第一个参数创建一个散列
  • 将列添加到数据库 - 如何更新 dbml 文件?

    有没有一种方法可以刷新 dbml 文件 还是必须删除它并生成一个全新的文件 据我所知 设计器中没有内置功能可以使用数据库中的更改来更新表 有第三方工具 http www huagati com dbmltools http www huag
  • 跳过列表,它们真的像 Pugh 论文声称的那样好吗?

    我正在尝试使用最小的额外内存开销来实现一个与 BST 一样好的跳过列表 目前即使不考虑任何内存限制 我的 SkipList 实现的性能也与非常幼稚的平衡 BST 实现相去甚远 可以这么说 手工制作的 BTS 作为参考 我使用 William