ChatGPT解决这个技术问题 Extra ChatGPT

双向地图是否有更有效的实现?

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

template<class T1, class T2> class Bimap
{
    std::map<T1, T2> map1;
    std::map<T2, T1> map2;
    // ...
};

是否有更有效的方法来实现不需要两倍内存的双向映射?

bimap 通常是如何实现的?

编辑:

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

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

编辑2:

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

“bimap 元素应该是可变的还是不可变的?”好吧,你需要它是可变的还是不可变的?一切都取决于您的要求。你需要那个 bimap 做什么?如果这是为了锻炼而锻炼,那么你可以自由地假设任何一种方式——更好的是,两种方式都尝试!
@KubaOber:老实说,我确实事先检查了 boost::bimap 的源代码,但我没有发现它特别有用,因为它是大量模板化的,而且关于 bimap 实现的有趣细节并不是真的裸露。它当然不容易阅读,也没有使用 C++11 特性(这可能有助于从头开始实现 bimap)。也许我应该在第一篇文章中提到它。另外,我确实在谷歌上查找了“双向地图实现”、“双向地图教程”等,但找不到好的文章。
@KubaOber:在我使用 std::map<T2, const T1*> 作为第二张地图进行测试后,mutability/immutability 问题浮出水面。问题是,如果我使用应该是用户可变的 T2,则在第一个和第二个映射中修改该 T2 实例变得有些困难。可能需要从第二个映射中删除键/值对并使用修改后的键插入一个新的键/值对。然后我意识到可能 bimap 的 internal 表示应该取决于键/值对是否应该是可变的?也许我应该检查类型是否为 POD?
如果类型是 POD,我可以只使用当前的实现,并且可以通过模板检查 sizeof 是否很大,如果是,则使用“const 指针”第二个映射实现。我之所以打开这个问题,是因为在网上查看并尝试了各种事情之后,我仍然有很多疑问 - 我相信一定有某种数据结构允许双向键/值交互,同时在一定程度上保持正常的效率地图。
@VittorioRomeo 好吧,有人提议制作一个元素向量+ 2 个指针映射,然后我的有一组对。您的解决方案具有一对向量 + 2 组指针。伟大的组合技能老兄:)

p
plasmacel

在 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 mapA 和 std::map mapB。插入哈希后,要插入的两个元素都将被插入以获取相应映射的键。 void insert(const A &a, const B &b) { size_t hashA = std::hash(a); size_t hashB = std::hash(b); mapA.insert({hashB, a}); mapB.insert({hashA, b});查找类似地实现。

使用 multimap 而不是 map 并通过分别在另一个映射中查找来验证您获得的每个元素(从 mapA 获取候选 b,散列 b 并查看 mapB,如果它匹配想要密钥,否则迭代到下一个候选者 b)这是一个有效的实现 - 但在我看来仍然是 hackish ......

通过使用用于比较条目(见上文)的元素副本作为唯一存储,您可以获得更好的解决方案。不过,要弄清楚这一点有点困难。详细说明:

一个更好的解决方案:创建两组对作为 std::set> 和 std::set> 并重载 operator< 和 operator== 只取考虑对的第一个元素(或提供相应的比较类)。有必要创建对集合而不是映射(内部看起来很相似),因为我们需要保证 A 和 B 在内存中的位置不变。插入 pair 后,我们将其拆分为适合上述集合的两个元素。

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

现在可以通过简单的 std::set 查找和指针取消引用来简单地完成查找。

这个更好的解决方案类似于 boost 使用的解决方案——尽管它们使用一些匿名指针作为对的第二个元素,因此必须使用 reinterpret_cast

请注意,对的 .second 部分需要是可变的(所以我不确定是否可以使用 std::pair),或者即使对于这个简单的插入,您也必须添加另一个抽象层(std::set<pair<B, A**>> mapA)。在这两种解决方案中,您都需要临时元素来返回对元素的非常量引用。


显然,“好的解决方案”仍然只是实际实施的想法。关于可变性有一些微妙的地方。 hackish 解决方案应该是相当直接的——尽管您需要一些技巧(临时元素),即使使用该解决方案才能返回对元素的非常量引用。
我授予这个答案赏金,因为它显示了可能的 bimap 实现的具体示例。
使用两组对比使用两张地图更好吗?
@szx 带有您无权访问密钥存储位置的地图(因此不能保证它们始终位于内存中的相同位置)
这很有帮助,谢谢-您能确定这些报价的来源吗?
A
Arkaitz Jimenez

将所有元素存储在一个向量中并拥有 <T1*,T2*><T2*,T1*> 的 2 个映射会更有效,这样您就不会将所有内容复制两次。

在我看来,你试图存储 2 个东西,元素本身以及它们之间的关系,如果你的目标是标量类型,你可以将它保留为 2 个地图,但如果你的目标是处理复杂类型,那么更有意义将存储与关系分离,处理存储之外的关系。


指向向量元素的指针会使插入/删除变得非常粗糙。我可能会为 2 个地图使用后备存储列表和迭代器。
甚至,一个元素映射和一个指针。重新平衡树时,节点本身不会移动。
有趣的。但是我应该使用 std::vector<std::unique_ptr<T>>,因为指针不能失效并且某些类可能是不可复制的。我想访问成员的效率会降低(但内存使用量会减半)。
Casey 是对的,如果您期望频繁的插入/删除,您最好使用一个很好的后备存储,比如列表而不是向量,如果它是不可变的,那么向量将是一个不错的选择。
为什么不让两个地图都包含 std::shared_ptr 呢?
j
jrok

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

在 boost 源中的实现当然是相当复杂的。


我不明白这有什么帮助:我是否将 std::map<Pair>std::map<ReversePair> 存储在我的 bimap 类中?我了解 reinterpret_cast 发生了什么,但我不明白如何将其用于双向地图。
有人可以详细说明这个答案吗?看起来很有趣,我很好奇。
@VittorioRomeo:这可能会有所启发:boost.org/doc/libs/1_55_0/libs/bimap/doc/html/boost_bimap/…(见最后一段)。
@szx 我理解这背后的理论 - 我正在尝试拥有 std::set<T1*>std::set<T2*>,我可以从 T1 检索 T2 个项目,但我找不到将突变用于从 T2 检索 T1 个项目。这是我的进度:pastie.org/8752050我应该如何在这里使用反向对成语?
确实,boost 使用 Mutate 惯用语来隐藏底层对象的类型 - 但这几乎就是 mutate 惯用语与 bimap 的全部关系。算法相关部分是将元素 a 与指向元素 b 的指针配对,反之亦然,并将它们存储在各自的集合中(见我的回答)。
z
zinking

如果您为您的类型创建一组对std::set<std::pair<X,Y>>,您几乎已经实现了您的功能和关于可变性和常量性预设的规则(好吧,也许这些设置不是您想要的,但可以进行调整)。所以这里是代码:

#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;
}

