一、概述
后面我们会讲到很多种类的二叉树,它们都是为了弥补原有二叉树结构的缺陷或者解决一些实际问题而演变而来的,所以掌握好二叉树的基本操作对后面的学习是非常重要的。上一章已经讲到二叉树的三种存储结构:顺序存储、二叉链表存储和三叉链表存储,这里选择使用二叉链表存储来实现关于二叉树的基本操作。
private class Node { private T data; private Node left; private Node right; }
二、二叉树基本操作分析
根据二叉树本身的节点和层次的特点,所以二叉树的大部分操作都可以使用递归来实现,后面讲到的基本操作会包含构建二叉树、计算节点和深度、遍历二叉树等方面。
1. 二叉树的构建
二叉树的构建方法有很多,这里选择比较简单的一种,基于一个数组来构建一棵二叉树,上一章中提到了在二叉树的顺序存储中,节点的相对位置可以表示节点之间的关系,例如一个节点的位置为 i,那么它的左孩子位置为 2i+1,右孩子为 2i+2,根据这一特点,我们可以使用数组来实现构建一棵二叉树。
基于数组构建二叉树代码实现如下:
public void init(T[] arr) { if (arr == null || arr.length == 0) { return; } List<Node> list = new ArrayList<>(); for (T t : arr) { list.add(new Node(t, null, null)); } root = list.get(0); for (int i = 0; i < list.size() / 2; i++) { if ((i * 2 + 1) < list.size()) { list.get(i).left = list.get(i * 2 + 1); } if ((i * 2 + 2) < list.size()) { list.get(i).right = list.get(i * 2 + 2); } } }
2. 计算节点个数
2.1 计算所有节点个数
计算二叉树的节点个数,通过递归遍历所有节点进行计数,你可以想象成每次递归都是把当前节点的孩子节点拆分成左右两个子树进行操作。
private int nodeCount(Node node) { if (node == null) { return 0; } return nodeCount(node.left) + nodeCount(node.right) + 1; }
2.2 计算叶子节点个数
计算二叉树的叶子节点个数,在基于前面计算所有节点上,增加一个判断为叶子节点的条件,然后只针对符合该条件的节点进行计数。
private int leafNodeCount(Node node) { if (node == null) { return 0; } if (node.left == null && node.right == null) { return 1; } return leafNodeCount(node.left) + leafNodeCount(node.right); }
3. 计算深度
计算二叉树的深度看上去和计算节点个数的方法一样,只是递归方法一样,计数的逻辑不一样。因为孩子节点的深度等于父亲节点的深度加1,基于这一推论,可以实现计算二叉树深度的递归算法。
private int depth(Node node) { if (node == null) { return 0; } return Math.max(depth(node.left), depth(node.right)) + 1; }
4. 深度遍历
基于深度优先遍历,沿着树的深度来遍历节点,尽可能深的搜索树的分支。它按照访问节点的顺序不同可以分为:先序遍历、中序遍历和后序遍历。
4.1 先序遍历
先序遍历是先访问节点数据,再遍历左子树,然后遍历右子树,节点访问顺序可以简写为DLR(Data->Left->Right),可以使用递归来简单地实现。
先序遍历代码实现如下:
private void preOrderTraverse(Node node) { if (node == null) { return; } operate(node); preOrderTraverse(node.left); preOrderTraverse(node.right); } ope
operate(node)
是对当前遍历节点的操作方法,具体对节点的操作逻辑可以自定义实现,比如在控制台打印节点数据。
4.2 中序遍历
中序遍历是先遍历左子树,再访问节点数据,然后遍历右子树,节点访问顺序可以简写为LDR。
中序遍历代码实现如下:
private void inOrderTraverse(Node node) { if (node == null) { return; } inOrderTraverse(node.left); operate(node); inOrderTraverse(node.right); }
4.3 后序遍历
后序遍历是先遍历左子树,再遍历右子树,然后访问节点数据 ,节点访问顺序可以简写为LRD。
后序遍历代码实现如下:
private void postOrderTraverse(Node node) { if (node == null) { return; } postOrderTraverse(node.left); postOrderTraverse(node.right); operate(node); }
三种基于深度优先的遍历方法逻辑实现一致,只是对节点数据和左右子树的访问顺序不一样。
5. 层次遍历
基于二叉树的层次遍历,又称作广度优先遍历,从根节点开始,沿着树的宽度一层一层向下遍历,这里可以使用队列来实现遍历逻辑。
广度优先遍历代码实现如下:
public void levelOrderTraverse() { Queue<Node> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { Node node = queue.poll(); operate(node); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } }
三、二叉树基本操作实现
针对前面讲的基本操作分析,来定义一个二叉树的基本操作接口。
基本操作接口定义
public interface BinaryTree<T> { /** * 初始化树 */ void init(T[] arr); /** * 计算节点个数 */ int nodeCount(); /** * 计算叶子节点个数 */ int leafNodeCount(); /** * 计算深度 */ int depth(); /** * 先序遍历 */ void preOrderTraverse(); /** * 中序遍历 */ void inOrderTraverse(); /** * 后序遍历 */ void postOrderTraverse(); /** * 层次遍历 */ void levelOrderTraverse(); }