C# 大型无向图的高效存储、查找和操作

C# 大型无向图的高效存储、查找和操作,c#,performance,data-structures,graph,hashset,C#,Performance,Data Structures,Graph,Hashset,我有一个无向图G,有大约5000个节点。任何一对节点都可以通过一条边连接。边的长度、方向或其他特征是不相关的,并且两点之间最多可以有一条边,因此节点之间的关系是二进制的。因此,总共有12497500个潜在边缘 每个节点都由字符串名称标识,而不是数字 我想存储这样一个图(作为输入数据加载到我的程序中),但我不确定哪种数据结构是最好的 我需要多次查找给定的一对节点是否连接,因此查找性能可能是主要关注点 添加和删除元素的性能成本并不重要 如果可能的话,我还希望语法保持简单和优雅,以减少引入bug的可

我有一个无向图G,有大约5000个节点。任何一对节点都可以通过一条边连接。边的长度、方向或其他特征是不相关的,并且两点之间最多可以有一条边,因此节点之间的关系是二进制的。因此,总共有12497500个潜在边缘

每个节点都由字符串名称标识,而不是数字

我想存储这样一个图(作为输入数据加载到我的程序中),但我不确定哪种数据结构是最好的

  • 我需要多次查找给定的一对节点是否连接,因此查找性能可能是主要关注点
  • 添加和删除元素的性能成本并不重要
  • 如果可能的话,我还希望语法保持简单和优雅,以减少引入bug的可能性,并使调试更容易
两种可能性:

  • bool[numNodes,numNodes]
    和一个
    字典
    将每个节点名称与索引匹配。优点:简单、快速的查找。缺点:无法轻松删除或添加节点(必须添加/删除行/列),冗余(必须小心
    g[n1,n2]
    vs
    g[n2,n1]
    ),语法笨拙,因为我每次都要查看HashMap

  • HashSet
    。优点:直观,节点由字符串直接标识,易于“添加/删除节点”,因为只存储边,节点本身是隐式的。缺点:可能输入垃圾输入(由于集合有三个成员,“连接”三个节点的边)

