【问题标题】:Find first value not in std::set<int> min-max查找不在 std::set<int> min-max 中的第一个值
【发布时间】:2018-09-06 19:56:24
【问题描述】:

如何在std::set&lt;int&gt; ids 中找到not 的第一个值h,以便将结果限制在[0, *ids.rbegin() + 1] 范围内。

我看到这是一个相当简单的问题,但我还没有找到任何匹配的问题。基本上我想要ids 的倒置集,以便我可以使用它们。

到目前为止,我有以下内容:

#incldue <set>
std::set<int> ids;
int h = 0;
for(; !ids.empty() && (h <= *ids.rbegin() + 1); ++h) {
    if(!ids.count(h)) {
        break;
    }
}
// h is now smallest value not in ids.

我怀疑这会进一步改进,例如不需要循环?

@edit:澄清集合中的值:在我的用例中,算法生成的值被插入集合中。我真的应该说std::set&lt;unsigned int&gt;。我很高兴就这个问题进行了如此多的讨论!

【问题讨论】:

  • 使用 sorted std::vector,您可以随机访问,并且可以检查中间元素,然后只检查左侧或右侧。但您无法从std::set... 访问随机元素
  • 顺便说一句,您可以在 std::set 上进行迭代,而不是进行线性查找。
  • 你不需要int h = ids.begin() - 1;吗?
  • @NathanOliver:如果0 在集合中,这不会导致-1 吗?
  • @NathanOliver:这不会在一组{0,1,2,4,5} 中检测到3

标签: c++ algorithm set


【解决方案1】:

由于std::set的元素已排序,您可以使用std::adjacent_find

std::adjacent_find(set.begin(), set.end(), [](int a, int b){ return a+1 != b; } );

这将返回一个迭代器到第一个元素a,它是not,后跟值a+1。或者 set.end() 如果没有这样的值。

示例用法:

std::set<int> ids { -2, -1, 0, 1, 2, 4, 5, 6 };

// This code assumes a non-empty set, since the range in the question requires same
if ( *ids.begin() > 0 )
{
    // Special case for [0
    std::cout << 0;
}
else
{
    auto res = std::adjacent_find(ids.begin(),
                                  ids.end(),
                                  [](int a, int b){ return a+1 != b; } );
    if ( res == ids.end() )
    {
        // Special case for *ids.rbegin() + 1]
        std::cout << *ids.rbegin() + 1;
    }
    else
    {
        // Print the value that should have followed this one.
        std::cout << *res + 1;
    }
}

输出:

3

【讨论】:

  • 优雅的方法。
  • 谢谢你,@StephanLechner。该特定算法似乎是一种迷失在抽屉后面并被遗忘的算法。
  • 对于{1, 2, 3, 5},您返回4 而不是0
  • 从范围[0, *ids.rbegin() + 1]或简单的[0, ids.size()],保证set 至少有一个缺失的数字。
  • 不确定是否必须处理负数,但如果是 {-3, 0, 1, 2, 4} 返回 -2 而不是 3 (据我了解)。如果不是,您的样本具有误导性。
【解决方案2】:
std::set<int> ids;
int h = 0;
for( auto id : ids ) {
    if( id != h )
        break;
     h++;
}

【讨论】:

  • 当 -1 和 0 在集合中时,结果为 0。
【解决方案3】:

std::set 没有针对这个问题进行优化。

朴素的方法给你 O(n) 的性能和 糟糕 O(n) 的性能,因为你正在遍历一个基于节点的数据结构。

您想要的是一个已排序的“可跳跃”数据结构(可以是某种树,也可以是跳过列表),其中记录了跳跃的大小。

然后您可以跟踪 delta-id 和跳跃的大小;如果跳跃小于 id 差异,则那里有一个空的 id。如果没有,那里面就没有空id。

std 中的任何关联容器都不会跟踪您需要的信息,并且改造它们是不切实际的,因为您无法访问基于“飞跃”的迭代或结构。

使用这样的数据结构,插入和删除是 O(lgn),就像“找到第一个未使用的 id”一样。没有它,发现第一个未使用的id是O(n)。


这涉及一些繁重的代码锻造。只需存储一组范围并将其包装以保证范围不重叠,我们几乎可以做到同样好,但成本更高。

struct range {
  int base = 0;
  int length = 0;
  friend bool operator<( range lhs, range rhs ) {
    if (lhs.base != rhs.base) return lhs.base < rhs.base;
    return lhs.length < rhs.length;
  }
  bool operator()(int x) const {
    ERROR( if (length < 0) std::cout << "bad length\n"; )
    ERROR( if (base < 0) std::cout << "bad base\n"; )
    return (x>=base) && (x<(base+length));
  }
};

