Python实现二叉搜索树


Posted in Python onFebruary 03, 2016

二叉搜索树

我们已经知道了在一个集合中获取键值对的两种不同的方法。回忆一下这些集合是如何实现ADT(抽象数据类型)MAP的。我们讨论两种ADT MAP的实现方式,基于列表的二分查找和哈希表。在这一节中,我们将要学习二叉搜索树,这是另一种键指向值的Map集合,在这种情况下我们不用考虑元素在树中的实际位置,但要知道使用二叉树来搜索更有效率。

搜索树操作

在我们研究这种实现方式之前,让我们回顾一下ADT MAP提供的接口。我们会注意到,这种接口和Python的字典非常相似。

  1. Map() 创建了一个新的空Map集合。
  2. put(key,val) 在Map中增加了一个新的键值对。如果这个键已经在这个Map中了,那么就用新的值来代替旧的值。
  3. get(key) 提供一个键,返回Map中保存的数据,或者返回None。
  4. del 使用del map[key]这条语句从Map中删除键值对。
  5. len() 返回Map中保存的键值对的数目
  6. in 如果所给的键在Map中,使用key in map这条语句返回True。

搜索树实现

一个二叉搜索树,如果具有左子树中的键值都小于父节点,而右子树中的键值都大于父节点的属性,我们将这种树称为BST搜索树。如之前所述的,当我们实现Map时,BST方法将引导我们实现这一点。图 1 展示了二叉搜索树的这一特性,显示的键没有关联任何的值。注意这种属性适用于每个父节点和子节点。所有在左子树的键值都小于根节点的键值,所有右子树的键值都大于根节点的键值。

Python实现二叉搜索树

图 1:一个简单的二叉搜索树

现在你知道什么是二叉搜索树了,我们再来看如何构造一个二叉搜索树,我们在搜索树中按图 1 显示的节点顺序插入这些键值,图 1 搜索树存在的节点:70,31,93,94,14,23,73。因为 70 是第一个被插入到树的值,它是根节点。接下来,31 小于 70,因此是 70 的左子树。接下来,93 大于 70,因此是 70 的右子树。我们现在填充了该树的两层,所以下一个键值,将会是 31 或者 93 的左子树或右子树。由于 94 大于 70 和 93,就变成了 93 的右子树。同样,14 小于 70 和 31,因此成为了 31 的左子树。23 也小于 31,因此必须是 31 的左子树。然而,它大于 14,所以是 14 的右子树。

为了实现二叉搜索树,我们将使用节点和引用的方法,这类似于我们实现链表和表达式树的过程。因为我们必须能够创建和使用一个空的二叉搜索树,所以我们将使用两个类来实现,第一个类我们称之为 BinarySearchTree,第二个类我们称之为TreeNode。BinarySearchTree类有一个TreeNode类的引用作为二叉搜索树的根,在大多数情况下,外部类定义的外部方法只需检查树是否为空,如果在树上有节点,要求BinarySearchTree类中含有私有方法把根定义为参数。在这种情况下,如果树是空的或者我们想删除树的根,我们就必须采用特殊操作。BinarySearchTree类的构造函数以及一些其他函数的代码如Listing 1 所示。

Listing 1

class BinarySearchTree:

  def __init__(self):
    self.root = None
    self.size = 0

  def length(self):
    return self.size

  def __len__(self):
    return self.size

  def __iter__(self):
    return self.root.__iter__()

TreeNode类提供了许多辅助函数,使得BinarySearchTree类的方法更容易实现过程。如Listing 2 所示,一个树节点的结构,是由这些辅助函数实现的。正如你看到的那样,这些辅助函数可以根据自己的位置来划分一个节点作为左或右孩子和该子节点的类型。TreeNode类非常清楚地跟踪了每个父节点的属性。当我们讨论删除操作的实现时,你将明白为什么这很重要。

对于Listing 2 中的TreeNode实现,另一个有趣的地方是,我们使用Python的可选参数。可选的参数很容易让我们在几种不同的情况下创建一个树节点,有时我们想创建一个新的树节点,即使我们已经有了父节点和子节点。与现有的父节点和子节点一样,我们可以通过父节点和子节点作为参数。有时我们也会创建一个包含键值对的树,我们不会传递父节点或子节点的任何参数。在这种情况下,我们将使用可选参数的默认值。

Listing 2

