首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >双向词典

双向词典
EN

Code Review用户
提问于 2019-09-04 04:26:46
回答 2查看 2.1K关注 0票数 9

双向映射的管理是一个重新出现的话题。我花时间写了一个(希望是)高效的实现。

代码语言:javascript
复制
using System;
using System.Collections;
using System.Collections.Generic;

namespace buka_core.misc
{
    /// <summary>
    /// 
    /// File Bijection.cs
    /// 
    /// Provides an implementation of a discrete bijective mapping
    /// 
    /// The inverses are created using shallow copies of the underlying datastructures, which leads to
    /// the original object and all its derived inverses being modified if one object changes. For this
    /// reason the class implements the interface ICloneable which allows the user to create deep copies
    /// 
    /// The class also implements the interface IDictionary which provides easy access to the proto-
    /// type
    /// 
    /// </summary>
    /// <typeparam name="T_Proto">Datatype of keys for the prototype</typeparam>
    /// <typeparam name="T_Inv">Datatype of keys for its inverse</typeparam>
    public class Bijection<T_Proto, T_Inv> : ICloneable, IDictionary<T_Proto, T_Inv>
    {
        /// <summary>
        /// Creates an empty discrete bijective mapping
        /// </summary>
        public Bijection()
        {

        }

        /// <summary>
        /// Used internally to efficiently generate inverses
        /// </summary>
        /// <param name="proto">The prototype mapping</param>
        /// <param name="inverse">Its inverse mapping</param>
        private Bijection(IDictionary<T_Proto, T_Inv> proto, IDictionary<T_Inv, T_Proto> inverse)
        {
            _Proto = proto;
            _Inv = inverse;
        }

        /// <summary>
        /// Indexer to insert and modify records
        /// </summary>
        /// <param name="key">Object for which the corresponding dictionary entry should be returned</param>
        /// <returns>The value that key maps to</returns>
        public T_Inv this[T_Proto key]
        {
            get
            {
                if (!_Proto.ContainsKey(key))
                {
                    throw new KeyNotFoundException("[Bijection] The key " + key + " could not be found");
                }

                return _Proto[key];
            }
            set
            {
                this.Add(key, value);
            }
        }

        /// <summary>
        /// Returns a bijection for which keys and values are reversed
        /// </summary>
        public Bijection<T_Inv, T_Proto> Inverse
        {
            get
            {
                if (null == _inverse)
                {
                    _inverse = new Bijection<T_Inv, T_Proto>(_Inv, _Proto);
                }

                return _inverse;
            }
        }
        private Bijection<T_Inv, T_Proto> _inverse = null;                  // Backer for lazy initialisation of Inverse

        /// <summary>
        /// Prototype mapping
        /// </summary>
        private IDictionary<T_Proto, T_Inv> _Proto
        {
            get
            {
                if (null == _proto)
                {
                    _proto = new SortedDictionary<T_Proto, T_Inv>();
                }

                return _proto;
            }
            /* private */
            set
            {
                _proto = value;
            }
        }
        private IDictionary<T_Proto, T_Inv> _proto = null;                  // Backer for lazy initialisation of _Proto

        /// <summary>
        /// Inverse prototype mapping
        /// </summary>
        private IDictionary<T_Inv, T_Proto> _Inv
        {
            get
            {
                if (null == _inv)
                {
                    _inv = new SortedDictionary<T_Inv, T_Proto>();
                }

                return _inv;
            }
            /* private */
            set
            {
                _inv = value;
            }
        }
        private IDictionary<T_Inv, T_Proto> _inv = null;                    // Backer for lazy initialisation of _Inv

        #region Implementation of ICloneable
        /// <summary>
        /// Creates a deep copy
        /// </summary>
        public object Clone()
        {
            return new Bijection<T_Proto, T_Inv>(
                new SortedDictionary<T_Proto, T_Inv>(_Proto),
                new SortedDictionary<T_Inv, T_Proto>(_Inv)
                );
        }
        #endregion

        #region Implementation of IDictionary<T_Proto, T_Inv>
        public ICollection<T_Proto> Keys => _Proto.Keys;
        public ICollection<T_Inv> Values => _Proto.Values;
        public int Count => _Proto.Count;
        public bool IsReadOnly => _Proto.IsReadOnly;

