如何在不使用广度优先遍历的情况下找到二叉树级别k的节点数?

时间:2011-09-17 16:54:08

标签: php mysql algorithm binary-tree tree-traversal

鉴于这个二叉树(实际上,二叉树可以是随机的和动态的,这只是一个例子......):

请参阅二叉树图像的链接:binary tree example

这是给定的事实:

  1. 所有节点都连接到他们的父亲,这样我们就可以从下到上遍历(当然也可以从上到下)。
  2. 所有节点都保存有关左右部分有多少后代的信息。
  3. 问题在于:我需要找到一种方法来计算第2级中的节点总数(实际上,在任何级别,但现在,让我们专注于第二级)。显然,如果我们事先知道二叉树的结构,答案是3,但假设我们没有这个图像,只有给定的事实。

    这里的另一个问题是我们将从级别2(我们的目标级别)而不是根目录的节点开始。在这个例子中,我选择了NODE F。

    我知道使用广度优先顺序遍历是直接的解决方案,但我发现它太耗时,因为每次读取节点时,我都会从数据库中查询它。

    我正在寻找更实用的方法。但是,如果由于给定数据不足而无法解决此问题,请告知我应该提供哪些其他数据以使其可以解决。如果可行,我会评估它。

    我正在创建一个网站并使用PHP和MySQL。但我只想要解决方案的概念或解释,更像是算法而不是编程片段或代码......

    我希望有人能回答我......非常感谢你!

7 个答案:

答案 0 :(得分:2)

“广度优先搜索”就是这样做的方法。但是,如果您不想使用它,我建议在节点中包含指向兄弟的指针。如果你必须通常执行这种查询,这将是一个很大的节省。

修改

如果您可以对节点进行非规范化并在表格中存储所有节点的sibilings和级别,那么您可以毫无问题地进行查询。

SELECT * FROM nodes where level=2

答案 1 :(得分:1)

一个选项是加载php中的完整表,并创建一个数组树。如果你有很多行(100k +),你可以在表加载完成之前启动该过程,但是你需要更多的代码来控制它。

另外,您可以使用触发器将结果存储在每个节点上。

编辑:稍微思考一下,并阅读不同的答案和评论。我认为最好的选择是拆分解决方案:

  1. 创建一个表nodesPerLevel,包含2个列:level,nbNodes。
  2. 在每个节点的插入/删除时创建一个触发器,它将/减1添加到此新表中的相应级别。
  3. 需要结果时,请select sum(nbNodes) from nodesPerLevel where level >= ?

答案 2 :(得分:1)

对于DBMS中的树,您可以使用WITH RECURSIVE cte-idiom并在适当的递归级别剪辑(==给定节点的递归级别,这可能需要另一个递归子选择)

编辑:(添加代码)

-- the test table
DROP table tree CASCADE;
CREATE table tree
    ( id CHAR(1) NOT NULL PRIMARY KEY
    , pa CHAR(1) REFERENCES tree(id)
    , le CHAR(1)     REFERENCES tree(id)
    , ri CHAR(1) REFERENCES tree(id)
    );

-- generate some data
INSERT INTO tree (id, pa, le, ri) VALUES
      ( 'a', NULL, 'b', 'c' )
    , ( 'b', 'a', 'd', 'e' )
    , ( 'c', 'a', 'f', NULL )
    , ( 'd', 'b', 'g', NULL )
    , ( 'e', 'b', NULL, 'h' )
    , ( 'f', 'c', NULL, 'i' )
    , ( 'g', 'd', NULL, NULL )
    , ( 'h', 'e', NULL, NULL )
    , ( 'i', 'f', NULL, NULL )
    ;
-- a room with a view
CREATE VIEW reteview AS (
    WITH RECURSIVE re AS (
        SELECT 0 AS lev,id, pa, le, ri FROM tree
        WHERE pa IS NULL
        UNION
        SELECT 1+re.lev AS lev
        , tr.id,  tr.pa,  tr.le,  tr.ri
            FROM tree tr, re
            WHERE re.id = tr.pa
        )
    SELECT * FROM re
    );

/* EXPLAIN ANALYZE */ -- SELECT * FROM reteview ;

/* EXPLAIN ANALYZE */ SELECT re0.*
    FROM reteview re0
    , reteview re1
    WHERE re1.id = 'f'
    AND re0.lev <= re1.lev
    ;

结果:

 lev | id | pa | le | ri 
-----+----+----+----+----
   0 | a  |    | b  | c
   1 | b  | a  | d  | e
   1 | c  | a  | f  | 
   2 | d  | b  | g  | 
   2 | e  | b  |    | h
   2 | f  | c  |    | i
(6 rows)