class TreeNode:
  def __init__(self,key,val,left=None,right=None,
                    parent=None):
    self.key = key
    self.payload = val
    self.leftChild = left
    self.rightChild = right
    self.parent = parent

  def hasLeftChild(self):
    return self.leftChild

  def hasRightChild(self):
    return self.rightChild

  def isLeftChild(self):
    return self.parent and self.parent.leftChild == self

  def isRightChild(self):
    return self.parent and self.parent.rightChild == self

  def isRoot(self):
    return not self.parent

  def isLeaf(self):
    return not (self.rightChild or self.leftChild)

  def hasAnyChildren(self):
    return self.rightChild or self.leftChild

  def hasBothChildren(self):
    return self.rightChild and self.leftChild

  def replaceNodeData(self,key,value,lc,rc):
    self.key = key
    self.payload = value
    self.leftChild = lc
    self.rightChild = rc
    if self.hasLeftChild():
      self.leftChild.parent = self
    if self.hasRightChild():
      self.rightChild.parent = self

现在,我们拥有了BinarySearchTree和TreeNode类,是时候写一个put方法使我们能够建立二叉搜索树。put方法是BinarySearchTree类的一个方法。这个方法将检查这棵树是否已经有根。如果没有,我们将创建一个新的树节点并把它设置为树的根。如果已经有一个根节点,我们就调用它自己,进行递归,用辅助函数_put按下列算法来搜索树:

从树的根节点开始,通过搜索二叉树来比较新的键值和当前节点的键值,如果新的键值小于当前节点,则搜索左子树。如果新的关键大于当前节点,则搜索右子树。

当搜索不到左(或右)子树,我们在树中所处的位置就是设置新节点的位置。
向树中添加一个节点,创建一个新的TreeNode对象并在这个点的上一个节点中插入这个对象。

Listing 3 显示了在树中插入新节点的Python代码。_put函数要按照上述的步骤编写递归算法。注意,当一个新的子树插入时,当前节点(CurrentNode)作为父节点传递给新的树。

我们执行插入的一个重要问题是重复的键值不能被正确的处理,我们的树实现了键值的复制,它将在右子树创建一个与原来节点键值相同的新节点。这样做的后果是,新的节点将不会在搜索过程中被发现。我们用一个更好的方式来处理插入重复的键值,旧的值被新键关联的值替换。我们把这个错误的修复,作为练习留给你。

Listing 3

def put(self,key,val):
  if self.root:
    self._put(key,val,self.root)
  else:
    self.root = TreeNode(key,val)
  self.size = self.size + 1

def _put(self,key,val,currentNode):
  if key < currentNode.key:
    if currentNode.hasLeftChild():
        self._put(key,val,currentNode.leftChild)
    else:
        currentNode.leftChild = TreeNode(key,val,parent=currentNode)
  else:
    if currentNode.hasRightChild():
        self._put(key,val,currentNode.rightChild)
    else:
        currentNode.rightChild = TreeNode(key,val,parent=currentNode)

随着put方法的实现,我们可以很容易地通过__setitem__方法重载[]作为操作符来调用put方法(参见Listing 4)。这使我们能够编写像myZipTree['Plymouth'] = 55446一样的python语句,这看上去就像Python的字典。

Listing 4

def __setitem__(self,k,v):
  self.put(k,v)

图 2 说明了将新节点插入到一个二叉搜索树的过程。灰色节点显示了插入过程中遍历树节点顺序。

Python实现二叉搜索树

图 2: 插入一个键值 = 19 的节点

一旦树被构造,接下来的任务就是为一个给定的键值实现检索。get方法比put方法更容易因为它只需递归搜索树,直到发现不匹配的叶节点或找到一个匹配的键值。当找到一个匹配的键值后,就会返回节点中的值。

Listing 5 显示了get,_get和__getitem__的代码。用_get方法搜索的代码与put方法具有相同的选择左或右子树的逻辑。请注意,_get方法返回TreeNode中get的值,_get就可以作为一个灵活有效的方式,为BinarySearchTree的其他可能需要使用TreeNode里的数据的方法提供参数。

通过实现__getitem__方法,我们可以写一个看起来就像我们访问字典一样的Python语句,而事实上我们只是操作二叉搜索树,例如Z = myziptree ['fargo']。正如你所看到的,__getitem__方法都是在调用get。

Listing 5

def get(self,key):
  if self.root:
    res = self._get(key,self.root)
    if res:
        return res.payload
    else:
        return None
  else:
    return None

def _get(self,key,currentNode):
  if not currentNode:
    return None
  elif currentNode.key == key:
    return currentNode
  elif key < currentNode.key:
    return self._get(key,currentNode.leftChild)
  else:
    return self._get(key,currentNode.rightChild)

def __getitem__(self,key):
  return self.get(key)

