【问题标题】:splitable data structure (in c++11)可拆分数据结构(c++11)
【发布时间】:2014-06-12 02:38:41
【问题描述】:

我想知道是否有人可以帮助我。

我寻找支持这四种操作的数据结构(如列表、队列、堆栈、数组、向量、二叉树等):

  • isEmpty(真/假)
  • 插入单个元素
  • pop(即获取&删除)单个元素
  • 拆分为两个结构,例如取大约一半(比如说 +/- 20%)的元素并将它们移动到另一个结构中

注意我根本不关心元素的顺序。

插入/弹出示例:

A.insert(1), A.insert(2), A.insert(3), A.insert(4), A.insert(5) // contains 1,2,3,4,5 in any order
A.pop() // 3
A.pop() // 2
A.pop() // 5
A.pop() // 1
A.pop() // 4

以及拆分示例:

A.insert(1), A.insert(2), A.insert(3), A.insert(4), A.insert(5)
A.split(B)
// A = {1,4,3}, B={2,5} in any order

我需要结构尽可能快 - 最好是 O(1) 中的所有四个操作。我怀疑它已经在std中实现了所以我会自己实现它(在C++ 11中,所以可以使用std::move)。

注意 insertpopisEmpty 的调用频率大约是 split 的十倍.

我用listvector 尝试了some coding,但没有成功:

#include <vector>
#include <iostream>

// g++ -Wall -g -std=c++11
/*
output:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
5 6 7 8 9
*/

int main ()
{
        std::vector<int> v1;

        for (int i = 0; i < 10; ++i) v1.push_back(i);

        for (auto i : v1) std::cout << i << " ";
        std::cout << std::endl;

        auto halfway = v1.begin() + v1.size() / 2;
        auto endItr  = v1.end();

        std::vector<int> v2;
        v2.insert(v2.end(),
                std::make_move_iterator(halfway),
                std::make_move_iterator(endItr));

        // sigsegv
        /*
        auto halfway2 = v1.begin() + v1.size() / 2;
        auto endItr2  = v1.end();
        v2.erase(halfway2, endItr2);
        */

        for (auto i : v1) std::cout << i << " ";
        std::cout << std::endl;

        for (auto i : v2) std::cout << i << " ";
        std::cout << std::endl;

        return 0;
}

任何示例代码、想法、链接或任何有用的东西?谢谢

相关文献:

【问题讨论】:

  • I tried some coding with list and vector but with no success.到目前为止你有什么?
  • 拆分需要在中间如何?缓存效率有多重要(由vector 等连续容器提供)?拆分时通常存储多少字节?
  • 如果你可以容忍在中间分割不完全,它可以比O(n)更快,看我的回答。
  • 我已经用我刚刚编写的容器编辑了我的答案,以便在 O(log n) 中执行所有操作。

标签: c++ c++11 split structure


【解决方案1】:

您的删除问题是由于代码中的错误造成的。

// sigsegv
auto halfway2 = v1.begin() + v1.size() / 2;
auto endItr2  = v1.end();
v2.erase(halfway2, endItr2);

您尝试使用指向v1 的迭代器从v2 中擦除。这行不通,您可能想在v1 上致电erase

这解决了拆分向量时的删除问题,并且向量似乎是您想要的最佳容器。

请注意,如果您仅在末尾插入,则除 split 之外的所有操作都可以在向量上以 O(1) 完成,但由于顺序对您来说并不重要,我认为它没有任何问题,split 将是 O (n) 修复后在您的实现中进行,但这应该非常快,因为数据在向量中彼此相邻,并且对缓存非常友好。

【讨论】:

  • @Walter 为什么不回答这个问题?在像 OP 这样的情况下,向量通常是性能最高的。他还说,由于删除问题,矢量无法正常工作,但这只是他这边的一个错误,我修复了。
  • a vector::push_back() 仅摊销 O(1)。好吧,向量是否最好在某种程度上取决于元素的数量。 Vector 适用于许多元素和不频繁的拆分。
  • @Walter 这是 OP 应该像所有与速度有关的东西一样衡量的东西。但鉴于信息有限,如果只是与其他人进行比较,它绝对是可行的。它很简单,并且在某些测量中实施它应该不会花费很长时间。
