【问题标题】:What data structure best mixes strengths of dictionary and list什么数据结构最好地结合了字典和列表的优势
【发布时间】:2011-06-22 02:33:35
【问题描述】:

我最近问过的question 让我对以下问题感到疑惑:

存储集合(键,值)对的数据结构是什么:

  • 它的元素是有序的
  • d[key] -> val 具有复杂性 O(dict)
  • d(index) -> (key, val) 具有复杂性 O(list)
  • 提供反向查找d{val} -> (index, key) 复杂性O(dict)
  • 使用尽可能少的存储空间

当我输入 O(type) 时,我的意思是操作的复杂性与数据结构 type 相同。

例如,如果有序集合是:

c = {key_1:val_1, key_2:val_2, key_3:val_3}

我想获得

 c[key_1] # returns val_1, as in a dictionary
 c(1)     # returns val_2, as in a list
 c{val_3} # returns (2, key_3) as in a sort of "indexed dictionary"

【问题讨论】:

    标签: algorithm data-structures


    【解决方案1】:

    您要求通过键和索引进行 O(1) 次查找以及 O(1) 次值查找。您可以通过维护键/值的哈希数据结构、反向查找的第二个哈希结构以及有序列表->键映射的列表数据结构来做到这一点。不过,您仍然会有 O(n) 次插入和删除,而且您的空间复杂度将是正常情况的 3 倍。

    如果你愿意在速度上妥协,你可以节省空间,有很多基于树的集合数据结构(例如 Java 中的 TreeSet),其操作的复杂度为 log(n)。

    这总是一种权衡

    【讨论】:

    • 没有字典在最坏的情况下具有恒定的时间查找。哈希表将是O(n),因为所有元素都有可能在同一个桶中结束。
    • @Justin - 虽然您在技术上是正确的,但哈希表的合理实现不会有任何现实概率的 o(n) 查找。有提供恒定时间查找的特定实现,但大多数都是摊销 O(1)。当他们说 O(1).... 时,大多数关于哈希的对话都意味着这一点。
    • @Justin 除了@spinning_plate 关于病理情况的观点之外,还有一些哈希表具有 O(1) 最坏情况查找时间; Cuckoo Hashing 就是一个例子。
    • 要获得恒定的时间查找,您需要一个至少与键数一样大的哈希码池。如果我在 has 中仅使用字母数字字符,则代码的长度因此为 log(n)/log(36)。哈希的计算必须至少与哈希本身的长度一样复杂(因为转录),所以它是 O(log(n)),尽管常数特别小。
    【解决方案2】:

    您没有提到插入成本,这也是一个重要问题。您可以使用按词法排序的字典来执行此操作,并使用二进制搜索(即log(n))处理查找。但是,您需要维护两个这样的结构,一个是 key->val,一个是 val->key,因此插入成本将增加一倍,并且需要在中间插入元素将其放在 O(n) (即,与列表相同)。

    【讨论】:

    • 我没有假设“列表”是指“基于数组的集合,具有恒定时间随机查找”
    • 我想我是这个意思。抱歉,我的 CS 术语(如您所见)几乎不存在。
    • 不是你的错,整个 CS 领域在这一点上自相矛盾。 C#的List是一个基于数组的集合,而C++的list是一个链表。
    【解决方案3】:

    我遇到了同样的问题。于是我拿了java.util.TreeMap的源代码写了IndexedTreeMap。它实现了我自己的 IndexedNavigableMap

    public interface IndexedNavigableMap<K, V> extends NavigableMap<K, V> {
       K exactKey(int index);
       Entry<K, V> exactEntry(int index);
       int keyIndex(K k);
    }
    

    该实现基于在红黑树更改时更新节点权重。权重是给定节点下的子节点数加一 - self。例如当一棵树向左旋转时:

        private void rotateLeft(Entry<K, V> p) {
        if (p != null) {
            Entry<K, V> r = p.right;
    
            int delta = getWeight(r.left) - getWeight(p.right);
            p.right = r.left;
            p.updateWeight(delta);
    
            if (r.left != null) {
                r.left.parent = p;
            }
    
            r.parent = p.parent;
    
    
            if (p.parent == null) {
                root = r;
            } else if (p.parent.left == p) {
                delta = getWeight(r) - getWeight(p.parent.left);
                p.parent.left = r;
                p.parent.updateWeight(delta);
            } else {
                delta = getWeight(r) - getWeight(p.parent.right);
                p.parent.right = r;
                p.parent.updateWeight(delta);
            }
    
            delta = getWeight(p) - getWeight(r.left);
            r.left = p;
            r.updateWeight(delta);
    
            p.parent = r;
        }
      }
    

    updateWeight 只是将权重更新到根:

       void updateWeight(int delta) {
            weight += delta;
            Entry<K, V> p = parent;
            while (p != null) {
                p.weight += delta;
                p = p.parent;
            }
        }
    

    当我们需要通过索引找到元素时,这里是使用权重的实现:

    public K exactKey(int index) {
        if (index < 0 || index > size() - 1) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return getExactKey(root, index);
    }
    
    private K getExactKey(Entry<K, V> e, int index) {
        if (e.left == null && index == 0) {
            return e.key;
        }
        if (e.left == null && e.right == null) {
            return e.key;
        }
        if (e.left != null && e.left.weight > index) {
            return getExactKey(e.left, index);
        }
        if (e.left != null && e.left.weight == index) {
            return e.key;
        }
        return getExactKey(e.right, index - (e.left == null ? 0 : e.left.weight) - 1);
    }
    

    查找键的索引也非常方便:

        public int keyIndex(K key) {
        if (key == null) {
            throw new NullPointerException();
        }
        Entry<K, V> e = getEntry(key);
        if (e == null) {
            throw new NullPointerException();
        }
        if (e == root) {
            return getWeight(e) - getWeight(e.right) - 1;//index to return
        }
        int index = 0;
        int cmp;
        if (e.left != null) {
            index += getWeight(e.left);
        }
        Entry<K, V> p = e.parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            while (p != null) {
                cmp = cpr.compare(key, p.key);
                if (cmp > 0) {
                    index += getWeight(p.left) + 1;
                }
                p = p.parent;
            }
        } else {
            Comparable<? super K> k = (Comparable<? super K>) key;
            while (p != null) {
                if (k.compareTo(p.key) > 0) {
                    index += getWeight(p.left) + 1;
                }
                p = p.parent;
            }
        }
        return index;
    }
    

    你可以在http://code.google.com/p/indexed-tree-map/找到这项工作的结果

    【讨论】:

      猜你喜欢
      • 2011-05-25
      • 2011-11-23
      • 1970-01-01
      • 1970-01-01
      • 2011-11-28
      • 2012-02-03
      • 1970-01-01
      • 1970-01-01
      • 2018-08-10
      相关资源
      最近更新 更多