【问题标题】:O(1) lookup in non-contiguous memory?O(1) 在非连续内存中查找?
【发布时间】:2010-01-20 01:31:03
【问题描述】:

是否有任何已知的数据结构提供 O(1) 随机访问,而不使用大小为 O(N) 或更大的连续内存块?这是受到this answer 的启发,出于好奇而不是任何特定的实际用例而提出要求,尽管它可能在堆严重碎片化的情况下有用。

【问题讨论】:

  • 专门解决来自std::vector 问题的一些混淆:大多数时候人们在将&vector[0] 视为C 数组的上下文中谈论这个。病态向量实现可能会破坏 C 数组的兼容性,同时只需以相反的顺序存储元素即可轻松满足 C++98 的 O(1) 随机访问要求。
  • 那个defect已经在C++03中修复了。对于在现实世界的图书馆中从未被误解的已知缺陷,真的不值得争论可能拥有的东西。在velocityreviews.com/forums/… 上查看 Stroustrup 的 cmets
  • 对,我只是在解释为什么 C++98 的要求只暗示了连续内存而不是需要连续内存(这就是引发这个问题的原因)。我完全同意这不是问题。
  • @jamesdlin:啊,我没有从问题中看到这种混乱。

标签: performance arrays data-structures memory-management big-o


【解决方案1】:

是的,这里有一个 C++ 示例:

template<class T>
struct Deque {
  struct Block {
    enum {
      B = 4*1024 / sizeof(T), // use any strategy you want
                              // this gives you ~4KiB blocks
      length = B
    };
    T data[length];
  };
  std::vector<Block*> blocks;

  T& operator[](int n) {
    return blocks[n / Block::length]->data[n % Block::length]; // O(1)
  }

  // many things left out for clarity and brevity
};

与 std::deque 的主要区别在于它具有 O(n) push_front 而不是 O(1),实际上实现 std::deque 以具有 all 存在一些问题的:

  1. O(1) push_front
  2. O(1) push_back
  3. O(1) 运算[]

也许我误解了“不使用大小为 O(N) 或更大的连续内存块”,这似乎很尴尬。你能澄清你想要什么吗?我将其解释为“没有单个分配包含一个代表序列中的每个项目的项目”,例如有助于避免大量分配。 (即使我确实为向量分配了一个大小为 N/B 的单一分配。)

如果我的回答不符合您的定义,那么除非您人为地限制容器的最大尺寸,否则什么都不会。 (例如,我可以将您限制为 LONG_MAX 个项目,将上述块存储在树中,然后调用该 O(1) 查找。)

【讨论】:

  • Block::data的长度不是O(N)吗?它是 N 的一部分,但它是一个常数分数,因此是 O(N)。
  • 这是一个连续的结构:所有元素都在一条线上,没有间隙。很好的寻址语义,但对问题没有真正的响应。
  • @dsimcha:不,长度是编译时间常数。
  • @dmckee:这些元素没有存储在“一个[单个]连续的内存块”中,它是如何解决这个问题的?
  • @Roger: 好吧,你说得对,我看错了,但块不是 O(N) 吗?不会 blocks.size() == N / B?
【解决方案2】:

您可以使用trie,其中键的长度是有界的。由于在长度为m 的键的树中查找是O(m),如果我们绑定键的长度,那么我们绑定m,现在查找是O(1)

所以想一想其中键是字母表上的字符串 { 0, 1 } 的 trie(即,我们将键视为整数的二进制表示)。如果我们将键的长度限制为 32 个字母,则我们可以将其视为由 32 位整数索引的结构,并且可以在O(1) 时间随机访问。

这是 C# 中的一个实现:

class TrieArray<T> {
    TrieArrayNode<T> _root;

    public TrieArray(int length) {
        this.Length = length;
        _root = new TrieArrayNode<T>();
        for (int i = 0; i < length; i++) {
            Insert(i);
        }
    }

    TrieArrayNode<T> Insert(int n) {
        return Insert(IntToBinaryString(n));
    }

    TrieArrayNode<T> Insert(string s) {
        TrieArrayNode<T> node = _root;
        foreach (char c in s.ToCharArray()) {
            node = Insert(c, node);
        }
        return _root;
    }

    TrieArrayNode<T> Insert(char c, TrieArrayNode<T> node) {
        if (node.Contains(c)) {
            return node.GetChild(c);
        }
        else {
            TrieArrayNode<T> child = new TrieArray<T>.TrieArrayNode<T>();
            node.Nodes[GetIndex(c)] = child;
            return child;
        }

    }

    internal static int GetIndex(char c) {
        return (int)(c - '0');
    }

    static string IntToBinaryString(int n) {
        return Convert.ToString(n, 2);
    }

    public int Length { get; set; }

    TrieArrayNode<T> Find(int n) {
        return Find(IntToBinaryString(n));
    }

    TrieArrayNode<T> Find(string s) {
        TrieArrayNode<T> node = _root;
        foreach (char c in s.ToCharArray()) {
            node = Find(c, node);
        }
        return node;
    }

    TrieArrayNode<T> Find(char c, TrieArrayNode<T> node) {
        if (node.Contains(c)) {
            return node.GetChild(c);
        }
        else {
            throw new InvalidOperationException();
        }
    }

    public T this[int index] {
        get {
            CheckIndex(index);
            return Find(index).Value;
        }
        set {
            CheckIndex(index);
            Find(index).Value = value;
        }
    }

    void CheckIndex(int index) {
        if (index < 0 || index >= this.Length) {
            throw new ArgumentOutOfRangeException("index");
        }
    }