        public bool Contains(KeyValuePair<T_Proto, T_Inv> item)
        {
            return _Proto.Contains(item);
        }

        public bool ContainsKey(T_Proto key)
        {
            return _Proto.ContainsKey(key);
        }

        public void Clear()
        {
            _Proto.Clear();
            _Inv.Clear();
        }

        public void Add(T_Proto key, T_Inv value)
        {
            if (_Proto.ContainsKey(key))
            {
                _Inv.Remove(_Proto[key]);
            }

            if (_Inv.ContainsKey(value))
            {
                throw new ArgumentException("[Bijection] The inverse already maps " + value + " to " + _Inv[value]);
            }

            _Proto.Add(key, value);
            _Inv.Add(value, key);
        }
        public void Add(KeyValuePair<T_Proto, T_Inv> item)
        {
            this.Add(item.Key, item.Value);
        }

        public bool Remove(T_Proto key)
        {
            if (_Proto.ContainsKey(key))
            {
                bool removed_inv = _Inv.Remove(_Proto[key]);
                bool removed_proto = _Proto.Remove(key);

                return (removed_proto && removed_inv);              // == true
            }
            else
            {
                return false;
            }
        }
        public bool Remove(KeyValuePair<T_Proto, T_Inv> item)
        {
            return this.Remove(item.Key);
        }

        public bool TryGetValue(T_Proto key, out T_Inv value)
        {
            return _Proto.TryGetValue(key, out value);
        }

        public void CopyTo(KeyValuePair<T_Proto, T_Inv>[] array, int arrayIndex)
        {
            _Proto.CopyTo(array, arrayIndex);
        }

        public IEnumerator<KeyValuePair<T_Proto, T_Inv>> GetEnumerator()
        {
            return _Proto.GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator()
        {
            return _Proto.GetEnumerator();
        }
        #endregion

        #region Overrides
        public override bool Equals(object obj)
        {
            Bijection<T_Proto, T_Inv> obj_bijection = (obj as Bijection<T_Proto, T_Inv>); if (null == obj) return false;

            if (this.Count != obj_bijection.Count) return false;
            if (!_Proto.Equals(obj_bijection._Proto)) return false;
            if (!_Inv.Equals(obj_bijection._Inv)) return false;

            return true;
        }

        public override int GetHashCode()
        {
            return _Proto.GetHashCode();
        }

        public override string ToString()
        {
            return _Proto.ToString();
        }
        #endregion
    }
}

实例将按以下方式使用

代码语言:javascript
复制
Bijection<int, string> b = new Bijection<int, string>();

b[1] = "frog";
b[2] = "fish";
b[3] = "dog";
b[5] = "cat";
b[8] = "snake";
b[13] = "crocodile";

Console.WriteLine(b.Inverse["crocodile"]);
Console.WriteLine(b[13]);

欢迎任何反馈/建议。保持对象及其逆序像这样绑定是合理的吗?还是更改逆对象也会改变原始对象是意外的行为?

EN

回答 2

Code Review用户

发布于 2019-09-04 15:54:54

KISS

我觉得这门课太复杂了。它存储两个字典,但只允许从一个类型的角度进行操作。它需要第二个实例,从另一个角度交换字典来操作数据。

此外,双射应该被看作是两个集合之间的函数,而不是从任何一个角度来看都是字典。

干脆不选一个视角怎么样。从公众的角度来看,它只是一个集合(实际上是一个集合),集合x的元素和集合y的元组。在我看来,双射的理想用法如下:

代码语言:javascript
复制
var bijection = new Bijection<int, string>();

bijection.Add((1, "USA"));
bijection.Add((2, "UK"));

// X and Y chosen from set theory: https://en.wikipedia.org/wiki/Bijection
var country = bijection.X[1];
var id = bijection.Y["UK"];

您不再从proto或inv类型透视双射。相反,您可以使用原子类型(X, Y)。只提供阅读词典XY,让您了解这两种类型的透视图。

代码语言:javascript
复制
public class Bijection<TX, TY> : ICollection<(TX, TY)>
{
    private readonly IDictionary<TX, TY> _x = new Dictionary<TX, TY>();
    private readonly IDictionary<TY, TX> _y = new Dictionary<TY, TX>();

