【问题标题】:std::set with user defined type, how to ensure no duplicatesstd::set 与用户定义的类型,如何确保没有重复
【发布时间】:2009-07-11 22:50:07
【问题描述】:

所以我有一个 std::set 需要保持特定的顺序以及不允许重复用户定义的(由我)类型。现在我可以通过在我的类型中重载“

用户定义类型:

//! An element used in the route calculation.
struct RouteElem {
    int shortestToHere; // Shortest distance from the start.
    int heuristic;      // The heuristic estimate to the goal.
    Coordinate position;
    bool operator<( const RouteElem& other ) const
    {
        return (heuristic+shortestToHere) < (other.heuristic+other.shortestToHere);
    }
    bool operator==( const RouteElem& other ) const
    {
        return (position.x == other.position.x && position.y == other.position.y);
    }
};

所以当它们的位置相等时,元素是等价的,如果一个元素的组合功能小于另一个元素,则它小于另一个元素。排序有效,但集合将接受相同位置的两个元素。

【问题讨论】:

    标签: c++ set


    【解决方案1】:

    operator== 未被std::set 使用。元素 ab 被认为是相等的,如果 !(a &lt; b) &amp;&amp; !(b &lt; a)

    【讨论】:

    • 我明白了,这很不幸。是否有另一个可能更好的标准容器? IE。允许我定义自己的“非重复”标准以及排序标准吗?
    • @DeusAduro:无法将比较功能与排序功能分开。原因很明显。 set 是内部的二叉树。如果一个元素满足相等标准(不是更大,也不是更小),它应该在相同的位置。
    • 嗯我明白了,我想要做的是需要完全遍历树中的每个节点,以验证不同的相等条件。由于这不是不能做到的,而且我需要“平等”和“排序”标准,我认为集合不合适。
    • 如果您在与排序不同的意义上定义相等,则集合可能不合适。集合中的相等性本质上意味着两个元素将在排序的项目序列中具有相同的位置
    • @Paul:std::unique 要求元素已经排序,使得相等的元素相邻。考虑到 DeusAduro 试图解决的问题,我认为这引出了一个问题,即他的重复标准与他的排序顺序不匹配。
    【解决方案2】:

    std::set 支持指定比较函数。默认值为less,它将使用operator &lt; 来检查相等性。您可以定义一个自定义函数来检查相等性并改用该函数:

    std::set<RouteElem, mycomparefunction> myset; 
    

    请注意,无法将比较功能与排序功能分开。 std::set 是二叉树,如果二叉树中的元素不大于也不小于特定元素,则它应该在同一个位置。它在地点查找算法中做了这样的事情:

    if (a < b) {
        // check the left subtree
    } else if (b < a) {
        // check the right subtree
    } else {
        // the element should be placed here.
    }
    

    【讨论】:

    • set 模板参数中的比较函数是小于比较,以防您要比较的事物还没有这样的比较成员函数。这不是一个相等测试函数。见:sgi.com/tech/stl/set.html
    • 确实如此。它使用 小于 函数来排序 作为副作用,相等。
    • 确实如此,因此它不能解决问题中所述的问题。它所做的只是将小于函数的实现移到别处。
    • 不,它没有。这就是set 类的本质。我不认为这个数据结构适合这个问题(因为我在上一段中提到的原因)。
    • 是的,也许不是我需要知道的!但内容丰富。我想我会应用我自己的类型,我需要在这个应用程序中提高速度(二维路由算法),没有一个标准容器似乎特别合适。至少在容器正常功能之外应用额外的排序/唯一性标准时是这样。
    【解决方案3】:

    rlbond 的比较器不会阻止插入比较相等的元素。显然,考虑到字符限制,很难在 cmets 中证明这一点,因为 rlbond 似乎认为 std::set 保证它永远不会包含两个带有 !compare(a,b) &amp;&amp; !compare(b,a) 的元素作为他的比较器。但是,rlbond 的比较器没有定义严格的顺序,因此不是 std::set 的有效参数。

    #include <set>
    #include <iostream>
    #include <iterator>
    #include <algorithm>
    
    struct BrokenOrder {
        int order;
        int equality;
    
        public:
        BrokenOrder(int o, int e) : order(o), equality(e) {}
    
        bool operator<(const BrokenOrder &rhs) const {
            return order < rhs.order;
        }
        bool operator==(const BrokenOrder &rhs) const {
            return equality == rhs.equality;
        }
    };
    
    std::ostream &operator<<(std::ostream &stream, const BrokenOrder &b) {
        return stream << b.equality;
    }
    
    // rlbond's magic comparator
    struct LessThan : public std::binary_function<BrokenOrder, BrokenOrder, bool> {
        bool operator()(const BrokenOrder& lhs, const BrokenOrder& rhs) const
        {
            return !(lhs == rhs) && (lhs < rhs);
        }
    };
    
    int main() {
        std::set<BrokenOrder,LessThan> s;
        for (int i = 0; i < 5; ++i) {
            s.insert(BrokenOrder(i,i));
        }
        for (int i = 0; i < 5; ++i) {
            s.insert(BrokenOrder(10-i,i));
        }
        std::copy(s.begin(), s.end(), 
            std::ostream_iterator<BrokenOrder>(std::cout, "\n"));
    }
    

    输出:

    0
    1
    2
    3
    4
    3
    2
    1
    0
    

    重复。魔法比较器失败了。集合中的不同元素具有相同的equality 值,因此与operator== 比较相同,因为在插入过程中,集合从未将新元素与其副本进行比较。唯一被排除的重复项是 4,因为这两个 4 的排序顺序为 4 和 6。这使它们在集合中足够接近,可以相互比较。

    来自 C++ 标准:25.3:3“为了使算法正常工作,comp 必须对值进行严格的弱排序”。

    25.3:4 "...要求 comp 和 equiv 都是传递关系:

    comp(a,b) && comp(b,c) implies comp(a,c)"
    

    现在,考虑元素a = BrokenOrder(1,1)b = BrokenOrder(2,2)c = BrokenOrder(9,1)comp 当然等于魔法比较器。那么:

    • comp(a,b) 为真,因为 1 != 2(相等)和 1
    • comp(b,c) 为真,因为 2 != 1(相等)和 2
    • comp(a,c) 为假,因为 1 == 1(相等)

    【讨论】:

    • 我的立场是正确的。你是对的,很难理解你想在 cmets 中说什么。我现在明白了。
    • @rlbond:我没想到你会删除你的答案:你所说的 A* 看起来真的很有用,因为它表明 OP 实际上并不想解决他的问题posed(即他不想排除重复,他想选择最好的重复)。
    • @rlbond:干得好,我读过的严格弱排序的最佳解释之一。
    【解决方案4】:

    STL 集实现在概念上做了类似这样的事情来检测相等性:

    bool equal = !(a < b) && !(b < a);
    

    也就是说,如果两个元素都不小于另一个,那么它们一定相等。您可以通过在 operator==() 方法上设置断点并检查它是否被调用来检查这一点。

    我通常会怀疑检查完全不同事物的比较运算符。您的 &lt; 运算符是根据与您的 == 运算符的定义方式不同的两件事来定义的。通常,您会希望此类比较使用一致的信息。

    【讨论】:

      【解决方案5】:

      您可以尝试以下方法:

      //! An element used in the route calculation.
      struct RouteElem {
          int shortestToHere; // Shortest distance from the start.
          int heuristic;              // The heuristic estimate to the goal.
          Coordinate position;
          bool operator<( const RouteElem& other ) const
          {
            return (heuristic+shortestToHere) < (other.heuristic+other.shortestToHere);
          }
          bool operator==( const RouteElem& other ) const
          {
            return (position.x == other.position.x && position.y == other.position.y);
          }
      };
      
      struct CompareByPosition {
          bool operator()(const RouteElem &lhs, const RouteElem &rhs) {
              if (lhs.position.x != rhs.position.x) 
                  return lhs.position.x < rhs.position.x;
              return lhs.position.y < rhs.position.y;
          }
      };
      
      // first, use std::set to remove duplicates
      std::set<RouteElem,CompareByPosition> routeset;
      // ... add each RouteElem to the set ...
      
      // now copy the RouteElems into a vector
      std::vector<RouteElem> routevec(routeset.begin(), routeset.end());
      
      // now sort via operator<
      std::sort(routevec.begin(), routevec.end());
      

      显然中间有副本,看起来很慢。但是,任何按两个不同标准对项目进行索引的结构,因此与集合相比,每个项目都会产生某种额外的开销。上面的整个代码是 O(n log n),假设你的 std::sort 实现使用了 introsort。

      如果你有它,在这个方案下你可以使用unordered_set 而不是set 来进行初始唯一化。由于散列只需要依赖于 x 和 y,因此它应该比插入集合所需的 O(log N) 比较快。

      编辑:刚刚注意到您说您想“保持”排序顺序,而不是您想批量处理所有内容。对于那个很抱歉。如果您想在添加元素时有效地维护顺序并排除重复项,那么我建议使用我上面定义的集合或无序集合,基于位置,以及std::multiset&lt;RouteElem&gt;,它将保持operator&lt; 的顺序。对于每个新元素,请执行以下操作:

      if (routeset.insert(elem).second) {
          routemultiset.insert(elem);
      }
      

      虽然请注意,这不提供任何例外保证。如果第二次插入抛出,则路由集已被修改,因此状态不再一致。所以我想你真的需要:

      if (routeset.insert(elem).second) {
          try {
              routemultiset.insert(elem); // I assume strong exception guarantee
          } catch(...) {
              routeset.erase(elem); // I assume nothrow. Maybe should check those.
              throw;
          }
      }
      

      或者 RAII 的等价物,如果您的代码中只有一个地方您曾经使用过 RAII 类,这将更加冗长,但如果有很多重复则更好。

      【讨论】:

      • 同时,实现一个线性数组我觉得可以做得更好。插入操作必须首先检查重复,因为排序顺序与“重复”标准不相关,这必须是 O(n)。如果元素不在列表中,我们现在必须找到合适的位置(二分查找 -> O(log n)。所以总时间是 O(n + log n) + 最坏的 O(n) 班次。
      • 可能我误解了你的建议,但是如果每个插入操作都涉及 O(n) 检查重复,那么总数是 O(n^2),因为要插入 n 个项目.
      • 可能是我对大 O 表示法的误解。但是,如果我的定义是 O(n^2),那么你的也是 O(n^2),至少在我的使用中。基本上我在列表中添加了一个元素,之后需要对其进行排序。因此,使用上面的实现,我将为每个操作转换 b/w 集和向量,因此我们将对 n 个元素进行 O(n^2) 复制操作。虽然是的,如果我要在一个点上添加所有元素,那么上面的实现会更好,然后只复制/排序一次。
      • 在这一点上感到困惑的是我,你说得对,我的第一个建议是在多个单独的插入操作中维护排序的垃圾。我想我现在已经在编辑中解决了这个问题。我的 Big-O 用于插入所有 n 个元素,你的用于插入 1。我的新建议是 O(log N) 用于插入新元素同时拒绝重复并保持顺序,但使用的内存大约是单个元素的两倍容器。
      【解决方案6】:

      当心这件事的后果。看起来您正在尝试执行 A* 之类的操作,如果您尝试插入“重复”,即使有“更好”的路线,它也会被忽略。

      注意:此解决方案不起作用,请参阅下面的说明

      struct RouteElem 
      {
          int shortestToHere; // Shortest distance from the start.
          int heuristic;              // The heuristic estimate to the goal.
          Coordinate position;
          bool operator<( const RouteElem& other ) const
          {
              return (heuristic+shortestToHere) < (other.heuristic+other.shortestToHere);
          }
          bool operator==( const RouteElem& other ) const
          {
              return (position.x == other.position.x && position.y == other.position.y);
          }
      };
      
      struct RouteElemLessThan : public std::binary_function<RouteElem, RouteElem, bool>
      {
          bool operator()(const RouteElem& lhs, const RouteElem& rhs) const
          {
              return !(lhs == rhs) && (lhs < rhs);
          }
      };
      
      std::set<RouteElem, RouteElemLessThan> my_set;
      

      【讨论】:

      • "当你插入一个新的 RouteElem 时,如果它们使用 operator==" 相等,它将被检测为等于已经存在的一个。不必要。为将新元素放入树中而执行的 log(N) 比较可能恰好不包括与其相等的元素的比较。
      • 我同意 onebyone 的观点,就像提到的其他一些海报一样,考虑到该集合在内部是一棵树,可能会错过等效元素。就你的最后一句话而言,我在做 A*。我不确定我是否遵循你所说的。听起来您是在说,如果我将副本添加到打开列表中,那不是问题吗?这肯定会让事情变得更简单......你能详细说明一下吗?
      • @onebyone 看看正在使用的比较函子。
      • @DeusAduro 如果您尝试做 A*,那么您做错了。您想将 std::priority_queue 适配器用于开放集,将 std::set 用于封闭集。然后,取开集的顶部元素;如果它已经在封闭集中,则丢弃它,否则,处理它。问题是 A* 的规则表明,在开放集中,已经存在的节点应该被具有更好 g 值(或等效地 f 值)的节点替换。如果你使用 std::set,“更糟糕”的路线永远不会被更好的路线取代。
      • 还有其他方法可以使用 std::set,但优先级队列更容易 IMO。使用priority_queue 的主体功能,您应该能够在大约30-40 行内完成。
      猜你喜欢
      • 2016-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-09
      相关资源
      最近更新 更多