注意:所有这些都是对完整实现的粗略代码草图,即使在完善和扩展代码之后,我也不是暗示这将是 boost::bimap 的替代品,而只是 < strong>自制 方法,通过值和键搜索关联容器。

Live example


这似乎效率低下:std::set 的迭代速度很慢,并且在最坏的情况下插入/查找需要 O(n) - 使用映射的好处是即使有很多元素也可以快速查找/插入
setmap 都具有此类操作的相同(对数)复杂性。也许您将它们与 unordered 容器(哈希表)混淆了。正如 Josuttis 所说:您可以将集合视为特殊类型的映射,其中值与键相同。事实上,所有这些关联容器类型通常都是通过使用二叉树的相同基本实现来实现的
是的,我很抱歉,我很困惑。考虑到 std::set 的复杂性与 std::map 相似这一事实,这似乎是一个不错的解决方案。有什么明显的缺点吗? (例如,与我的幼稚实现相比,查找中的任何性能损失?)
好吧,树是用 right_key X 的比较运算符构造的,所以可能对 Y 的查找不是最佳的,但我不知道内部结构足以猜测(也许那里没有后顾之忧)。无论如何,我希望通过只提供一个 _data 来弥补这一点(更新、插入、删除、检查等),而不是两个会强制执行更复杂的代码(更复杂的错误)、更多内存的 _data使用和重复操作
find 的实现似乎效率低下,因为 std::find_if 具有线性复杂性 (en.cppreference.com/w/cpp/algorithm/find),因此在此处使用 std::set 是毫无意义的(快速插入除外)。
d
divkakwani

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

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 的实例,那么:

在 mapA 中插入 (X, sz) --- O(lg sz) 在 mapB 中插入 (Y, sz) --- O(lg sz) 然后在寄存器中 push_back (IterX, IterY) --- O(1)。这里 IterX 和 IterY 是 mapA 和 mapB 中对应元素的迭代器,分别从 (1) 和 (2) 获得。

抬头

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

获取映射到 mapA 中 X 的 int。 --- O(lg n) 使用 int 索引到寄存器并得到对应的 IterY。 --- O(1) 一旦有了IterY,就可以通过IterY->first 得到Y。 --- O(1)

所以这两个操作都是在 O(lg n) 中实现的。

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

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

可以在 here 中找到此实现的更详细版本


A
Arun

这个怎么样?

在这里,我们避免了一种类型(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_;
};

一个缺点是,如果 Map1 被重新散列,我们必须重建 map2_,因为 map2_ 中的所有迭代器都已失效。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