Python数据结构与算法分析 第六章-树

370 阅读20分钟

Python数据结构与算法分析

第六章   树

6.1 何谓树

  • 树: 一种“非线性”数据结构
  • 数据结构树分为:

1665973017658.png

6.1.1特点

  • 树的分类体系是层次化

    • 越接近顶部的层越普遍
    • 越解决底部的层越独特
  • 一个节点的子节点与另一个节点的子节点相互之间是隔离、独立

  • 每一个叶节点都具有唯一性

6.1.2树结构相关术语

  • 节点 Node :组成树的基本部分

    • 节点是树的基础部分。它可以有自己的名字,我们称作“”。节点也可以带有附加信息,我们称作“有效载荷”
  • 边 Edge: 边是树的另一个基础部分

    • 。两个节点通过一条边相连,表示它们之间存在关系。除了根节点以外,其他每个节点有一条入边,出边则可能有多条。
  • 根节点 Root :根节点是树中唯一没有入边的节点。

  • 路径 Path :由边连接的有序节点列表。

    • 哺乳纲→食肉目→猫科→猫属→家猫就是一条路径。
  • 子节点 Children :入边均来自于同一个节点的若干节点 (图中的 B C是根节点的子节点)

  • 父节点 Parent :一个节点是其所有出边所连接节点的父节点 (图中的 B是DE的父节点 C是F的父节点)

  • 兄弟点 Sibling :具有同一个父节点的节点之间称为兄弟节点 (图中B C是兄弟节点)

  • 子树 Subtree :一个节点和其所有子孙节点,以及相关边的集合

  • 叶子节点 Leaf :没有子节点的节点成为叶子节点 (图中D E F是叶子节点)

  • 层级 Leval :从根节点开始到达一个节点的路径,所包含的边的数量称为这个节点的层级 (图中D层级为2 根节点层级为0)

  • 高度 :树中所有节点的最大层级称为树的高度 (图中树的高度为2)

1665974968783.png

6.1.3 树的定义

  • 树是由若干节点,以及两两连接节点的边组成,并有如下性质:

    • 有一个根节点;
    • 除根节点外,其他每个节点都与其唯一的父节点相连;
    • 从根节点到其他每个节点都有且仅有一条路径;
    • 如果每个节点最多有两个子节点,我们就称这样的树为二叉树。

1665975728762.png

  • 一棵树要么为空,要么由一个根节点零棵多棵子树构成,子树本身也是一棵树

    • 空集是树
    • 每个子树的根到根节点具有边相连

1665975892768.png

6.2 实现树:嵌套列法

  • Python List来实现二叉树数据结构;

  • 递归的嵌套列表实现二叉树,由三个元素的列表来实现

    • 第一个元素为根节点的值
    • 第二个元素是左子树(列表)
    • 第三个元素是右子树(列表)
  • 辅助函数

    • BinaryTree() 创建一个只有根节点二叉树
    • insertLeft(val)/insertRight(val) 新建一棵二叉树,并将其作为当前节点的左/右子节点
    • setRootVal(val)getRootVal() 返回/获取当前节点存储的对象。
    • getLeftChild()/getRightChild() 返回当前节点的左/右子树

1665976197365.png

#树
## 嵌套列表实现树
"""0:根
   1:左
   2:右
"""
def BinaryTree(r):
    return [r,[],[]] #只有根节点,左右子树为空
def insertLeft(root,newBranch): #插入左子树
    t = root.pop(1) #把左子树弹出 放入变量t
    if len(t) > 1: #有左子树
        root.insert(1,[newBranch,t,[]]) #插入这个节点newbranch 作为左子树的根节点,原来节点t作为newBranch的左节点
    else:#没有节点
        root.insert(1,[newBranch,[],[]]) #做为左子树的根节点,无子节点
    return root

def insertRight(root,newBranch):#插入右子树
    t = root.pop(2)
    if len(t) > 1: #若右子树上有节点
        root.insert(2,[newBranch,[],t]) #插入这个节点newbranch 作为右子树的根节点,原来节点t作为newBranch的右节点
    else:#没有节点
        root.insert(2,[newBranch,[],[]]) #做为左右子树的根节点,无子节点
    return root
def getRootVal(root): #获取根节点
    return root[0]
def setRootVal(root,newVal): #设置根节点
     root[0] = newVal
def getLeftChild(root):#获取左子树
    return root[1]
def getRightChild(root):#获取右子树
    return root[2]


r = BinaryTree(3) #创建一个根节点为3的二叉树
insertLeft(r,4) #3的左子树插入4
insertLeft(r,5) #左子树插入5 现在3的左子树的根节点为5 左:3~5~4
insertRight(r,6) #3的右子树插入6
insertRight(r,7)#右子树插入7 现在3的右子树的根节点为7 右:3~7~6
l = getLeftChild(r) #获取左子树
print(l)

