【发布时间】:2013-01-03 19:23:57
【问题描述】:
我可以看到,在 BST 中查找值时,每次将节点与要查找的值进行比较时,我们会留下一半的树。
但是我不明白为什么时间复杂度是O(log(n))。所以,我的问题是:
如果我们有一棵包含 N 个元素的树,为什么查找树并检查是否存在特定值的时间复杂度是 O(log(n)),我们如何得到?
【问题讨论】:
标签: data-structures time-complexity big-o binary-search-tree
我可以看到,在 BST 中查找值时,每次将节点与要查找的值进行比较时,我们会留下一半的树。
但是我不明白为什么时间复杂度是O(log(n))。所以,我的问题是:
如果我们有一棵包含 N 个元素的树,为什么查找树并检查是否存在特定值的时间复杂度是 O(log(n)),我们如何得到?
【问题讨论】:
标签: data-structures time-complexity big-o binary-search-tree
如果我们有一棵包含 N 个元素的树,为什么查找的时间复杂度 树并检查是否存在特定值是 O(log(n)),怎么办 我们明白了吗?
这不是真的。默认情况下,二叉搜索树中的查找不是O(log(n)),其中n 是节点数。在最坏的情况下,它可能变成O(n)。例如,如果我们插入以下序列n, n - 1, ..., 1的值(以相同的顺序),那么树将表示如下:
n
/
n - 1
/
n - 2
/
...
1
查找值为1 的节点的时间复杂度为O(n)。
为了提高查找效率,树必须平衡,使其最大高度与log(n) 成正比。在这种情况下,查找的时间复杂度为O(log(n)),因为查找任何叶子都受log(n) 操作的限制。
但同样,并非每个二叉搜索树都是平衡二叉搜索树。你必须平衡它以保证O(log(n))的时间复杂度。
【讨论】:
这可以很容易地在数学上显示出来。
在我介绍之前,让我澄清一些事情。在平衡二叉搜索树中查找或查找的复杂度为 O(log(n))。一般来说,对于二叉搜索树,它是 O(n)。我将在下面显示两者。
在平衡二叉搜索树中,在最坏的情况下,我正在寻找的值在树的叶子中。由于 BST 的有序结构,我将基本上从根遍历到叶子,只查看树的每一层一次。因此,我需要做的搜索次数是树的层数。因此,问题归结为为具有 n 个节点的树的层数找到一个封闭形式的表达式。
我们将在这里进行简单的归纳。只有 1 层的树只有 1 个节点。 2 层的树有 1+2 个节点。 3 层 1+2+4 节点等。模式很清楚:一棵有 k 层的树恰好有
n=2^0+2^1+...+2^{k-1}
节点。这是一个几何级数,这意味着
n=2^k-1,
相当于:
k = log(n+1)
我们知道 big-oh 对 n 的大值感兴趣,因此常数是无关紧要的。因此复杂度为 O(log(n))。
我将给出另一种更短的方法来显示相同的结果。因为在寻找一个值时,我们不断地将树分成两半,并且我们必须这样做 k 次,其中 k 是层数,以下是正确的:
(n+1)/2^k = 1,
这意味着完全相同的结果。您必须说服自己 n+1 中的 +1 来自哪里,但即使您不注意也没关系,因为我们正在谈论 n 的较大值。
现在让我们讨论一般的二叉搜索树。在最坏的情况下,它是完全不平衡的,这意味着它的所有节点都只有一个孩子(并且它变成了一个链表)参见例如https://www.cs.auckland.ac.nz/~jmor159/PLDS210/niemann/s_fig33.gif
在这种情况下,要在叶子中找到值,我需要遍历所有节点,因此需要 O(n)。
最后一点是,这些复杂性不仅适用于查找,还适用于插入和删除操作。
(当我达到 10 个代表点时,我将使用更好看的 Latex 数学样式编辑我的方程式。所以现在不会让我这样做。)
【讨论】:
每当您看到一个包含 O(log n) 因素的运行时,there's a very good chance that you're looking at something of the form "keep dividing the size of some object by a constant." 所以考虑这个问题的最佳方法可能是 - 当您在二叉搜索树中进行查找时,究竟是什么它被一个常数因子削减了,这个常数究竟是什么?
首先,让我们假设您有一个完美平衡的二叉树,如下所示:
*
/ \
* *
/ \ / \
* * * *
/ \ / \ / \ / \
* * * * * * * *
在进行搜索的每一点,您都会查看当前节点。如果它是您正在寻找的那个,那就太好了!你完全完成了。另一方面,如果不是,那么你要么下降到左子树或右子树,然后重复这个过程。
如果您走进两个子树中的一个,您实际上是在说“我根本不关心另一个子树中的内容。”你把里面的所有节点都扔掉了。那里有多少个节点?好吧,通过快速的目视检查 - 最好是进行一些漂亮的数学检查 - 你会发现你正在丢弃树中大约一半的节点。
这意味着在查找的每一步中,您要么 (1) 找到要查找的节点,要么 (2) 丢弃树中的一半节点。由于您在每一步都在做恒定的工作量,因此您正在查看 O(log n) 行为的标志性行为 - 每一步的工作量都会下降一个常数因子,因此它只能以对数方式进行多次次。
当然,并不是所有的树都是这样的。 AVL 树有一个有趣的特性,即每次您下降到子树时,您都会丢弃大约占总节点的黄金比例部分。因此,这可以保证您在用完节点之前只能采取对数许多步骤 - 因此 O(log n) 高度。在红/黑树中,每一步都会丢弃(大约)四分之一的总节点,并且由于您缩小了一个常数因子,您再次获得了您想要的 O(log n) 查找时间。非常有趣的替罪羊树有一个可调整的参数,用于确定它的平衡程度,但你可以再次证明,你采取的每一步都会根据这个可调整的参数丢弃一些常数因子,给出 O(log n) 查找。
但是,对于不平衡的树,此分析会失效。如果你有一棵纯粹的退化树——每个节点都只有一个子节点——那么你沿着树向下走的每一步只会扔掉一个节点,而不是一个恒定的分数。这意味着在最坏的情况下查找时间会达到 O(n),因为从 n 中减去一个常数的次数是 O(n)。
【讨论】:
对我来说,最简单的方法是查看 log2(n) 的图,其中 n 是二叉树中的节点数。作为一个表格,它看起来像:
log2(n) = d
log2(1) = 0
log2(2) = 1
log2(4) = 2
log2(8) = 3
log2(16)= 4
log2(32)= 5
log2(64)= 6
然后我画了一棵小二叉树,这棵从深度 d=0 到 d=3:
d=0 O
/ \
d=1 R B
/\ /\
d=2 R B R B
/\ /\ /\ /\
d=3 R B RB RB R B
因此,树中的节点数 n 有效地加倍(例如,当深度 d 从d=2 到 d=3,增加 1。)因此所需的额外处理量(或所需时间)仅增加 1 额外计算(或迭代),因为处理量是相关的到 d。
我们可以看到,在将节点数加倍后,我们只需要向下增加 1 级深度 d,从 d=2 到 d=3,以在所有节点 n 中找到我们想要的节点。这是真的,因为我们现在已经搜索了整棵树,嗯,我们需要搜索的一半来找到我们想要的节点。
我们可以将其写为d = log2(n),其中 d 告诉我们当树中有 n 个节点时,我们需要(平均)执行多少计算(多少次迭代)才能到达树中的任何节点。
【讨论】:
您的问题似乎得到了很好的回答here,但总结一下您的具体问题,最好反过来考虑; “随着节点数量的增加,BST 求解时间会发生什么变化”?
基本上,在 BST 中,每次将节点数量增加一倍时,您只会将求解的步骤数增加一倍。为了扩展这一点,四倍的节点给出了两个额外的步骤。八倍的节点给出了三个额外的步骤。十六倍的节点给出了四个额外的步骤。以此类推。
这些对中第一个数字的以 2 为底的对数是这些对中的第二个数字。它是基于 2 的日志,因为这是一个二分搜索(每一步都将问题空间减半)。
【讨论】: