Python 二叉树

树是一种由边连接的节点组成的层次数据结构。

每个节点包含一个值和对其子节点的引用。

二叉树

二叉树是一种树数据结构,其中每个节点最多可以有两个子节点,即左子节点和右子节点。

这种限制,即一个节点最多可以有两个子节点,为我们带来了许多好处:

  • 遍历、搜索、插入和删除等算法更容易理解、实现和运行得更快。
  • 在二叉搜索树(BST)中保持数据有序可以使搜索非常高效。
  • 使用 AVL 二叉树等例子,可以更容易地平衡具有有限数量子节点的树。
  • 二叉树可以表示为数组,使树更加内存高效。

二叉树实现

R A B C D E F G

上面的二叉树可以像链表一样实现,只是我们不是将每个节点链接到一个下一个节点,而是创建一个结构,其中每个节点可以链接到其左子节点和右子节点。

实例

在 Python 中创建二叉树:

class TreeNode:
  def __init__(self, data):
    self.data = data
    self.left = None
    self.right = None

root = TreeNode('R')
nodeA = TreeNode('A')
nodeB = TreeNode('B')
nodeC = TreeNode('C')
nodeD = TreeNode('D')
nodeE = TreeNode('E')
nodeF = TreeNode('F')
nodeG = TreeNode('G')

root.left = nodeA
root.right = nodeB

nodeA.left = nodeC
nodeA.right = nodeD

nodeB.left = nodeE
nodeB.right = nodeF

nodeF.left = nodeG

# 测试
print("root.right.left.data:", root.right.left.data)

亲自试一试

二叉树的类型

二叉树有不同的变体或类型,讨论这些有助于更好地理解二叉树的结构。

现在也值得一提的是不同类型的二叉树,因为这些词汇和概念将在本教程的后面部分使用。

以下是不同类型二叉树结构的简短解释,以及这些结构的示意图,以便尽可能容易理解。

平衡二叉树:对于树中的每个节点,其左子树和右子树的高度差最多为 1。

完全二叉树:除了最后一层可能不满外,其他所有层都满,且最后一层从左到右填充。完全二叉树的性质意味着它也是平衡的。

二叉树:每个节点要么有0个子节点,要么有 2 个子节点。

完美二叉树:所有叶子节点都在同一层,这意味着所有层都满,且所有内部节点都有两个子节点。完美二叉树的性质意味着它也是满的、平衡的和完全的。

11 7 15 3 9 13 19 18
Balanced
11 7 15 3 9 13 19 2 4 8
Complete and balanced
11 7 15 13 19 12 14
Full
11 7 15 3 13 19 9
Perfect, full, balanced and complete

二叉树遍历

遍历树意味着逐个访问每个节点。

由于数组和链表是线性数据结构,因此只有一种明显的遍历方式:从第一个元素或节点开始,继续访问下一个,直到访问完所有元素或节点。

但由于树可以向不同方向分支(非线性),因此有几种不同的遍历树的方式。

树遍历方法主要有两大类:

广度优先搜索(BFS):在访问下一层的节点之前,先访问同一层的所有节点。这意味着树的探索方向更偏向于横向。

深度优先搜索(DFS):遍历一直向下到叶子节点,以向下的方向逐个分支探索树。

DFS 有三种不同的类型:

  • 前序遍历
  • 中序遍历
  • 后序遍历

二叉树的前序遍历

前序遍历是深度优先搜索的一种类型,其中每个节点都按照一定的顺序被访问。

前序遍历是通过首先访问根节点,然后递归地对左子树进行前序遍历,最后递归地对右子树进行前序遍历来完成的。它用于创建树的副本、表达式树的前缀表示等。

这种遍历被称为“前序”,是因为节点在递归前序遍历左子树和右子树之前被访问。

以下是前序遍历的代码示例:

实例

前序遍历:

def preOrderTraversal(node):
  if node is None:
    return
  print(node.data, end=", ")
  preOrderTraversal(node.left)
  preOrderTraversal(node.right)

亲自试一试

第一个被打印的节点是节点 R,因为前序遍历的工作方式是首先访问或打印当前节点(第 4 行),然后再递归地调用左子节点和右子节点(第 5 行和第 6 行)。

preOrderTraversal() 函数会一直递归地遍历左子树(第 5 行),然后再去遍历右子树(第 6 行)。因此,接下来被打印的节点是 'A',然后是 'C'。

第一次当参数 nodeNone 时,是当节点C的左子节点作为参数给出时(C 没有左子节点)。

在第一次调用 C 的左子节点返回 None 后,C 的右子节点也返回 None,然后递归调用继续回溯,以便接下来打印 A 的右子节点 D。

代码继续回溯,以便打印 R 的右子树中的其余节点。

二叉树的中序遍历

中序遍历是深度优先搜索的一种类型,其中每个节点都按照一定的顺序被访问。

中序遍历首先递归地对左子树进行中序遍历,然后访问根节点,最后递归地对右子树进行中序遍历。这种遍历主要用于二叉搜索树,其中它按升序返回值。

这种遍历之所以被称为“中序”,是因为节点在递归函数调用之间被访问。节点在左子树的中序遍历之后被访问,在右子树的中序遍历之前被访问。

以下是中序遍历的代码示例:

实例

创建中序遍历:

def inOrderTraversal(node):
  if node is None:
    return
  inOrderTraversal(node.left)
  print(node.data, end=", ")
  inOrderTraversal(node.right)

亲自试一试

inOrderTraversal() 函数会一直以当前左子节点作为参数调用自身(第 4 行),直到该参数为 None 且函数返回(第 2-3 行)。

第一次当参数 nodeNone 时,是当节点 C 的左子节点作为参数给出时(C 没有左子节点)。

之后,打印节点 C 的 data 部分(第 5 行),这意味着 'C' 是第一个被打印的内容。

然后,将节点 C 的右子节点作为参数给出(第 6 行),该节点为 None,因此函数调用返回而不执行其他任何操作。

在 'C' 被打印后,之前的 inOrderTraversal() 函数调用继续运行,以便 'A' 被打印,然后是 'D',接着是 'R',依此类推。

二叉树的后序遍历

后序遍历是深度优先搜索的一种类型,其中每个节点都按照一定的顺序被访问。

后序遍历首先递归地对左子树和右子树进行后序遍历,然后访问根节点。它用于删除树、表达式树的后缀表示等。

这种遍历之所以被称为“后序”,是因为在递归调用左子节点和右子节点之后才访问节点。

以下是后序遍历的代码示例:

实例

Post-order Traversal:
def postOrderTraversal(node):
  if node is None:
    return
  postOrderTraversal(node.left)
  postOrderTraversal(node.right)
  print(node.data, end=", ")

亲自试一试

postOrderTraversal() 函数会一直递归地遍历左子树(第 4 行),直到当 C 的左子节点作为 node 参数调用时返回 None。

在 C 的左子节点返回 None 后,第 5 行运行,C 的右子节点也返回 None,然后打印字母 'C'(第 6 行)。

这意味着 C 是在其左子节点和右子节点被遍历之后被访问或打印的,这就是为什么它被称为“后序”遍历。

postOrderTraversal() 函数继续回溯到之前的递归函数调用,因此下一个被打印的节点是 'D',然后是 'A'。

函数继续回溯并打印节点,直到所有节点都被打印或访问。