【问题标题】:Map from integer ranges to arbitrary single integers从整数范围映射到任意单个整数
【发布时间】:2010-02-04 18:46:52
【问题描述】:

在 Linux 环境中使用 C++ 工作时,我遇到了一种情况,其中定义了多个整数范围,并且整数输入根据它们所属的范围映射到不同的任意整数。没有一个范围重叠,而且它们并不总是连续的。

解决这个问题的“最简单”的方法是为每个范围使用一堆 if 语句,但是范围的数量、它们的界限和目标值都可以变化,因此 if 语句是不可维护的。

例如,范围可能是 [0, 70],称为 r_a,[101, 150],称为 r_b,和 [201, 400],称为 r_c。 r_a 中的输入映射到 1,r_b 中的输入映射到 2,r_c 中的输入映射到 3。任何不在 r_a、r_b、r_c 中的都映射到 0。

我可以想出一个数据结构和算法来存储(边界,地图目标)的元组并遍历它们,因此找到目标值在边界对的数量上需要线性时间。我还可以想象一种方案,它使对保持有序并针对所有下限(或上限)使用二进制排序算法,找到最接近输入的,然后与相反的界限进行比较。

有没有比基于二分搜索的算法更好的方法来完成映射?更好的是,是否已经有一些 C++ 库可以做到这一点?

【问题讨论】:

  • 您输入数字的最大整体范围是多少?查找表可行吗?
  • 您说范围不重叠,但您的示例包含重叠范围。
  • "binary searchish" 可能是你最好的选择

标签: c++ algorithm


【解决方案1】:

这里最好的方法确实是二分搜索,但是任何有效的基于顺序的搜索都会做得很好。您实际上不必显式地实现搜索和数据结构。您可以通过使用标准关联容器来间接使用它。

由于您的范围不重叠,因此解决方案非常简单。您可以立即使用std::map 解决此问题,只需几行代码即可解决。

例如,这是一种可能的方法。假设我们将[ int, int ] 范围映射到int 值。让我们将我们的范围表示为封闭开放范围,即如果原始范围是 [0, 70],让我们考虑一个 [0, 71) 范围。另外,让我们使用0 的值作为“保留”值,这意味着“没有映射”(正如您在问题中所要求的那样)

const int EMPTY = 0;

你需要做的就是声明一个从intint的映射:

typedef std::map<int, int> Map;
Map map;

并用您的封闭式开放范围的每一端填充它。左(封闭)端应映射到整个范围映射到的所需值,而右(开放)端应映射到我们的EMPTY 值。对于您的示例,它将如下所示

map[0] = r_a;
map[71] = EMPTY;

map[101] = r_b;
map[251] = EMPTY;

map[260] = r_c; // 260 adjusted from 201
map[401] = EMPTY;

(我调整了您的最后一个范围,因为在您的原始示例中它与前一个范围重叠,并且您说您的范围不重叠)。

这就是初始化。

现在,为了确定i 的给定值映射到哪里,您需要做的就是

Map::iterator it = map.upper_bound(i);

如果it == map.begin(),那么i 不在任何范围内。否则,做

--it;

如果it-&gt;second(对于递减的it)是EMPTY,那么i不在任何范围内。

组合的“未命中”检查可能如下所示

Map::iterator it = map.upper_bound(i);
if (it == map.begin() || (--it)->second == EMPTY)
  /* Missed all ranges */;

否则,it-&gt;second(对于递减的it)是您的映射值

int mapped_to = it->second;

请注意,如果原始范围是“接触”的,如[40, 60][61, 100],那么封闭-开放范围将看起来像[40, 61)[61, 101),这意味着61 的值将是在地图初始化期间映射两次。在这种情况下,确保61 的值映射到正确的目标值而不是EMPTY 的值很重要。如果您按照从左到右(即递增)的顺序映射如上所示的范围,它将自行正常工作。

请注意,只有范围的端点被插入到地图中,这意味着内存消耗和搜索性能仅取决于范围的总数,而与它们的总长度完全无关。


如果你愿意,你可以在初始化过程中添加一个“守卫”元素到地图中

map[INT_MIN] = EMPTY;

(对应“负无穷大”)和“未命中”检查会变得更简单

Map::iterator it = map.upper_bound(i);

assert(it != map.begin());
if ((--it)->second == EMPTY)
  /* Missed all ranges */;

但这只是个人喜好问题。

当然,如果您只想为非映射值返回0,则根本不需要进行任何检查。只需从递减的迭代器中取出 it-&gt;second 即可。

【讨论】:

  • 非常有用的解释如何使用upper_bound操作,很好地抓住了范围的意图。
【解决方案2】:

我会使用一个非常简单的东西:std::map

class Range
{
public:
  explicit Range(int item);  // [item,item]
  Range(int low, int high);  // [low,high]

  bool operator<(const Range& rhs) const
  {
    if (mLow < rhs.mLow)
    {
      assert(mHigh < rhs.mLow); // sanity check
      return true;
    }
    return false;
  } // operator<

