簡體   English   中英

C++ std::map,按鍵旋轉

[英]C++ std::map, rotation of keys

我需要在 map 中實現類似“第一個密鑰輪換”的東西。 對問題的更詳細的解釋。 有一個 map:

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] 從 map 中刪除第一個元素

c] 為 map 中的所有剩余元素設置新鍵:

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

d] 使用新密鑰將記住的密鑰添加到 map:最后一個密鑰和記住的密鑰之和

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;

這樣的 map 有 n-1 次旋轉...

如何盡可能高效地執行此操作(包含數千個項目的地圖)? 我使用了所有鍵的列表和另一個從旋轉列表創建的臨時 map,但這個過程可能不是最佳的。

我可以要求一些代碼示例? 謝謝。

看起來你需要一對雙端隊列,而不是 map:

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

這是一個有趣的問題,但更多的是關於算法而不是數據結構。

我會注意到Map^i[n]可以在恆定時間內解決......如果不是修改結構而是調整訪問權限。

根據我對問題的理解,這些值只是“循環”: [15, 20, 12, 18] -> [20, 12, 18, 15] -> [12, 18, 15, 20] -> [18, 15, 20, 12]

公式:

  • 令 N 為序列的大小 - 1 和[0, N]中的 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] -> [ba, ca, da, d] -> [cb, db, d-b+a, d] -> [dc, 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";
  }
}

旋轉值並保留鍵

一種可能性是使用@Eugene 提到的deque 然后,當然,您沒有對密鑰的快速 O(log n) 訪問權限。 如果您想保留 map,則可以通過以下方式將 map 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 那樣構建一個全新的 map。 但是,問題中似乎沒有正確定義新鍵。 鍵 k1, k2, ..., kn 被轉換為 k2-k1, k3-k1, ..., kn-k1, kn。 如果例如 k[n-1] - k1 = kn,我們會得到重復的鍵,就像將 (-1, 2, 5, 6) 轉換為 (3, 6, 7, 6) 時一樣。

我假設 std::map 是基於樹結構的,並且它的元素具有升序。 所述旋轉操作不改變相對鍵位。 因此,關鍵更改不應制動 map 結構。 我創建了修改鍵值的旋轉 function。 這似乎是不好的做法,但仍然適用於 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;
}

編輯:描述的旋轉操作僅在所有鍵都是非負的情況下才改變相關鍵的位置。 在其他情況下,我的算法錯誤地修改了 map 結構,因此無法使用。

我閱讀了您的評論,但我仍然想提供一個答案,並展示如何使用原始 map 來完成。 所以,這里是代碼:

#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鍵無法修改,因此您必須刪除舊元素並插入新元素;
  • 問題陳述保證新插入的元素不會覆蓋現有的元素;
  • 從 map 中刪除元素不會使不指向已刪除元素的迭代器無效。

這是另一個想法。

使用兩種數據結構而不是一種。 將值保存在list<double>中,並將您的 map 表示為map<double, list<double>::iterator>

也就是說,map 從double到迭代器(即指針)進入值列表。

還要跟蹤您總共執行了多少次旋轉; 稱之為k 要執行查找,請在 map 中查找鍵,將k添加到迭代器,然后取消引用它。 (是的,這是 O(k)。)此外,要使其工作,您需要一個循環鏈表; 或者更容易地,通過處理環繞的列表實現您自己的行進。

請注意,一般情況下,您不會輪換列表本身; 你只是增加k 所有成本都是在查找期間產生的。

請注意,插入仍然是O(log n) ,因為插入列表不會使其迭代器無效,並且您可以在O(log n)時間內從 map 獲取插入列表的位置。

現在這里是原始位。 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 的一個因素。)

您可以在 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;
}

我注意到,只要沒有通過任何其他方式從 map 中添加或刪除鍵,就可以在輪換期間跟蹤鍵的變化。

首先,請注意 map 中的最大鍵永遠不會改變。

現在請注意,除了從 map 的一個極端到另一個極端“環繞”的鍵值之外,您可以通過跟蹤被減去的鍵的總值來總結旋轉的效果。

看看最小的鍵會發生什么——你可以想象它被自己減去,產生一個零值。 但是現在我們決定值 0 太小而無法忍受,就像模算術一樣,我們可以為其添加一個模數以使其回到范圍內。 該值是 map 中(不變的)最大鍵。 因此,向前預測鍵的規則是減去到目前為止處理的所有最小鍵的總和,然后根據需要將最大鍵值相加,以使該值進入范圍。 類似地,向后推導鍵的規則將是減去到目前為止處理的所有最小鍵的總和,然后將最大鍵值添加所需的次數,以使該值進入范圍。

使用它,您應該能夠允許用戶旋轉 map 並以 O(1) 成本查找鍵值,只需要處理 O(n log n) - (或可能 O(n) 取決於有多棘手並且要小心)在添加或刪除鍵值時重寫 map 的工作。

我認為按順序索引查找值(例如,第一個值、第二個值、第三個值)是相同的原則 - 跟蹤一個要添加或減去的值,該值占迄今為止的總旋轉,並添加或減去總數map 中的物品,以將物品帶回范圍內。

如果在旋轉之后您只需要有效地執行查找,那么您應該為 Matthieu 的解決方案 go。 在他出色的回答中,他只忘記了 Key^i[n] 而不是Key^i[n]您需要的是 function 的倒數,即給定一些值k和旋轉次數ii旋轉后密鑰k的等級(索引) n (如果存在這樣的密鑰)。

我不認為逆 function 可以在分析術語中找到並像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) 查找。 如果要修改 map(例如插入新元素),則必須使用 getMap() function 重置整個 map,這需要 O(n) 時間。

因此,如果您需要在旋轉后進行有效的查找和插入(刪除等),那么您應該堅持使用 kitaras 的解決方案,它為您提供所有操作(包括旋轉、查找和修改)的統一 O(log n) 時間。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM