【问题标题】:C++ std::map, rotation of keysC++ std::map,键的旋转
【发布时间】:2011-10-26 17:29:57
【问题描述】:

我需要在地图中实现“第一个键轮换”之类的东西。对问题的更详细的解释。有一张地图:

std::map <double, double> test;

插入以下元素:

test[0.5] = 15;
test[1] = 20;
test[2.3] = 12;
test[3.7] = 18

旋转算法可以改写为:

a] 记住 map 中的第一个元素(键最低的元素):rem_el = map[0] //符号表示法

b] 从地图中删除第一个元素

c] 为 map 中的所有剩余元素设置新键:

map[i].key = map[i].key - rem_el.key

d] 用新的 key 将记住的 key 添加到 map 中:最后一个 key 和记住的 key 之和

test[rem_el.key + test[n-1].key] = rem_el.value

第一次轮换:

test[0.5] = 20;
test[1.8] = 12;
test[3.2] = 18;
test[3.7] = 15;

第二次轮换:

test[1.3] = 12;
test[2.7] = 18;
test[3.2] = 15;
test[3.7] = 20;

这样的地图有 n-1 次旋转...

如何尽可能高效地执行此操作(包含数千个项目的映射)?我使用了所有键的列表和从旋转列表创建的另一个临时映射,但这个过程可能不是最佳的。

我可以要求一些代码示例吗?谢谢。

【问题讨论】:

  • 地图中没有“第一个元素”这样的东西。你能定义“第一”吗——那是键值最低的那个吗?
  • 除了上面提到的,还需要按键读取项目吗?
  • @Ed Heal:地图是有序的,它是按键排序的。您正在考虑unordered_maphash_map
  • @EugeneHomyakov:我认为std::map::begin() 会产生非空映射中的第一个元素。
  • @RasmusStorjohann:不正确。地图中的键从不比较身份(==),它们只与地图的排序关系(通常是&lt;)进行比较。我同意 double 仍然不是(通常)地图的好键。

标签: c++ algorithm map rotation


【解决方案1】:

您可以在 O(log N) 内进行旋转

#include<map>

using namespace std;

map<double, double> m;
double diff; //the keys in m are actually diff bigger then in needed map

void rotate()
{
     double savedValue = m.begin()->second;
     double removedKey = m.begin()->first - diff;//save the value of first element
     m.erase(m.begin()); //erase first element
     diff += removedKey; //do the (key -=) thing
     m[m.rbegin()->second + removedKey] = savedValue;//add new value
}

//dont need to do this, just remember that key = storedKey - diff
map<double, double> getMap()
{
        map<double, double> res;
        for(map<double, double>::iterator it = m.begin(); it != m.end(); ++it)
                m[it->first-diff] = it->second;
        return res;
}

【讨论】:

  • +1 如果我们希望能够在旋转后轻松修改地图(如插入新元素),我认为这是最佳解决方案。您应该添加明确显示如何使用 diff 变量执行查找和插入的代码。
  • 你的代码有错误,应该有diff += removedKey - diff;或简单的diff = removedKey;
  • 并且下一行也坏了,所以我想在 rotate() 的第二行中修复所有内容是最简单的:double removedKey = m.begin()-&gt;first - diff;
  • 谢谢。我真的应该先把它全部写在纸上。
【解决方案2】:

如果在轮换之后您只需要有效地执行查找,那么您应该选择 Matthieu 的解决方案。 在他出色的回答中,他只是忘记了,而不是Key^i[n],您需要的是该函数的逆函数,即给定一些值k 和旋转次数i 什么将是键k 的等级(索引)ni 旋转之后(如果存在这样的密钥)。

我认为反函数不能像 Key^i[n] 那样在解析术语中找到并在 O(1) 中计算,但您可以使用二进制搜索在 O(log n) 时间内进行有效查找。

double get(Vector const& vec, size_t const i, double key) {
    assert(n < vec.size() && "Wrong index");
    size_t rank = binarySearch(vec, i, 0, vec.size()-1, key);
    if (rank < 0) {
        // not found, return NaN or something
    }
    return vec[rank].second;    
}

size_t binarySearch(Vector const& vec, size_t const i, size_t const low, size_t const high, double value) {
    if (high < low)
        return -1; // not found
    mid = low + (high - low) / 2;
    if (key(vec, i, mid) > value)
        return binarySearch(vec, i, low, mid-1, value);
    else if (key(vec, i, mid) < value)
        return binarySearch(vec, i, mid+1, high, value);
    else
        return mid; // found
}

