搜索树中模式的算法

时间:2015-03-25 18:51:01

标签: algorithm search tree

我正在开发一个大量使用树结构进行数据处理的项目。我正在寻找一种方法来在树中找到匹配的模式。例如,考虑如下的树:

    (1:a) ----- (2:b) ---- (4:c) ---- (5:e) ---- (8:c) ---- (9:f)
          |---- (3:d)            |--- (6:f)            |--- (10:g) 
                                 |--- (7:g)

(1有两个孩子2和3,4有孩子5,6,7,8有孩子9和10),字母是每个节点的值。

我需要找到类似

之类的所有事件
    c ---- f
      |--- g

应返回4和8作为父节点的索引。对此有什么好的算法?它可能是BFS,但是对于这种搜索是否有更专业的搜索算法?

2 个答案:

答案 0 :(得分:1)

想法1

提高速度的一种简单方法是将每个字母的地图预先计算到树中出现该字母的所有位置的列表。

所以在你的例子中,c将映射到[4,8]。

然后,当您搜索给定的模式时,您只需要探索至少第一个元素正确的子树。

创意2

对某些使用模式有帮助的扩展可以预先计算从每个字母到树中所有位置的父母列表的第二个映射。

因此,例如,f将映射到[4,8]和e到[4]。

如果位置列表按排序顺序存储,那么这些地图可用于有效地查找头部和某些孩子的模式。

我们通过使用第一张地图查找头部来获取可能的位置列表,并使用第二张地图查找其他列表以查找孩子。

然后,您可以合并这些列表(这可以有效地完成,因为列表已排序),以查找出现在每个列表中的条目 - 这些将是所有匹配的位置。

答案 1 :(得分:1)

这是我的一些理论制作,所以当我错了时,请随时纠正我。

它受前缀/后缀trie结构的影响,这使得人们可以在字符串中查找匹配的子字符串。虽然我将选择的数据结构将更像树状,但通过连接节点的引用,它本质上也将是非常类似图形的。

输出最终(希望)显示在快速时间内包含模式的子树根的所有索引。

我将决定使用的数据结构类似于树节点,它包含字符串值,发生这种情况的每个位置的索引,包含公共值的节点的所有可能父节点的索引,以及子节点存储为O(1)最佳案例搜索的地图。

以下所有代码均以C#完成。

    public class Node
    {
        public String value;                       //This will be the value. ie: “a”
        public Dictionary<int, int> connections;   //connections will hold {int reference (key), int parent (value)} pairs
        public Dictionary<String, Node> childs;    //This will contain all childs, with it’s value
                                                   //as the key.
        public Node()
        {
            connections = new Dictionary<int, int>();
            childs = new Dictionary<String, Node>();
        }
    }

其次,我们假设您的基础数据是一种非常传统的树结构,尽管差异可能很小。

    public class TreeNode
    {
        public int index;
        public String value;
        public List<TreeNode> childs;
        public TreeNode()
        {
            childs = new List<TreeNode>();
        }
        public TreeNode(String value)
        {
            childs = new List<TreeNode>();
            this.value = value;
        }
        public void add(String name)
        {
            TreeNode child = new TreeNode(name);
            childs.Add(child);
        }
    }

最后,基本TreeNode结构的节点都被编入索引(在您的示例中,您使用了基于1的索引,但以下是在基于0的索引中完成的)

        int index = 0;
        Queue<TreeNode> tempQ = new Queue<TreeNode>();
        tempQ.Enqueue(root);
        while (tempQ.Count > 0)
        {
            temp = tempQ.Dequeue();
            temp.index = index;
            index++;
            foreach (TreeNode tn in temp.childs)
            {
                tempQ.Enqueue(tn);
            }
        }
        return root;