    class TrieArrayNode<TNested> {
        public TrieArrayNode<TNested>[] Nodes { get; set; }
        public T Value { get; set; }
        public TrieArrayNode() {
            Nodes = new TrieArrayNode<TNested>[2];
        }

        public bool Contains(char c) {
            return Nodes[TrieArray<TNested>.GetIndex(c)] != null;

        }

        public TrieArrayNode<TNested> GetChild(char c) {
            return Nodes[TrieArray<TNested>.GetIndex(c)];
        }
    }
}

这里是示例用法:

class Program {
    static void Main(string[] args) {
        int length = 10;
        TrieArray<int> array = new TrieArray<int>(length);
        for (int i = 0; i < length; i++) {
            array[i] = i * i;
        }
        for (int i = 0; i < length; i++) {
            Console.WriteLine(array[i]);
        }
    }
}

【讨论】:

  • 那是假的——一旦你限制了键的大小,在 any 结构中的查找是恒定时间的。最多 32 层深度的二叉树是 O(1),与最多 32 层深度的二叉树相同。不过,树会比 trie 快得多,因为在返回之前将其填充到访问 32 个节点的点需要更长的时间。 trie 仅在第一次插入后实现最坏情况的树行为。
  • 绝对不是。现在其他想法是哈希表,它具有 O(1) 查找但可能或可能不被视为连续内存,以及指向数组的指针数组,这绝对满足了这个问题。
  • @Potatoswatter:你的意思是什么?除非您限制了最大索引的大小,否则理论数组不是O(1)(因为除非您绑定了操作数,否则加法不是O(1))。我以与数组可索引相同的方式模拟了可索引的数组。
  • 所有这些狡辩真的没有帮助。事实是,这是对这个问题的一个很好的实际答案。虽然 trie 查找在理论上是 O(log n),但表示以 1024 为底的对数的常数因子使得它们在实践中的执行类似于 O(1),这与大多数 O(log n) 数据结构不同。它们可以用像T&amp; operator[](size_t x) { return table[index0(x)][index1(x)][index2(x)][index3(x)]; } 这样的直线 O(1) 代码来实现,即使对于大型数据集,它们也可能最终胜过其他一些提议的 O(1) 答案。对比二叉树。
  • @Potatoswatter:哇——你说得对,我没有仔细阅读答案。我认为 Jason 提出了一个真正的 trie,在树的每一层都有 8-12 位,而不是一个位。你是对的,二进制尝试在这里是一个非常愚蠢的选择。
【解决方案3】:

好吧,既然我已经花时间思考它,可以认为所有哈希表要么是大小 >N 的连续块,要么具有与 N 成比例的存储桶列表,而 Roger 的Blocks 的顶级数组是 O(N),系数小于 1,我在 cmets 中针对他的问题提出了解决方案,如下:

int magnitude( size_t x ) { // many platforms have an insn for this
    for ( int m = 0; x >>= 1; ++ m ) ; // return 0 for input 0 or 1
    return m;
}

template< class T >
struct half_power_deque {
    vector< vector< T > > blocks; // max log(N) blocks of increasing size
    int half_first_block_mag; // blocks one, two have same size >= 2

    T &operator[]( size_t index ) {
        int index_magnitude = magnitude( index );
        size_t block_index = max( 0, index_magnitude - half_first_block_mag );
        vector< T > &block = blocks[ block_index ];
        size_t elem_index = index;
        if ( block_index != 0 ) elem_index &= ( 1<< index_magnitude ) - 1;
        return block[ elem_index ];
    }
};

template< class T >
struct power_deque {
    half_power_deque forward, backward;
    ptrdiff_t begin_offset; // == - backward.size() or indexes into forward
    T &operator[]( size_t index ) {
        ptrdiff_t real_offset = index + begin_offset;
        if ( real_offset < 0 ) return backward[ - real_offset - 1 ];
        return forward[ real_offset ];
    }
};

half_power_deque 实现擦除除最后一个块之外的所有块,适当地改变 half_first_block_mag。这允许 O(max over time N) 内存使用,在两端分摊 O(1) 插入,永远不会使引用无效,以及 O(1) 查找。

【讨论】:

  • 哎呀,这个方案分配的最大块至少是总容量的 1/4,这确实符合 O(N) 的条件。尽管如此,这是一个不错的想法,应该比双端队列表现更好。 (我的实现变得比我想要的更丑,我放弃了它......应该回到它......)
【解决方案4】:

地图/字典怎么样?最后我检查了一下,这是 O(1) 性能。

【讨论】:

  • 哈希映射占用 O(n) 连续内存。树形图或跳过列表需要 O(log n) 查找时间(尽管如果您始终是顺序的,则展开树可以将其转回 O(1) 摊销)。
  • 好吧,有点。如果可以通过引用访问类型,则可以实现不采用这种方式 - 基本数组不必是 O(n),任何溢出的分配当然不必都是连续的,并且如果它们存储为引用/指针,则实际存储的类型可以存储在任意位置。不过,那里存在某种程度的语言依赖性。
  • kyoryu 是对的,您可以通过几种不同的方式存储哈希映射,不需要单个连续分配容器大小,但是如果您知道某些事情,查找只是 O(1)哈希函数和数据集。
  • @Roger:这是真的——性能可能会降低到 O(n),但在一般情况下,我通常认为它被接受为 O(1)(鉴于有关数据集的足够信息,您可以设计一个方案来有效地获得 O(1) perf)
  • 如果您有一些提供 O(1) 查找并使用非连续内存的数据结构,您可以将 that 用于哈希表。
猜你喜欢
  • 2013-02-08
  • 1970-01-01
  • 2023-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-15
  • 2019-12-15
  • 1970-01-01
相关资源
最近更新 更多