树的知识以及示例代码

本文对树相关的知识进行梳理,并给出示例代码或者代码模板,方便以后解题。

一些定义

二叉树

二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。树的问题很多都可以由广度优先搜索或深度优先搜索解决。

二叉搜索树(Binary Search Tree)

它或者是一棵空树,或者是具有下列性质的二叉树:

  1. 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 它的左、右子树也分别为二叉搜索树。

遍历相关

注意点

  • 以根访问顺序决定是什么遍历
  • 左子树都是优先右子树

前序遍历

前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。(从上到下)

中序遍历

中序遍历是先遍历左子树,然后访问根节点,然后遍历右子树。对于二叉搜索树,我们可以通过中序遍历得到一个递增的有序序列。

后序遍历

后序遍历是先遍历左子树,然后遍历右子树,最后访问树的根节点。

当你删除树中的节点时,删除过程将按照后序遍历的顺序进行。 也就是说,当你删除一个节点时,你将首先删除它的左节点和它的右边的节点,然后再删除节点本身。

层序遍历

层序遍历就是逐层遍历树结构。广度优先搜索是一种广泛运用在树或图这类数据结构中,遍历或搜索的算法。 该算法从一个根节点开始,首先访问节点本身。 然后遍历它的相邻节点,其次遍历它的二级邻节点、三级邻节点,以此类推。通常,我们使用一个叫做队列的数据结构来帮助我们做广度优先搜索。

BFS,广度/宽度优先。其实就是从上到下,先把每一层遍历完之后再遍历一下一层。使用Queue的数据结构。我们将root节点初始化进队列,通过消耗尾部,插入头部的方式来完成BFS。

运用递归解决树的问题

树可以以递归的方式定义为一个节点(根节点),它包括一个值和一个指向其他节点指针的列表。 递归是树的特性之一。因此,许多树问题可以通过递归的方式来解决。 对于每个递归层级,我们只能关注单个节点内的问题,并通过递归调用函数来解决其子节点问题。

“自顶向下” 的解决方案

“自顶向下” 意味着在每个递归层级,我们将首先访问节点来计算一些值,并在递归调用函数时将这些值传递到子节点。 所以 “自顶向下” 的解决方案可以被认为是一种前序遍历

“自底向上” 的解决方案

“自底向上” 是另一种递归方法。 在每个递归层次上,我们首先对所有子节点递归地调用函数,然后根据返回值和根节点本身的值得到答案。 这个过程可以看作是后序遍历的一种。

二叉树解题策略

一 递归
二 队列 + 迭代 (层次遍历)
三 栈 + 迭代 (非递归遍历)
四 其它

三种基本的遍历方式,都可以用递归来实现。写递归算法的时候,需要注意递归退出条件以及递归操作的表达。

示例代码

  1. 二叉搜索树插入操作:从根节点开始,若插入的值比根节点的值小,则将其插入根节点的左子树;若比根节点的值大,则将其插入根节点的右子树。该操作可使用递归进行实现。
def insert(self, root, val):
        '''二叉搜索树插入操作'''
        if root == None:
            root = TreeNode(val)
        elif val < root.val:
            root.left = self.insert(root.left, val)
        elif val > root.val:
            root.right = self.insert(root.right, val)
        return root
  1. 二叉搜索树的查询操作:从根节点开始查找,待查找的值是否与根节点的值相同,若相同则返回True;否则,判断待寻找的值是否比根节点的值小,若是则进入根节点左子树进行查找,否则进入右子树进行查找。该操作使用递归实现。
def query(self, root, val):
        '''二叉搜索树查询操作'''
        if root == None:
            return False
        if root.val == val:
            return True
        elif val < root.val:
            return self.query(root.left, val)
        elif val > root.val:
            return self.query(root.right, val)
  1. 查找二叉搜索树中的最大(小值)

查找最小值:从根节点开始,沿着左子树一直往下,直到找到最后一个左子树节点,按照定义可知,该节点一定是该二叉搜索树中的最小值节点。

def findMin(self, root):
        '''查找二叉搜索树中最小值点'''
        if root.left:
            return self.findMin(root.left)
        else:
            return root

查找最大值:从根节点开始,沿着右子树一直往下,直到找到最后一个右子树节点,按照定义可知,该节点一定是该二叉搜索树中的最大值节点。

def findMax(self, root):
        '''查找二叉搜索树中最大值点'''
        if root.right:
            return self.findMax(root.right)
        else:
            return root
  1. 二叉搜索树删除节点操作
def delNode(self, root, val):
        '''删除二叉搜索树中值为val的点'''
        if root == None:
            return 
        if val < root.val:
            root.left = self.delNode(root.left, val)
        elif val > root.val:
            root.right = self.delNode(root.right, val)
        # 当val == root.val时,分为三种情况:只有左子树或者只有右子树、有左、右子树、即无左子树又无右子树
        else:
            if root.left and root.right:
                # 既有左子树又有右子树,则需找到右子树中最小值节点
                temp = self.findMin(root.right)
                root.val = temp.val
                # 再把右子树中最小值节点删除
                root.right = self.delNode(root.right, temp.val)
            elif root.right == None and root.left == None:
                # 左右子树都为空
                root = None
            elif root.right == None:
                # 只有左子树
                root = root.left
            elif root.left == None:
                # 只有右子树
                root = root.right
        return root
  1. 二叉搜索树打印操作:实现二叉搜索树的中序遍历,并打印出来。该方法打印出来的数列将是按照递增顺序排列。