    public IReadOnlyDictionary<TX, TY> X => new ReadOnlyDictionary<TX, TY>(_x);
    public IReadOnlyDictionary<TY, TX> Y => new ReadOnlyDictionary<TY, TX>(_y);

    // ICollection members ..
}
票数 3
EN

Code Review用户

发布于 2019-09-06 03:42:35

由于这个问题在24小时内就有超过1000次浏览,所以我决定尽可能多地回复这个课程。

如有进一步改进意见,敬请谅解。

因为编辑这个问题会导致t3chb0t提到的回滚,所以我决定将更改放在一个单独的答案中。

代码语言:javascript
复制
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;

namespace com.stackexchange.codereview.datastruc
{
    /// <summary>
    /// File Bijection.cs
    /// 
    /// This class implements a Bijection (which can be thought of a bidirectional Dictionary)
    /// 
    /// Link to Discussion 
    /// https://codereview.stackexchange.com/questions/227445/bidirectional-dictionary
    ///
    /// Link to Source 
    /// https://github.com/pillepalle1/stackexchange-codereview/blob/master/datastruc/Bijection.cs
    ///
    /// </summary>

    /*
     * Thanks to (see below) for their valuable input
     * ---------------+---------------------------------------------------------------------------
     * Henrik Hansen  | https://codereview.stackexchange.com/users/73941/henrik-hansen
     * dfhwze         | https://codereview.stackexchange.com/users/200620/dfhwze
     * t3chb0t        | https://codereview.stackexchange.com/users/59161/t3chb0t
     * Pieter Witvoet | https://codereview.stackexchange.com/users/51173/pieter-witvoet
     * JAD            | https://codereview.stackexchange.com/users/140805/jad
     * 
     * Remarks
     * -------------------------------------------------------------------------------------------
     * 
     * IDictionary has been removed as suggested by dfhwze . This does not cause a loss of functionality
     * due to the introduced properties .MappingXtoY and .MappingYtoX which provide read only access to
     * the internal Dictionaries
     * 
     * JAD and Pieter Witvoet seemed to be irritated by using a SortedDictionary rather than a Dictionary.
     * In the end it is a question of optimizing space or access time. Given that the structure maintains
     * two dictionaries, I first considered it reasonable to rather optimize space but it seems like that
     * the expected default behaviour is to optimize speed
     * 
     * Implementation of .Equals .GetHashcode .ToString has been changed given the remarks of Pieter Witvoet
     *
     */

    public class Bijection<T_SetX, T_SetY> : ICollection<(T_SetX, T_SetY)>
    {
        #region Exceptions the Structure might throw
        private static ArgumentException _xCollisionEx = new ArgumentException(String.Empty
            + "A collision occured in subset X when attempting to add the current element"
            + "You might want to: "
            + "- have a look at the property .CollisionHandlingProperty"
            + "- consider changing the implementation of x.Equals"
        );

        private static ArgumentException _yCollisionEx = new ArgumentException(String.Empty
            + "A collision occured in subset Y when attempting to add the current element"
            + "You might want to: "
            + "- have a look at the property .CollisionHandlingProperty"
            + "- consider changing the implementation of y.Equals"
        );

        private static Exception _internalError = new Exception(String.Empty
            + "[Bijection] Internal error / Inconsistent state"
        );
        #endregion

        private IDictionary<T_SetX, T_SetY> _x_to_y = null;                             // Mapping x to y (Get y given x)
        private IDictionary<T_SetY, T_SetX> _y_to_x = null;                             // Mapping y to x (Get x given y)

        public Bijection() :
        this(new Dictionary<T_SetX, T_SetY>(), new Dictionary<T_SetY, T_SetX>())
        {
        }

        public Bijection(IDictionary<T_SetX, T_SetY> dict)
        {
            _x_to_y = new Dictionary<T_SetX, T_SetY>();
            _y_to_x = new Dictionary<T_SetY, T_SetX>();

            foreach (T_SetX x in dict.Keys)
            {
                this.Add((x, dict[x]));
            }
        }

        private Bijection(IDictionary<T_SetX, T_SetY> x_to_y, IDictionary<T_SetY, T_SetX> y_to_x)
        {
            _x_to_y = x_to_y;
            _y_to_x = y_to_x;
        }