range 是从[base, base+length) 开始的半开区间。因此[x,0) 是所有x 的空范围,而[x,1) 只包含x

它是由base 订购的。如果您在[x,0) 上询问有序集合的下限

现在我们创建一个std::set&lt;range&gt; 并将其包装起来:

struct id_set {
  bool taken(int x) const;
  int find_unused() const;
  void take(int x);
  void recycle(int x);
  int take_unused() {
    auto r = find_unused();
    take(r);
    return r;
  }
  std::size_t count() const {
    std::size_t r = 0;
    for (auto e:state)
      r += e.length;
    ERROR( if (r!=counter) std::cout << "Counter failure\n"; )
    return r;
  }
private:
  std::set<range> state;
  using iterator = std::set<range>::iterator;
  iterator get_interval(int x) const;
  std::size_t counter = 0;
};

id_set::iterator id_set::get_interval(int x) const {
  auto start = state.lower_bound( {x,0} );
  if (start != state.end())
      if ((*start)(x))
        return start;

  if (start == state.begin() )
    return state.end();

  auto prev = std::prev(start);
  if ((*prev)(x))
    return prev;

  return state.end();
}
bool id_set::taken(int x) const {
  return get_interval(x) != state.end();
}

int id_set::find_unused() const {
  auto it = state.begin();
  if (it == state.end()) return 0;
  auto r = it->base + it->length; // we ensure these intervals are never adjacent; thus the element after the first interval is untaken
  ERROR( if (taken(r)) std::cout << "find_unused failed\n"; )
  return r;
}

void id_set::take(int x) {
  if (taken(x)) return; // nothing to do
  ++counter;

  auto merge_with_next = [&](auto next) {
    VERBOSE(std::cout << "merge_with_next\n"; )
    auto tmp = *next;
    tmp.base = x;
    ++tmp.length;
    state.erase(next);
    state.insert(tmp);
    ERROR( if (!taken(x)) std::cout << "merge_with_next failed\n"; )
  };
  auto merge_with_prev = [&](auto prev) {
    VERBOSE(std::cout << "merge_with_prev\n"; )
    auto tmp = *prev;
    ++tmp.length;
    state.erase(prev);
    state.insert(tmp);
    ERROR( if (!taken(x)) std::cout << "merge_with_prev failed\n"; )
  };
  auto merge_prev_and_next = [&](auto prev, auto next) {
    VERBOSE(std::cout << "merge_prev_and_next\n"; )
    auto tmp = *prev;
    tmp.length += next->length + 1;
    state.erase(prev);
    state.erase(next);
    state.insert(tmp);
    ERROR( if (!taken(x)) std::cout << "merge_prev_and_next failed\n"; )
  };
  auto insert_in_gap = [&] {
    VERBOSE(std::cout << "insert_in_gap\n"; )
    state.insert( {x, 1} );
    ERROR( if (!taken(x)) std::cout << "insert_in_gap failed\n"; )
  };

  if (state.empty())
    return insert_in_gap();

  auto start = state.lower_bound( {x,0} );

  // this is before the beginning, and there is a gap:
  if (start == state.begin() && start->base > x+1)
    return insert_in_gap();
  if (start == state.begin()) {
    // no gap and just before start
    return merge_with_next(state.begin());
  }
  // this is valid, because we are not begin:
  auto prev = std::prev(start);
  if (start == state.end() || start->base != x + 1) {
    if (prev->base + prev->length == x)
      return merge_with_prev(prev);

    return insert_in_gap();
  }     
  // both prev and start are valid iterators
  // start->base == x+1
  if (prev->base + prev->length == x)
    return merge_prev_and_next(prev, start);

  return merge_with_next(start);
}
// return an id:
void id_set::recycle(int x) {
  auto it = get_interval(x);
  if (it == state.end()) return; // nothing to do
  --counter;

  // create two intervals, one before and one after:      
  auto lhs = *it;
  lhs.length = x-lhs.base;
  auto rhs = *it;
  rhs.base = x+1;
  rhs.length -= lhs.length+1;
  // remove this interval:
  state.erase(it);
  // insert either non-zero length interval:
  if (lhs.length > 0)
    state.insert(lhs);
  if (rhs.length > 0)
    state.insert(rhs);
  ERROR( if (taken(x)) std::cout << "recycle failed\n"; )
}

上面可能有错别字。但核心思想是takerecycle 都是O(lgn) 操作,find_unused 也是如此。因此take_unused 也是 O(lgn)。

Live example