  int low() const { return mLow; }
  int high() const { return mHigh; }

private:
  int mLow;
  int mHigh;
}; // class Range

那么,让我们来一张地图:

typedef std::map<Range, int> ranges_type;

并编写一个在此地图中搜索的函数:

int find(int item, const ranges_type& ranges)
{
  ranges_type::const_iterator it = ranges.lower_bound(Range(item));
  if (it != ranges.end() && it->first.low() <= item)
    return it->second;
  else
    return 0; // No mapping ?
}

主要好处:

  • 将在插入集合期间检查范围是否有效地重叠(您可以使其仅处于调试模式)
  • 支持动态版本的 Ranges
  • 查找速度很快(二分查找)

如果范围被冻结(即使它们的值不是),您可能希望使用Loki::AssocVector 来减少内存开销并稍微提高性能(基本上,它是一个带有映射接口的排序向量)。

【讨论】:

  • 很好的解决方案!非常紧凑!
  • 在您的find 函数中,Range(r) 中的 r 是什么?
  • 你可能真的想用return mLow &lt; rhs.mLow &amp;&amp; mHigh &lt; rhs.mLow;定义find,因为它也是lower_bound使用的函数。但随后您失去了在施工时检查重叠的能力。
  • 在 Range::operator
  • 我希望能够插入更多范围并在有任何重叠时出错,因此在我的程序中我对 find 函数进行了一些小的编辑以接受单个项目或范围(在这种情况下,它会找到第一个重叠范围。)然后我让我的 insert 函数首先调用 find 函数来测试是否发生了重叠,如果发生了错误。它似乎工作正常。
【解决方案3】:

一个简单的数组还不够吗?你不是说你有多少项目,但到目前为止最快的数据结构是一个简单的数组。

如果范围是:

  • 0..9 --> 25
  • 10..19 --> 42

那么数组将是这样的:

[25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42]

【讨论】:

  • 如果范围是[10, 20] -&gt; 25[10000, 30000] -&gt; 42,这个数组会是什么样子?
  • 嗯,它会更大,但仍然很快。因此,我对“不说你有多少物品”发表了评论。
  • 这就是我立即想到的解决方案。经典的速度/内存权衡,但谁能击败O(1) 复杂性?
  • 即使你必须去一个稀疏数组,你应该能够让它比二叉树更快。
【解决方案4】:

您可以有两个排序数组:一个用于下限,一个用于上限。使用std::lower_bound(lower_bound_array, value)std::upper_bound(upper_bound_array, value)。如果两个结果的索引相同,return index + 1。否则,return 0

如果返回的索引匹配,则表示该值为&gt;= 下限和&lt; 上限。如果他们不这样做,那么你就介于两者之间。

【讨论】:

    【解决方案5】:

    理想的是interval tree(专用二叉树)。维基百科完整地描述了该方法。比我更好。在不牺牲性能空间的情况下,您不会比这更优化。

    【讨论】:

    • 欣赏详细信息。
    【解决方案6】:

    您的示例范围重叠,但问题说它们不会。我会假设范围是一个错字。您可以,可以,将目的地存储在一个数组中,并将索引用作范围。这很容易,但丑陋且不易维护。您需要将数组初始化为 0,然后对于每个范围,遍历这些索引并将每个索引设置为目标值。非常丑陋,但查找时间固定,因此如果数字不太高且范围不经常变化,可能会很有用。

    【讨论】:

      【解决方案7】:

      将限制记录到set(或map)中。当您调用insert 时,您将得到一个返回值,它是一对。一个迭代器和一个布尔值。如果布尔值为真,则创建一个新元素,您必须稍后将其删除。在迭代器的第一步之后,看看你找到了什么。

      http://www.cplusplus.com/reference/stl/set/insert/返回值

      【讨论】:

      • :-D cplusplus.com/reference/stl/set/lower_bound 你也应该检查一下。
      • 如果您不想插入项目,为什么要调用insert?为什么不直接打电话给map::find(或set::find)就可以了? find 的返回值也更容易处理:指向元素的迭代器(如果找到)或 container.end() 如果没有找到。
      • Jeffy:你误解了这个问题。我们不搜索确切的数字,而是搜索附近的数字。其次:我的评论比建议的解决方案更有线索,但我不喜欢编辑。
      【解决方案8】:

      它是一维空间索引。例如,四叉树风格的二叉树就可以了 - 还有其他几种广泛使用的方法。

      【讨论】:

        【解决方案9】:

        包含范围条目的简单链接列表应该足够快,即使对于 50-100 个范围也是如此。此外,您可以在上限上实现Skip List,以加快这些范围查询。还有一种可能是Interval Tree

        最终我会选择最简单的:二分查找。

        【讨论】:

          【解决方案10】:

          您可能会发现 Minimal Perfect Hashing Function 很有用,http://cmph.sourceforge.net/

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-03-03
            • 1970-01-01
            • 2013-04-29
            • 1970-01-01
            • 1970-01-01
            • 2023-03-03
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多