二叉树体现了节点的关系,和链表类似,以节点组成

二叉树是树的一种,区别在于二叉树会严格地将节点二分地排列。

相关概念

  • 根节点:二叉树顶层节点,且没有父节点
  • 叶节点:没有子节点的节点
  • 边:连接两个节点的线
  • 二叉树的高度:从根节点到最远叶节点所经过的边的数量
  • 节点所在的层:从顶至底递增,根节点在 1 层
  • 节点的度:节点的子节点的数量。在二叉树中,度的取值范围是 0、1、2
  • 节点的深度:从根节点到该节点所经过的边的数量
  • 节点的高度:从距离该节点最远的叶节点到该节点所经过的边的数量

构建二叉树

由于树由节点组成,所以需要先实现节点:

1
2
3
4
5
6
7
8
class TreeNode<T>(T value)
{
public T Value { get; set; } = value; // 节点的值

public TreeNode<T> LeftChild { get; set; } // 左子节点

public TreeNode<T> RightChild { get; set; } // 右子节点
}

与链表不同的是,二叉树的节点需要保存对两个子节点的引用

建立联系

通过将根节点的LeftChildRightChild指向另一个TreeNode以建立节点间的联系:

1
2
3
4
5
var root = new TreeNode<int>(1);
var leftChild = new TreeNode<int>(2);
var rightChild = new TreeNode<int>(3);
root.LeftChild = leftChild; // 1 -> 2
root.RightChild = rightChild; // 1 -> 3

插入与删除节点

1
2
3
var newNode = new TreeNode<int>(4);
root.LeftChild = newNode; // 将父节点的左子节点指向新节点
newNode.LeftChild = leftChild; // 将新节点的左子节点指向曾经的左子节点
1
root.LeftChild = leftChild;  // 将父节点的左子节点指向原来的左子节点,被删除的节点将不被引用

遍历二叉树

由于二叉树是非线性的,且数据以类似链表的方式存储为节点,那么简单的遍历是行不通的。

二叉树主要有以下几种遍历算法:

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

层序遍历

层序遍历会逐层地并从左往右地遍历二叉树的节点,思路上等同于广度优先搜索

首先,创建一个简单的二叉树,与上图相同:

1
2
3
4
5
6
7
var root = new TreeNode<int>(1);
root.LeftChild = new TreeNode<int>(2);
root.RightChild = new TreeNode<int>(3);
root.LeftChild.LeftChild = new TreeNode<int>(4);
root.LeftChild.RightChild = new TreeNode<int>(5);
root.RightChild.LeftChild = new TreeNode<int>(6);
root.RightChild.RightChild = new TreeNode<int>(7);

之后采取层序遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IEnumerable<T> LevelOrder<T>(TreeNode<T> root)
{
var queue = new Queue<TreeNode<T>>(); // 初始化队列
queue.Enqueue(root); // 根节点入队
while (queue.Count != 0)
{
TreeNode<T> node = queue.Dequeue();
yield return node.Value; // 迭代地返回节点值
if (node.LeftChild != null)
queue.Enqueue(node.LeftChild); // 左子节点入队
if (node.RightChild != null)
queue.Enqueue(node.RightChild); // 右子节点入队
}
}

遍历结果:1 2 3 4 5 6 7

前、中、后序遍历

与层序遍历不同,前、中、后序遍历的思路上是与深度优先搜索相符的,故通常采用递归实现。

图中的虚线绕了二叉树一圈,途中遇到的三个位置对应三种遍历的顺序。

在递归过程中,会将每一个子节点视为下一个根节点,从而组成子树来判断。

前序遍历:

1
2
3
4
5
6
7
8
/* 根节点 -> 左子树 -> 右子树 */
void PreOrder<T>(TreeNode<T> root)
{
if (root == null) return;
Console.WriteLine(root.Value); // 1 2 4 5 3 6 7
PreOrder(root.LeftChild);
PreOrder(root.RightChild);
}

中序遍历:

1
2
3
4
5
6
7
8
/* 左子树 -> 根节点 -> 右子树 */
void InOrder<T>(TreeNode<T> root)
{
if (root == null) return;
InOrder(root.LeftChild);
Console.WriteLine(root.Value); // 4 2 5 1 6 3 7
InOrder(root.RightChild);
}

后序遍历:

1
2
3
4
5
6
7
/* 左子树 -> 右子树 -> 根节点 */
void PostOrder<T>(TreeNode<T> root)
{
if (root == null) return;
PostOrder(root.LeftChild);
PostOrder(root.RightChild);
Console.WriteLine(root.Value); // 4 5 2 6 7 3 1