【讨论】:

    【解决方案4】:

    集合中的元素已排序。因此,当您迭代元素时,您只需要检测第一个“泄漏”(例如,如果集合的值大于线性增加的积分值)。您可以定义从哪里开始,例如如果你想忽略值0,你可以写h=1,并且还覆盖了一组连续数字的极端情况(结果是max+1):

    int main() {
        std::set<int> mySet = {5,4,6,2,1,0};
        int h=0;
        for(auto val : mySet) {
            if (val != h) {
                break;
            }
            h++;
        }
    
        cout << "not in mySet:" << h << endl;
    
        return 0;
    }
    

    【讨论】:

      【解决方案5】:

      您的版本是O(n log(n))
      您可以在线性时间内拥有它 (O(n)):

      int h = 0;
      for (const int e : ids) {
          if (e != h) {
              return h;
          }
          ++h;
      }
      return h;
      

      如果ids 可以包含负数,则将循环更改为:

      int h = 0;
      for (auto it = ids.lower_bound(0); it != ids.end(); ++it) {
          if (*it != h) {
              return h;
          }
          ++h;
      }
      return h;
      

      使用排序向量,您甚至可以将复杂度降低到O(log(n))

      【讨论】:

        【解决方案6】:

        如果您坚持使用std::set 来存储一组整数,那么不幸的是线性遍历(在其他答案中建议)是最好的选择。

        通过对搜索树的小修改,您可以实现O(log n) 搜索最小的缺失整数,同时保留所有其他属性 - 您只需要为每个树节点存储小于键的条目数量。不确定是否可以在不重写整个 std::set 的情况下实现它。
        UPD:事实证明你可以 - 检查@Yakk的答案 - Adam Nevraumont

        事实上,通过在你的容器上每次操作后缓存最小值,你可以让它在O(1)中“找到”你的号码,这是渐近无与伦比的。当然,它不会为您带来任何实际的性能提升。

        最后,如果您的整数有任何额外的限制,您有时可以想出一些巧妙的技巧(通常发生在培训网站的编码任务中) - 例如如果您知道某个范围内恰好缺少一个数字,您可以在O(1) 中进行算术计算。

        【讨论】:

          【解决方案7】:

          我已经接受了@Drew Dormann 的回答。但是我修改了算法,以便允许将值插入std::list&lt;int&gt; 以保持列表排序。 我还把它做成了一个通用模板函数:

          #include <algorithm>
          #include <list>
          #include <iterator>
          #include <iostream>
          // Make smallest sequential val that is not in c.
          // returns [val, *c.rbegin() + 1] and suitable iterator to insert val into c.
          template<typename Cont>
          void make_minimal_id(const Cont & c, typename Cont::value_type & val,
                               typename Cont::const_iterator & x)
          {
              if( c.empty()) {
                  x = c.begin();
              } else if ( *c.begin() > val ) {
                  x = c.begin();
                  val = *x - 1;
              } else {
                  // "one algo that gets lost in the back of the drawer and forgotten."
                  x = std::adjacent_find(c.begin(), c.end(),
                      [](typename Cont::value_type a, typename Cont::value_type b)
                      { return a + 1 != b; } );
                  if ( x == c.end() ) {
                      val = *c.rbegin() + 1;
                  } else {
                      val = *x++ + 1;
                  }
              }
          }
          // pretty print container 
          template<typename C>
          void print_cont(const C & c) {
              std::cout << std::accumulate(std::next(c.begin()), c.end(),
                  std::to_string(c.front()), // start with first element
                  [](std::string a, int b) {
                      return a + ", " + std::to_string(b); }) << std::endl;
          }
          
          int main() {
              std::list<unsigned int> ids = { 3, 4, 5, 6, 9 };
              print_cont(ids);
              for(int i = 0; i < 6; ++i) {
                  unsigned int id = 0;
                  std::list<unsigned int>::iterator x;
                  make_minimal_id(ids, id, x);
                  ids.insert(x, id);
                  print_cont(ids);
              }
              // prints:
              //{3, 4, 5, 6, 9}
              //{2, 3, 4, 5, 6, 9}
              //{1, 2, 3, 4, 5, 6, 9}
              //{0, 1, 2, 3, 4, 5, 6, 9}
              //{0, 1, 2, 3, 4, 5, 6, 7, 9}
              //{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
              //{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
              return 0;
          }
          

          make_minimal_id() 现在适用于任何已排序的容器和值。 :-)

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2014-03-15
            • 1970-01-01
            • 2011-08-02
            • 1970-01-01
            • 2019-02-21
            • 1970-01-01
            • 2013-11-23
            • 1970-01-01
            相关资源
            最近更新 更多