关于第二种选择,我也不清楚一些事情:

  • 它会比一个
    bool
    数组占用更多内存吗
  • 两个.NET集合是否等同于数学集合,即它们相等,当且仅当它们具有完全相同的成员时(而不是通过元素的容量或顺序等进行区分),以便
    HashSet
    成为成员?(ie正在使用
    outerSets.Contains(新的HashSet{“node1”、“node2”})
    进行查询,实际工作了吗?)
  • 查找是否要比一个
    bool
    数组花费更长的时间

  • C5集合库中有一些关于图形的有用信息

    这个问题看起来也很有用


    SortedDictionary在引擎盖下,是一棵高度平衡的红黑树,因此查找是O(logn)。

    我很好奇在生成表示哈希表中边缘的键时使用字符串连接而不是元组,以便接近O(1)查找性能。这里有两种处理无向边要求的可能性:

  • 规范化关键点,以便无论在边的描述中首先指定哪个节点,关键点都是相同的。在我的测试中,我只是选择序号比较值最低的节点作为键中的第一个组件

  • 在哈希表中创建两个条目,一个用于边的每个方向

  • 这里的一个关键假设是字符串节点标识符不是很长,因此与查找相比,密钥规范化是便宜的

    带有键规范化的字符串连接和元组版本似乎工作得差不多:在发布模式下的VirtualBox VM中,我们在大约3秒钟内完成了大约200万次随机查找

    为了查看键规范化是否会淹没查找操作的效果,第三种实现不进行键规范化,而是保持关于边的两个可能方向的对称项。这似乎是大约30-40%的查找速度慢,这是有点出乎意料(对我来说)。也许底层哈希表存储桶具有更高的平均占用率,这是因为其元素数是原来的两倍,需要在每个哈希表存储桶中进行更长的线性搜索(平均)

    interface-IEdgeCollection
    {
    布尔加法(字符串节点1、字符串节点2);
    bool包含sedge(字符串节点1、字符串节点2);
    布尔删除边缘(字符串节点1、字符串节点2);
    }
    类EdgeSet1:IEdgeCollection
    {
    私有HashSet_edges=新HashSet();
    私有静态字符串MakeEdgeKey(字符串节点1、字符串节点2)
    {
    返回StringComparer.Ordinal.Compare(node1,node2)<0?node1+node2:node2+node1;
    }
    公共布尔加法(字符串节点1、字符串节点2)
    {
    var key=MakeEdgeKey(节点1、节点2);
    返回_边。添加(键);
    }
    公共布尔包含sedge(字符串节点1、字符串节点2)
    {
    var key=MakeEdgeKey(节点1、节点2);
    返回_edges.Contains(键);
    }
    公共布尔删除(字符串节点1、字符串节点2)
    {
    var key=MakeEdgeKey(节点1、节点2);
    返回边缘。移除(键);
    }
    }
    类EdgeSet2:IEdgeCollection
    {
    私有HashSet_edges=新HashSet();
    私有静态元组MakeEdgeKey(字符串节点1、字符串节点2)
    {
    返回StringComparer.Ordinal.Compare(节点1、节点2)<0
    ?新元组(节点1、节点2)
    :新元组(node2,node1);
    }
    公共布尔加法(字符串节点1、字符串节点2)
    {
    var key=MakeEdgeKey(节点1、节点2);
    返回_边。添加(键);
    }
    公共布尔包含sedge(字符串节点1、字符串节点2)
    {
    var key=MakeEdgeKey(节点1、节点2);
    返回_edges.Contains(键);
    }
    公共布尔删除(字符串节点1、字符串节点2)
    {
    var key=MakeEdgeKey(节点1、节点2);
    返回边缘。移除(键);
    }
    }
    类边集合3:IEdgeCollection
    {
    私有HashSet_edges=新HashSet();
    私有静态元组MakeEdgeKey(字符串节点1、字符串节点2)
    {
    返回新元组(node1、node2);
    }
    公共布尔加法(字符串节点1、字符串节点2)
    {
    var key1=MakeEdgeKey(节点1、节点2);
    var key2=MakeEdgeKey(节点2,节点1);
    返回边。添加(键1)和边。添加(键2)
    
    interface IEdgeCollection
    {
        bool AddEdge(string node1, string node2);
        bool ContainsEdge(string node1, string node2);
        bool RemoveEdge(string node1, string node2);
    }
    
    class EdgeSet1 : IEdgeCollection
    {
        private HashSet<string> _edges = new HashSet<string>();
    
        private static string MakeEdgeKey(string node1, string node2)
        {
            return StringComparer.Ordinal.Compare(node1, node2) < 0 ? node1 + node2 : node2 + node1;
        }
    
        public bool AddEdge(string node1, string node2)
        {
            var key = MakeEdgeKey(node1, node2);
            return _edges.Add(key);
        }
    
        public bool ContainsEdge(string node1, string node2)
        {
            var key = MakeEdgeKey(node1, node2);
            return _edges.Contains(key);
        }
    
        public bool RemoveEdge(string node1, string node2)
        {
            var key = MakeEdgeKey(node1, node2);
            return _edges.Remove(key);
        }
    }
    
    class EdgeSet2 : IEdgeCollection
    {
        private HashSet<Tuple<string, string>> _edges = new HashSet<Tuple<string, string>>();
    
        private static Tuple<string, string> MakeEdgeKey(string node1, string node2)
        {
            return StringComparer.Ordinal.Compare(node1, node2) < 0 
                ? new Tuple<string, string>(node1, node2) 
                : new Tuple<string, string>(node2, node1);
        }
    
        public bool AddEdge(string node1, string node2)
        {
            var key = MakeEdgeKey(node1, node2);
            return _edges.Add(key);
        }
    
        public bool ContainsEdge(string node1, string node2)
        {
            var key = MakeEdgeKey(node1, node2);
            return _edges.Contains(key);
        }
    
        public bool RemoveEdge(string node1, string node2)
        {
            var key = MakeEdgeKey(node1, node2);
            return _edges.Remove(key);
        }
    }
    
    class EdgeSet3 : IEdgeCollection
    {
        private HashSet<Tuple<string, string>> _edges = new HashSet<Tuple<string, string>>();
    
        private static Tuple<string, string> MakeEdgeKey(string node1, string node2)
        {
            return new Tuple<string, string>(node1, node2);
        }
    
        public bool AddEdge(string node1, string node2)
        {
            var key1 = MakeEdgeKey(node1, node2);
            var key2 = MakeEdgeKey(node2, node1);
            return _edges.Add(key1) && _edges.Add(key2);
        }
    
        public bool ContainsEdge(string node1, string node2)
        {
            var key = MakeEdgeKey(node1, node2);
            return _edges.Contains(key);
        }
    
        public bool RemoveEdge(string node1, string node2)
        {
            var key1 = MakeEdgeKey(node1, node2);
            var key2 = MakeEdgeKey(node2, node1);
            return _edges.Remove(key1) && _edges.Remove(key2);
        }
    }
    
    class Program
    {
        static void Test(string[] nodes, IEdgeCollection edges, int edgeCount)
        {
            // use edgeCount as seed to rng to ensure test reproducibility
            var rng = new Random(edgeCount);
    
            // store known edges in a separate data structure for validation
            var edgeList = new List<Tuple<string, string>>();
    
            Stopwatch stopwatch = new Stopwatch();
    
            // randomly generated edges
            stopwatch.Start();
            for (int i = 0; i < edgeCount; i++)
            {
                string node1 = nodes[rng.Next(nodes.Length)];
                string node2 = nodes[rng.Next(nodes.Length)];
                edges.AddEdge(node1, node2);
                edgeList.Add(new Tuple<string, string>(node1, node2));
            }
            var addElapsed = stopwatch.Elapsed;
    
            // non random lookups
            int nonRandomFound = 0;
            stopwatch.Start();
            foreach (var edge in edgeList)
            {
                if (edges.ContainsEdge(edge.Item1, edge.Item2))
                    nonRandomFound++;
            }
            var nonRandomLookupElapsed = stopwatch.Elapsed;
            if (nonRandomFound != edgeList.Count)
            {
                Console.WriteLine("The edge collection {0} is not working right!", edges.GetType().FullName);
                return;
            }
    
            // random lookups
            int randomFound = 0;
            stopwatch.Start();
            for (int i = 0; i < edgeCount; i++)
            {
                string node1 = nodes[rng.Next(nodes.Length)];
                string node2 = nodes[rng.Next(nodes.Length)];
                if (edges.ContainsEdge(node1, node2))
                    randomFound++;
            }
            var randomLookupElapsed = stopwatch.Elapsed;
    
            // remove all
            stopwatch.Start();
            foreach (var edge in edgeList)
            {
                edges.RemoveEdge(edge.Item1, edge.Item2);
            }
            var removeElapsed = stopwatch.Elapsed;
    
            Console.WriteLine("Test: {0} with {1} edges: {2}s addition, {3}s non-random lookup, {4}s random lookup, {5}s removal",
                edges.GetType().FullName,
                edgeCount,
                addElapsed.TotalSeconds,
                nonRandomLookupElapsed.TotalSeconds,
                randomLookupElapsed.TotalSeconds,
                removeElapsed.TotalSeconds);
        }
    
    
        static void Main(string[] args)
        {
            var rng = new Random();
    
            var nodes = new string[5000];
            for (int i = 0; i < nodes.Length; i++)
            {
                StringBuilder name = new StringBuilder();
                int length = rng.Next(7, 15);
                for (int j = 0; j < length; j++)
                {
                    name.Append((char) rng.Next(32, 127));
                }
                nodes[i] = name.ToString();
            }
    
            IEdgeCollection edges1 = new EdgeSet1();
            IEdgeCollection edges2 = new EdgeSet2();
            IEdgeCollection edges3 = new EdgeSet3();
    
            Test(nodes, edges1, 2000000);
            Test(nodes, edges2, 2000000);
            Test(nodes, edges3, 2000000);
    
            Console.ReadLine();
        }
    }