【OJ算法训练】06__十二月__树

本文介绍了树的基本概念,重点讨论了二叉树的定义、特点、分类(如完美二叉树、完全二叉树和AVL树),以及如何用Python实现二叉树的结构、遍历算法(前序、中序、后序和层次遍历)。此外,还展示了如何使用matplotlib和pygraphviz进行树的可视化,包括LeetCode题目实例演示。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考资料,以下内容来自优快云/知乎大佬们:

https://blog.youkuaiyun.com/wannuoge4766/article/details/83998377

https://zhuanlan.zhihu.com/p/90255760

... ...

定义

树,由n(n >= 1)个有限节点组成的具有层次关系的,具有倒立的树状结构的数据集合,根朝上,叶朝下。

 特点:

1. 每个节点有零个或多个子节点;

2. 没有父节点的节点称为根节点;

3. 每一个非根节点有且只有一个父节点;

4. 除了根节点外,每个子节点可以分为多个不相交的子树;

术语:度,根节点,叶子节点,父节点,子节点,深度,高度……

二叉树

每个节点最多含有两个子树的树。

完满二叉树:

除叶子节点之外的每一个节点都有两个孩子节点。(除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。)

完全二叉树:

除最后一层之外的其他每一层都被完全填充,并且所有节点都保持向左对齐。(若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边。)

 完美二叉树:

除叶子节点之外的每一个节点都有两个孩子节点,每一层(包括最后一层)都被完全填充。

二叉树遍历方式

前序遍历:根节点->左子树->右子树(根左右)

中序遍历:左子树->根节点->右子树(左根右)

后序遍历:左子树->右子树->根节点(左右根)

前中后对应的根节点位置,即前序根节点在最前,中序根节点在中间,后序根节点在最后

层次遍历:从上至下逐层自左至右遍历(上图层次遍历的串行是:-+/a*efb-cd)

二叉查找树

Binary Search Tree,又称二叉搜索树、有序二叉树、排序二叉树。

1.若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
2.若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
3.任意节点的左、右子树也分别为二叉查找树;
4.没有键值相等的节点。

平衡二叉树

AVL树:当且仅当任何节点的两棵子树的高度差不大于1的二叉树,严格的平衡二叉树。适用于插入删除次数比较少,但查找多的情况。AVL树的左右子节点也是AVL树。

时间复杂度O(n)。

实现方式

Python树

本次只提供python的实现方式,C/C++可自行了解。

# !/usr/bin/env python
# coding: utf-8

from collections import deque
from typing import List


# 节点类
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right


# 初始化树(参考大佬https://www.cnblogs.com/yezigege/p/13441377.html)
class BinaryTree:
    def __init__(self):
        self.root = None

    def init_tree(self, values=None):
        if not values:
            return None
        self.root = TreeNode(values[0])
        queue = deque([self.root])
        index = 1
        while index < len(values):
            node = queue.popleft()
            if node:
                node.left = TreeNode(values[index]) if values[index] else None
                queue.append(node.left)
                if index + 1 < len(values):
                    node.right = TreeNode(values[index + 1]) if values[index + 1] else None
                    queue.append(node.right)
                    index += 1
                index += 1

Python树可视化

一些比较好玩的尝试,用到了matplotlib.pyplot、networkx和pygraphviz两种。

1. networkx

networkx详细说明请参考大佬文章:

https://blog.youkuaiyun.com/CUFEECR/article/details/103007712

https://blog.youkuaiyun.com/weixin_44301621/article/details/104837275

# !/usr/bin/env python
# coding: utf-8

import matplotlib.pyplot as plt
import networkx as nx


# 注意:整体代码有简化
def draw_tree(node):
    pos = dict()

    def create_graph_two(tree_graph, _node, x=0, y=0, layer=1):
        pos[_node.val] = (x, y)
        if _node.left:
            tree_graph.add_edge(_node.val, _node.left.val)
            l_x, l_y = x - 1 / 2 ** layer, y - 1
            l_layer = layer + 1
            create_graph_two(tree_graph, _node.left, x=l_x, y=l_y, layer=l_layer)
        if _node.right:
            tree_graph.add_edge(_node.val, _node.right.val)
            r_x, r_y = x + 1 / 2 ** layer, y - 1
            r_layer = layer + 1
            create_graph_two(tree_graph, _node.right, x=r_x, y=r_y, layer=r_layer)
        return tree_graph, pos

    graph = nx.DiGraph()
    graph, pos = create_graph_two(graph, node)
    fig, ax = plt.subplots(figsize=(10, 10))  # 比例可以根据树的深度适当调节
    nx.draw_networkx(graph, pos, ax=ax, node_size=1000, node_color='green', font_size=16)
    plt.show()