查询计划(Postgres 9.01)

                                                                   QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=949.93..2773.55 rows=35167 width=36) (actual time=0.159..0.337 rows=6 loops=1)
   Join Filter: (re.lev <= re1.lev)
   ->  CTE Scan on re  (cost=474.97..566.71 rows=4587 width=36) (actual time=0.034..0.151 rows=9 loops=1)
         CTE re
           ->  Recursive Union  (cost=0.00..474.97 rows=4587 width=36) (actual time=0.021..0.129 rows=9 loops=1)
                 ->  Seq Scan on tree  (cost=0.00..23.10 rows=7 width=32) (actual time=0.012..0.014 rows=1 loops=1)
                       Filter: (pa IS NULL)
                 ->  Hash Join  (cost=2.28..36.01 rows=458 width=36) (actual time=0.018..0.022 rows=2 loops=4)
                       Hash Cond: (tr.pa = re.id)
                       ->  Seq Scan on tree tr  (cost=0.00..23.10 rows=1310 width=32) (actual time=0.001..0.003 rows=9 loops=4)
                       ->  Hash  (cost=1.40..1.40 rows=70 width=12) (actual time=0.003..0.003 rows=2 loops=4)
                             Buckets: 1024  Batches: 1  Memory Usage: 1kB
                             ->  WorkTable Scan on re  (cost=0.00..1.40 rows=70 width=12) (actual time=0.001..0.002 rows=2 loops=4)
   ->  Materialize  (cost=474.97..578.52 rows=23 width=4) (actual time=0.013..0.018 rows=1 loops=9)
         ->  Subquery Scan on re1  (cost=474.97..578.40 rows=23 width=4) (actual time=0.111..0.157 rows=1 loops=1)
               ->  CTE Scan on re  (cost=474.97..578.17 rows=23 width=36) (actual time=0.110..0.156 rows=1 loops=1)
                     Filter: (id = 'f'::bpchar)
                     CTE re
                       ->  Recursive Union  (cost=0.00..474.97 rows=4587 width=36) (actual time=0.008..0.135 rows=9 loops=1)
                             ->  Seq Scan on tree  (cost=0.00..23.10 rows=7 width=32) (actual time=0.002..0.008 rows=1 loops=1)
                                   Filter: (pa IS NULL)
                             ->  Hash Join  (cost=2.28..36.01 rows=458 width=36) (actual time=0.021..0.024 rows=2 loops=4)
                                   Hash Cond: (tr.pa = re.id)
                                   ->  Seq Scan on tree tr  (cost=0.00..23.10 rows=1310 width=32) (actual time=0.001..0.004 rows=9 loops=4)
                                   ->  Hash  (cost=1.40..1.40 rows=70 width=12) (actual time=0.004..0.004 rows=2 loops=4)
                                         Buckets: 1024  Batches: 1  Memory Usage: 1kB
                                         ->  WorkTable Scan on re  (cost=0.00..1.40 rows=70 width=12) (actual time=0.001..0.001 rows=2 loops=4)
 Total runtime: 0.764 ms
(28 rows)

答案 3 :(得分:0)

除了从根完全读取树到级别2并计算实例之外,唯一的事情是在同一级别的实例之间设置关系字段,或者,如果您的数据太易于使该方法更好地执行,实现一个短时但快速的(内存?)caching system,它允许您快速识别同一级别的节点。

答案 4 :(得分:0)

由于您的节点存储在数据库中,最佳解决方案可能是这样的SQL查询(语法可能有误):

select count(third.id) from node as first, node as second, node as third where first.id =`your top node id`  and second.parent = first.id and third.parent =  second.id

这样的查询将为您提供节点,而不会为每个节点提供额外的DB访问。但是数据库的成本可能很高(取决于数据库和节点数量)。您还可以在节点本身中存储节点级别 - 这样查询将更简单并且需要更少的资源。

答案 5 :(得分:0)

最简单的方法是

1.找到树的所有路径

       50
     /    \
   30      60
 /   \    /  \
10   20  55   70

路径是:

50-30-10
50-30-20
50-60-55
50-60-70

2.存储在单独的数组中。

3.访问每个数组中的第k个元素。

找到所有路径的一些sudo代码:

Inorder(root)
{
 //do the required null checks
         if(root==null)
            return

  1.     PushOnStack(root->info)  

  2.     Inorder(root->left)

  3.     peekAllStackElement (read all the elements in stack and store in array an         reverse)

  4.     PopFromStack(root->info)         

  5.     Inorder(root->right)

 }

答案 6 :(得分:0)

将数据导入二叉搜索树后,可以像下面一样递归。使用递归来跟踪树中每个节点的深度。

在每次递归调用中,将当前级别的节点推送到哈希表,使用级别作为键,将节点作为值。

在JavaScript中,您可以使用数组或对象文字来执行此操作。我将所有内容存储在JavaScript对象文字中,类似于引擎盖下的哈希表。像这样:

level = 0
object[level] = [node1] => { '0': [node, node2] }
object[level] = [node2] => { '0': [node1, node2] }

level = 1
object[level] = [node3] => { '0': [node, node2], '1': [node3] }

等...

在推送之前,检查密钥是否存在。如果它不存在,只需将节点插入包含在数组中的哈希。

如果存在一个键(意味着存在一个级别冲突),只需按下该键上的数组即可调用冲突解决方案。

现在,每个级别的所有节点都存储在对象内的唯一数组中。它应该是这样的:

{ '0': [ 20 ],
  '1': [ 8, 22 ],
  '2': [ 4, 12, 24 ],
  '3': [ 10, 14 ] } */

如果您要存储整个节点,请执行以下操作:

{ '0': [ { value: 20, right: [Object], left: [Object] } ],
  '1':
   [ { value: 8, right: [Object], left: [Object] },
     { value: 22, right: [Object], left: null } ],
  '2':
   [ { value: 4, right: null, left: null },
     { value: 12, right: [Object], left: [Object] },
     { value: 24, right: null, left: null } ],
  '3':
   [ { value: 10, right: null, left: null },
     { value: 14, right: null, left: null } ] }

此后你可以用它们做你想做的事。对每个级别的值求和,转换为链表,或者在您的情况下,只需检查所需级别的数组长度。这将为您提供节点数。

BinaryTree.prototype.kNodesAtDepth = function(level) {

    var levels = {};

    var traverse = function(current, depth) {
        if (!current) return null;
        if (!levels[depth]) levels[depth] = [current.value];
        else levels[depth].push(current.value);
        traverse(current.left, depth + 1);
        traverse(current.right, depth + 1);
    };

    traverse(this.root, 0);
    return levels[level].length;
};

//tests
var bst = new BinaryTree();
bst.add(20, 22, 8, 4, 12, 10, 14, 24);
var nodeCount = bst.kNodesAtDepth(2); //3
console.log(nodeCount); //3