python数据结构-树(草稿)

定义一:树由节点及连接节点的边构成。

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

定义二:(递归定义)
一棵树要么为空,要么由一个根节点和零棵或多棵子树构成,子树本身也是一棵树。每棵子树的根节点通过一条边连到父树的根节点。

在这里插入图片描述
实现

以下函数创建并操作二叉树

实现树的关键在于选择一个好的内部存储技巧。Python提供两种有意思的方式,我们在选择前会仔细了解这两种方式。

  • 第一种称作“列表之列表”
  • 第二种称作“节点与引用”。

列表之列表
在这里插入图片描述“列表之列表”表示法有个很好的性质,那就是表示子树的列表结构符合树的定义,这样的结构是递归的!

还有一个很好的性质,那就是这种表示法可以推广到有很多子树的情况。如果树不是二叉树,则多一棵子树只是多一个列表。

def BinaryTree(r):
	return [r,[],[]]

def insertLeft(root,newBracnch):
	t=root.pop(1)
	if len(t)>1:
		#如果插入节点有左子树,就把其作为newBracnch的左子树
		root.insert(1,[newBracnch,t,[]])
	else:
		root.insert(1,[newBracnch,[],[]])
	return root
	

def rightLeft(root,newBracnch):
	t=root.pop(2)
	if len(t)>1:
		#如果插入节点有左子树,就把其作为newBracnch的左子树
		root.insert(2,[newBracnch,[],t])
	else:
		root.insert(2,[newBracnch,[],[]])
	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)
print(r)

insertLeft(r,4)
print(r)

6.4.2 节点与引用

节点与引用”表示法的要点是,属性left和right会指向BinaryTree类的其他实例。举例来说,在向树中插入新的左子树时,我们会创建另一个BinaryTree实例,并将根节点的self.leftChild改为指向新树。

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)
		else:
			t=BinaryTree(newNode)
			t.leftChild=self.leftChild 
			self.leftChild=t 
	
	def insertRight(self,newNode):
		if self.rightChild==None:
			self.rightChild=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")
print(r.getLeftChild())

二叉树的应用

解析树

  • ❏ 如何根据完全括号表达式构建解析树;
  • ❏ 如何计算解析树中的表达式;
  • ❏ 如何将解析树还原成最初的数学表达式。

((7 + 3) ∗ (5-2))
可以表示为:

在这里插入图片描述

构建解析树的第一步是将表达式字符串拆分成标记列表。
标记列表中有四种元素: ( , 运算符 , 运算数 , )

定义下列四个规则:

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

按照该规则,下列表达式的树构建过程如下:
[’(’, ‘3’, ‘+’, ‘(’, ‘4’, ‘*’, ‘5’, ‘)’, ‘)’]

在这里插入图片描述
本例表明,在构建解析树的过程中,需要追踪当前节点及其父节点。
如何追踪父节点呢?一个简单的办法就是在遍历这棵树时使用栈记录父节点。每当要下沉至当前节点的子节点时,先将当前节点压到栈中。当要返回到当前节点的父节点时,就将父节点从栈中弹出来。


from pythonds.basic import Stack 
from pythonds.trees import BinaryTree 

