高效的插入数据结构

Posted

技术标签:

【中文标题】高效的插入数据结构【英文标题】:Efficient Data Structure for Insertion 【发布时间】:2012-04-20 01:47:47 【问题描述】:

我正在寻找一种允许将值快速(比 O(N) 快)任意插入到结构中的数据结构(类似数组)。数据结构必须能够以插入方式打印出其元素。这类似于 List.Insert() 之类的东西(它太慢了,因为它必须移动每个元素),除了我不需要随机访问或删除。插入将始终在“数组”的大小范围内。所有的价值观都是独一无二的。无需其他操作。

例如,如果 Insert(x, i) 在索引 i(0 索引)处插入值 x。那么:

Insert(1, 0) 给出 1 Insert(3, 1) 给出 1,3 Insert(2, 1) 给出 1,2,3 Insert(5, 0) 给出 5,1,2,3

并且它需要能够在最后打印出 5,1,2,3。

我正在使用 C++。

【问题讨论】:

“类似数组”是什么意思? 你对遍历数据结构的复杂度有要求吗? @juanchopanza 我的意思是在表面上,它应该像一个线性阵列。它应该以我插入它们的方式保持元素。 @LucTouraille 所以插入应该是次线性的(O(lgN)等),但输出数组的内容不必非常快(O(N)或O(NlgN)是很好)。 @juanchopanza 如果我没记错的话,如果你有一个指向要插入的索引的指针(迭代器),std::list 会给出 O(1) 的插入。但是,获取该指针需要进行线性搜索。 【参考方案1】:

this 数据结构将插入时间从 O(N) 缩短到 O(sqrt(N)),但我并没有那么印象深刻。我觉得应该可以做得更好,但我还得努力。

【讨论】:

【参考方案2】:

GCC 默认包含的一个解决方案是 rope 数据结构。这是documentation。通常,在处理长字符串时会想到绳索。这里我们有ints 而不是字符,但它的工作原理是一样的。只需使用int 作为模板参数。 (也可以是pairs等)

这是description of rope on Wikipedia。

基本上,它是一棵二叉树,它维护左右子树中有多少元素(或等效信息,即所谓的顺序统计),并且这些计数被适当地更新为插入和删除元素时会旋转子树。这允许 O(lg n) 操作。

【讨论】:

【参考方案3】:

关于您的评论:

List.Insert() (这太慢了,因为它必须将每个元素都移过来),

列表不会改变它们的值,它们会遍历它们以找到你想要插入的位置,小心你说的话。这可能会让像我这样的新手感到困惑。

【讨论】:

【参考方案4】:

使用skip list。另一个选项应该是tiered vector。跳过列表在 const O(log(n)) 处执行插入并保持数字有序。分层向量支持在 O(sqrt(n)) 中插入,并且可以再次按顺序打印元素。

编辑:根据 amit 的评论,我将解释如何在跳过列表中找到第 k 个元素:

对于每个元素,您都有一个指向下一个元素的链接的塔,对于每个链接,您都知道它跳过了多少个元素。因此,寻找第 k 个元素,您从列表的头部开始并沿着塔向下直到找到一个跳过不超过 k 个元素的链接。您转到该节点指向的节点,并随着您跳过的元素数量减少 k。继续这样做,直到你有 k = 0。

【讨论】:

