175
175
176
176
< li > < a class ="toctree-l3 " href ="#_2 "> 插入</ a > </ li >
177
177
178
- < li > < a class ="toctree-l3 " href ="#_3 "> 删除 </ a > </ li >
178
+ < li > < a class ="toctree-l3 " href ="#_3 "> 删除节点 </ a > </ li >
179
179
180
180
</ ul >
181
181
182
182
183
- < li class ="toctree-l2 "> < a href ="#_4 " > 练习题: </ a > </ li >
183
+ < li class ="toctree-l2 "> < a href ="#_7 " > 时间复杂度分析 </ a > </ li >
184
184
185
185
186
- < li class ="toctree-l2 "> < a href ="#_5 "> 延伸阅读</ a > </ li >
186
+ < li class ="toctree-l2 "> < a href ="#_8 "> 练习题:</ a > </ li >
187
+
188
+
189
+ < li class ="toctree-l2 "> < a href ="#_9 "> 延伸阅读</ a > </ li >
187
190
188
191
189
192
</ ul >
224
227
< h1 id ="bst "> 二叉查找树(BST)</ h1 >
225
228
< p > 二叉树的一种应用就是来实现堆,今天我们再看看用二叉查找树。
226
229
前面有章节说到了查找操作,包括线性查找和二分查找,线性查找效率比较低,二分又要求必须是有序的序列,
227
- 为了维持有序插入的代价比较高。能不能有一种插入和查找都比较快的数据结构呢?</ p >
230
+ 为了维持有序插入的代价比较高。能不能有一种插入和查找都比较快的数据结构呢?二叉查找树就是这样一种结构,可以高效地插入和查询节点。 </ p >
228
231
< h1 id ="bst_1 "> BST 定义</ h1 >
229
232
< p > 二叉查找树是这样一种二叉树结构,它的每个节点包含一个 key 和它附带的数据,对于每个内部节点 V:
230
233
- 所有 key 小于 V 的都被存储在 V 的左子树
@@ -320,9 +323,17 @@ <h2 id="key">获取最大和最小 key 的节点</h2>
320
323
</ code > </ pre >
321
324
322
325
< h2 id ="_2 "> 插入</ h2 >
323
- < p > 插入节点的时候我们需要一直保持 BST 的性质,每次插入一个节点,我们都通过递归比较把它放到正确的位置。</ p >
326
+ < p > 插入节点的时候我们需要一直保持 BST 的性质,每次插入一个节点,我们都通过递归比较把它放到正确的位置。
327
+ 你会发现新节点总是被作为叶子结点插入。(请你思考这是为什么)</ p >
328
+ < p > < img alt ="" src ="../bst_insert.png " /> </ p >
324
329
< pre > < code class ="py "> def _bst_insert(self, subtree, key, value):
325
- if subtree is None:
330
+ """ 插入并且返回根节点
331
+
332
+ :param subtree:
333
+ :param key:
334
+ :param value:
335
+ """
336
+ if subtree is None: # 插入的节点一定是根节点,包括 root 为空的情况
326
337
subtree = BSTNode(key, value)
327
338
elif key < subtree.key:
328
339
subtree.left = self._bst_insert(subtree.left, key, value)
@@ -341,14 +352,80 @@ <h2 id="_2">插入</h2>
341
352
return True
342
353
</ code > </ pre >
343
354
344
- < h2 id ="_3 "> 删除</ h2 >
345
- < h1 id ="_4 "> 练习题:</ h1 >
355
+ < h2 id ="_3 "> 删除节点</ h2 >
356
+ < p > 删除操作相比上边的操作要麻烦很多,首先需要定位一个节点,删除节点后,我们需要始终保持 BST 的性质。
357
+ 删除一个节点涉及到三种情况:</ p >
358
+ < ul >
359
+ < li > 节点是叶节点</ li >
360
+ < li > 节点有一个孩子</ li >
361
+ < li > 节点有两个孩子</ li >
362
+ </ ul >
363
+ < p > 我们分别来看看三种情况下如何删除一个节点:</ p >
364
+ < h4 id ="_4 "> 删除叶节点</ h4 >
365
+ < p > 这是最简单的一种情况,只需要把它的父亲指向它的指针设置为 None 就好。</ p >
366
+ < p > < img alt ="" src ="../bst_remove_leaf.png " /> </ p >
367
+ < h4 id ="_5 "> 删除只有一个孩子的节点</ h4 >
368
+ < p > 删除有一个孩子的节点时,我们拿掉需要删除的节点,之后把它的父亲指向它的孩子就行,以为根据 BST
369
+ 左子树都小于节点,右子树都大于节点的特性,删除它之后这个条件依旧满足。</ p >
370
+ < p > < img alt ="" src ="../bst_remove_node_with_one_child.png " /> </ p >
371
+ < h4 id ="_6 "> 删除有两个孩子的内部节点</ h4 >
372
+ < p > 假如我们想删除 12 这个节点改怎么做呢?你的第一反应可能是按照下图的方式:</ p >
373
+ < p > < img alt ="" src ="../remove_interior_replace.png " /> </ p >
374
+ < p > 但是这种方式可能会影响树的高度,降低查找的效率。这里我们用另一种非常巧妙的方式。
375
+ 还记得上边提到的吗,如果你中序遍历 BST 并且输出每个节点的 key,你会发现就是一个有序的数组。
376
+ [1 4 12 23 29 37 41 60 71 84 90 100]。 这里我们定义两个概念,逻辑前任(predecessor)和后继(successor),请看下图:</ p >
377
+ < p > < img alt ="" src ="../predecessor_successor.png " /> </ p >
378
+ < p > 12 在中序遍历中的逻辑前任和后继分别是 4 和 23 节点。于是我们还有一种方法来删除 12 这个节点:</ p >
379
+ < ul >
380
+ < li > 找到节点待删除节点 N(12) 的后继节点 S(23)</ li >
381
+ < li > 复制节点 S 到节点 N</ li >
382
+ < li > 删除节点 S</ li >
383
+ </ ul >
384
+ < p > 说白了就是找到后继并且替换,这里之所以能保证这种方法是正确的,你会发现替换后依旧是保持了 BST 的性质。
385
+ 有个问题是如何找到后继节点呢?待删除节点的右子树的最小的节点不就是后继嘛,上边我们已经实现了找到最小 key 的方法了。</ p >
386
+ < p > < img alt ="" src ="../find_successor.png " /> </ p >
387
+ < p > 我们开始编写代码实现,和之前的操作类似,我们还是通过辅助函数的形式来实现,这个递归函数会比较复杂,请你仔细理解:</ p >
388
+ < pre > < code class ="py "> def _bst_remove(self, subtree, key):
389
+ """删除节点并返回根节点"""
390
+ if subtree is None:
391
+ return None
392
+ elif key < subtree.key:
393
+ subtree.left = self._bst_remove(subtree.left, key)
394
+ return subtree
395
+ elif key > subtree.key:
396
+ subtree.right = self._bst_remove(subtree.right, key)
397
+ return subtree
398
+ else: # 找到了需要删除的节点
399
+ if subtree.left is None and subtree.right is None: # left node
400
+ return None
401
+ elif subtree.left is None or subtree.right is None: # 只有一个孩子
402
+ if subtree.left is not None:
403
+ return subtree.left
404
+ else:
405
+ return subtree.right
406
+ else: # 俩孩子
407
+ successor_node = self._bst_min_node(subtree.right)
408
+ subtree.key, subtree.value = successor_node.key, subtree.value
409
+ subtree.right = self._bst_remove(subtree.right, successor_node.key)
410
+ return subtree
411
+
412
+ def remove(self, key):
413
+ assert key in self
414
+ self.size -= 1
415
+ </ code > </ pre >
416
+
417
+ < p > 完整代码你可以在本章的 bst.py 找到。</ p >
418
+ < h1 id ="_7 "> 时间复杂度分析</ h1 >
419
+ < p > 上边介绍的操作时间复杂度和二叉树的形状有关。平均来说时间复杂度是和树的高度成正比的,树的高度 h 是 log(n),
420
+ 但是最坏情况下以上操作的时间复杂度都是 O(n)。为了改善 BST 有很多变种,感兴趣请参考延伸阅读中的内容。</ p >
421
+ < p > < img alt ="" src ="../bst_worstcase.png " /> </ p >
422
+ < h1 id ="_8 "> 练习题:</ h1 >
346
423
< ul >
347
424
< li > 请你实现查找 BST 最大值的函数</ li >
348
425
</ ul >
349
- < h1 id ="_5 "> 延伸阅读</ h1 >
426
+ < h1 id ="_9 "> 延伸阅读</ h1 >
350
427
< ul >
351
- < li > 《Data Structures and Algorithms in Python》14 章</ li >
428
+ < li > 《Data Structures and Algorithms in Python》14 章,树的概念和算法还有很多,我们这里介绍最基本的 </ li >
352
429
</ ul >
353
430
354
431
</ div >
0 commit comments