setRootVal(l,9) #把l的左子树的根节点替换为9 左:3~9~4 
print(r)
insertLeft(l,11) #把l的左子树插入11
print(r)
print(getRightChild(getRightChild(r))) #先获取r的右子树 7~6 再获取以7为根节点的右子树6



[5, [4, [], []], []]
[3, [9, [4, [], []], []], [7, [], [6, [], []]]]
[3, [9, [11, [4, [], []], []], []], [7, [], [6, [], []]]]
[6, [], []]

1666057189136.png

1666057975642.png

6.3 树的链表实现

  • 用节点链接法实现树

    • 每个节点保存根节点的数据项,以及指向左右子树的链接

1666058208561.png

  • 定义一个BinaryTree 类

    • 成员key保存根节点数据项
    • 成员leftchild/rightchild 保存指向左/右子树的引用
## 链表实现树
class BinaryTree:
    def __init__(self,rootObj):  #初始化只有根节点的二叉树
        self.key = rootObj #根节点
        self.leftchild = None
        self.rightchild = None

    def insertLeft(self,newNode):
        if self.leftchild == None:
            self.leftchild = BinaryTree(newNode) #左子树为空,根的左节点指向以binarytree生成的以newNode为根的二叉树
        else:
            t = BinaryTree(newNode) #用binarytree生成一颗以newnode为根节点的二叉树t
            t.leftchild = self.leftchild #新t左子树指向给原self左子树
            self.leftchild = t #把self根的左子树指向t

    def insertRight(self,newNode):
        if self.rightchild ==  None:
            self.rightchild = BinaryTree(newNode) #根节点右子树为空,根的右节点指向以binarytree生成的以newNode为根的二叉树
        else:
            t = BinaryTree(newNode)
            t.rightchild = self.rightchild
            self.rightchild = t

    def getRightChild(self):
        return self.rightchild
    def getLeftChild(self):
        return self.leftchild
    def setRootVal(self,obj):
        self.key = obj
    def getRootVal(self):
        return  self.key

r = BinaryTree("a")
r.insertLeft("b")
r.insertRight("c")
r.getRightChild().setRootVal("hello") #把右子树的根改为hello
r.getLeftChild().insertRight("d") #在左子树b后插入一个右子树d

1666060203621.png

6.4 二叉树的应用

6.4.1 表达式解析

语法分析树:主谓宾,定状补

1666060435365.png

表达式树:((7+5)*(5-2))

1666060600670.png

树中每个子树都表示一个子表达式

1666060722310.png

本节研究问题

  • 如何根据完全括号表达式构建解析树

    • 全括号表达式要分解成单词列表

      • 左括号( 表达式的开始右括号) 表达式的结束

1666061043746.png

规则:

  • 如果当前标记是左括号(,就为当前节点添加一个左子节点,并下沉至该左子节点
  • 如果当前标记在列表['+', '-', '/', '*']中,就将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节点,并下沉至右子节点
  • 如果当前标记是数字,就将当前节点的值设为这个数返回至父节点
  • 如果当前标记是右括号),就跳到当前节点的父节点

过程:

(a) 创建一棵空树(灰色)。