2. pygraphviz

pygraphviz的安装可能会遇到问题,本次尝试(Win10 X64)是先安装了graphviz,再安装pygraphviz的whl,还重启了PyCharm才OK的,Linux有所不同。

下载graphviz:https://graphviz.org/download/

下载pygraphviz:https://www.lfd.uci.edu/~gohlke/pythonlibs/#pygraphviz

注意:pygraphviz需要与Python版本匹配。

pygraphviz详细说明请参考知乎大佬:

https://zhuanlan.zhihu.com/p/104636240

AGraph参数:

参数说明取值
directed有向图False/True
strict简单图False/True
nodesep同级节点最小间距
ranksep不同级节点最小间距
rankdir绘图方向TB (从上到下), LR (从左到右), BT (从下到上), RL (从右到左)
splines线条类型ortho (直角), polyline (折线), spline (曲线), line (线条), none (无)
concentrate合并线条 (双向箭头)False/True
bgcolor背景颜色
compound多子图时,不允许子图相互覆盖False/True
normalize以第一个节点作为顶节点False/True

示例代码:

# !/usr/bin/env python
# coding: utf-8
​
import pygraphviz as pgv
​

# pygraphviz 添加节点/边即可写入dot并导出和绘制,比较简单
# 又写了一个公用的方法,获取二叉树父子节点组合列表,遍历进行add_edge
​# 注意:整体代码有简化
if __name__ == '__main__':
    tree_pic = pgv.AGraph(directed=True, strict=True)
    tree_pic.add_edge(1, 2)
    tree_pic.add_edge(1, 3)
    tree_pic.add_edge(2, 5)
    tree_pic.add_edge(2, 4)
    tree_pic.add_edge(5, 6)
    tree_pic.add_edge(5, 7)
    tree_pic.add_edge(3, 8)
    tree_pic.add_edge(3, 9)
    tree_pic.add_edge(8, 10)
    tree_pic.add_edge(8, 11)
    tree_pic.graph_attr['epsilon'] = '0.01'
    print(tree_pic.string())  # print dot file to standard output
    tree_pic.write('tree_pic.dot')
    tree_pic.layout('dot')  # layout with dot
    tree_pic.draw('tree_pic.png')
​

LeetCode题目

94.二叉树的中序遍历

https://leetcode-cn.com/problems/binary-tree-inorder-traversal/

给定一个二叉树的根节点 root ,返回它的 中序 遍历。

解题思路

本地调试时候发现示例1给的列表"[1, None, 2, 3]",初始化树有点问题。普通的(2i+1, 2i+2)方法,对于元素个数小于(2*高度-1)的情况右子树会少一部分。思考和查询之后,参考大佬办法利用deque解决此问题。

Python3方法

# !/usr/bin/env python
# coding: utf-8
​
from collections import deque
from typing import List
​
​
# 节点类 + 初始化树
# 具体代码见上文
​
​
class Solution:
    @staticmethod
    def inorder_traversal(root: TreeNode) -> List[int]:
        stack, res = [root], []
​
        while stack:
            i = stack.pop()
            if isinstance(i, TreeNode):
                stack.extend([i.right, i.val, i.left])
            elif isinstance(i, int):
                res.append(i)
​
        return res
​
​
if __name__ == '__main__':
    tree_data = [1, None, 2, 3]
    # tree_data = [1, 2, 3, None, 4, 5]
​
    rt = BinaryTree()
    rt.init_tree(tree_data)
​
    result = Solution()
    print(result.inorder_traversal(rt.root))

​95.不同的二叉搜索树 II

https://leetcode-cn.com/problems/unique-binary-search-trees-ii/

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。

解题思路

二叉搜索树关键性质:根节点值大于左子树所有节点值,小于右子树所有节点值。且左子树、右子树同为二叉搜索树。

