是否有更高效的双向映射实现?

时间:2014-02-13 16:48:01

标签: c++ c++11 data-structures map bimap

我创建了一个简单的双向映射类,它通过内部存储两个std::map实例,具有相反的键/值类型,并提供用户友好的界面:

template<class T1, class T2> class Bimap
{
    std::map<T1, T2> map1;
    std::map<T2, T1> map2;
    // ...
};
  • 是否有更有效的方法来实现不需要两倍内存的双向映射?

  • 如何实施bimap?


编辑:

  • bimap元素应该是可变的还是不可变的? (更改map1中的一个元素应更改map2中的键,但键是常量,这是不可能的 - 解决方案是什么?)

  • 元素的所有权也是另一个问题:当用户在bimap中插入键值对时,bimap应该复制该键值对并存储它,然后是内部第二个映射(带有倒置键/值)不应复制,而是指向原始对。如何实现这一目标?


编辑2:

I've posted a possible implementation I made on Code Review.

6 个答案:

答案 0 :(得分:20)

在bimap的所有简单实现中双重存储数据存在一定问题。如果你可以将它分解为外部指针的bimap,那么你可以很容易地忽略这一点,只需保留像{Arkaitz Jimenez这样的std::map<A*,B*>形式的地图(尽管与他的回答相反,你必须关心它从外部存储以避免A->A*查找)。但是,如果你还有指针,为什么不简单地将std::pair<A,B>存储在另外存储AB的位置?

使用std::map<A,B*>而不是std::map<A*,B*>会很高兴,因为这样可以通过新创建的具有相同内容的字符串而不是指针来查找与字符串关联的元素创建该对的原始字符串。但是习惯上在每个条目中存储密钥的完整副本,并且只依赖于散列来找到正确的桶。这样,即使在哈希冲突的情况下,返回的项也是正确的...

如果你想让它快速而又脏,那就有了这个

  

hackish解决方案:

     

创建两个地图std::map<size_t, A> mapAstd::map<size_t, B> mapB。在插入时,哈希要插入的两个元素以获取相应映射的键。

void insert(const A &a, const B &b) {
    size_t hashA = std::hash<A>(a);
    size_t hashB = std::hash<B>(b);

    mapA.insert({hashB, a});
    mapB.insert({hashA, b});
}
     

查找的实现类似。

使用multimap代替map并在相应的其他地图中验证您获得的每个元素(从b获取候选人mapA,哈希{{ 1}}并查看b是否与所需的密钥匹配,否则迭代到下一个候选b)这是一个有效的实现 - 但在我看来仍然是hackish ......

通过使用用于比较条目(见上文)的元素副本作为唯一存储,您可以获得更好的解决方案。尽管如此,还是有点难以理解。详细说明:

  

更好的解决方案:

     

创建两组mapBstd::set<pair<A, B*>>,并重载std::set<pair<B, A*>>operator<以仅考虑对中的第一个元素(或提供相应的比较课)。有必要创建一组对而不是地图(内部看起来类似),因为我们需要保证operator==A始终位于内存中的相同位置。在插入B后,我们将其分成两个适合上述集合的元素。

pair<A,B>
  

现在可以通过简单的 std::set<pair<B, A*>> mapA; std::set<pair<A, B*>> mapB; void insert(const A &a, const B &b) { auto aitr = mapA.insert({b, nullptr}).first; // creates first pair B *bp = &(aitr->first); // get pointer of our stored copy of b auto bitr = mapB.insert({a, bp}).first; // insert second pair {a, pointer_to_b} A *ap = &(bitr->first); // update pointer in mapA to point to a aitr->second = ap; } 查找和指针解除引用来完成查找。

这个更好的解决方案类似于boost使用的解决方案 - 即使他们使用一些匿名指针作为对的第二个元素,因此必须使用std::set s。

请注意,reinterpret_cast部分需要是可变的(所以我不确定.second是否可以使用),或者你必须添加另一层抽象({{1}即使是这个简单的插入。在这两种解决方案中,您都需要临时元素来返回对元素的非const引用。

答案 1 :(得分:17)

将所有元素存储在矢量中并使用<T1*,T2*><T2*,T1*>的2个地图会更高效,这样您就不会将所有内容复制两次。

我看到它的方式你试图存储2个东西,元素本身以及它们之间的关系,如果你的目标是标量类型,你可以像2个地图一样留下它,但如果你的目标是处理复杂的类型,它会使更有意义的是将存储与关系分开,并处理存储之外的关系。

答案 2 :(得分:13)

Boost Bimap使用Boost Mutant Idiom

从链接的维基百科页面:

  

Boost变体惯用法使用reinterpret_cast,并且在很大程度上取决于具有相同数据成员(类型和顺序)的两个不同结构的内存布局是可互换的假设。虽然C ++标准不保证这个属性,但几乎所有编译器都满足它。

template <class Pair>
struct Reverse
{
    typedef typename Pair::first_type  second_type;
    typedef typename Pair::second_type first_type;
    second_type second;
    first_type first;
};

template <class Pair>
Reverse<Pair> & mutate(Pair & p)
{
  return reinterpret_cast<Reverse<Pair> &>(p);
}

int main(void)
{
  std::pair<double, int> p(1.34, 5);

  std::cout << "p.first = " << p.first << ", p.second = "  << p.second << std::endl;
  std::cout << "mutate(p).first = " << mutate(p).first << ", mutate(p).second = "  << mutate(p).second << std::endl;
}

增强源的实施当然相当有趣。

答案 3 :(得分:6)

如果你为类型std::set<std::pair<X,Y>>创建了一组对,你几乎已经实现了你的功能和关于mutabillity和constness预设的规则(好的可能这些设置不是你想要的但可以调整)。所以这是代码:

#ifndef MYBIMAP_HPP
#define MYBIMAP_HPP

#include <set>
#include <utility>
#include <algorithm>

using std::make_pair;

template<typename X, typename Y, typename Xless = std::less<X>, 
                     typename Yless = std::less<Y>>
class bimap
{
    typedef std::pair<X, Y>                             key_type;
    typedef std::pair<X, Y>                             value_type;
    typedef typename std::set<key_type>::iterator       iterator;
    typedef typename std::set<key_type>::const_iterator const_iterator;

    struct Xcomp
    {
        bool operator()(X const &x1, X const &x2)
        {
            return !Xless()(x1, x2) && !Xless()(x2, x1);
        }
    };
    struct Ycomp
    {
        bool operator()(Y const &y1, Y const &y2)
        {
            return !Yless()(y1, y2) && !Yless()(y2, y1);
        }
    };
    struct Fless 
    { // prevents lexicographical comparison for std::pair, so that 
        // every .first value is unique as if it was in its own map
        bool operator()(key_type const &lhs, key_type const &rhs)
        {
            return Xless()(lhs.first, rhs.first);
        }
    };
    /// key and value type are interchangeable
    std::set<std::pair<X, Y>, Fless> _data;

public:
    std::pair<iterator, bool> insert(X const &x, Y const &y)
    {
        auto it = find_right(y);
        if (it == end()) { // every .second value is unique
            return _data.insert(make_pair(x, y));
        }
        return make_pair(it, false);
    }
    iterator find_left(X const &val)
    {
        return _data.find(make_pair(val,Y()));
    }
    iterator find_right(Y const &val)
    {
        return std::find_if(_data.begin(), _data.end(), 
            [&val](key_type const &kt)
        {
            return Ycomp()(kt.second, val);
        });
    }
    iterator end()   { return _data.end();   }
    iterator begin() { return _data.begin(); }
};

#endif

使用示例

template<typename X, typename Y, typename In>
void PrintBimapInsertion(X const &x, Y const &y, In const &in)
{
    if (in.second) {
        std::cout << "Inserted element (" 
              << in.first->first << ", " << in.first->second << ")\n";
    }
    else {
        std::cout << "Could not insert (" << x << ", " << y 
                      << ") because (" <<  in.first->first << ", " 
                      << in.first->second << ") already exists\n";
    }
}


int _tmain(int argc, _TCHAR* argv[])
{
    bimap<std::string, int> mb;
    PrintBimapInsertion("A", 1, mb.insert("A", 1) );
    PrintBimapInsertion("A", 2, mb.insert("A", 2) );
    PrintBimapInsertion("b", 2, mb.insert("b", 2));
    PrintBimapInsertion("z", 2, mb.insert("z", 2));

    auto it1 = mb.find_left("A");
    if (it1 != mb.end()) {
        std::cout << std::endl << it1->first << ", " 
                      << it1->second << std::endl;
    }

    auto it2 = mb.find_right(2);
    if (it2 != mb.end()) {
        std::cout << std::endl << it2->first << ", " 
                      << it2->second << std::endl;
    }

    return 0;
}

注意:所有这些都是粗略的代码草图,描述了完整的实现,甚至在抛光和扩展代码之后,我并不是说这可以替代{{1}但只是一种自制的方式,可以通过值和键搜索关联容器。

Live example

答案 4 :(得分:4)

使用中间数据结构和间接的一种可能的实现是:

int sz;  // total elements in the bimap

std::map<A, int> mapA;
std::map<B, int> mapB;

typedef typename std::map<A, int>::iterator iterA;
typedef typename std::map<B, int>::iterator iterB;

std::vector<pair<iterA, iterB>> register;  
// All the operations on bimap are indirected through it.

<强>插入

假设您必须插入(X,Y),其中X,Y分别是A和B的实例,然后:

  1. mapA --- O(lg sz)
  2. 中插入(X,sz)
  3. mapB --- O(lg sz)
  4. 中插入(Y,sz)
  5. 然后在push_back --- O(1)中register(IterX,IterY)。这里IterX和IterY是mapAmapB中相应元素的迭代器,分别从(1)和(2)获得。
  6. <强>查找

    查找A类型的元素X的图像:

    1. mapA中获取映射到X的int。 --- O(lg n)
    2. 使用int索引到register并获得相应的IterY。 --- O(1)
    3. 一旦你有了IterY,你可以通过IterY->first得到Y. --- O(1)
    4. 因此两个操作都在O(lg n)中实现。

      空格:A和B对象的所有副本只需存储一次。然而,有很多簿记的东西。但是当你拥有大型物体时,这也不会太重要。

      注意:此实现依赖于映射的迭代器永远不会失效的事实。因此,register的内容始终有效。

      可以找到更详细的此实施版本here

答案 5 :(得分:1)

这个怎么样?

这里,我们避免一种类型(T1)的双重存储。另一种类型(T2)仍然是双重存储。

// Assume T1 is relatively heavier (e.g. string) than T2 (e.g. int family).
// If not, client should instantiate this the other way.
template <typename T1, typename T2>
class UnorderedBimap {

  typedef std::unordered_map<T1, T2> Map1;
  Map1 map1_;

  std::unordered_map<T2, Map1::iterator> map2_;
};
相关问题