我也在考虑跳过列表的行,您能否详细说明在任意位置插入元素后如何修改访问链接列表[保证O(logn)搜索的人] ?这不会导致需要改变很多吗?我相信它 [skip-list] 可以修改为适合这里,但这点应该在 IMO 中详细说明 事实上,我不久前实施的跳过列表的方式是你永远不会改变节点的高度。这依赖于这样一个事实,即如果您插入具有均匀分布高度的每个新节点,则元素的高度将足够接近完美的高度。互联网上对这种方法的摊销复杂性进行了一些分析,表明它并不比最好的方法差多少。 我不明白的是如何修改不是高度,而是索引,你怎么知道元素是第k个?如果您的“键”是索引,那么每次任意插入是否都需要更改链表的整个尾部? [我担心的不是高度,使用非确定性链表巧妙地解决了这个问题] 对于每个元素,您都有一个指向下一个元素的链接的塔,对吗?对于每个链接,您都知道它跳过了多少个元素。因此,寻找第 k 个元素,您从列表的头部开始并沿着塔向下直到找到一个跳过不超过 k 个元素的链接。您转到一个新节点并随着您跳过的元素数量减少 k。继续这样做,直到你有 k = 0。 太好了,完美地解释了这一点。 +1。我建议将此解释添加到答案本身。 [其实我知道问起来很笨,这和在BST中维护索引的想法非常相似,在每个节点上添加一个“numberOfSons”字段]【参考方案5】:

您可以使用std::map 将(索引、插入时间)对映射到值,其中插入时间是“自动增量”整数(在 SQL 术语中)。对的排序应该是

(i, t) < (i*, t*)

如果

i < i* or t > t*

在代码中:

struct lt 
    bool operator()(std::pair<size_t, size_t> const &x,
                    std::pair<size_t, size_t> const &y)
    
        return x.first < y.first || x.second > y.second;
    
;

typedef std::map<std::pair<size_t, size_t>, int, lt> array_like;

void insert(array_like &a, int value, size_t i)

    a[std::make_pair(i, a.size())] = value;

【讨论】:

假设我们在 0 处插入 300,然后在 0 处插入 100,然后在 1 处插入 200。会发生什么情况:[],然后是 [300],然后是 [100 300],然后是 [100 200 300]。但实际发生的情况是:[],然后是 [((0, 1), 300)],然后是 [((0, 2), 100), ((0, 1), 300)],到目前为止一切顺利,然后是 [((0, 2), 100), ((0, 1), 300), ((1, 3), 200)]。结论:没有订单统计,这类事情通常很难做到。【参考方案6】:

在 c++ 中,您可以只使用向量图,如下所示:

int main() 
  map<int, vector<int> > data;
  data[0].push_back(1);
  data[1].push_back(3);
  data[1].push_back(2);
  data[0].push_back(5);
  map<int, vector<int> >::iterator it;
  for (it = data.begin(); it != data.end(); it++) 
    vector<int> v = it->second;
    for (int i = v.size() - 1; i >= 0; i--) 
      cout << v[i] << ' ';
    
  
  cout << '\n';

打印出来:

5 1 2 3 

如你所愿,插入是 O(log n)。

【讨论】:

如果您接下来尝试在第二个索引中推送 10,它将失败。【参考方案7】:

您是否考虑过使用std::mapstd::vector

您可以使用 std::map 并将插入等级作为键。而vector 有一个reserve 成员函数。

【讨论】:

OP想要比线性任意插入更快,vector和map不都是O(n)吗? 是的,std::vector 插入到位置 i 将是 O(n),因为元素 in 需要移动。对于std::map,由于必须更新密钥,因此会发生类似的情况。 @Yavar:但是您必须在每次插入后修改以下所有元素的索引。假设您有 map=[(1,a),(2,b),(3,c)] 并且您想在位置 0 添加 z,您需要将地图修改为 [(1,z), (2,a),(3,b),(4,c)]。如果有解决方法 - 应该详细说明.. @juanchopanza:是的,但它强制执行唯一键。你需要做额外的工作来保持允许多次插入到同一个索引而不擦除以前的元素。

以上是关于高效的插入数据结构的主要内容,如果未能解决你的问题,请参考以下文章

C++:用于高效插入和检索自定义数据的数据结构

MySQL 最佳实践 —— 高效插入数据

更高效地从 qtableWidget 更新和插入 MYSQL 数据库

将数组数据插入mysql表的高效php代码?

Mysql高效插入/更新数据

Oracle:高效插入大量数据经验之谈