【解决方案2】:

我想不出所有操作都在 O(1) 中的解决方案。

使用 list,您可以在 O(1) 中进行推送和弹出,并在 O(n) 中进行拆分(因为您需要找到列表的中间部分)。

使用平衡二叉树(不是搜索树),您可以在 O(log n) 内完成所有操作。

编辑

有人建议保持列表中间会产生 O(1)。情况并非如此,因为当您拆分函数时,您必须计算左侧列表的中间和右侧列表的中间,从而导致 O(n)。

其他一些建议是首选矢量,因为它对缓存友好。我完全同意这一点。

为了好玩,我实现了一个平衡的二叉树容器,它在 O(log n) 内执行所有操作。 insertpop 显然在 O(log n) 中。实际的拆分在 O(1) 中,但是我们留下了根节点,我们必须将其插入其中一个半部分,导致 split 的 O(log n) 也。但是不涉及复制。

这是我对上述容器的尝试(我还没有彻底测试正确性,它可以进一步优化(比如在循环中转换递归)。

#include <memory>
#include <iostream>
#include <utility>
#include <exception>

template <class T>
class BalancedBinaryTree {
  private:
    class Node;

    std::unique_ptr<Node> root_;

  public:
    void insert(const T &data) {
      if (!root_) {
        root_ = std::unique_ptr<Node>(new Node(data));
        return;
      }
      root_->insert(data);
    }

    std::size_t getSize() const {
      if (!root_) {
        return 0;
      }
      return 1 + root_->getLeftCount() + root_->getRightCount();
    }

    // Tree must not be empty!!
    T pop() {
      if (root_->isLeaf()) {
        T temp = root_->getData();
        root_ = nullptr;
        return temp;
      }
      return root_->pop()->getData();
    }

    BalancedBinaryTree split() {
      if (!root_) {
        return BalancedBinaryTree();
      }

      BalancedBinaryTree left_half;
      T root_data = root_->getData();
      bool left_is_bigger = root_->getLeftCount() > root_->getRightCount();

      left_half.root_ = std::move(root_->getLeftChild());
      root_ = std::move(root_->getRightChild());

      if (left_is_bigger) {
        insert(root_data);
      } else {
        left_half.insert(root_data);
      }

      return std::move(left_half);
    }
};


template <class T>
class BalancedBinaryTree<T>::Node {
  private:
    T data_;
    std::unique_ptr<Node> left_child_, right_child_;
    std::size_t left_count_ = 0;
    std::size_t right_count_ = 0;

  public:
    Node() = default;
    Node(const T &data, std::unique_ptr<Node> left_child = nullptr,
         std::unique_ptr<Node> right_child = nullptr)
        : data_(data), left_child_(std::move(left_child)),
         right_child_(std::move(right_child)) {
    }

    bool isLeaf() const {
      return left_count_ + right_count_ == 0;
    }

    const T& getData() const {
      return data_;
    }
    T& getData() {
      return data_;
    }

    std::size_t getLeftCount() const {
      return left_count_;
    }

    std::size_t getRightCount() const {
      return right_count_;
    }

    std::unique_ptr<Node> &getLeftChild() {
      return left_child_;
    }
    const std::unique_ptr<Node> &getLeftChild() const {
      return left_child_;
    }
    std::unique_ptr<Node> &getRightChild() {
      return right_child_;
    }
    const std::unique_ptr<Node> &getRightChild() const {
      return right_child_;
    }

    void insert(const T &data) {
      if (left_count_ <= right_count_) {
        ++left_count_;
        if (left_child_) {
          left_child_->insert(data);
        } else {
          left_child_ = std::unique_ptr<Node>(new Node(data));
        }
      } else {
        ++right_count_;
        if (right_child_) {
          right_child_->insert(data);
        } else {
          right_child_ = std::unique_ptr<Node>(new Node(data));
        }
      }
    }

    std::unique_ptr<Node> pop() {
      if (isLeaf()) {
        throw std::logic_error("pop invalid path");
      }

      if (left_count_ > right_count_) {
        --left_count_;
        if (left_child_->isLeaf()) {
          return std::move(left_child_);
        }
        return left_child_->pop();
      }

      --right_count_;
      if (right_child_->left_count_ == 0 && right_child_->right_count_ == 0) {
        return std::move(right_child_);
      }
      return right_child_->pop();
    }
};

用法:

  BalancedBinaryTree<int> t;
  BalancedBinaryTree<int> t2;

  t.insert(3);
  t.insert(7);
  t.insert(17);
  t.insert(37);
  t.insert(1);

  t2 = t.split();

  while (t.getSize() != 0) {
    std::cout << t.pop() << " ";
  }
  std::cout << std::endl;

  while (t2.getSize() != 0) {
    std::cout << t2.pop() << " ";
  }
  std::cout << std::endl;

输出:

1 17
3 37 7

【讨论】:

  • 我同意。但这应该是评论。
  • 我会选择一个向量,顺序无关紧要,所以最后一切都可以在 O(1) 中完成,除了拆分。但是矢量对缓存非常友好,因此它应该仍然非常快。
【解决方案3】:

如果你的容器中任意时刻存储的元素/字节数很大,那么Youda008的解决方案(使用列表并跟踪中间)可能没有你希望的那么高效。

或者,您可以使用list&lt;vector&lt;T&gt;&gt; 甚至list&lt;array&lt;T,Capacity&gt;&gt; 并且跟踪列表的中间,即仅在两个子容器之间拆分,但从不拆分子容器.这应该为您提供所有操作的 O(1) 和合理的缓存效率。如果Capacity 的单个值始终满足您的需求,请使用array&lt;T,Capacity&gt;(对于Capacity=1,这将恢复为普通的list)。 否则,使用vector&lt;T&gt; 并根据需求调整新向量的容量。

bolov 正确地指出,从拆分一个列表中找到列表的中间不是 O(1)。这意味着跟踪中间没有用。但是,使用list&lt;sub_container&gt; 仍然比列表快,因为拆分只需要 O(n/Capacity) 而不是 O(n)。您为此付出的代价是拆分的粒度为Capacity 而不是 1。因此,您必须在拆分的准确性和成本之间做出折衷。

【讨论】:

  • 同@Youda008:当你分裂时,你必须计算左列表的中间和右列表的中间。
  • 我用 O(log n) 写了一个容器(树)。我很好奇这将如何与您的解决方案和直向量进行比较。你的解决方案让我想起了哈希图。
【解决方案4】:

另一种选择是使用链表和指向要拆分的中间元素的指针来实现自己的容器。该指针将在每次修改操作时更新。通过这种方式,您可以在所有操作上实现 O(1) 复杂度。

【讨论】:

  • 你无法达到 O(1)。当你拆分时(假设你有列表的中间),你必须得到左边列表的中间和右边列表的中间:O(n)。
  • @bolov 实际上,当您已经获得了中间容器的迭代器时,您可以使用列表轻松地做到这一点。只需将新列表的开始/结束指针分别指向该迭代器指向的中间项和原始列表的结束指针。指向中间项的原始列表的结束指针。瞧,3 指针变化,不管列表有多长。
  • @RaphaelMiedl 对于列表,获取两个指针之间的中间不是 O(1)。
  • @Walter 确实,如果你已经有一个指向中间的指针/迭代器,它只是 O(1)。也就是说,我真的不赞成这样的实现。
  • 是的,对不起,愚蠢的回答:(
猜你喜欢
  • 2023-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-20
  • 2020-04-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多