C ++ std :: map,键的旋转

时间:2011-08-14 09:25:49

标签: c++ algorithm map rotation

我需要在地图中实现类似“第一次按键旋转”的功能。对问题的更详细解释。有一张地图:

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[i].key = map[i].key - rem_el.key

d]使用新密钥将记忆密钥添加到地图中:最后一个密钥和记住密钥的总和

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个旋转...

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

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

9 个答案:

答案 0 :(得分:2)

看起来你需要一对副本,而不是地图:

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));

答案 1 :(得分:2)

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

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

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

公式:

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

键虽然不同:

  • [0.5, 1, 2.3, 3.7] - &gt; [0.5, 1.8, 3.2, 3.7] - &gt; [1.3, 2.7, 3.2, 3.7] - &gt; [1.4, 1.9, 2.4, 3.7]
  • 让我们试着看一个模式:[a, b, c, d] - &gt; [b-a, c-a, d-a, d] - &gt; [c-b, d-b, d-b+a, d] - &gt; [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[n+i % (N+1)] - Key[i-1]) % Key[N],后面的3行可以在Key[-1] = 0中汇总。

现在我们有了公式,我们需要一个具有随机访问权限的结构,我只需选择一个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";
  }
}

答案 2 :(得分:1)

旋转值并保持键

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

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)时。

答案 3 :(得分:1)

我假设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;
}

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

答案 4 :(得分:1)

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

#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密钥无法修改,因此您必须删除旧元素并插入新元素;
  • 问题陈述保证新插入的元素不会覆盖现有元素;
  • 从地图中删除元素不会使未指向已删除元素的迭代器失效。

答案 5 :(得分:1)

这是另一个想法。

使用两个数据结构而不是一个。将值保存在list<double>中,并将地图表示为map<double, list<double>::iterator>

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

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

请注意,通常,您不会旋转列表本身;你只需增加k。所有费用都是在查询期间产生的。

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

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

因此,该公式提供:

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

你可能会或可能不会考虑比其他建议更好,但至少它是不同的......

使用此配方,您可以权衡查找旋转速度。例如,如果您在O(sqrt(n))到达k时执行“重置”操作,则您的轮播平均需要log n,但查找仍为O(n / log n)。 (此处的大多数其他提案要么轮流进行O(log n)要么轮换进行O(n),所以此版本胜过那些......虽然只有log n的因子。)

答案 6 :(得分:1)

您可以在O(日志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;
}

答案 7 :(得分:0)

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

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

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

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

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

我认为通过订单索引查找值(例如第一个值,第二个值,第三个值)是相同的原则 - 跟踪一个值来增加或减去到目前为止总轮换的帐户,并加或减地图中的项目总数,以便将事物重新纳入范围。

答案 8 :(得分:0)

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

我不认为反函数可以在分析术语中找到,并且以i的方式在O(1)中计算,但是你可以使用二进制搜索在O(log n)时间内进行有效的查找

Key^i[n]

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

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