def buildParseTree(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 in "+-*/)":
			currentTree.setRootVal(eval(i))
			currentTree.insertRight("")
			pStack.pusk(currentTree)
			currentTree=currentTree.getRightChild()			

		elif i not in "+-*/)":
			currentTree.setRootVal(eval(i))
			paretnt=pStack.pop()
			currentTree=paretnt

		elif i==")":
			currentTree=paretnt.pop()
		else:
			raise ValueError("unkown operator"+i)

	return eTree

计算二叉解析树的递归函数


def evaluate(parseTree):
	opers={"+":operator.add,"-":operator.sub,"*":operator.mul,"/":operatoe.truediv}
	leftC=parseTree.getLeftChild()
	rightC=parseTree.getRightChild()

	if leftC and rightC:
		fn=opers[parseTree.getRootVal()]
		return fn(evaluate(leftC),evaluate(rightC))
	else:
		return parseTree.getRootVal()

6.5.2 树的遍历

前序遍历

在前序遍历中,先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树

(根,左,右)

中序遍历

在中序遍历中,先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
(左根右)

后序遍历

在后序遍历中,先递归地后序遍历左子树,然后递归地后序右遍历子树,最后访问根节点。

(左右根)

将前序遍历算法实现为外部函数

def preorder(tree):
	if tree:
		print(tree.getRootVal())
		#访问根节点
		preorder(tree.getLeftChild())
		#访问左节点
		preorder(tree.getRightChild())
		#访问右节点

将前序遍历算法实现为BinaryTree类的方法

def preorder(self):
	print(self.key)
	if self.leftChild:
		self.leftChild.preorder()
	if self.rightChild:
		self.rightChild.preorder()

哪种实现方式更好呢?在本例中,将preorder实现为外部函数可能是更好的选择。
原因在于,很少会仅执行遍历操作,在大多数情况下,还要通过基本的遍历模式实现别的目标。

后序遍历函数

def postorder(tree):
	if tree!=None:
		preorder(tree.getLeftChild())
		#访问左节点
		preorder(tree.getRightChild())
		#访问右节点
		print(tree.getRootVal())
		#访问根节点

我们已经见识过后序遍历的一个常见用途,那就是计算解析树。


def postordereval(tree):
	opers={"+":operator.add,"-":operator.sub,"*":operator.mul,"/":operatoe.truediv}

	res1=None 
	res2=None
	if tree:
		res1=postordereval(tree.getLeftChild()) 
		res2=postordereval(tree.getRightChild())
		if res1 and res2:
			return opers[tree.getRootVal()](res1,res2)

		else:
			return tree.getRootVal()

中序遍历

def postorder(tree):
	if tree!=None:
		preorder(tree.getLeftChild())
		#访问左节点
		preorder(tree.getRightChild())
		#访问右节点
		print(tree.getRootVal())
		#访问根节点

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

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

对于一些图算法来说,优先级队列是一个有用的数据结构。

现优先级队列的经典方法是使用叫作二叉堆的数据结构。
二叉堆的入队操作和出队操作均可达到O(log n)。

二叉堆有两个常见的变体:
最小堆(最小的元素一直在队首)与最大堆(最大的元素一直在队首)

为了使二叉堆能高效地工作,我们利用树的对数性质来表示它。
你会在6.7.3节学到,
为了保证对数性能,必须维持树的平衡。
平衡的二叉树是指,其根节点的左右子树含有数量大致相等的节点。
在实现二叉堆时,我们通过创建一棵完全二叉树来维持树的平衡。
在完全二叉树中,除了最底层,其他每一层的节点都是满的。

完全二叉树的另一个有趣之处在于,可以用一个列表来表示它,
而不需要采用“列表之列表”或“节点与引用”表示法。

中处于位置p的节点来说,它的左子节点正好处于位置2p;
同理,右子节点处于位置2p+1。
若要找到树中任意节点的父节点,
只需使用Python的整数除法即可。给定列表中位置n处的节点,其父节点的位置就是n/2。

在这里插入图片描述

堆的有序性是指:对于堆中任意元素x及其父元素p, p都不大于x

def __init__(self):
	self.heapList=[0]
	self.currentSize=0
	

在这里插入图片描述

#将插入的元素调整到合适位置
def percUp(self,i):
	while i//2>0:
		if self.heapList[i]<self.heapList[i//2]:
			temp=self.heapList[i//2]
			self.heapList[i//2]=self.heapList[i]
			self.heapList[i]=tmp
		i=i//2 


def insert(self,k):
	self.headList.appned(k)
	self.currentSize=self.currentSize+1
	self.percUp(self.currentSize)

思考题:
找到两个列表中
第k个小的数字?

找到一个列表中
第k个小的数字?

两个列表和一个列表解决问题的关键差异点是哪里呢?

编写delMin方法。

移除根节点之后需要做的操作:
第一步,取出列表中的最后一个元素,将其移到根节点的位置。移动最后一个元素保证了堆的结构性质,但可能会破坏二叉堆的有序性。第二步,将新的根节点沿着树推到正确的位置,以重获堆的有序性

在这里插入图片描述

`def percDown(self,i):
	while (i*2)<=self.currentSize:
		mc=self.minChild(i)
		if self.heapList[i]>self.heapList[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[i*2+1]:
			return i*2
		else:
			return i*2+1

从二叉堆中删除最小的元素

def delMin(self):
    retval=self.heapList[1]
    self.heapList[1]=self.heapList[self.currentSize]
    self.currentSize-=1 
    self.heapList.pop()
    self.perDown(1)
    return retval

根据元素列表构建堆

def buildHeap(self,alist):
    i=len(alist)//2 
    self.currentSize=len(alist)
    self.heapList=[0]+alist[:]
    while (i>0):
        self.perDown(i)
        i=i-1

利用建堆的时间复杂度为O(n)这一点,可以构造一个使用堆为列表排序的算法,使它的时间复杂度为O(n log n)。这个算法留作练习

堆排序算法


6.7 二叉搜索树
回想一下,我们讨论过映射抽象数据类型的两种实现,
它们分别是列表二分搜索和散列表。
本节将探讨二叉搜索树,它是映射的另一种实现。

6.7.1 搜索树的操作

复习一下映射抽象数据类型提供的接口。
你会发现,这个接口类似于Python字典。

6.7.2 搜索树的实现

二叉搜索树性质::小于父节点的键都在左子树中,大于父节点的键则都在右子树中。

在这里插入图片描述

6.7 二叉搜索树

我们已经学习了两种从集合中获取键-值对的方法。回想一下,我们讨论过映射抽象数据类型的两种实现,它们分别是列表二分搜索和散列表。本节将探讨二叉搜索树,它是映射的另一种实现。我们感兴趣的不是元素在树中的确切位置,而是如何利用二叉树结构提供高效的搜索。

6.8 平衡二叉搜索树

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 hasrRightChild(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 isRootNode(self):
        return not self.parent

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

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

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

    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.hasrRightChild():
            self.rightChild.parent=self

现在有了BinarySearchTree和TreeNode,是时候写一个帮我们构建二叉搜索树的put方法了

搜索放置位置的方法:

❏ 从根节点开始搜索二叉树,比较新键与当前节点的键。如果新键更小,搜索左子树。如果新键更大,搜索右子树。
❏ 当没有可供搜索的左(右)子节点时,就说明找到了新键的插入位置。
❏ 向树中插入一个节点,做法是创建一个TreeNode对象,并将其插入到前一步发现的位置上。

为二叉搜索树插入新节点


def put(self,key,val):
    if self.root:
        self._put(key,val,self.root)
        #存在根节点,调用_put函数放置合适位置

    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)

插入方法有个重要的问题:不能正确地处理重复的键。遇到重复的键时,它会在已有节点的右子树中创建一个具有同样键的节点。这样做的结果就是搜索时永远发现不了较新的键。要处理重复键插入,更好的做法是用关联的新值替换旧值。这个修复工作留作练习。

重载[]运算符

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

新节点的插入过程:

在这里插入图片描述
查找键对应的值

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)

检查树中是否有某个键

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

删除一个键

def delete(self,key):
    if self.size>1:
        nodeToremove=self._get(key.self.root)
        if nodeToremove:
            self.remove(nodeToremove)
            self.size=self.size-1
        else:
            raise KeyError("key not in tree")

    elif self.size==1 and self.root.key==key:
            self.root=None
            self.size=self.size-1
    else:
         raise KeyError("key not in tree")

def __delitem__(self,key):
    self.delete(key)

一旦找到待删除键对应的节点,就必须考虑3种情况。

删除的节点没有子节点
在这里插入图片描述删除的节点只有一个子节点

在这里插入图片描述删除的节点有两个子几点

在这里插入图片描述
remove函数实现比较复杂。暂时搁置好了。

pass

6.7.3 搜索树的分析

搁置。。

6.8 平衡二叉搜索树

6.8.3 映射实现总结

本章和第5章介绍了可以用来实现映射这一抽象数据类型的多种数据结构,包括有序列表、散列表、二叉搜索树以及AVL树。表6-1总结了每个数据结构的性能。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值