Skip to content

Commit 3d91750

Browse files
committed
add new page
1 parent 18e7ded commit 3d91750

File tree

74 files changed

+665
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+665
-0
lines changed
Loading
Loading
Loading
Loading
Loading
Loading
Loading

Diff for: 6.树和树的算法/6.14.查找树分析/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## 6.14.查找树分析
2+
3+
4+
随着二叉搜索树的实现完成,我们将对我们已经实现的方法进行快速分析。让我们先来看看 `put` 方法。其性能的限制因素是二叉树的高度。从词汇部分回忆一下树的高度是根和最深叶节点之间的边的数量。高度是限制因素,因为当我们寻找合适的位置将一个节点插入到树中时,我们需要在树的每个级别最多进行一次比较。
5+
6+
二叉树的高度可能是多少?这个问题的答案取决于如何将键添加到树。如果按照随机顺序添加键,树的高度将在 log2^⁡n 附近,其中 n 是树中的节点数。这是因为如果键是随机分布的,其中大约一半将小于根,一半大于根。请记住,在二叉树中,根节点有一个节点,下一级节点有两个节点,下一个节点有四个节点。任何特定级别的节点数为 2^d ,其中 d 是级别的深度。完全平衡的二叉树中的节点总数为 2^h+1 - 1,其中 h 表示树的高度。
7+
8+
完全平衡的树在左子树中具有与右子树相同数量的节点。在平衡二叉树中,`put` 的最坏情况性能是 O(log2^⁡n ),其中 n 是树中的节点数。注意,这是与前一段中的计算的反比关系。所以 log2^⁡n 给出了树的高度,并且表示了在适当的位置插入新节点时,需要做的最大比较次数。
9+
10+
不幸的是,可以通过以排序顺序插入键来构造具有高度 n 的搜索树!这样的树的示例见 Figure 6。在这种情况下,put方法的性能是 O(n)。
11+
12+
![6.14.查找树分析.figure6](assets/6.14.%E6%9F%A5%E6%89%BE%E6%A0%91%E5%88%86%E6%9E%90.figure6.png)
13+
*Figure 6*
14+
15+
现在你明白了 `put` 方法的性能受到树的高度的限制,你可能猜测其他方法 `get``in``del` 也是有限制的。 由于 `get` 搜索树以找到键,在最坏的情况下,树被一直搜索到底部,并且没有找到键。 乍一看,`del` 似乎更复杂,因为它可能需要在删除操作完成之前搜索后继。 但请记住,找到后继者的最坏情况也只是树的高度,这意味着你只需要加倍工作。 因为加倍是一个常数因子,它不会改变最坏的情况
16+
Loading
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## 6.15.平衡二叉搜索树
2+
3+
在上一节中,我们考虑构建一个二叉搜索树。正如我们所学到的,二叉搜索树的性能可以降级到 O(n) 的操作,如 `get``put` ,如果树变得不平衡。在本节中,我们将讨论一种特殊类型的二叉搜索树,它自动确保树始终保持平衡。这棵树被称为 AVL树,以其发明人命名:G.M. Adelson-Velskii 和E.M.Landis。
4+
5+
AVL树实现 Map 抽象数据类型就像一个常规的二叉搜索树,唯一的区别是树的执行方式。为了实现我们的 AVL树,我们需要跟踪树中每个节点的平衡因子。我们通过查看每个节点的左右子树的高度来做到这一点。更正式地,我们将节点的平衡因子定义为左子树的高度和右子树的高度之间的差。
6+
7+
>balanceFactor = height(leftSubTree) - height(rightSubTree)
8+
9+
10+
使用上面给出的平衡因子的定义,我们说如果平衡因子大于零,则子树是左重的。如果平衡因子小于零,则子树是右重的。如果平衡因子是零,那么树是完美的平衡。为了实现AVL树,并且获得具有平衡树的好处,如果平衡因子是 -1,0 或 1,我们将定义树平衡。一旦树中的节点的平衡因子是在这个范围之外,我们将需要一个程序来使树恢复平衡。Figure 1展示了不平衡,右重树和每个节点的平衡因子的示例。
11+
12+
![6.15.平衡二叉搜索树.figure1](assets/6.15.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.figure1.png)
13+
*Figure 1*
14+
15+
16+
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## 6.16.AVL平衡二叉搜索树
2+
3+
在我们继续之前,我们来看看执行这个新的平衡因子要求的结果。我们的主张是,通过确保树总是具有 -1,0或1 的平衡因子,我们可以获得更好的操作性能的关键操作。 让我们开始思考这种平衡条件如何改变最坏情况的树。有两种可能性,一个左重树和一个右重树。 如果我们考虑高度0,1,2和3的树,Figure 2 展示了在新规则下可能的最不平衡的左重树。
4+
5+
![6.16.AVL平衡二叉搜索树.figure1](assets/6.16.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.figure1.png)
6+
*Figure 2*
7+
8+
看树中节点的总数,我们看到对于高度为0的树,有1个节点,对于高度为1的树,有1 + 1 = 2个节点,对于高度为2的树 是1 + 1 + 2 = 4,对于高度为3的树,有1 + 2 + 4 = 7。 更一般地,我们看到的高度h(Nh) 的树中的节点数量的模式是:
9+
![6.16.AVL平衡二叉搜索树.1](assets/6.16.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.1.png)
10+
11+
这种可能看起来很熟悉,因为它非常类似于斐波纳契序列。 给定树中节点的数量,我们可以使用这个事实来导出AVL树的高度的公式。 回想一下,对于斐波纳契数列,第i个斐波纳契数字由下式给出:
12+
![6.16.AVL平衡二叉搜索树.2](assets/6.16.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.2.png)
13+
14+
一个重要的数学结果是,随着斐波纳契数列越来越大,Fi/Fi-1 的比率越来越接近黄金比率 `Φ= (1 +√5)/2`。 如果要查看上一个方程的导数,可以查阅数学文本。 我们将简单地使用该方程来近似 Fi,如 Fi =Φ^i / 5。 如果我们利用这个近似,我们可以重写 Nh 的方程为:
15+
![6.16.AVL平衡二叉搜索树.3](assets/6.16.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.3.png)
16+
17+
通过用其黄金比例近似替换斐波那契参考,我们得到:
18+
![6.16.AVL平衡二叉搜索树.4](assets/6.16.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.4.png)
19+
20+
如果我们重新排列这些项,并取两边的底数为2的对数,然后求解 h,我们得到以下推导:
21+
![6.16.AVL平衡二叉搜索树.5](assets/6.16.AVL%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.5.png)
22+
23+
这个推导告诉我们,在任何时候,我们的AVL树的高度等于树中节点数目的对数的常数(1.44)倍。 这是搜索我们的AVL树的好消息,因为它将搜索限制为O(logN)。
24+
25+
26+
27+
28+
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
## 6.17.AVL平衡二叉搜索树实现
2+
3+
现在我们已经证明保持 AVL树的平衡将是一个很大的性能改进,让我们看看如何增加过程来插入一个新的键到树。由于所有新的键作为叶节点插入到树中,并且我们知道新叶的平衡因子为零,所以刚刚插入的节点没有新的要求。但一旦添加新叶,我们必须更新其父的平衡因子。这个新叶如何影响父的平衡因子取决于叶节点是左孩子还是右孩子。如果新节点是右子节点,则父节点的平衡因子将减少1。如果新节点是左子节点,则父节点的平衡因子将增加1。这个关系可以递归地应用到新节点的祖父节点,并且应用到每个祖先一直到树的根。由于这是一个递归过程,我们来看一下用于更新平衡因子的两种基本情况:
4+
5+
* 递归调用已到达树的根。
6+
* 母公司的平衡因子已调整为零。你应该说服自己,一旦一个子树的平衡因子为零,那么它的祖先节点的平衡不会改变。
7+
8+
我们将实现 AVL 树作为 `BinarySearchTree` 的子类。首先,我们将覆盖`_put` 方法并编写一个新的 `updateBalance` 辅助方法。这些方法如Listing 1所示。你将注意到,`_put` 的定义与简单二叉搜索树中的完全相同,除了第 7 行和第 13 行上对 `updateBalance` 的调用的添加。
9+
10+
````
11+
def _put(self,key,val,currentNode):
12+
if key < currentNode.key:
13+
if currentNode.hasLeftChild():
14+
self._put(key,val,currentNode.leftChild)
15+
else:
16+
currentNode.leftChild = TreeNode(key,val,parent=currentNode)
17+
self.updateBalance(currentNode.leftChild)
18+
else:
19+
if currentNode.hasRightChild():
20+
self._put(key,val,currentNode.rightChild)
21+
else:
22+
currentNode.rightChild = TreeNode(key,val,parent=currentNode)
23+
self.updateBalance(currentNode.rightChild)
24+
25+
def updateBalance(self,node):
26+
if node.balanceFactor > 1 or node.balanceFactor < -1:
27+
self.rebalance(node)
28+
return
29+
if node.parent != None:
30+
if node.isLeftChild():
31+
node.parent.balanceFactor += 1
32+
elif node.isRightChild():
33+
node.parent.balanceFactor -= 1
34+
35+
if node.parent.balanceFactor != 0:
36+
self.updateBalance(node.parent)
37+
````
38+
*Listing 1*
39+
40+
新的 `updateBalance` 方法完成了大多数工作。这实现了我们刚才描述的递归过程。 `updateBalance` 方法首先检查当前节点是否不够平衡,需要重新平衡(第16行)。如果平衡,则重新平衡完成,并且不需要对父节点进行进一步更新。如果当前节点不需要重新平衡,则调整父节点的平衡因子。如果父的平衡因子不为零,那么算法通过递归调用父对象上的 `updateBalance`,继续沿树向根向上运行。
41+
42+
当需要树重新平衡时,我们如何做呢?有效的重新平衡是使AVL树在不牺牲性能的情况下正常工作的关键。为了使AVL树恢复平衡,我们将在树上执行一个或多个旋转。
43+
44+
要理解旋转是什么让我们看一个非常简单的例子。考虑 Figure 3左半部分的树。这棵树平衡因子为 -2,不平衡。为了使这棵树平衡,我们将使用以节点 A 为根的子树的左旋转。
45+
46+
![6.16.平衡二叉搜索树实现.figure3](assets/6.16.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0.figure3.png)
47+
*Figure 3*
48+
49+
要执行左旋转,我们基本上执行以下操作:
50+
51+
* 提升右孩子(B)成为子树的根。
52+
* 将旧根(A)移动为新根的左子节点。
53+
* 如果新根(B)已经有一个左孩子,那么使它成为新左孩子(A)的右孩子。注意:由于新根(B)是A的右孩子,A 的右孩子在这一点上保证为空。这允许我们添加一个新的节点作为右孩子,不需进一步的考虑。
54+
55+
56+
虽然这个过程在概念上相当容易,但是代码的细节有点棘手,因为我们需要按照正确的顺序移动事物,以便保留二叉搜索树的所有属性。此外,我们需要确保适当地更新所有的父指针。
57+
58+
让我们看一个稍微更复杂的树来说明右旋转。Figure 4的左侧显示了树的左重,在根处的平衡因子为 2。要执行右旋转,我们基本上执行以下操作:
59+
60+
* 提升左子节点(C)为子树的根。
61+
* 将旧根(E)移动为新根的右子树。
62+
* 如果新根(C)已经有一个正确的孩子(D),那么使它成为新的右孩子(E)的左孩子。注意:由于新根(C)是 E 的左子节点,因此 E 的左子节点在此时保证为空。这允许我们添加一个新节点作为左孩子,不需进一步的考虑。
63+
64+
![6.16.平衡二叉搜索树实现.figure4](assets/6.16.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0.figure4.png)
65+
*Figure 4*
66+
67+
68+
现在你已经看到了旋转,并且有旋转的工作原理的基本概念,让我们看看代码。Listing 2展示了右旋转和左旋转的代码。在第2行中,我们创建一个临时变量来跟踪子树的新根。正如我们之前所说的,新的根是上一个根的右孩子。现在对这个临时变量存储了一个对右孩子的引用,我们用新的左孩子替换旧根的右孩子。
69+
70+
下一步是调整两个节点的父指针。如果 newRoot 有一个左子节点,那么左子节点的新父节点变成旧的根节点。新根的父节点设置为旧根的父节点。如果旧根是整个树的根,那么我们必须设置树的根以指向这个新根。否则,如果旧根是左孩子,则我们将左孩子的父节点更改为指向新根;否则我们改变右孩子的父亲指向新的根。(行10-13)。最后,我们将旧根的父节点设置为新根。这是一个很复杂的过程,所以我们鼓励你跟踪这个功能,同时看下 Figure 3。 `rotateRight` 方法是对称的 `rotateLeft`,所以我们将留给你来研究 `rotateRight` 的代码。
71+
72+
````
73+
def rotateLeft(self,rotRoot):
74+
newRoot = rotRoot.rightChild
75+
rotRoot.rightChild = newRoot.leftChild
76+
if newRoot.leftChild != None:
77+
newRoot.leftChild.parent = rotRoot
78+
newRoot.parent = rotRoot.parent
79+
if rotRoot.isRoot():
80+
self.root = newRoot
81+
else:
82+
if rotRoot.isLeftChild():
83+
rotRoot.parent.leftChild = newRoot
84+
else:
85+
rotRoot.parent.rightChild = newRoot
86+
newRoot.leftChild = rotRoot
87+
rotRoot.parent = newRoot
88+
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, 0)
89+
newRoot.balanceFactor = newRoot.balanceFactor + 1 + max(rotRoot.balanceFactor, 0)
90+
````
91+
*Listing 2*
92+
93+
最后,第16-17行需要一些解释。 在这两行中,我们更新旧根和新根的平衡因子。 由于所有其他移动都是移动整个子树,所以所有其他节点的平衡因子不受旋转的影响。 但是我们如何在不完全重新计算新子树的高度的情况下更新平衡因子呢? 以下推导应该能说服你这些行是正确的。
94+
95+
![6.16.平衡二叉搜索树实现.figure5](assets/6.16.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0.figure5.png)
96+
*Figure 5*
97+
98+
Figure 5 展示了左旋转。 B 和 D 是关键节点,A,C,E 是它们的子树。 设hx 表示以节点 x 为根的特定子树的高度。 根据定义,我们知道以下:
99+
100+
101+
> newBal(B)=hA−hC
102+
oldBal(B)=hA−hD
103+
104+
但我们知道,D 的旧高度也可以由 `1 + max(hC,hE)`给出,也就是说,D 的高度比其两个孩子的最大高度大 1。 记住,`hC``hE` 没有改变。 所以,让我们用第二个方程来代替它
105+
106+
> oldBal(B)=hA−(1+max(hC,hE))
107+
108+
然后减去这两个方程。 以下步骤进行减法并使用一些代数来简化 `newBal(B)` 的等式。
109+
110+
> newBal(B)−oldBal(B)=hA−hC−(hA−(1+max(hC,hE)))
111+
> newBal(B)−oldBal(B)=hA−hC−hA+(1+max(hC,hE))
112+
> newBal(B)−oldBal(B)=hA−hA+1+max(hC,hE)−hC
113+
> newBal(B)−oldBal(B)=1+max(hC,hE)−hC
114+
115+
接下来我们将 oldBal(B) 移动到方程的右边,并利用 `max(a,b) -c = max(a-c,b-c)`
116+
117+
> newBal(B)=oldBal(B)+1+max(hC−hC,hE−hC)
118+
119+
但是,hE-hC 与 -oldBal(D) 相同。因此,我们可以使用另一个表示 max(-a,-b) = -min(a,b) 的标识。 因此,我们可以完成我们的 newBal(B) 的推导,具有以下步骤:
120+
121+
> newBal(B)=oldBal(B)+1+max(0,−oldBal(D))
122+
> newBal(B)=oldBal(B)+1−min(0,oldBal(D))
123+
124+
现在我们有所有的部分,我们很容易知道。 我们记住 B 是 rotRoot 和 D 是newRoot 然后我们可以看到这正好对应第16行的语句,或者:
125+
126+
```
127+
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(0,newRoot.balanceFactor)
128+
```
129+
130+
类似的推导给出了更新的节点 D 的方程,以及右旋转后的平衡因子。 我们把这些作为你的练习。
131+
132+
现在你可能认为我们已经完成了。 我们知道如何做左右旋转,我们知道什么时候应该做左旋或右旋,但是看看 Figure 6。由于节点 A 的平衡因子为-2,我们应该做左旋转。 但是,当我们围绕A做左旋转时会发生什么?
133+
134+
![6.16.平衡二叉搜索树实现.figure6](assets/6.16.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0.figure6.png)
135+
*Figure 6*
136+
137+
Figure 7 展示了我们在左旋后,我们现在已经在另一方面失去平衡。 如果我们做右旋以纠正这种情况,我们就回到我们开始的地方。
138+
![6.16.平衡二叉搜索树实现.figure7](assets/6.16.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0.figure7.png)
139+
*Figure 7*
140+
141+
要纠正这个问题,我们必须使用以下规则集:
142+
143+
* 如果子树需要左旋转使其平衡,首先检查右子节点的平衡因子。 如果右孩子是重的,那么对右孩子做右旋转,然后是原来的左旋转。
144+
* 如果子树需要右旋转使其平衡,首先检查左子节点的平衡因子。 如果左孩子是重的,那么对左孩子做左旋转,然后是原来的右旋转。
145+
146+
Figure 8展示了这些规则如何解决我们在Figure 6和 Figure 7中遇到的困境。从围绕节点 C的 右旋转开始,将树放置在 A 的左旋转使整个子树恢复平衡的位置。
147+
148+
![6.16.平衡二叉搜索树实现.figure8](assets/6.16.%E5%B9%B3%E8%A1%A1%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E5%AE%9E%E7%8E%B0.figure8.png)
149+
150+
151+
实现这些规则的代码可以在我们的重新平衡方法中找到,如 Listing 3所示。上面的规则编号 1 是从第2行开始的if语句实现的。规则编号2是由第8行开始的elif语句实现的 。
152+
153+
154+
```
155+
def rebalance(self,node):
156+
if node.balanceFactor < 0:
157+
if node.rightChild.balanceFactor > 0:
158+
self.rotateRight(node.rightChild)
159+
self.rotateLeft(node)
160+
else:
161+
self.rotateLeft(node)
162+
elif node.balanceFactor > 0:
163+
if node.leftChild.balanceFactor < 0:
164+
self.rotateLeft(node.leftChild)
165+
self.rotateRight(node)
166+
else:
167+
self.rotateRight(node)
168+
```
169+
*Listing 3*
170+
171+
172+
通过保持树在所有时间的平衡,我们可以确保 get 方法将按 O(log2(n)) 时间运行。但问题是我们的 put 方法有什么成本?让我们将它分解为 put 执行的操作。由于将新节点作为叶子插入,更新所有父节点的平衡因子将需要最多log2^n 运算,树的每层一个运算。如果发现子树不平衡,则需要最多两次旋转才能使树重新平衡。但是,每个旋转在 O(1)时间中工作,因此我们的put操作仍然是O(log2^n )。
173+
174+
在这一点上,我们已经实现了一个功能AVL树,除非你需要删除一个节点的能力。我们保留删除节点和随后的更新和重新平衡作为一个练习。
175+
176+
177+
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## 6.18.Map抽象数据结构总结
2+
3+
在前面两章中,我们已经研究了可以用于实现 Map 抽象数据类型的几个数据结构。 二叉搜索表,散列表,二叉搜索树和平衡二叉搜索树。 总结这一节,让我们总结 Map ADT 定义的关键操作的每个数据结构的性能(见Table 1)。
4+
5+
![6.18.Map抽象数据结构总结.table1](assets/6.18.Map%E6%8A%BD%E8%B1%A1%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E6%80%BB%E7%BB%93.table1.png)
6+
7+
8+
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## 7.10.广度优先搜索分析
2+
3+
在继续使用其他图算法之前,让我们分析广度优先搜索算法的运行时性能。首先要观察的是,对于图中的每个顶点 `|V|` 最多执行一次 while 循环。因为一个顶点必须是白色,才能被检查和添加到队列。这给出了用于 while 循环的 O(v)。嵌套在 while 内部的 for 循环对于图中的每个边执行最多一次,`|E|`。原因是每个顶点最多被出列一次,并且仅当节点 u 出队时,我们才检查从节点 u 到节点 v 的边。这给出了用于 for 循环的 O(E) 。组合这两个环路给出了 O(V+E)。
4+
5+
当然做广度优先搜索只是任务的一部分。从起始节点到目标节点的链接之后是任务的另一部分。最糟糕的情况是,如果图是单个长链。在这种情况下,遍历所有顶点将是 O(V)。正常情况将是 |V| 的一小部分但我们仍然写 O(V)。
6+
7+
最后,至少对于这个问题,存在构建初始图形所需的时间。我们把 `buildGraph` 函数的分析作为一个练习。
8+

0 commit comments

Comments
 (0)