此解决方案为您提供 O(1) 次旋转(您只需增加旋转计数器并记住它)和 O(log n) 次查找。 如果要修改地图(例如插入新元素),则必须使用 getMap() 函数重置整个地图,这需要 O(n) 时间。

因此,如果您需要在旋转后进行有效的查找和插入(删除等),那么您应该坚持使用 kitaras 的解决方案,它为您提供了统一的 O(log n) 时间来进行所有操作,包括旋转、查找和修改。

【讨论】:

    【解决方案3】:

    我阅读了您的评论,但我仍然想提供一个答案,并展示如何使用原始地图来完成。所以,这里是代码:

    #include <map>
    #include <iostream>
    #include <utility>
    
    int main() {
        std::map <double, double> test;
    
        test[0.5] = 15;
        test[1] = 20;
        test[2.3] = 12;
        test[3.7] = 18;
    
        std::pair<double, double> first = *test.begin();
        test.erase(test.begin());
        std::map<double, double>::iterator i = test.begin();
        while ( i != test.end() ) {
            test[i->first - first.first] = i->second;
            std::map <double, double>::iterator prev = i;
            ++i;
            test.erase(prev);
        }
        test[test.rbegin()->first + first.first] = first.second;
    
        for ( i = test.begin(); i != test.end(); ++i ) {
            std::cout << "test[" << i->first << "] = " << i->second << "\n";
        }
    }
    

    几点说明:

    • std::map 键不能修改,所以你必须删除旧元素并插入新元素;
    • 问题陈述保证新插入的元素不会覆盖现有元素;
    • 从映射中删除元素不会使不指向已删除元素的迭代器失效。

    【讨论】:

    • 仔细查看键值集。键不是简单地旋转。它们的值也被修改了。
    • @tyz:你说的没错。我提供了一个 - 希望是更正的版本。
    【解决方案4】:

    这是另一个想法。

    使用两种数据结构而不是一种。将值保存在 list&lt;double&gt; 中,并将您的地图表示为 map&lt;double, list&lt;double&gt;::iterator&gt;

    也就是说,从 double 键映射到迭代器(即指针)到值列表中。

    还要记录您总共执行了多少次旋转;称之为k。要执行查找,请在映射中查找键,将 k 添加到迭代器,然后取消引用它。 (是的,这是 O(k)。)此外,要使其工作,您需要一个循环链表;或者更容易,通过处理环绕的列表实现您自己的行进。

    请注意,一般情况下,您不会旋转列表本身;你只需增加k。所有费用都在查找期间产生。

    请注意,插入仍然是O(log n),因为在列表中插入不会使其迭代器失效,您可以在O(log n)时间从地图中获取插入列表的位置。

    现在这里是原始位。当k 达到sqrt(n) 时,然后您实际上会旋转列表以将k 重置为零。此操作为O(n),但您每O(sqrt(n)) 轮换一次...意味着您的轮换操作的摊销(即平均)成本也是O(sqrt(n))。而k 永远不会大于sqrt(n),所以查找也是O(sqrt(n))

    因此,这个公式提供:

    插入:O(log n) 删除:O(log n) 查找:O(sqrt(n)) 旋转:O(sqrt(n))

    您可能会或可能不会认为比其他建议更好,但至少它是不同的......

    同样使用这个公式,您可以用查找来换取旋转速度。例如,如果您在k 达到log n 时执行“重置”操作,您的轮换平均将采用O(n / log n),但查找仍将是O(log n)。 (这里的大多数其他提议要么轮换采用O(n),要么插入采用O(n),所以这个版本优于那些......虽然只有log n的一个因素。)

    【讨论】:

      【解决方案5】:

      我假设 std::map 是基于树结构的,并且它的元素具有升序。所述旋转操作不改变相对键位。因此,关键更改不应破坏地图结构。我创建了修改键值的旋转函数。这似乎是一种不好的做法,it 仍然适用于 msvs 和 gcc。

      typedef std::map<double,double> Map;
      typedef Map::iterator MapIter;
      
      void rotate( Map &m ) {
          if ( m.empty() ) return;
          MapIter prev, iter = m.begin(), max_iter = m.end();
          Map::key_type rem_key = iter->first;
          Map::mapped_type rem_val = iter->second;
          for( prev = iter++; iter != max_iter; prev = iter++ )  {
              Map::key_type *key = const_cast<Map::key_type*>(&prev->first);
              *key = iter->first - rem_key;
              prev->second = iter->second;
          }
          prev->second = rem_val;
      }
      

      编辑:描述的旋转操作仅在所有键都是非负的情况下才不会改变相关键的位置。在其他情况下,我的算法错误地修改了地图结构,因此无法使用。

      【讨论】:

      • 地图中的键不可修改。您只能使用 const_cast 来修改它们,正如您也提到的那样。
      • @Jiri。你是莱特的。我毫无根据地假设所有的键都是非负数。
      【解决方案6】:

      这是一个有趣的问题,但更多的是关于算法而不是数据结构。

      我注意到Map^i[n] 可以在恒定时间内解决...如果您调整访问而不是修改结构。

      根据我对问题的理解,这些值只是“循环”:[15, 20, 12, 18] -> [20, 12, 18, 15] -> [12, 18, 15, 20] -> [18, 15, 20, 12]

      公式:

      • 令 N 为序列的大小 - [0, N] 中的 1 和 n 为序列中的索引
      • [0, N] 中的 i 成为一个迭代
      • Value^i[n] = Value[n+i%(N+1)]

      但是键是不同的:

      • [0.5, 1, 2.3, 3.7] -> [0.5, 1.8, 3.2, 3.7] -> [1.3, 2.7, 3.2, 3.7] -> [1.4, 1.9, 2.4, 3.7]
      • 让我们试着看看一个模式:[a, b, c, d] -> [b-a, c-a, d-a, d] -> [c-b, d-b, d-b+a, d] -> [d-c, d-c+a, d-c+b, d]

      让图案更明显:

      0: [a    , b      , c      , d      , e      , f]
      1: [b-a  , c-a    , d-a    , e-a    , f-a    , f]
      2: [c-b  , d-b    , e-b    , f-b    , f-(a-b), f]
      3: [d-c  , e-c    , f-c    , f-(a-c), f-(b-c), f]
      4: [e-d  , f-d    , f-(a-d), f-(b-d), f-(b-e), f]
      5: [f-e  , f-(a-e), f-(b-e), f-(c-e), f-(d-e), f]
      

      请注意,这在某种程度上也是循环,因为再次应用转换将产生原始序列。

      公式(我们重用之前的变量):

      Key^i[n] = | n = N    => Key[N]
                 | i = 0    => Key[n]
                 | n <= N-i => Key[n+i] - Key[i-1]
                 | n >  N-i => Key[N] - (Key[n+i % (N+1)] - Key[i-1])
      

      如果我们(任意)定义Key[-1] = 0,后3行可以聚合在(Key[n+i % (N+1)] - Key[i-1]) % Key[N]中。

      现在我们有了公式,我们需要一个随机访问的结构,我将简单地选择一个vector

      下面提供的可编译示例(或参见ideone)给出:

      [ 0.5: 15, 1: 20, 2.3: 12, 3.7: 18 ]
      [ 0.5: 20, 1.8: 12, 3.2: 18, 3.7: 15 ]
      [ 1.3: 12, 2.7: 18, 3.2: 15, 3.7: 20 ]
      [ 1.4: 18, 1.9: 15, 2.4: 20, 3.7: 12 ]
      

      例子:

      #include <cassert>
      #include <algorithm>
      #include <iostream>
      #include <vector>
      
      typedef std::pair<double, double> Pair;
      typedef std::vector<Pair> Vector;
      
      double key(Vector const& vec, size_t const i, size_t const n) {
        assert(n < vec.size() && "Wrong index");
        if (i == 0) { return vec[n].first; }
      
        size_t const N = vec.size() - 1;
      
        if (n == N) { return vec.back().first; }
      
        double const partial = vec[(n+i) % (N+1)].first - vec[(i-1) % (N+1)].first;
        return (n <= N-i) ? partial : partial + vec[N].first;
      } // key
      
      double value(Vector const& vec, size_t const i, size_t const n) {
        assert(n < vec.size() && "Wrong index");
        return vec[(n+i) % vec.size()].second;      
      } // value
      
      int main() {
        Vector vec{ Pair(0.5, 15), Pair(1, 20), Pair(2.3, 12), Pair(3.7, 18) };
        sort(vec.begin(), vec.end()); // just to be sure
      
        size_t const size = vec.size();
        for (size_t i = 0; i != size; ++i) {
          std::cout << "[ ";
          for (size_t n = 0; n != size; ++n) {
            if (n != 0) { std::cout << ", "; }
            std::cout << key(vec, i, n) << ": " << value(vec, i, n);
          }
          std::cout << " ]\n";
        }
      }
      

      【讨论】:

      • @abcdef:我前段时间注意到,尽管暴力破解在大多数情况下都有效,但当你想要效率时,你需要退后一步。尝试优化特定算法的实现是一回事,但最大的飞跃通常源于完全改变算法:)
      【解决方案7】:

      轮换值和保留键

      一种可能性是使用@Eugene 提到的deque。然后,当然,您没有对密钥的快速 O(log n) 访问权限。如果您想保留地图,则可以通过n 轮次“旋转”地图m

      void rotate(map<double, double>& m, int n) {
          vector<double> values(m.size());
          int j = 0;
          for (map<double, double>::const_iterator i = m.begin(); i != m.end(); ++i, ++j) {
              values[j] = (*i).second;
          }
          j = n;
          for (map<double, double>::iterator i = m.begin(); i != m.end(); ++i, ++j) {
              m[(*i).first] = values[j % m.size()];
          }   
      }
      

      如果您想以不同的轮数旋转几次,那么您可以将矢量 values 设为全局并仅填充一次。此外,应该考虑先旋转n1 然后再旋转n2 等于旋转n1 + n2。所以,例如获得所有你会打电话的轮换:

      rotate(m, 1);
      rotate(m, 1); // 1 again
      ...
      

      请注意:使用双精度作为键是相当有问题的。

      旋转值和更改键(已编辑)

      在这种情况下,需要像@abcdef 那样构建一个全新的地图。但是,问题中似乎没有正确定义新键。键 k1, k2, ..., kn 被转换为 k2-k1, k3-k1, ..., kn-k1, kn。如果例如,我们会得到重复的键k[n-1] - k1 = kn,就像将 (-1, 2, 5, 6) 转换为 (3, 6, 7, 6) 时一样。

      【讨论】:

      • 仔细查看键值集。键不是简单地旋转。它们的值也被修改了。
      • 如果双端队列已排序,您可以对键进行 O(log(n)) 访问。
      • @UncleBens:已排序的双端队列无法旋转,或者每次旋转后都需要重新排序。但是无论如何,带有转换键的整个东西都很奇怪。
      • @tyz:我根据您的评论扩展了我的答案。关键转换似乎很奇怪。
      【解决方案8】:

      我注意到,只要没有通过任何其他方式从地图中添加或删除键,就可以在轮换期间跟踪键的变化。

      首先,请注意地图中最大的键永远不会改变。

      现在请注意,除了从地图的一个极端“环绕”到另一个极端的键值之外,您可以通过跟踪被减去的键的总值来总结旋转的效果。

      看看最小的键会发生什么 - 你可以想象它被自己减去,产生一个零值。但是现在我们决定值 0 太小而无法忍受,就像模算术一样,我们可以为其添加一个模数以使其回到范围内。该值是地图中(不变的)最大键。因此,前向预测键的规则是减去到目前为止处理的所有最小键的总和,然后根据需要将最大键值添加到范围内。类似地,向后推导键的规则是减去到目前为止处理的所有最小键的总和,然后将最大的键值加到所需的次数内,以使该值进入范围。

      使用它,您应该能够允许用户旋转地图并以 O(1) 成本查找键值,只需要处理 O(n log n) - (或可能 O(n) 取决于关于你想变得多么棘手和小心)在添加或删除键值时重写地图的工作。

      我认为按顺序索引(例如,第一个值、第二个值、第三个值)查找值是相同的原则 - 跟踪一个要添加或减去的值,该值占到目前为止的总旋转,并添加或减去地图中将事物带回范围内的项目总数。

      【讨论】:

      • [-1:0, 0:1] -> [-1:0, 1:1]。地图中最大的键发生变化。
      • 在第一个键是 -ve 的情况下,旧的最后一个键确实增加了,并且重新编号的第一个键小于它 - 所以我们实际上没有旋转。如果第一个键 >= 0,则旧的最后一个键不会更大,重新编号的第一个键变得等于前一个最后一个键的值。我想我的方法只适用于最低键 >= 0 并且我们确实有轮换的情况。
      【解决方案9】:

      看起来您需要一对双端队列,而不是地图:

      std::deque<std::pair<double, double> > test;
      

      如果需要,您必须自己对其进行分类。然后就很简单了:

      std::pair<double, double> rem_el = test.front();
      test.pop_front();
      for (std::deque<std::pair<double, double> >::
           iterator it = test.begin(); it != test.end(); ++it)
      {
          it->first -= rem_el.first;
      }
      assert(!test.empty());
      test.push_back(std::make_pair(rem_el.first + test.back().first, rem_el.second));
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-02-26
        • 1970-01-01
        • 2011-10-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多