在我们初始化结构之后,假设基础数据存储在传统类型的TreeNode结构中,我们将尝试做三件事:

  1. 使用基础TreeNode

  2. 构建类似图形的结构
  3. 一个最大的特性是唯一值只能在一个节点中表示。例如,您的示例中的{C},{F}和{G}将仅用一个节点表示,而不是两个节点。 (简单地说,所有具有共同值的节点将组合成一个。)

  4. 所有唯一节点(来自第2步)将附加到根元素,我们将重建&#34;通过连接对引用的引用来实现树。 (图形表​​示很快将在下面显示)

  5. 以下是用于构建结构的C#代码,在O(n)中完成:

        private Node convert(TreeNode root)
        {
            Node comparisonRoot = new Node();   //root of our new comparison data structure.
                                                //this main root will contain no data except
                                                //for childs inside its child map, which will
                                                //contain all childs with unique values.
    
            TreeNode dataNode = root;           //Root of base data.
    
            Node workingNode = new Node();      //workingNode is our data structure's
                                                //copy of the base data tree's root.
            workingNode.value = root.value;
            workingNode.connections.Add(0, -1);
            // add workingNode to our data structure, because workingNode.value
            // is currently unique to the empty map of the root's child.
            comparisonRoot.childs.Add(workingNode.value, workingNode); 
    
            Stack<TreeNode> s = new Stack<TreeNode>();
            s.Push(dataNode);                   //Initialize stack with root.
    
            while (s.Count > 0) {               //Iteratively traverse the tree using a stack
                TreeNode temp = s.Pop();
                foreach(TreeNode tn in temp.childs) {
                    //fill stack with childs
                    s.Push(tn);
                }
                //update workingNode to be the "parent" of the upcoming childs.
                workingNode = comparisonRoot.childs[temp.value];
                foreach(TreeNode child in temp.childs) {
                   if(!comparisonRoot.childs.ContainsKey(child.value)) {
                       //if value of the node is unique
                       //create a new node for the unique value
                       Node tempChild = new Node();
                       tempChild.value = child.value;
    
                       //store the reference/parent pair
                       tempChild.connections.Add(child.index, temp.index);
    
                       //because we are working with a unique value that first appeared,
                       //add the node to the parent AND the root.
                       workingNode.childs.Add(tempChild.value, tempChild);
                       comparisonRoot.childs.Add(tempChild.value, tempChild);
                   } else {
                       //if value of node is not unique (it already exists within our structure)
                       //update values, no need to create a new node.
                       Node tempChild = comparisonRoot.childs[child.value];
                       tempChild.connections.Add(child.index, temp.index);
                       if (!workingNode.childs.ContainsKey(tempChild.value)) {
                           workingNode.childs.Add(tempChild.value, tempChild);
                       }
                   }
                }
            }
            return comparisonRoot;
        }
    

    所有唯一值都附加到非值根,只是为了将此根节点用作地图以快速跳转到任何引用。 (如下所示)

    enter image description here

    在这里,您可以看到所有连接都是基于原始示例树进行的,除了每个唯一值只有一个节点实例。最后,您可以看到所有节点也连接到根。

    重点是每个唯一副本只有一个真实的Node对象,并通过引用其他节点作为子节点指向所有可能的连接。它有点像带根的图结构。

    每个节点将包含所有{[index], [parent index]}对。

    以下是此数据结构的字符串表示形式:

    Childs { A, B, D, C, E, F, G } 
    Connections { A=[0, -1]; B=[1, 0]; D=[2, 0]; C=[3, 1][7, 4]; 
                  E=[4, 3]; F=[5, 3][8, 7]; G=[6, 3][9, 7] }
    

    在这里,您可能会注意到的第一件事是,在您的示例中没有真正父节点的节点A的父索引具有-1。它只是简单地说明节点A没有更多父节点并且是根节点。

    你可能注意到的其他事情是C的索引值分别为3和7,它们分别连接到1和4,你可以看到它们是节点B和节点E(检查你的例子,如果这不是义)

    所以希望这是对结构的一个很好的解释。

    那么为什么我决定使用这种结构呢?当与某种模式匹配时,这将如何帮助找出节点的索引?

    与后缀尝试类似,我认为最优雅的解决方案将返回所有&#34;成功搜索&#34;在单个操作中,而不是遍历所有节点以查看每个节点是否成功搜索(蛮力)。

    以下是搜索的工作方式。

    假设我们有模式

    c ---- f
      |--- g
    

    来自示例。

    在递归方法中,离开只返回所有可能的parentIndex(从我们的[index, parentIndex]对中检索)。

    之后,在自然DFS类型的遍历中,C将同时接收F和G的返回值。

    在这里,我们对所有子节点进行交集操作(AND操作),并查看集合共享的parentIndex。

    接下来,我们执行另一个AND操作,这次是在上一步的结果和所有可能的C(我们当前的分支)index之间。

    通过这样做,我们现在拥有一组包含G和F的所有可能的C索引。

    虽然该模式只有2级深度,但如果我们查看具有更深层次的模式,我们只需获取C索引的结果集,使用我们的[index, parentIndex]映射查找结果索引的所有父对,并返回该组parentIndexes并返回此方法的第2步。 (参见递归?)

    这是刚刚解释过的C#实现。

        private HashSet<int> search(TreeNode pattern, Node graph, bool isRoot)
        {
            if (pattern.childs.Count == 0)
            {
                //We are at a leaf, return the set of parents values.
                HashSet<int> set = new HashSet<int>();
                if (!isRoot)
                {
                    //If we are not at the root of the pattern, we return the possible
                    //index of parents that can hold this leaf.
                    foreach (int i in graph.connections.Keys)
                    {
                        set.Add(graph.connections[i]);
                    }
                }
                else
                {
                    //However if we are at the root of the pattern, we don't want to
                    //return the index of parents. We simply return all indexes of this leaf.
                    foreach (int i in graph.connections.Keys)
                    {
                        set.Add(i);
                    }
                }
                return set;
            }
            else
            {
                //We are at a branch. We recursively call this method to the
                //leaves.
                HashSet<int> temp = null;
                foreach(TreeNode tn in pattern.childs) {
                    String value = tn.value;
                    //check if our structure has a possible connection with the next node down the pattern.
                    //return empty set if connection not found (pattern does not exist)
                    if (!graph.childs.ContainsKey(value)){
                        temp = new HashSet<int>();
                        return temp;
                    }
                    Node n = graph.childs[value];
                    //Simply recursively call this method to the leaves, and
                    //we do an intersection operation to the results of the
                    //recursive calls.
                    if (temp == null)
                    {
                        temp = search(tn, n, false);
                    }
                    else
                    {
                        temp.IntersectWith(search(tn, n, false));
                    }
                }
                //Now that we have the result of the intersection of all the leaves,
                //we do a final intersection with the result and the current branch's
                //index set.
                temp.IntersectWith(graph.connections.Keys);
                //Now we have all possible indexes. we have to return the possible
                //parent indexes.
                if (isRoot)
                {
                    //However if we are at the root of the pattern, we don't want to
                    //return the parent index. We return the result of the intersection.
                    return temp;
                }
                else
                {
                    //But if we are not at the root of the pattern, we return the possible
                    //index of parents.
                    HashSet<int> returnTemp = new HashSet<int>();
                    foreach (int i in temp)
                    {
                        returnTemp.Add(graph.connections[i]);
                    }
                    return returnTemp;
                }
            }
        }
    

    要调用此方法,只需

    //pattern - root of the pattern, TreeNode object
    //root    - root of our generated structure, which was made with the compare() method
    //boolean - a helper boolean just so the final calculation will return its
    //          own index as a result instead of its parent's indices
    HashSet<int> answers = search(pattern, root.childs[pattern.value], true);
    
    P,这是一个很长的答案,我甚至不确定这是否和其他算法一样高效!我也确信在更大的树中搜索子树可能会有更高效和更优雅的方法,但这是一种进入我脑海的方法!随意留下任何批评,建议,编辑或优化我的解决方案:)