        /// <summary>
        /// Elements of set X
        /// </summary>
        public IList<T_SetX> X => new List<T_SetX>(_x_to_y.Keys);

        /// <summary>
        /// Elements of set Y
        /// </summary>
        public IList<T_SetY> Y => new List<T_SetY>(_y_to_x.Keys);


        public IReadOnlyDictionary<T_SetX, T_SetY> MappingXtoY => new ReadOnlyDictionary<T_SetX, T_SetY>(_x_to_y);
        public IReadOnlyDictionary<T_SetY, T_SetX> MappingYtoX => new ReadOnlyDictionary<T_SetY, T_SetX>(_y_to_x);


        #region Indexer and Inverse

        /*
         * The indexer remained because some users (including me) prefer to manage the object through indices
         * rather than calling the method .Add((x,y)) even though it is conceptually not entirely appropriate
         * 
         * The .Inverse has however been removed because it introduces the question on how to handle the prop
         * CollisionHandlingPolicy (is it supposed to be kept synchronous with its Inverse?) which then com-
         * plicates the code to an inappropriate extent.
         * 
         * This also removed the question of how to manage the inverse as mentioned by Pieter Witvoet
         * 
         * This introduces an asymmetrie and bias in favor of elements in X since elements cannot be added to
         * Y by using an indexer. This should however not cause a problem in practise, since both elements x
         * and y must be known when added to the collection as a tuple
         */

        public T_SetY this[T_SetX x]
        {
            get
            {
                return GetY(x);
            }
            set
            {
                Add((x, value));
            }
        }
        #endregion

        public T_SetX GetX(T_SetY y)
        {
            return _y_to_x[y];
        }

        public T_SetY GetY(T_SetX x)
        {
            return _x_to_y[x];
        }

        public void RemoveX(T_SetX x)
        {
            this.Remove((x, _x_to_y[x]));
        }

        public void RemoveY(T_SetY y)
        {
            this.Remove((_y_to_x[y], y));
        }

        /// <summary>
        /// Indicates the policy to be applied if an element cannot be added because it would break the bijection
        /// </summary>
        public ECollisionHandlingPolicy CollisionHandlingPolicy
        {
            get
            {
                return _collisionHandlingPolicy ?? ECollisionHandlingPolicy.ThrowX_ThrowY;
            }
            set
            {
                _collisionHandlingPolicy = value;
            }
        }
        protected ECollisionHandlingPolicy? _collisionHandlingPolicy = null;

        #region Implementation of Interface System.ICloneable

        /*
         *
         * Attempting to implement this ICloneable led to a conflict that suggested to discard it
         * alltogether
         *
         * The problem is that creating a deep copy would require T_SetX and T_SetY to implement
         * System.ICloneable which would severly limit the flexibility. It could however be reason-
         * able for immutable types but then the issue of having to properly inform the user before-
         * hand
         *
         */

        #endregion

        #region Implementation of Interface ICollection<T_SetX, T_SetY>
        public int Count => X.Count;

        public bool IsReadOnly => false;

        public void Add((T_SetX, T_SetY) item)
        {
            if (this.Contains(item)) return;

            if (X.Contains(item.Item1))
            {
                switch (CollisionHandlingPolicy)
                {
                    case (ECollisionHandlingPolicy.ThrowX_ThrowY):
                    case (ECollisionHandlingPolicy.ThrowX_ResolveY): throw _xCollisionEx;

                    case (ECollisionHandlingPolicy.ResolveX_ThrowY):
                    case (ECollisionHandlingPolicy.ResolveX_ResolveY): _x_to_y.Remove(item.Item1); break;

                    default: throw _internalError;
                }
            }

            if (Y.Contains(item.Item2))
            {
                switch (CollisionHandlingPolicy)
                {
                    case (ECollisionHandlingPolicy.ThrowX_ResolveY):
                    case (ECollisionHandlingPolicy.ResolveX_ResolveY): _y_to_x.Remove(item.Item2); break;

                    case (ECollisionHandlingPolicy.ThrowX_ThrowY):
                    case (ECollisionHandlingPolicy.ResolveX_ThrowY): throw _yCollisionEx;

                    default: throw _internalError;
                }
            }

            _x_to_y[item.Item1] = item.Item2;
            _y_to_x[item.Item2] = item.Item1;
        }

