C#中的双向/双向字典?
Posted
技术标签:
【中文标题】C#中的双向/双向字典?【英文标题】:Two-way / bidirectional Dictionary in C#? 【发布时间】:2012-06-13 13:04:56 【问题描述】:我想通过以下方式将单词存储在字典中:
我可以逐字获取代码:dict["SomeWord"]
-> 123
并逐字获取代码:dict[123]
-> "SomeWord"
这是真的吗?当然,一种方法是使用两个字典:Dictionary<string,int>
和 Dictionary<int,string>
,但还有其他方法吗?
【问题讨论】:
没有提供 O(1) 两种方式访问的标准(从 .NET 4 开始)数据类型...... AFAIK :) 也不是双向映射(关键字?)施加额外的限制,除非多双向映射... ***.com/questions/1227683/bi-directional-dictionary , ***.com/questions/268321/… 【参考方案1】:我写了几个快速的类,可以让你做你想做的事。您可能需要使用更多功能对其进行扩展,但这是一个很好的起点。
使用代码如下:
var map = new Map<int, string>();
map.Add(42, "Hello");
Console.WriteLine(map.Forward[42]);
// Outputs "Hello"
Console.WriteLine(map.Reverse["Hello"]);
//Outputs 42
定义如下:
public class Map<T1, T2>
private Dictionary<T1, T2> _forward = new Dictionary<T1, T2>();
private Dictionary<T2, T1> _reverse = new Dictionary<T2, T1>();
public Map()
this.Forward = new Indexer<T1, T2>(_forward);
this.Reverse = new Indexer<T2, T1>(_reverse);
public class Indexer<T3, T4>
private Dictionary<T3, T4> _dictionary;
public Indexer(Dictionary<T3, T4> dictionary)
_dictionary = dictionary;
public T4 this[T3 index]
get return _dictionary[index];
set _dictionary[index] = value;
public void Add(T1 t1, T2 t2)
_forward.Add(t1, t2);
_reverse.Add(t2, t1);
public Indexer<T1, T2> Forward get; private set;
public Indexer<T2, T1> Reverse get; private set;
【讨论】:
@Pedro77 - 我只是在厚颜无耻地暗示我的班级是新的“地图”解决方案。 这不会维护异常的类不变量。_forward.Add
可能会成功,_reverse.Add
可能会失败,从而为您留下部分添加的对。
@hvd - 就像我说的 - 这是一个快速组合的课程。
您可能不希望在 Indexer
类中使用 setter,这会破坏正向/反向查找。
@AaA 它没有修改它自己的Forward
字典属性(它具有private set;
),但它正在通过传递它的 Indexer 类的 Indexer 属性修改该字典上的值到字典。 public T4 this[T3 index] get return _dictionary[index]; set _dictionary[index] = value;
这打破了正向/反向查找。【参考方案2】:
很遗憾,您需要两本字典,每个方向一本。但是,您可以使用 LINQ 轻松获取逆字典:
Dictionary<T1, T2> dict = new Dictionary<T1, T2>();
Dictionary<T2, T1> dictInverse = dict.ToDictionary((i) => i.Value, (i) => i.Key);
【讨论】:
虽然方便,但如果你经常访问逆字典,它的表现就不会很好。每次需要时即时创建新词典的成本很高。【参考方案3】:通过添加初始化和包含方法扩展了 Enigmativity 代码。
public class Map<T1, T2> : IEnumerable<KeyValuePair<T1, T2>>
private readonly Dictionary<T1, T2> _forward = new Dictionary<T1, T2>();
private readonly Dictionary<T2, T1> _reverse = new Dictionary<T2, T1>();
public Map()
Forward = new Indexer<T1, T2>(_forward);
Reverse = new Indexer<T2, T1>(_reverse);
public Indexer<T1, T2> Forward get; private set;
public Indexer<T2, T1> Reverse get; private set;
public void Add(T1 t1, T2 t2)
_forward.Add(t1, t2);
_reverse.Add(t2, t1);
public void Remove(T1 t1)
T2 revKey = Forward[t1];
_forward.Remove(t1);
_reverse.Remove(revKey);
public void Remove(T2 t2)
T1 forwardKey = Reverse[t2];
_reverse.Remove(t2);
_forward.Remove(forwardKey);
IEnumerator IEnumerable.GetEnumerator()
return GetEnumerator();
public IEnumerator<KeyValuePair<T1, T2>> GetEnumerator()
return _forward.GetEnumerator();
public class Indexer<T3, T4>
private readonly Dictionary<T3, T4> _dictionary;
public Indexer(Dictionary<T3, T4> dictionary)
_dictionary = dictionary;
public T4 this[T3 index]
get return _dictionary[index];
set _dictionary[index] = value;
public bool Contains(T3 key)
return _dictionary.ContainsKey(key);
这是一个用例,检查有效括号
public static class ValidParenthesisExt
private static readonly Map<char, char>
_parenthesis = new Map<char, char>
'(', ')',
'', '',
'[', ']'
;
public static bool IsValidParenthesis(this string input)
var stack = new Stack<char>();
foreach (var c in input)
if (_parenthesis.Forward.Contains(c))
stack.Push(c);
else
if (stack.Count == 0) return false;
if (_parenthesis.Reverse[c] != stack.Pop())
return false;
return stack.Count == 0;
【讨论】:
public T4 this[T3 index] get return _dictionary[index]; set _dictionary[index] = value;
由于这段代码,字典可以从对中指出值
ex:``` var parent = parentMap.Reverse[pair.Key]; parentMap.Forward[parent] = ;```【参考方案4】:
什么鬼,我会把我的版本混在一起:
public class BijectiveDictionary<TKey, TValue>
private EqualityComparer<TKey> _keyComparer;
private Dictionary<TKey, ISet<TValue>> _forwardLookup;
private EqualityComparer<TValue> _valueComparer;
private Dictionary<TValue, ISet<TKey>> _reverseLookup;
public BijectiveDictionary()
: this(EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default)
public BijectiveDictionary(EqualityComparer<TKey> keyComparer, EqualityComparer<TValue> valueComparer)
: this(0, EqualityComparer<TKey>.Default, EqualityComparer<TValue>.Default)
public BijectiveDictionary(int capacity, EqualityComparer<TKey> keyComparer, EqualityComparer<TValue> valueComparer)
_keyComparer = keyComparer;
_forwardLookup = new Dictionary<TKey, ISet<TValue>>(capacity, keyComparer);
_valueComparer = valueComparer;
_reverseLookup = new Dictionary<TValue, ISet<TKey>>(capacity, valueComparer);
public void Add(TKey key, TValue value)
AddForward(key, value);
AddReverse(key, value);
public void AddForward(TKey key, TValue value)
ISet<TValue> values;
if (!_forwardLookup.TryGetValue(key, out values))
values = new HashSet<TValue>(_valueComparer);
_forwardLookup.Add(key, values);
values.Add(value);
public void AddReverse(TKey key, TValue value)
ISet<TKey> keys;
if (!_reverseLookup.TryGetValue(value, out keys))
keys = new HashSet<TKey>(_keyComparer);
_reverseLookup.Add(value, keys);
keys.Add(key);
public bool TryGetReverse(TValue value, out ISet<TKey> keys)
return _reverseLookup.TryGetValue(value, out keys);
public ISet<TKey> GetReverse(TValue value)
ISet<TKey> keys;
TryGetReverse(value, out keys);
return keys;
public bool ContainsForward(TKey key)
return _forwardLookup.ContainsKey(key);
public bool TryGetForward(TKey key, out ISet<TValue> values)
return _forwardLookup.TryGetValue(key, out values);
public ISet<TValue> GetForward(TKey key)
ISet<TValue> values;
TryGetForward(key, out values);
return values;
public bool ContainsReverse(TValue value)
return _reverseLookup.ContainsKey(value);
public void Clear()
_forwardLookup.Clear();
_reverseLookup.Clear();
向其中添加一些数据:
var lookup = new BijectiveDictionary<int, int>();
lookup.Add(1, 2);
lookup.Add(1, 3);
lookup.Add(1, 4);
lookup.Add(1, 5);
lookup.Add(6, 2);
lookup.Add(6, 8);
lookup.Add(6, 9);
lookup.Add(6, 10);
然后进行查找:
lookup[2] --> 1, 6
lookup[3] --> 1
lookup[8] --> 6
【讨论】:
我喜欢这个支持1:N @Sebastian,你可以添加 IEnumerable正如其他人所说,您可以使用两个字典,但还要注意,如果 TKey
和 TValue
属于同一类型(并且已知它们的运行时值域是不相交的),那么您可以只使用通过为每个键/值对创建两个条目来创建相同的字典:
dict["SomeWord"]= "123"
和dict["123"]="SomeWord"
这样一个字典就可以用于任何一种类型的查找。
【讨论】:
是的,这个方法在问题中得到了承认:) 这忽略了“键”和“值”中存在相同值的可能性。那么它会在这个解决方案中发生冲突。 @user1028741 同意,尽管从示例中可以看出它们的意思是“不同类型”而不是“相同类型” 这可能会导致将来出现意外结果,然后代码会进行重构。例如。然后左右两侧开始重叠。它几乎没有增加性能。【参考方案6】:您可以使用此扩展方法,尽管它使用枚举,因此对于大型数据集可能没有性能。如果您担心效率,那么您需要两个字典。如果您想将两个字典包装到一个类中,请参阅此问题的公认答案:Bidirectional 1 to 1 Dictionary in C#
public static class IDictionaryExtensions
public static TKey FindKeyByValue<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TValue value)
if (dictionary == null)
throw new ArgumentNullException("dictionary");
foreach (KeyValuePair<TKey, TValue> pair in dictionary)
if (value.Equals(pair.Value)) return pair.Key;
throw new Exception("the value is not found in the dictionary");
【讨论】:
虽然这是一个双向字典,但获取值是一个 O(n) 操作,而它应该是一个 O(1) 操作。这对于小型数据集可能无关紧要,但在处理大型数据集时可能会导致性能问题。空间性能的最佳答案是使用两个具有反向数据的字典。 @TomA 我完全同意 Tom,唯一需要真正双向字典的情况是当您有 100K、1M+ 条目时,只要不扫描它,实际上就是 NOOP。 我喜欢这个解决方案适合我的情况(小字典大小),因为我仍然可以使用集合初始化器。我认为接受的答案中的 Map 不能用于集合初始化程序。 @ChrisMarisic,这似乎是一件奇怪的事情。我敢打赌,如果这个查找是在一个紧密的循环中调用的,即使条目少于 500 个,你也会感到痛苦。它还取决于比较测试的成本。我不认为像你的评论这样笼统的陈述是有帮助的。 @LeeCampbell 我的概括性陈述是基于在实际现实中的经验,如在可测量和描述的现实中。如果您想使用某些复杂类型作为字典的键,那是您的问题,不是我的问题。【参考方案7】:Enigmativity 的答案有一个扩展版本,可作为 nuget 包使用 https://www.nuget.org/packages/BidirectionalMap/
它是开源的here
【讨论】:
【参考方案8】:Xavier John 答案的修改版本,带有一个额外的构造函数来进行正向和反向比较器。例如,这将支持不区分大小写的键。如果需要,可以添加更多构造函数,以将更多参数传递给正向和反向 Dictionary 构造函数。
public class Map<T1, T2> : IEnumerable<KeyValuePair<T1, T2>>
private readonly Dictionary<T1, T2> _forward;
private readonly Dictionary<T2, T1> _reverse;
/// <summary>
/// Constructor that uses the default comparers for the keys in each direction.
/// </summary>
public Map()
: this(null, null)
/// <summary>
/// Constructor that defines the comparers to use when comparing keys in each direction.
/// </summary>
/// <param name="t1Comparer">Comparer for the keys of type T1.</param>
/// <param name="t2Comparer">Comparer for the keys of type T2.</param>
/// <remarks>Pass null to use the default comparer.</remarks>
public Map(IEqualityComparer<T1> t1Comparer, IEqualityComparer<T2> t2Comparer)
_forward = new Dictionary<T1, T2>(t1Comparer);
_reverse = new Dictionary<T2, T1>(t2Comparer);
Forward = new Indexer<T1, T2>(_forward);
Reverse = new Indexer<T2, T1>(_reverse);
// Remainder is the same as Xavier John's answer:
// https://***.com/a/41907561/216440
...
使用示例,带有不区分大小写的键:
Map<int, string> categories =
new Map<int, string>(null, StringComparer.CurrentCultureIgnoreCase)
1, "Bedroom Furniture" ,
2, "Dining Furniture" ,
3, "Outdoor Furniture" ,
4, "Kitchen Appliances"
;
int categoryId = 3;
Console.WriteLine("Description for category ID 0: '1'",
categoryId, categories.Forward[categoryId]);
string categoryDescription = "DINING FURNITURE";
Console.WriteLine("Category ID for description '0': 1",
categoryDescription, categories.Reverse[categoryDescription]);
categoryDescription = "outdoor furniture";
Console.WriteLine("Category ID for description '0': 1",
categoryDescription, categories.Reverse[categoryDescription]);
// Results:
/*
Description for category ID 3: 'Outdoor Furniture'
Category ID for description 'DINING FURNITURE': 2
Category ID for description 'outdoor furniture': 3
*/
【讨论】:
【参考方案9】:字典
这是我在每个答案中喜欢的内容的混合。它实现了IEnumerable
,因此它可以使用集合初始化器,如您在示例中所见。
使用限制:
您正在使用不同的数据类型。 (即,T1
≠
T2
)
代码:
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
public static void Main()
Bictionary<string, int> bictionary =
new Bictionary<string,int>()
"a",1 ,
"b",2 ,
"c",3
;
// test forward lookup
Console.WriteLine(bictionary["b"]);
// test forward lookup error
//Console.WriteLine(bictionary["d"]);
// test reverse lookup
Console.WriteLine(bictionary[3]);
// test reverse lookup error (throws same error as forward lookup does)
Console.WriteLine(bictionary[4]);
public class Bictionary<T1, T2> : Dictionary<T1, T2>
public T1 this[T2 index]
get
if(!this.Any(x => x.Value.Equals(index)))
throw new System.Collections.Generic.KeyNotFoundException();
return this.First(x => x.Value.Equals(index)).Key;
小提琴:
https://dotnetfiddle.net/mTNEuw
【讨论】:
非常优雅的解决方案!你能不能进一步解释一下它的胆量?我说得对吗,即使所有字符串都是唯一的,您也无法创建Bictionary<string, string>
?
@Merlin2001,没错。更准确地说,你不能用它进行正向查找。我将不得不考虑如何克服这一点。它可以编译,但总是在T1 == T2
时首先找到反向索引器,因此正向查找失败。此外,我不能覆盖默认索引器,因为这样查找调用将是模棱两可的。我添加了这个约束并删除了前一个约束,因为T1
的值可以与T2
的值重叠。
反过来有一个相当严重的性能问题;字典被搜索两次,性能下降 O(n);使用第二个字典会快得多,并且会删除类型约束。
@SteveCooper,也许我可以通过将其包装在 try
中并将异常转换为 KeyNotFoundExceptions
来消除性能损失。
@toddmo 您可以通过这种方式将速度提高一倍。更大的问题是 .First 和 .Any 一次搜索一个项目,测试每个项目。因此,测试 1,000,000 项列表的搜索时间是 1 元素列表的 1,000,000 倍。字典要快得多,并且不会随着您添加更多项目而减慢速度,因此第二个逆向字典将在大型列表中节省大量时间。它可能无关紧要,但在测试期间使用少量数据就可以解决问题,然后它会在具有严重数据的真实服务器上扼杀性能。【参考方案10】:
这是一个老问题,但我想添加两个扩展方法,以防有人发现它有用。第二个没那么有用,但如果需要支持一对一字典,它提供了一个起点。
public static Dictionary<VALUE,KEY> Inverse<KEY,VALUE>(this Dictionary<KEY,VALUE> dictionary)
if (dictionary==null || dictionary.Count == 0) return null;
var result = new Dictionary<VALUE, KEY>(dictionary.Count);
foreach(KeyValuePair<KEY,VALUE> entry in dictionary)
result.Add(entry.Value, entry.Key);
return result;
public static Dictionary<VALUE, KEY> SafeInverse<KEY, VALUE>(this Dictionary<KEY, VALUE> dictionary)
if (dictionary == null || dictionary.Count == 0) return null;
var result = new Dictionary<VALUE, KEY>(dictionary.Count);
foreach (KeyValuePair<KEY, VALUE> entry in dictionary)
if (result.ContainsKey(entry.Value)) continue;
result.Add(entry.Value, entry.Key);
return result;
【讨论】:
【参考方案11】:这是我的代码。除了种子构造函数之外,一切都是 O(1)。
using System.Collections.Generic;
using System.Linq;
public class TwoWayDictionary<T1, T2>
Dictionary<T1, T2> _Forwards = new Dictionary<T1, T2>();
Dictionary<T2, T1> _Backwards = new Dictionary<T2, T1>();
public IReadOnlyDictionary<T1, T2> Forwards => _Forwards;
public IReadOnlyDictionary<T2, T1> Backwards => _Backwards;
public IEnumerable<T1> Set1 => Forwards.Keys;
public IEnumerable<T2> Set2 => Backwards.Keys;
public TwoWayDictionary()
_Forwards = new Dictionary<T1, T2>();
_Backwards = new Dictionary<T2, T1>();
public TwoWayDictionary(int capacity)
_Forwards = new Dictionary<T1, T2>(capacity);
_Backwards = new Dictionary<T2, T1>(capacity);
public TwoWayDictionary(Dictionary<T1, T2> initial)
_Forwards = initial;
_Backwards = initial.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
public TwoWayDictionary(Dictionary<T2, T1> initial)
_Backwards = initial;
_Forwards = initial.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
public T1 this[T2 index]
get => _Backwards[index];
set
if (_Backwards.TryGetValue(index, out var removeThis))
_Forwards.Remove(removeThis);
_Backwards[index] = value;
_Forwards[value] = index;
public T2 this[T1 index]
get => _Forwards[index];
set
if (_Forwards.TryGetValue(index, out var removeThis))
_Backwards.Remove(removeThis);
_Forwards[index] = value;
_Backwards[value] = index;
public int Count => _Forwards.Count;
public bool Contains(T1 item) => _Forwards.ContainsKey(item);
public bool Contains(T2 item) => _Backwards.ContainsKey(item);
public bool Remove(T1 item)
if (!this.Contains(item))
return false;
var t2 = _Forwards[item];
_Backwards.Remove(t2);
_Forwards.Remove(item);
return true;
public bool Remove(T2 item)
if (!this.Contains(item))
return false;
var t1 = _Backwards[item];
_Forwards.Remove(t1);
_Backwards.Remove(item);
return true;
public void Clear()
_Forwards.Clear();
_Backwards.Clear();
【讨论】:
我想知道如果你传递一个键和值类型相同的现有字典,构造函数会如何表现。它将如何解决是使用后向还是前向?error CS0121: The call is ambiguous between the following methods or properties: 'TwoWayDictionary<T1, T2>.TwoWayDictionary(Dictionary<T1, T2>)' and 'TwoWayDictionary<T1, T2>.TwoWayDictionary(Dictionary<T2, T1>)'
【参考方案12】:
以下是建议的替代解决方案。移除了内部类并保证了添加/移除项目时的一致性
using System.Collections;
using System.Collections.Generic;
public class Map<E, F> : IEnumerable<KeyValuePair<E, F>>
private readonly Dictionary<E, F> _left = new Dictionary<E, F>();
public IReadOnlyDictionary<E, F> left => this._left;
private readonly Dictionary<F, E> _right = new Dictionary<F, E>();
public IReadOnlyDictionary<F, E> right => this._right;
public void RemoveLeft(E e)
if (!this.left.ContainsKey(e)) return;
this._right.Remove(this.left[e]);
this._left.Remove(e);
public void RemoveRight(F f)
if (!this.right.ContainsKey(f)) return;
this._left.Remove(this.right[f]);
this._right.Remove(f);
public int Count()
return this.left.Count;
public void Set(E left, F right)
if (this.left.ContainsKey(left))
this.RemoveLeft(left);
if (this.right.ContainsKey(right))
this.RemoveRight(right);
this._left.Add(left, right);
this._right.Add(right, left);
public IEnumerator<KeyValuePair<E, F>> GetEnumerator()
return this.left.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
return this.left.GetEnumerator();
【讨论】:
【参考方案13】:以下封装类在 1 个字典实例上使用 linq(IEnumerable Extensions)。
public class TwoWayDictionary<TKey, TValue>
readonly IDictionary<TKey, TValue> dict;
readonly Func<TKey, TValue> GetValueWhereKey;
readonly Func<TValue, TKey> GetKeyWhereValue;
readonly bool _mustValueBeUnique = true;
public TwoWayDictionary()
this.dict = new Dictionary<TKey, TValue>();
this.GetValueWhereKey = (strValue) => dict.Where(kvp => Object.Equals(kvp.Key, strValue)).Select(kvp => kvp.Value).FirstOrDefault();
this.GetKeyWhereValue = (intValue) => dict.Where(kvp => Object.Equals(kvp.Value, intValue)).Select(kvp => kvp.Key).FirstOrDefault();
public TwoWayDictionary(KeyValuePair<TKey, TValue>[] kvps)
: this()
this.AddRange(kvps);
public void AddRange(KeyValuePair<TKey, TValue>[] kvps)
kvps.ToList().ForEach( kvp =>
if (!_mustValueBeUnique || !this.dict.Any(item => Object.Equals(item.Value, kvp.Value)))
dict.Add(kvp.Key, kvp.Value);
else
throw new InvalidOperationException("Value must be unique");
);
public TValue this[TKey key]
get return GetValueWhereKey(key);
public TKey this[TValue value]
get return GetKeyWhereValue(value);
class Program
static void Main(string[] args)
var dict = new TwoWayDictionary<string, int>(new KeyValuePair<string, int>[]
new KeyValuePair<string, int>(".jpeg",100),
new KeyValuePair<string, int>(".jpg",101),
new KeyValuePair<string, int>(".txt",102),
new KeyValuePair<string, int>(".zip",103)
);
var r1 = dict[100];
var r2 = dict[".jpg"];
【讨论】:
【参考方案14】:这使用索引器进行反向查找。 反向查找是 O(n) 但它也不使用两个字典
public sealed class DictionaryDoubleKeyed : Dictionary<UInt32, string>
// used UInt32 as the key as it has a perfect hash
// if most of the lookup is by word then swap
public void Add(UInt32 ID, string Word)
if (this.ContainsValue(Word)) throw new ArgumentException();
base.Add(ID, Word);
public UInt32 this[string Word]
// this will be O(n)
get
return this.FirstOrDefault(x => x.Value == Word).Key;
【讨论】:
例如:this[string Word]
中可能的 NRE。其他问题是变量名不符合惯例,注释与代码不一致(UInt16
vs UInt32
- 这就是为什么:不要使用 cmets!),解决方案不通用,...【参考方案15】:
在这个开源仓库中有一个BijectionDictionary
类型可用:
https://github.com/ColmBhandal/CsharpExtras.
这与给出的其他答案在质量上没有太大区别。它使用两个字典,就像大多数答案一样。
我认为,这本词典与迄今为止的其他答案相比,新颖之处在于,它不像一个双向词典,而是像一个单向、熟悉的词典,然后动态地允许您翻转使用 Reverse 属性的字典。翻转的对象引用很浅,因此它仍然可以修改与原始引用相同的核心对象。所以你可以有两个对同一个对象的引用,除了其中一个被翻转。
这本字典的另一个可能独特之处是,在该 repo 下的测试项目中为它编写了一些测试。我们已经在实践中使用它,并且到目前为止非常稳定。
【讨论】:
以上是关于C#中的双向/双向字典?的主要内容,如果未能解决你的问题,请参考以下文章