(b) 读入 ( 。子树的开始,为根节点添加一个左子节点当前节点(灰色)下降左子节点

(c) 读入 3 。3为操作数,将当前节点的值设为 3,并回到父节点

(d) 读入 + 。将当前节点的值设为+(根节点),添加一个右子节点当前节点下降到右子节点

(e) 读入(。为当前节点添加一个左子节点当前节点下降左子节点

(f) 读入 4。将当前节点的值设为 4,当前节点回到父节点

(g) 读入*。将当前节点的值设为*,并添加右子节点。新节点成为当前节点

(h) 读入 5。将当前节点的值设为 5,并回到父节点。

(i) 读入)。将*的父节点作为当前节点

(j) 读入。根据规则 4,将+的父节点作为当前节点。因为+没有父节点,所以工作完成。

1666062023364.png

解析完全表达式解析树

  • 创建树过程中关键在于对当前节点的跟踪

    • 创建左右子树,调用 insertRight/insertLeft
    • 当前节点设置值,调用serRootVal
    • 下降到左右子树,调用getLeft/getRight
  • 用栈Stack来跟踪父节点

    • 节点下降,下降前节点用push入栈
    • 节点上升到父节点,上升用pop出栈
from part_3 import Stack


def bulidParseTree(fpexp):
    fplist = fpexp.split() #划分表达式
    pStack = Stack() #建立空栈
    eTree = BinaryTree('') #创建空树
    pStack.push(eTree) #空树入栈
    currentTree = eTree #当前节点
    for i in fplist :#遍历表达式
        if i == "(": #当前标记是左括号(,当前节点添加一个左子节点,当前节点并下沉至该左子节点
            currentTree.insertLeft("") #添加左子节点
            pStack.push(currentTree) #当前节点入栈
            currentTree = currentTree.getLeftChild() #下沉至左子节点
        elif i not in ['+','-','*','/',')']: #操作数 将当前节点的值设为这个数i,并返回至父节点 上升 出栈
            currentTree.setRootVal(int(i)) #将当前节点的值设为这个数i
            pStack.pop(currentTree) #弹出当前节点
            currentTree = parent #当前节点为父节点
        elif i in ['+','-','*','/',')']: #将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节,并下沉至右子节点;
            currentTree.setRootVal(i)
            currentTree.insertRight("") #添加右子节点
            pStack.push(currentTree) #节点下降
            currentTree = currentTree.getRightChild() #当前节点下降到右子节点
        elif i == ")": #右括号 就跳到当前节点的父节点 上升。
            currentTree = pStack.pop() #上升到父节点
        else:
            raise ValueError
    return eTree
  • ** ② 如何计算解析树中的表达式**

    • 求值递归函数evaluate

      • 从树的底层子树开始逐步向上层求值,最终得到整个表达式的值
      • 基本结束条件:叶节点没有左右子树,则根节点的数据项为子表达树的值
      • 减小规模:将表达式划分为左右子树
      • 调用自身:分别调用evaluate计算左右子树的值,再将左右子树的值依据根节点的操作符进行计算,得表达式的解

1666064748667.png

1666065185839.png

from part_3 import Stack
import operator

def bulidParseTree(fpexp):
    fplist = fpexp.split() #划分表达式
    pStack = Stack() #建立空栈
    eTree = BinaryTree('') #创建空树
    pStack.push(eTree) #空树入栈
    currentTree = eTree #当前节点
    for i in fplist :#遍历表达式
        if i == "(": #当前标记是左括号(,当前节点添加一个左子节点,当前节点并下沉至该左子节点
            currentTree.insertLeft("") #添加左子节点
            pStack.push(currentTree) #当前节点入栈
            currentTree = currentTree.getLeftChild() #下沉至左子节点
        elif i not in ['+','-','*','/',')']: #操作数 将当前节点的值设为这个数i,并返回至父节点 上升 出栈
            currentTree.setRootVal(int(i)) #将当前节点的值设为这个数i
            pStack.pop(currentTree) #弹出当前节点
            currentTree = parent #当前节点为父节点
        elif i in ['+','-','*','/',')']: #将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节,并下沉至右子节点;
            currentTree.setRootVal(i)
            currentTree.insertRight("") #添加右子节点
            pStack.push(currentTree) #节点下降
            currentTree = currentTree.getRightChild() #当前节点下降到右子节点
        elif i == ")": #右括号 就跳到当前节点的父节点 上升。
            currentTree = pStack.pop() #上升到父节点
        else:
            raise ValueError
    return eTree

def evaluate(parseTree):
    opers = {'+':operator.add,
             '-':operator.sub,
             '*':operator.mul,
             '/':operator.truediv} #操作符字典 操作树到实际操作函数的映射

    leftC = parseTree.getLeftChild() #缩小规模:划分左右子树
    rightC = parseTree.getRightChild()

    if leftC in rightC : #存在左右子树
        fn = opers[parseTree.getRootVal()] #获取操作符
        return fn(evaluate(leftC),evaluate(rightC)) #运算左右子树
    else:#没有左右子树  基本情况 退出 
        return parseTree.getRootVal()

6.5 树的遍历

  • 序遍历:根左右
  • 序遍历:左根右
  • 序遍历:左右根

1666144791058.png 前序:书->第一章->1.1节->1.2节->1.2.1节->1.2.2节->第二章->2.1节->2.2节->2.2.1节->2.2.2节

#树的遍历
def preorder(tree): #前序
    if tree:
        print(tree.getRootVal()) #根
        preorder(tree.getLeftChild())#左
        preorder(tree.getRightChild())#右

def inorder(tree) :#中序
    if tree != None:
        inorder(tree.getLeftChild()) #左
        print(inorder(tree.getRootVal()))#根
        inorder(tree.getRightChild())#右


def postorder(tree): #后序
    if tree != None:
       postorder(tree.getLeftChild())#左
       postorder(tree.getRightChild())#右
       print(tree.getRootVal())#根
  • 如何将解析树还原成最初的数学表达式

    • 后序遍历
def postordereval(tree):
  opers = {'+':operator.add,
           '-':operator.sub,
           '*':operator.mul,
           '/':operator.truediv} #操作符字典 操作树到实际操作函数的映射

  res1 = None
  res2 = None
  if tree:
      res1 = postorder(tree.getLeftChild()) #获取左子树操作树
      res2 = postorder(tree.getRightChild())# 获取右子树操作数
      if res1 and res2:#存在左右子树
          return opers[tree.getRootVal()](res1,res2) #获取根节点对应的操作符 对 左右操作数进行运算 返回数值
      else :
          return tree.getRootVal
  • 全括号中缀表达式

    • 中序遍历
#中序表达式:生成全括号中缀表达式
def printexp(tree):
    sVal = ""
    if tree:
        sVal = "("+printexp(tree.getLeftChild())
        sVal = sVal + str(tree.getRootVal())
        sVal = sVal + printexp(tree.getRightChild()+")")
    return sVal

6.6 利用二叉堆实现优先级队列(FIFO)

  • 队列有一个重要的变体,叫作优先级队列

    • 优先级队列从头部移除元素,不过元素的逻辑顺序是由优先级决定的。优先级最高的元素在最前优先级最低的元素在最后

6.6.1 利用二叉堆实现优先级队列

  • 二叉堆逻辑结构是二叉树,利用非嵌套的列表实现
  • 最小Key排在队首为‘最小堆’,最大Key在队首为‘最大堆

1666147166339.png

二叉堆操作

  • BinaryHeap() 新建一个空的二叉堆。

  • insert(k) 往堆中加入一个新元素。

  • findMin() 返回最小的元素,元素在堆中。

  • delMin() 返回最小的元素,并将该元素从堆中移除

  • isEmpty() 在堆为空时返回 True,否则返回 False。

  • size() 返回堆中元素的个数

  • buildHeap(list) 根据一个key列表创建堆

用非嵌套列表实现二叉堆

  • 采用二叉树结构使二叉堆保持在对数水平(二叉树的平衡:左右子树拥有相同数量的节点(满二叉树))

  • 采用完全二叉树近似实现平衡

    • 完全二叉树:在完全二叉树中,除了最底层,其他每一层的节点都是满的。在最底层,我们从左往右填充节点,都连续集中在最左边。

 如果节点的下标P,左节点下标2P,右节点下标2P+1,其父节点下标P//2

1666148066729.png

  • 堆次序:任何一个节点X,其父节点P中的key均小于x中的key;

    • 对于复合‘堆’性质的二叉树,其中任何一条路劲,均是一个已排序数列,根节点Key最小

1666148868536.png

二叉堆操作的实现

  • 二叉堆初始化

    • 采用一个列表来保存堆数据,其中表收下标为0的项无用。

class BinHeap:
  def __init__(self):
      self.heaplist = [] #空列表保存堆数据
      self.currentSize = 0 #下标为0
  • insert(key)

    • 为了保持完全二叉树结构,。如果新元素小于其父元素,就将二者交换。

1666149772885.png

1666149788752.png

1666149799942.png

def perUp(self,i):
    while i //2 > 0:#根节点不为0
        if self.heaplist[i] < self.heaplist[i//2]:#当前下标i对应的key 小于 父节点的key
            temp = self.heaplist[i//2]
            self.heaplist[i//2] = self.heaplist[i]
            self.heaplist[i] = temp #与父节点进行交换
        i = i//2 #沿着路径向上
def insert(self,k):
    self.heaplist.append(k) #添加末尾
    self.currentSize = self.currentSize+1 #列表长度加1
    self.perUp(self.currentSize) #判断插入值 平衡完全二叉树
  • delMin()方法

    • 移走整个堆最小的Key:根节点heapList[1],在保持平衡完全树的前提下
    • 将新节点沿着一条路径“下沉”,直到比两个子节点都小(选择左右节点中较小的下沉)

1666151243428.png

1666151253422.png

1666151265993.png

1666151279412.png

def percDown(self,i): #下沉
    while (i*2) <= self.currentSize: #左节点小于总列表长度 存在左子树
        mc = self.minChild(i) #mc 为子节点的最小值
        if self.heaplist[i] > self.heaplist[mc]: #如果输入的i 比最小值mc 大 需要交换
            tmp = self.heaplist[i]
            self.heaplist[i] = self.heaplist[mc]
            self.heaplist[mc] = tmp
        i = mc #沿着路径下层
def minChild (self,i):
    if i*2 +1 > self.currentSize:#不存在右子树
       return  i*2 #直接返回左子树
    else:
        if self.heaplist[i*2] < self.heaplist[2*i+1]: #左节点的值小于右节点的值
           return  i*2  #左节点下标
        else:
           return 2*i+1 #有节点下标
def delMin(self):
    retval = self.heaplist[1]#移走堆顶节点
    self.heaplist[1] = self.heaplist[self.currentSize] #把二叉树最后一个节点移动到堆顶节点
    self.currentSize = self.currentSize-1 #堆列表长度-1
    self.heaplist.pop() #把最后一个数值弹出
    self.percDown(1) #新移动到根节点的数值比较下沉
    return retval #返回被移除的原堆顶节点
  • bulidHeap(lst)方法

    • 无序表生成“堆”

    • 使用“下层”法,让总代价控制在O(n)

      • 叶子节点不需要下沉,对叶子节点的父节点进行下沉

1666231803064.png

def buildHeap(self,lst):#用无序表生成堆
    i  = len(lst)//2 #用叶子节点的父节点做i 进行下沉操作
    self.currentSize = len(lst)
    self.heaplist = [0] + lst[:] #插入0
    print(len(self.heaplist),i)
    while (i>0):#存在父节点
        print(self.heaplist,i)
        self.percDown(i) #下沉
        i = i-1 #向上移动到父子节点
    print(self.heaplist,i)

6.7 二叉搜索树

ADT MAP

  • Map() 新建一个空映射

  • put(key, val) 往映射中加入一个新的键–值对。如果键已经存在,就用新值替换旧值。

  • get(key) 返回 key 对应值。如果 key 不存在,则返回 None。

  • del 通过 del map[key]这样的语句从映射中删除键–值对

  • len() 返回映射中存储的键–值对的数目

  • in 通过 key in map 这样的语句,在键存在时返回 True,否则返回 False。

二叉搜索树的性质

  • 比父节点的都出现在子树,比父节点的出现在子树

构造二叉搜索树 BST

[70,31,93,94,14,23,73]

首先70 成为 树根

  • 31<70 ,放入左子树
  • 93>70 ,放入右子树
  • 94>70 ,放入右子树;94>93,放入父节点为93的右子树
  • 14<70 ,放入左子树;14<31,放入父节点为31的左子树
  • 23<70 ,放入左子树;23<31&23>14,放入父节点为14的右子树
  • 73>70 ,放入右子树;73<93,放入父节点为93的左子树

1666232877425.png

实现二叉搜索树

  • BinarySearchTree类
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类
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 Leaf(self): #是否是叶节点
        return not (self.leftChild 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,val,lc,rc):  #把左右父节点都更换
        self.key = key
        self.pyload = val
        self.leftChild = lc
        self.rightChild = rc
        if self.hasLeftChild():
            self.leftChild.parent = self
        else:
            self.rightChild.parent = self
  • put(key,val) 方法:插入key构建BST

    • 首先判断二叉搜索树是否为空,是,让key成为根节点root
    • 否则,调用递归函数_put(key,val,root) 来放置key
  • _put(key,val,currentNode) 辅助方法

    • 如果key<currentNode,那么key插入到左子树

      • 没有左子树,key就成为左子节点
    • 如果key>currentNode,那么key插入到右子树

      • 没有右子树,key就成为右子节点
def put(self,key,val):
    if self.root:#存在根节点
        self._put(key,val,self.root) #调用递归函数
    else:
        self.root = TreeNode(key,val)#让key成为根节点root
    self.size = self.size + 1 #树的长度+1

def _put(self,key,val,currentNode):
    if key < currentNode.key: #输入key小于当前key
        if  currentNode.hasLeftChild() :#当前key存在左子树
            self._put(key,val,currentNode.leftChild) #key插入到当前节点左子树
        else: #没有左子树
            currentNode.leftChild = TreeNode(key,val,parent = currentNode) #key成为左子节点

    else : #输入key大于当前key
        if currentNode.hasRightChild():#当前key存在右子树
            self._put(key,val,currentNode.rightChild)#key插入到当前节点右子树
        else:
            currentNode.rightChild = TreeNode(key,val,parent= currentNode) ##key成为右子节点

def __setitem__(self, key, value): #索引赋值
    self.put(key,value)
    

1666237939169.png

BST.put的图示:currentNode的变化(灰色)

  • 插入key=19

    • currentNode从树根17开始
    • 19>17 插入到右子树,curentNode移动到35
    • 19<35 插入到左子树,currentNode 移动到29
    • 19<29 插入到左子树,但29无左子节点,19作为29的新左子节点

1666236574423.png

  • get(key) 通过key所在节点找到payload方法
def get(self,key):
    if self.root:#存在树
        res = self._get(key,self.root) #找到节点
        if res: #找到节点res
             return res.pyload
        else:
            return None
    else:#空树
        return None

def _get(self,key,currentNode):
    if not currentNode:
        return None
    elif currentNode.key == key: #currentNode等于要找的Key
        return currentNode
    elif key < currentNode.key: #小于
        return self._get(key,currentNode.leftChild) #在左子树上继续找
    else:
        return self._get(key,currentNode.rightChild) #在右子树上继续找

def __setitem__(self, key, value): #索引取值
    return  self.get(key)

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

__iter__迭代器:使用for迭代,实际上是递归函数yield对每次迭代的返回值,中序遍历的迭代

def __iter__(self):#迭代器
    if self:
        if self.hasLeftChild():
            for elem in self.leftChild:
                yield elem
        yield self.key
        if self.hasRightChild():
            for elem in self.rightChild:
                yield elem

1666237883526.png

1666237897601.png

  • delete(key) :删除节点

    • 用_get找到删除的节点,然后调用remove来删除,找不到则提示错误
def delete(self,key):
   if self.size>1:#存在树
       nodeToRemove = self._get(key,self.root)#从当前节点为根节点开始寻找
       if nodeToRemove:#找到节点
           self.remove(nodeToRemove) #删除节点
           self.size = self.size - 1 #树的长度-1

       else:
           raise KeyError("Error,key not in tree")

   elif self.size ==1 and self.root.key == key:#树长度为1,且需查找值 与 根节点对应值相等
       self.root = None #直接设置根节点为空
       self.size = self.size - 1
   else:
       raise KeyError('Error, key not in tree')
  • delitem(self, key) :删除节点的特殊方法

    • 实现del myZipTree['PKU']的语句操作
def __delitem__(self, key):#删除节点的特殊方法
    self.delete(key)

BST.remove() :在删除节点后,还需要保证BST结构的性质

  • 被删除节点无子节点 :直接删除

1666320152139.png

def remove(self,currentNode): #移除节点
    """被删除是叶子节点,没有子节点"""
    if currentNode.isLeaf():#需要删除的节点为叶子节点,直接删除
        if currentNode == currentNode.parent.leftChild: #如果被删除节点== 当前节点父节点的左子节点
            currentNode.parent.leftChild = None #设置为空删除
        else:
            currentNode.parent.rightChild = None #左节点删除
    elif currentNode.hasBothChild():
        succ = currentNode.findSuccessor()
        succ.spiceOut()
        currentNode.key = succ.key
        currentNode.payload = succ.pyload
  • 有1个子节点:将这个唯一的子节点上移,替换被删除节点的位置

1666320296752.png

"""被删除节点有一个子节点  :左子节点"""
#1. 被删除的节点为左子节点
if currentNode.hasLeftChild():#被删除节点有唯一左子节点
    if currentNode.isLeftChild():#且被删除节点是左节点
        currentNode.leftChild.parent = currentNode.parent #被删除节点左子节点的父节点 变为 被删除节点的父节点
        currentNode.parent.leftChild = currentNode.leftChild  #被删除节点的父节点的左节点,变为被删除的左节点
#2.被删除的节点为右子节点 且有左子节点
    elif currentNode.isRightChild():#被删除节点是右节点 同理
         currentNode.rightChild.parent =  currentNode.parent
         currentNode.parent.rightChild = currentNode.rightChild
#3.被删除节点为根节点
    else:
         currentNode.replaceNodeData(currentNode.leftChild.key,
                                currentNode.leftChild.pyload,
                                currentNode.leftChild.leftChild,
                                currentNode.leftChild.rightChild)
#右子节点
else: #被删除节点是右子节点
    if currentNode.isLeftChild(): #且被删除节点是左节点
        currentNode.rightChild.parent = currentNode.parent
        currentNode.parent.leftChild = currentNode.rightChild
    elif currentNode.isRightChild(): #被删除节点存在右节点 同理
        currentNode.rightChild.parent = currentNode.parent
        currentNode.parent.rightChild = currentNode.rightChild
    else:#被删除节点为根节点
        currentNode.replaceNodeData(currentNode.rightChild.key,
                                    currentNode.rightChild.payload,
                                    currentNode.rightChild.leftChild,
                                    currentNode.rightChild.rightChild)
  • 有2个子节点

    • 找到下一个Key节点,被删节点右子树中最小的值称为后继节点

      • 如果节点有右子节点,那么后继节点就是右子树中最小的节点。
      • 如果节点没有右子节点,并且其本身是父节点的左子节点,那么后继节点就是父节点。
      • 如果节点是父节点的右子节点,并且其本身没有右子节点,那么后继节点就是除其本身外父节点的后继节点。

1666527414438.png

def findSuccessor(self):#寻找后继节点
    succ = None
    if self.hasRightChild():#如果节点有右子节点
        succ = succ.rightChild().fidMin()#那么后继节点就是右子树中最小的节点
    else: #目前不会遇到
        if self.parent:
            if self.isLeftChild():
                succ = self.parent
            else:
                self.parent.rightChild = None
                succ = self.parent.findSucceesor()
                self.parent.rightChild = self
    return succ
def findMin(self):
    current = self
    while current.hasRightChild(): #当前节点有右子节点 右子节点作为根 current
        current = current.leftChild #找current的左子树 找到最左下的节点 返回
    return current

def spliceOut(self): #摘除节点
    if self.isLeaf(): #后继节点为叶节点,直接摘除
        if self.isLeftChild():
            self.parent.leftChild = None
        else:
            self.parent.rightChild = None
    elif self.hasAnyChildren():  #目前不会遇到
        if self.hasLeftChild():
              if self.isLeftChild():
                  self.parent.leftChild = self.leftChild
              else:
                  self.parent.rightChild = self.leftChild
              self.leftChild.parent = self.parent
        else:#后继节点有右子节点
            if self.isLeftChild(): #如果后继节点本身左子节点
                self.parent.leftChild = self.rightChild #把后继节点的左子节点挂到后继节点的父节点的左子节点上
            else:
                self.parent.rightChild = self.rightChild #目前不会遇到
            self.rightChild.parent = self.parent

#被删除节点有两个个子节点 
if currentNode.hasBothChildren():
    succ = currentNode.findSuccessor() #找到被删除节点的后继节点
    succ.spliceOut() #把后继节点提出
    currentNode.key = succ.key
    currentNode.payload = succ.payload

二叉搜索树算法分析(以put为例)

  • 限制其性能的因素是二叉树的高度,高度收到数据项Key插入顺序的影响

    • 如果key的列表是随机分布,大于小于根节点Key的值都大致相等
    • BST的高度是log2n(n是节点个数)为平衡树
    • put最差性能为O(log2n)

6.8 平衡二叉搜索树 AVL树

AVL树 能自动维持平衡

 AVL 树实现映射抽象数据类型的方式与普通的二叉搜索树一样,唯一的差别就是性能。

 实现AVL 树要记录每个节点的平衡因子。根据每个节点左右子树的高度来实现这将平衡因子,定义为左右子树的高度差。

balanceFactor = height(left SubTree)-height(right SubTree)

  • 如果平衡因子>0,称为"左重 Left-heavy";<0,称为"右重 Right-heavy"
  • 平衡因子 = 0 ,则称为平衡

 如果在一个二叉搜索树中每一个节点的平衡因子都在(-1,0,1)之间,则称为平衡树

1666578117439.png

  • 如果在操作过程中,有节点平衡因子超出范围,在保持BST性质的前提下,如何重新平衡

1666578255701.png

AVL树的性能

  • 最不平衡左重

    • 总节点:

      • h = 1,N=1
      • h = 2,N=2=1+1
      • h = 3,N=4=1+2+1
      • h = 4,N=7=1+2+3+1

1666579663305.png

  • 类似 斐波那契数列

    • 定义斐波那契数列Fi,利用Fi重写Ni
    • 斐波那契数列性质:Fi/Fi-1趋向黄金分割,可写出Fi的通式
    • 将Fi通式代入Nh中,得到Nh的通式
    • 移项,两边以 2 为底取对数,求h

1666579862650.png


1666580095020.png


1666580581453.png


1666580674048.png


1666580729438.png

  • 在任何时间,AVL 树的高度都等于节点数取对数再乘以一个常数(1.44)。对于搜索 AVL 树来说,这是一件好事,因为时间复杂度被限制为O (log N ) 。

6.8.1 AVL树的Python实现

  • 作为BST,新key以叶节点插入AVL树中

    • 叶节点平衡因子为0,无需重新平衡,但为影响父节点的平衡因子

      • 作为节点插入,父节点平衡因子+1
      • 作为节点插入,父节点平衡因子-1
    • 这重影响会一直传递到根节点,或者某个父节点的平衡因子被调整为0,不再影响到上层平衡因子为止。

      • 无论从-1,1,0,都不会改变子树的高度

1666580992972.png


1666581247599.png

### AVL树的实现
    def _put(self, key, val, currentNode):
        if key < currentNode.key: #输入key小于当前key
            if  currentNode.hasLeftChild() :#当前key存在左子树
                self._put(key,val,currentNode.leftChild) #key插入到当前节点左子树
            else: #没有左子树
                currentNode.leftChild = TreeNode(key,val,parent = currentNode) #key成为左子节点
                self.updateBalance(currentNode.leftChild) #调整因子

        else : #输入key大于当前key
            if currentNode.hasRightChild():#当前key存在右子树
                self._put(key,val,currentNode.rightChild)#key插入到当前节点右子树
            else:
                currentNode.rightChild = TreeNode(key,val,parent= currentNode) ##key成为右子节点
                self.updateBalance(currentNode.rightChild) #调整因子

    def updateBalance(self,Node):
        if Node.balanceFactor > 1 or Node.balanceFactor < -1: #平衡因子在(-1~1)之间
            self.rebalance(Node)  #重新平衡
            return
        if Node.parent != None:#存在父节点
            if Node.isLeftChild():#插入的节点为左子节点 其父节点平衡因子+1
                Node.parent.balanceFactor += 1
            elif Node.isRightChild():#插入的节点为右子节点 其父节点平衡因子-1
                Node.parent.balanceFactor -= 1
            if Node.parent.balanceFactor != 0: #查看父节点平衡因子是否为0 不是0着调整父子节点平衡因子
               self.updateBalance(Node.parent)

AVL树的Python实现:rebalance的重新平衡

  • 右重树,根节点的平衡因子是-2 :做左旋

    • 将右子节点(节点 B)提升为子树的根节点。
    • 将旧根节点(节点 A)作为新根节点的左子节点。
    • 若新根节点(B)原来右左子节点,则将此节点设为A的右子节点(A的右子节点一定为空)

1666583566538.png

  • 左重树,根节点的平衡因子是2 :做右旋

    • 将左子节点(节点 C)提升为子树的根节点。
    • 将旧根节点(节点 E)作为新根节点的右子节点。
    • 如果新根节点(节点 C)已经有一个右子节点(节点 D),将其作为新右子节点(节点 E)的左子节点。

1666663839929.png

1666667448417.png

## 左旋
    def rotateLeft(self,rotRoot):  #rotRoot 旧根
        newRoot = rotRoot.rightChld #newRoot 新根为旧根的右子节点
        rotRoot.rightChld = newRoot.leftChild #旧根的右子节点指向 新根的左子节点
        if newRoot.leftChild.parent != None:#存在newRoot
            newRoot.leftChild.parent = rotRoot #将新根的左子节点指向旧根
        newRoot.leftChild = rotRoot.parent
        if rotRoot.isRoot():#旧节点是树根
            self.root = newRoot #确立新树根
        else: #否则
            if rotRoot.isLeftChild(): #如果旧节点是为左节点
                rotRoot.parent.leftChild = newRoot #
            else:#如果是右子节点
                rotRoot.parent.rightChild = newRoot
        newRoot.leftChild = rotRoot #新节点的左子节点指向旧节点
        rotRoot.parent = newRoot #旧节点的父节点指向新节点
        rotRoot.balanceFactor = rotRoot.balanceFactor +\
                                1 - min(newRoot.balanceFactor,0)
        newRoot.balanceFactor = newRoot.balanceFactor + \
                                1 + max(rotRoot.balanceFactor,0)
   ##右旋
    def rotateRight(self,rotRoot):#rotRoot旧根
        newRoot = rotRoot.leftChild #newRoot 新根为旧根的左子节点
        rotRoot.leftChild  = newRoot.rightChild #旧根的左子节点指向 新根的右子节点
        if newRoot.rightChild.parent != None:#存在新根且不为根节点
            newRoot.rightChild.parent = rotRoot #将新根的右子节点指向旧根
        newRoot.rightChild = rotRoot.parent #新根的右子节点为旧根的父节点
        if rotRoot.isRoot():#旧节点为树根
            self.root = newRoot #确立新树根
        else:#否则
            if rotRoot.isLeftChild():#如果就节点为左节点
                rotRoot.parent.leftChild = newRoot
            else:
                rotRoot.parent.rightChild = newRoot
        newRoot.rightChild = rotRoot  #新节点的右子节点指向旧节点
        rotRoot.balanceFactor = rotRoot.balanceFactor +\
                                1+max(newRoot.balanceFactor,0)
        rotRoot.balanceFactor = newRoot.balanceFactor+\
                                1-min(rotRoot.balanceFactor,0)

左旋对平衡因子的影响

1666752033113.png

1666753084704.png

AVL树的实现:更复杂的情形

  • 下图右重子树,单纯左旋无法实现平衡

1666753375803.png

  • 解决办法:在左旋前检测右子节点的因子, 在右旋前检测左子节点的因子

    • 子节点左重先右再左
    • 子节点右重先左再右

1666754036953.png

def rebalance(self,node):
    if node.balanceFactor < 0:
        if node.rightChild.balanceFactor > 0:
            self.rotateRight(node.rightChild)
            self.rotateLeft(node)
        else:
            self.rotateLeft(node)
    elif node.balanceFactor > 0:
        if node.leftChild.balanceFactor < 0:
            self.rotateLeft(node.leftChild)
            self.rotateRight(node)

    else:
        self.rotateRight(node)

AVL树得实现:复杂度

  • put操作

    • 因为新节点作为叶子节点插入,所以更新所有父节点的平衡因子最多需要log2n 次操作.每一层一次。如果树失衡了,恢复平衡最多需要旋转两次。每次旋转的时间复杂度是O(1) ,所以 put 操作的时间复杂度仍然是O(log2n)
  • get操作

    • 通过维持树的平衡,可以保证 get 方法的时间复杂度为O(log2n)

总结

  • 性能对比

1666839830642.png