为什么MinDepth级解决方案与递归解决方案相比如此慢?

时间:2018-12-17 23:38:03

标签: java binary-tree depth-first-search breadth-first-search

问题是找到二叉树的最小深度,以便在以下树上运行:

    *           :Level 1
   / \
  *   *         :Level 2
     / \
    *  NULL     :Level 3

将最小深度返回2。

根据leetcode,我能够获得一个优于其他解决方案的100%的递归解决方案,这对我来说没有意义,因为如果必须访问每个节点的每个子节点,怎么可能这么快(DFS的实现) )。

相反,我决定以BFS方式而不是DFS方式进行操作,并检查每个级别中是否有没有子级的节点,并且该节点是最小深度的。

这是我的递归解决方案:

public int minDepth(TreeNode root) {
    if (root == null)
        return 0;
    else if (root.left == null) 
        return minDepth(root.right) + 1;
    else if (root.right == null) 
        return minDepth(root.left) + 1;
    else 
        return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
}

这是我的关卡解决方案:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int minDepth(TreeNode root) {
        ArrayList<TreeNode> q = new ArrayList<TreeNode>(); // We will store each new node here
        q.add(root); // add the root to our queue
        return root != null ? minDepthHelper(q, 0) : 0; // If the root node is null, dont bother and return 0

    }

    private int minDepthHelper(ArrayList<TreeNode> q, int depth){
        if(q.size() == 0) // Empty queue means nothing to do, so return
            return depth;

        int size = q.size(); // How many we will need to pop (the parents)
        for(int i = 0; i < size; i++){
            TreeNode curr = q.get(0); // FIFO!
            if(curr.left == null && curr.right == null){
                return depth +1; // If you have no children, then you are a leaf so return your depth. 
                                            // nodes 0 through size are on the same level, so any of them , if they have 
                                            // no children, will return the same value which will be min depth.
            }else{
                // Add only non-null children!
                if(curr.left != null)
                    q.add(curr.left);
                if(curr.right != null)
                    q.add(curr.right);
            }
            q.remove(0);
        }
        // Will only reach here if no nodes in level depth have no right and no left
        return minDepthHelper(q, depth+1);
    }
}

有人可以解释一下为什么第二个解决方案比较慢的原因,尽管它应该进行较少的比较?

1 个答案:

答案 0 :(得分:1)

我不会对LeetCode百分比施加太大的负担。当我提交您的递归解决方案时,它显示胜过34%。 LeetCode还显示100%和34%段的完全相同的示例代码。只能猜测他们的测试用例到底是什么。我提交的所有实现都在1毫秒内运行,因此,他们的所有41个测试用例都是很小的树,因此性能差异可以忽略不计。您也不知道哪种树结构在示例情况中占主导地位-它们可能或多或少都是最差情况下的时间复杂度,在这种情况下,BFS与DFS相比几乎没有优势。

考虑到这一点,让我们在大型测试用例上对代码进行基准测试,看看是否可以获得对LeetCode提供的黑盒测试环境无法获得的了解。

在开始之前,让我们检查一下您的BFS解决方案,该解决方案使用递归并像对待队列一样操作ArrayList。确实,将ArrayList移位操作摊销为O(1),但是使用ArrayDeque是Java排队操作中更快,更语义上合适的数据结构。

此外,通过在BFS实现中使用递归,您将否定BFS的主要好处之一,那就是它是迭代的。不必操纵调用堆栈将减少很多开销。

综合考虑,我将编写BFS函数,如:

public int minDepth(TreeNode root) {
    ArrayDeque<Pair<TreeNode, Integer>> q = new ArrayDeque<>();
    q.offer(new Pair(root, 1));

    while (!q.isEmpty()) {
        Pair<TreeNode, Integer> curr = q.poll();

        if (curr.first != null) {
            if (curr.first.left == null && curr.first.right == null) {
                return curr.second;
            }

            q.offer(new Pair(curr.first.left, curr.second + 1));
            q.offer(new Pair(curr.first.right, curr.second + 1));
        }
    }

    return 0;
}

现在,快速进行基准测试。在这里,BFS2是您的BFS实现,BFS是我的:

long BFSTotal = 0;
long BFS2Total = 0;
long DFSTotal = 0;

for (int i = 0; i < 10000; i++) {
    TreeNode root = randomTree(10000);

    long start = System.currentTimeMillis();
    minDepthDFS(root);
    DFSTotal += System.currentTimeMillis() - start;

    start = System.currentTimeMillis();
    minDepthBFS(root);
    BFSTotal += System.currentTimeMillis() - start;

    start = System.currentTimeMillis();
    minDepthBFS2(root);
    BFS2Total += System.currentTimeMillis() - start;
}

System.out.println("BFS: " + BFSTotal);
System.out.println("BFS2: " + BFS2Total);
System.out.println("DFS: " + DFSTotal);

此代码创建10000个不同的树,每个树有10000个节点,这些节点使用抛硬币算法创建,并在每棵树上运行算法。这是几次运行的结果:

BFS: 1906
BFS2: 5484
DFS: 3351

BFS: 1709
BFS2: 6101
DFS: 3773

BFS: 1527
BFS2: 5567
DFS: 3856

继续,run the code yourself,然后尝试一下。我不认为这些结果是绝对结论性的,但它们确实加强了我的基本前提:BFS击败DFS,因为其开销较小且有可能提早保释(最坏的情况是时间复杂度相同),以及使用递归的非递归BFS实现高效的数据结构优于使用低效的数据结构的递归BFS实现。

这也表明您的BFS实现速度大约是DFS实现速度的两倍,这可以解释您的LeetCode结果,但是同样地,考虑到它们的小巧之处,我还是犹豫不决。测试用例似乎是这样。