回溯。

增加了一个打印的方法。

Python3方法

# !/usr/bin/env python
# coding: utf-8
​
from typing import List
​
​
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
​
​
class Solution:
    @staticmethod
    def generate_trees(n: int) -> List[TreeNode]:
        dp = []
        for i in range(n + 1):
            dp.append([])
            for j in range(n + 1):
                if i == j:
                    dp[i].append([TreeNode(i)])
                elif i < j:
                    dp[i].append([])
                else:
                    dp[i].append([None])
​
        dp[0][0] = None
        for i in range(n - 1, 0, -1):
            for j in range(i + 1, n + 1):
                for r in range(i, j + 1):
                    left = r + 1 if r < j else r
                    for x in dp[i][r - 1]:
                        for y in dp[left][j]:
                            node = TreeNode(r)
                            node.left = x
                            node.right = y
                            if r == j:
                                node.right = None
                            dp[i][j].append(node)
        return dp[1][n]
​
    @staticmethod
    def generate_trees_2(n: int) -> List[TreeNode]:
        def get_trees(start, end):
            if start > end:
                return [None, ]
​
            all_trees = []
            for i in range(start, end + 1):  # 枚举可行根节点
                # 获得所有可行的左子树集合
                left_trees = get_trees(start, i - 1)
​
                # 获得所有可行的右子树集合
                right_trees = get_trees(i + 1, end)
​
                # 从左子树集合中选出一棵左子树,从右子树集合中选出一棵右子树,拼接到根节点上
                for l in left_trees:
                    for r in right_trees:
                        curr_tree = TreeNode(i)
                        curr_tree.left = l
                        curr_tree.right = r
                        all_trees.append(curr_tree)
​
            return all_trees
​
        return get_trees(1, n) if n else []
​
    @staticmethod
    def print_trees(trees):
        tree_list = list()
        for tree in trees:
            if not tree:
                return []
            res = [tree]
            nums = []
​
            while res:
                node = res.pop(0)
                nums.append(node.val)
​
                if node.left:
                    res.append(node.left)
                if node.right:
                    res.append(node.right)
            tree_list.append(nums)
​
        print(tree_list)
​
​
if __name__ == '__main__':
    num = 3
​
    result = Solution()
    # dp
    result.print_trees(result.generate_trees(num))
    # 回溯
    result.print_trees(result.generate_trees_2(num))
​

98.验证二叉搜索树

https://leetcode-cn.com/problems/validate-binary-search-tree/

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。

解题思路

根据二叉搜索树的定义判断。

Python3方法

# !/usr/bin/env python
# coding: utf-8
​
from collections import deque
​
INT_MAX = 2 ** 31
INT_MIN = -(2 ** 31) - 1
​
​
# 节点类 + 初始化树
# 具体代码见上文
​
​
class Solution:
    def __init__(self):
        self.is_ok = True
​
    def is_valid_bst(self, root: TreeNode) -> bool:
        self.validate(root, INT_MIN, INT_MAX)
        return self.is_ok
​
    def validate(self, node, low, high):
        if self.is_ok:
            if node.val >= high or node.val <= low:
                self.is_ok = False
            if node.left:
                self.validate(node.left, low, node.val)
            if node.right:
                self.validate(node.right, node.val, high)
​
​
if __name__ == '__main__':
    # tree_data = [2, 1, 3]
    tree_data = [5, 1, 4, None, None, 3, 6]
​
    rt = BinaryTree()
    rt.init_tree(tree_data)
 
    res = Solution()
    print(res.is_valid_bst(rt.root))
​

总结

十二月的算法训练,树。因为工作和学习一直在用Python,写C++手生了都。本月也更多时间沉迷于更好玩的可视化,所以直接略去了三道题的C++方法解答。还有就是,做题过程中发现C++在初始化树时候(个人)不太好实现,94题给的示例1列表,Python方法转树结构一开始都没搞定。

三道题做完之后多想了点,于是就有了树的可视化这一小节内容。工作中遇到有同事在用DiGraph方法,正好就借此学习学习。本来可以独立成篇,不过先就这样吧,毕竟主题是算法。

OJ算法训练,第一阶段就此结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值