使用get,我们可以通过写一个BinarySearchTree的__contains__方法来实现操作,__contains__方法简单地调用了get方法,如果它有返回值就返回True,如果它是None就返回False。如Listing 6 所示。

Listing 6

def __contains__(self,key):
  if self._get(key,self.root):
    return True
  else:
    return False

回顾一下__contains__重载的操作符,这允许我们写这样的语句:

if 'Northfield' in myZipTree:
  print("oom ya ya")
Python 相关文章推荐
Python字符串逐字符或逐词反转方法
May 21 Python
详解python发送各类邮件的主要方法
Dec 22 Python
回调函数的意义以及python实现实例
Jun 20 Python
Python编程之gui程序实现简单文件浏览器代码
Dec 08 Python
Python简单计算文件MD5值的方法示例
Apr 11 Python
Python subprocess模块功能与常见用法实例详解
Jun 28 Python
python pytest进阶之xunit fixture详解
Jun 27 Python
windows上安装python3教程以及环境变量配置详解
Jul 18 Python
python对验证码降噪的实现示例代码
Nov 12 Python
python操作gitlab API过程解析
Dec 27 Python
解决paramiko执行命令超时的问题
Apr 16 Python
matplotlib 生成的图像中无法显示中文字符的解决方法
Jun 10 Python
Python的组合模式与责任链模式编程示例
Feb 02 #Python
举例讲解Python中的Null模式与桥接模式编程
Feb 02 #Python
简介Python设计模式中的代理模式与模板方法模式编程
Feb 02 #Python
Python找出9个连续的空闲端口
Feb 01 #Python
Python 爬虫的工具列表大全
Jan 31 #Python
python在不同层级目录import模块的方法
Jan 31 #Python
在Python中移动目录结构的方法
Jan 31 #Python
You might like
PHP分页显示制作详细讲解
2006/10/09 PHP
打造计数器DIY三步曲(上)
2006/10/09 PHP
PHP脚本的10个技巧(6)
2006/10/09 PHP
PHP实现阿里大鱼短信验证的实例代码
2017/07/10 PHP
PHP基于GD2函数库实现验证码功能示例
2019/01/27 PHP
php项目中类的自动加载实例讲解
2019/09/12 PHP
Mootools 1.2教程 类(一)
2009/09/15 Javascript
基于JQuery的数字改变的动画效果--可用来做计数器
2010/08/11 Javascript
原生Js与jquery的多组处理, 仅展开一个区块的折叠效果
2011/01/09 Javascript
javascript jscroll模拟html元素滚动条
2012/12/18 Javascript
深入理解Javascript中的循环优化
2013/11/09 Javascript
使用js判断TextBox控件值改变然后出发事件
2014/03/07 Javascript
jQuery中的Deferred和promise 的区别
2016/04/03 Javascript
Bootstrap框架动态生成Web页面文章内目录的方法
2016/05/12 Javascript
微信公众号 摇一摇周边功能开发
2016/12/08 Javascript
AngularJS学习第二篇 AngularJS依赖注入
2017/02/13 Javascript
Angular2 之 路由与导航详细介绍
2017/05/26 Javascript
微信小程序 新建登录页并实现tabBar隐藏
2017/06/13 Javascript
浅谈vue中慎用style的scoped属性
2017/11/28 Javascript
讲解vue-router之什么是编程式路由
2018/05/28 Javascript
javascript数组元素删除方法delete和splice解析
2019/12/09 Javascript
JS访问对象两种方式区别解析
2020/08/29 Javascript
python实现可将字符转换成大写的tcp服务器实例
2015/04/29 Python
详解在Python中处理异常的教程
2015/05/24 Python
Scrapy基于selenium结合爬取淘宝的实例讲解
2018/06/13 Python
python将txt文档每行内容循环插入数据库的方法
2018/12/28 Python
使用keras实现densenet和Xception的模型融合
2020/05/23 Python
Canvas中设置width与height的问题浅析
2018/11/01 HTML / CSS
中国综合网上购物商城:苏宁易购
2016/08/09 全球购物
英超联赛的首选足球:Mitre足球
2019/05/06 全球购物
自动化专业本科毕业生求职信
2013/10/20 职场文书
初中体育教学反思
2014/01/14 职场文书
协议书模板
2014/04/23 职场文书
2019年预备党员的思想汇报:加深对党的认知
2019/09/25 职场文书
教你用Python爬取英雄联盟皮肤原画
2021/06/13 Python
MYSQL优化之数据表碎片整理详解
2022/04/03 MySQL