def printTree(self, root):
        # 打印二叉搜索树(中序打印,有序数列)
        if root == None:
            return 
        self.printTree(root.left)
        print(root.val, end = ' ')
        self.printTree(root.right)
  1. 验证二叉搜索树
def isValidBST(self, root):
        
        if root is None:
            return True
        
        s = [(root, float('-inf'), float('inf'))]
        while len(s) > 0:
            node, low, up = s.pop()
            if node.left is not None:
                if node.left.val <= low or node.left.val >= node.val:
                    return False
                s.append((node.left, low, node.val))
            if node.right is not None:
                if node.right.val <= node.val or node.right.val >= up:
                    return False
                s.append((node.right, node.val, up))
        return True
  1. 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 保证原始二叉搜索树中不存在新值。
def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
        
        if root is None:
            return TreeNode(val)
        
        if val > root.val:
            root.right = self.insertIntoBST(root.right, val)
        else:
            root.left = self.insertIntoBST(root.left, val)
        
        return root
  1. 给定一个二叉树,判断它是否是高度平衡的二叉树。
def isBalanced(self, root: TreeNode) -> bool:
        
        # post-order iterative
        
        s = [[TreeNode(), -1, -1]]
        node, last = root, None
        while len(s) > 1 or node is not None:
            if node is not None:
                s.append([node, -1, -1])
                node = node.left
                if node is None:
                    s[-1][1] = 0
            else:
                peek = s[-1][0]
                if peek.right is not None and last != peek.right:
                    node = peek.right
                else:
                    if peek.right is None:
                        s[-1][2] = 0
                    last, dl, dr = s.pop()
                    if abs(dl - dr) > 1:
                        return False
                    d = max(dl, dr) + 1
                    if s[-1][1] == -1:
                        s[-1][1] = d
                    else:
                        s[-1][2] = d
        
        return True

递归模板

递归实现二叉树遍历非常简单,不同顺序区别仅在于访问父结点顺序

前序遍历先访问根节点,再前序遍历左子树,再前序遍历右子树

def preorder_rec(root):
    if root is None:
        return
    visit(root)
    preorder_rec(root.left)
    preorder_rec(root.right)
    return

def inorder_rec(root):
    if root is None:
        return
    inorder_rec(root.left)
    visit(root)
    inorder_rec(root.right)
    return

def postorder_rec(root):
    if root is None:
        return
    postorder_rec(root.left)
    postorder_rec(root.right)
    visit(root)
    return

本质上是图的DFS的一个特例,因此可以用栈来实现

# 前序非递归
    def preorderTraversal(self, root) :
        preorder = []
        if root is None:
            return preorder
        
        stack = [root]
        while len(stack) > 0:
            node = stack.pop()
            preorder.append(node.val)
            if node.right is not None:
                stack.append(node.right)
            if node.left is not None:
                stack.append(node.left)
        
        return preorder
# 中序非递归
def inorderTraversal(self, root):
    stack, inorder = [], []
    node = root
    while len(stack) > 0 or node is not None:
        if node is not None:
            stack.append(node)
            node = node.left
        else:
            node = stack.pop()
            inorder.append(node.val)
            node = node.right
    return inorder

    # 后序非递归
    def postorderTraversal(self, root):
        s, postorder = [], []
        node, last_visit = root, None
        
        while len(s) > 0 or node is not None:
            if node is not None:
                s.append(node)
                node = node.left
            else:
                peek = s[-1]
                if peek.right is not None and last_visit != peek.right:
                    node = peek.right
                else:
                    last_visit = s.pop()
                    postorder.append(last_visit.val)
        
        
        return postorder

核心就是:根节点必须在右节点弹出之后,再弹出

二叉树算法的设计的总路线:明确一个节点要做的事情,然后剩下的事抛给框架。

def traverse(root):
    // root 需要做什么?在这做。

    // 其他的不用 root 操心,抛给框架
    traverse(root.left)
    traverse(root.right)

如何把二叉树所有的节点中的值加一?

def plusOne(root):
    if not root: return None
    root.val += 1
    plusOne(root.left)
    plusOne(root.right)

DFS模板

DFS 深度搜索-从下向上(分治法)

def preorderTraversal(self, root):
    if root is None:
        return []
    
    left_result = self.preorderTraversal(root.left)
    right_result = self.preorderTraversal(root.right)
    
    return [root.val] + left_result + right_result

DFS 深度搜索(从上到下) 和分治法区别:前者一般将最终结果通过指针参数传入,后者一般递归返回结果最后合并

# BFS 层次遍历
def levelOrder(self, root: TreeNode):
    from collections import deque    
    levels = []
    if root is None:
        return levels
    
    bfs = deque([root])
    
    while len(bfs) > 0:
        levels.append([])
        
        level_size = len(bfs)
        for _ in range(level_size):
            node = bfs.popleft()
            levels[-1].append(node.val)
            
            if node.left is not None:
                bfs.append(node.left)
            if node.right is not None:
                bfs.append(node.right)
    
    return levels

在递归中,如果层级过深,我们很可能保存过多的临时变量,导致栈溢出。函数调用的参数是通过栈空间来传递的,在调用过程中会占用线程的栈资源。而递归调用,只有走到最后的结束点后函数才能依次退出,而未到达最后的结束点之前,占用的栈空间一直没有释放,如果递归调用次数过多,就可能导致占用的栈资源超过线程的最大值,从而导致栈溢出,导致程序的异常退出。

99%的递归转非递归,都可以通过栈来进行实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343