        public void Clear()
        {
            _x_to_y.Clear();
            _y_to_x.Clear();
        }

        public bool Contains((T_SetX, T_SetY) item)
        {
            if (!X.Contains(item.Item1)) return false;
            if (!Y.Contains(item.Item2)) return false;
            if (!_x_to_y[item.Item1].Equals(item.Item2)) return false;

            return true;
        }

        public void CopyTo((T_SetX, T_SetY)[] array, int arrayIndex)
        {
            foreach (T_SetX x in X)
            {
                array[arrayIndex++] = (x, _x_to_y[x]);
            }
        }

        public bool Remove((T_SetX, T_SetY) item)
        {
            if (!this.Contains(item)) return false;

            _x_to_y.Remove(item.Item1);
            _y_to_x.Remove(item.Item2);
            return true;
        }

        public IEnumerator<(T_SetX, T_SetY)> GetEnumerator()
        {
            return new BijectionEnumerator(this);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new BijectionEnumerator(this);
        }
        #endregion

        #region Bijection Specific Nested Data Structures
        /// <summary>
        /// Enumerator for element-wise access to a bijection
        /// </summary>
        public class BijectionEnumerator : IEnumerator<(T_SetX, T_SetY)>
        {
            private Bijection<T_SetX, T_SetY> _bijection = null;
            private List<T_SetX> _keys = null;
            private int _keyIndex;

            public BijectionEnumerator(Bijection<T_SetX, T_SetY> bijection)
            {
                _bijection = bijection;
                _keys = new List<T_SetX>(bijection.X);
                _keyIndex = 0;
            }
            public (T_SetX, T_SetY) Current
            {
                get
                {
                    return (_keys[_keyIndex], _bijection.GetY(_keys[_keyIndex]));
                }
            }

            object IEnumerator.Current
            {
                get
                {
                    return (_keys[_keyIndex], _bijection.GetY(_keys[_keyIndex]));
                }
            }

            public bool MoveNext()
            {
                return (_keyIndex < (_keys.Count - 1));
            }

            public void Reset()
            {
                _keyIndex = 0;
            }

            public void Dispose()
            {
                // This enumerator does not occupy any ressources that need to be released
            }

        }
        #endregion

        #region Overrides
        public override string ToString()
        {
            StringBuilder b = new StringBuilder();

            b.Append("Count=" + this.Count);
            b.Append(' ');
            b.Append("[" + typeof(T_SetX).ToString() + " <-> " + typeof(T_SetY).ToString() + "]");

            return b.ToString();
        }
        public override int GetHashCode()
        {
            return Count;
        }
        public override bool Equals(object obj)
        {
            Bijection<T_SetX, T_SetY> obj_bijection = (obj as Bijection<T_SetX, T_SetY>);
            if (null == obj_bijection) return false;

            if (Count != obj_bijection.Count) return false;

            foreach (var t in this)
            {
                if (!obj_bijection.Contains(t)) return false;
            }

            return true;
        }
        #endregion
    }

    #region Bijection Specific External Data Structures
    /// <summary>
    /// Available policies on resolving a conflict caused by attempting to map an element a to b which already maps to c
    /// - Throw will cause an ArgumentException to be thrown
    /// - Resolve will remove the existing mapping and replace it by the one provided
    /// </summary>
    public enum ECollisionHandlingPolicy
    {
        ThrowX_ThrowY,
        ThrowX_ResolveY,
        ResolveX_ThrowY,
        ResolveX_ResolveY
    }
    #endregion
}

我还添加了一个属性,允许用户在发生冲突时决定行为。

关于如何使用结构的示例

代码语言:javascript
复制
public static void Main(string[] args)
{
    Bijection<int, string> bijection = new Bijection<int, string>();

    bijection[1] = "frog";
    bijection.Add((2, "camel"));
    bijection.[3] = "horse";

    if(bijection.Y.Contains("frog"))
    {
        bijection.RemoveY("frog");
        EatFrog();
    }

    foreach(int i in bijection.X)
    {
        Console.WriteLine(bijection[i]);
    }

    foreach(var t in bijection)
    {
        Console.WriteLine(t.item2);
    }
}

这应该涵盖了大部分案件

票数 2
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/227445

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档