树之构建和查找

前面我们根据一棵手工建好的树遍历全部元素,现在我们看怎么查找某个特定的元素。最笨的办法就是遍历一遍,找到为止。但是这样是不可接受的,要是这样,不如就弄个序列得了,何必费力弄出一棵树来呢?树形结构的一大优点就是查找的性能比较好,自然前提是构建树的时候要按照某种次序构建。
举个例子,对于数字序列:["4","2","1","7","3","9"],我们可以按照这个次序构建树:每个节点比左子树的节点大,比右子树的节点小。如下图:

数据结构:​树之构建和查找_数据结构

构建过程是这样的,刚开头,只有一个初始的没有数据的根节点:
拿到序列第一个数4,看到树的根节点,发现里面数据为空,直接把4放进这个节点;

数据结构:​树之构建和查找_数据结构_02

拿到序列的第二个数2,再看树的根节点,不为空,比较大小,一看比节点里的数4要小,所以就看左子树节点,这个时候左子树节点为空,就创建左子树节点,把2放进左子树节点中;

数据结构:​树之构建和查找_数据结构_03

拿到序列的第三个数1,再看树的根节点,不为空,比较大小,一看比节点里的数4要小,所以就看左子树节点,这个时候左子树节点里的数为2,当前数为1,更小,所以继续看下层的左子树节点,这个时候下层的左子树节点为空,就创建下层左子树节点,把1放进左子树节点中;

数据结构:​树之构建和查找_数据结构_04

拿到序列的第四个数7,再看树的根节点,不为空,比较大小,一看比节点里的数4要大,所以就看右子树节点,右子树节点为空,就创建右子树节点,把7放进右子树节点中;

数据结构:​树之构建和查找_数据结构_05

拿到序列的第五个数3,再看树的根节点,不为空,比较大小,一看比节点里的数4要小,所以就看左子树节点,这个时候左子树节点里的数为2,当前数为3,要大,所以看下层的右子树节点,这个时候下层的有子树节点为空,就创建下层右子树节点,把3放进右子树节点中;

数据结构:​树之构建和查找_数据结构_06

拿到序列的第六个数9,再看树的根节点,不为空,比较大小,一看比节点里的数4要大,所以就看右子树节点,右子树节点为7,要大,就继续看下层右子树节点,为空,就创建下层的右子树节点,把9放进下层右子树节点中;

数据结构:​树之构建和查找_数据结构_07

到此构建完毕。
我们从上面的过程看到了,最关键的操作就是正确地把一个数据放到树的正确位置。可以通过如下递归函数实现:

def inserttree(c,node):
    if node.data is None:
        node.data = c
    elif c < node.data:
        if node.left is None:
            node.left = Tree.TreeNode()
        inserttree(c,node.left)
    elif c > node.data:
        if node.right is None:
            node.right = Tree.TreeNode()
        inserttree(c,node.right)

上面的函数参数为c和node,c即为新数据,node是树的某个节点。逻辑很简单,比大小决定放在左子树还是右子树。
有了这个函数,把一个数字序列按照树形结构组织的程序就很简单了:

def loadtree(arr,rootnode):
    for c in arr:
        inserttree(c,rootnode)

测试一下:

array = ["4","2","1","7","3","9","5"]
root =  Tree.TreeNode()
loadtree(array, root)

组织好了一棵树之后,查找很简单,按照同样的比较大小的办法查找就是了,代码如下:

def findtree(nodeobj,node):
    currentnode = node
    while not currentnode is None:
        if nodeobj == currentnode.data:
            return currentnode.data
        elif nodeobj < currentnode.data:
            if not currentnode.left is None:
                currentnode = currentnode.left
            else:
                return None
        elif nodeobj > currentnode.data:
            if not currentnode.right is None:
                currentnode = currentnode.right
            else:
                return None

到此我们介绍了树的构建遍历和查找。
我们回顾一下程序代码,会发现一个技巧性问题:我们对树的节点存的数据是数字和字符,比较大小的时候都是用的==、<和>,这个就限制了数据的适用范围(对数字和字符串可以,对普通对象就不可以),为了得到一个更加通用的数据结构的实现,我们应该抽象出一个NodeObject,由它来自己指定对象间的大小关系。按照Python的规定,执行==、<和>的时候会调用eq__(), _lt_()和__gt()函数,我们因此定义NodeObject如下:

class NodeObject:
    def __init__ (self, data):
        self.data = data
    def __eq__ (self, obj):
        return self.data == obj.data
    def __lt__ (self, obj):
        return self.data < obj.data
    def __gt__ (self, obj):
        return self.data > obj.data

我们构建树的时候,TreeNode里面的data统一使用NodeObject。程序几乎不用修改,只在构建的时候改成如下代码即可:

def loadtree(arr,rootnode):
    for c in arr:
        inserttree(NodeObject(c),rootnode)

查找的时候,也使用这个类:
findtree(NodeObject("9"),root)
这么做的好处就是通用。比如有一个普通类Student,有学号有姓名等等字段,我们也需要按照树形结构存储,也需要比较大小。我们就可以用一个StudentObject类来继承NodeObject类,改写需要改的几个方法就可以了。

class StudentObject(NodeObject):
    def __eq__ (self, obj):
        return self.data["no"] == obj.data["no"]
    def __lt__ (self, obj):
        return self.data["no"] < obj.data["no"]
    def __gt__ (self, obj):
        return self.data["no"] > obj.data["no"]

定义这个类的时候,类名后带上了(NodeObject),按照Python的规定,这表示我们定义了一个子类继承NodeObject类。它将NodeObject类中的方法继承下来,需要改的方法只要重新写就可以了。上面,我们改写了比较大小的三个方法。
创建树的时候,用新的这个类:

def loadtree(arr,rootnode):
    for c in arr:
        inserttree(StudentObject(c),rootnode)

测试一下:

array=[{"no":"4","name":"Alice"},
       {"no":"2","name":"Bob"},
       {"no":"1","name":"Clive"},
       {"no":"7","name":"Donald"},
       {"no":"3","name":"Ellen"},
       {"no":"9","name":"Fiona"}]
root =  Tree.TreeNode()
loadtree(array, root)
findtree(StudentObject({"no":"9","name":"anyone"}),root)

运行结果是找到这个9号学生信息了,但是返回的是{'no': '9', 'name': 'Fiona'}。因为我们再StudentObject中只按照学号进行了比较。
通过这种方式,我们定义了一棵抽象的树,可以处理各种不同的对象。