数据结构_011_二叉树的创建和遍历
在对二叉树进行操作之前,需要创建一棵二叉树,可以使用递归来创建一棵二叉树。
创建一棵二叉树完毕后,需要遍历二叉树中的元素,遍历二叉树的方法大致有四种,分别是前序遍历,中序遍历,后续遍历,层次遍历。
二叉树可以有链式存储和顺序存储,顺序存储一般是针对完全二叉树。
二叉树的遍历
>>前面两节讲的是链式存储<<
因为二叉树的创建需要了解遍历的次序,所以先了解二叉树的遍历。
我们一般遵循先左后右的遍历习惯,也就是先左子树,再右子树,那么二叉树的遍历分为四种:前序遍历,中序遍历,后续遍历,层次遍历。不难发现,前中后的次序指的是父节点的遍历顺序。
手动创建二叉树
在遍历二叉树之前,需要先手动创建一个二叉树。
>>结点对象<<
/**
* 二叉树的结点对象
*
* @param <E>
*/
public static class TreeNode<E> {
/**
* 结点元素域
*/
private E element;
/**
* 左子树指针
*/
private TreeNode<E> left;
/**
* 右子树指针
*/
private TreeNode<E> right;
// 省略get set和构造方法
}
>>二叉树对象<<
二叉树对象,有一个根结点root,和其他的一些遍历方法,遍历方法的实现后面介绍。
/**
* @author: Dylan kwok GSGB
* @date: 2021/1/4 21:22
* 二叉树对象
*/
public class BinaryTree<E> {
/**
* 根结点
*/
private TreeNode<E> root;
public BinaryTree() {
}
public BinaryTree(TreeNode<E> root) {
this.root = root;
}
}
>>手动创建一棵二叉树<<
创建一个测试类
/**
* @author: Dylan kwok GSGB
* @date: 2021/1/4 21:17
*
* 二叉树遍历测试
*/
public class BinaryTreeErgodic {
public static void main(String[] args) {
// 手动创建一个二叉树
BinaryTree<String> binaryTree = initBinaryTree();
}
/**
* 手动创建一个二叉树,直观图如下
*
* 曹操
* / \
* 曹丕 曹植
* / \ \
* 曹睿 曹协 曹志
* \
* 曹芳
*/
private static BinaryTree<String> initBinaryTree() {
// 在遍历前,先在内存中创建一个二叉树出来,先用呆板的创建方法
BinaryTree<String> binaryTree = new BinaryTree<>();
// 设置根节点等结点
BinaryTree.TreeNode<String> root = new BinaryTree.TreeNode<>("1_曹操");
BinaryTree.TreeNode<String> node2 = new BinaryTree.TreeNode<>("2_曹丕");
BinaryTree.TreeNode<String> node3 = new BinaryTree.TreeNode<>("3_曹植");
BinaryTree.TreeNode<String> node4 = new BinaryTree.TreeNode<>("4_曹睿");
BinaryTree.TreeNode<String> node5 = new BinaryTree.TreeNode<>("5_曹协");
BinaryTree.TreeNode<String> node6 = new BinaryTree.TreeNode<>("6_曹志");
BinaryTree.TreeNode<String> node7 = new BinaryTree.TreeNode<>("7_曹芳");
// 手动设置结点之间的关系
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setRight(node6);
node4.setRight(node7);
binaryTree.setRoot(root);
return binaryTree;
}
}
递归前序遍历
前序遍历:假如二叉树不为空,先访问根节点,遍历左子树,假如左子树遍历完毕或者为空才能遍历右子树。
在二叉树BinaryTree
类中增加下面两个方法。
思路:
-
首先,我们需要判断根节点是否为空。 -
假如不为空,且左子结点不为空,则递归遍历左子结点。 -
假如左子树遍历完毕,且当前结点的右子节点不为空,则递归遍历右子树。
打印结果:曹操 > 曹丕 > 曹睿 > 曹芳 > 曹协 > 曹植 > 曹志。
/**
* 前序遍历
* 1.先访问根节点
* 2.输出当前结点 (父节点先输出)
* 3.假如当前结点的左子结点不为空,则递归遍历左子树
* 4.假如左子树遍历完毕,且当前结点的右子节点不为空,则递归遍历右子树
*/
public void preOrder() {
if (root == null) {
return;
}
preOrder(root);
}
/**
* 1_曹操 > 2_曹丕 > 4_曹睿 > 7_曹芳 > 5_曹协 > 3_曹植 > 6_曹志
*/
private void preOrder(TreeNode node) {
// 前序遍历,先打印父结点
System.out.println(node);
// 假如当前结点的左子结点不为空,继续遍历左子树
if (node.getLeft() != null) {
preOrder(node.getLeft());
}
// 假如当前结点的右子结点不为空,继续遍历右子树
if (node.getRight() != null) {
preOrder(node.getRight());
}
}
递归中序遍历
中序遍历其实和前序遍历的代码90%是一样的,只是打印父节点的位置不同而已。
/**
* 中序遍历
* 1.先访问根节点
* 2.假如当前结点的左子结点不为空,则递归遍历左子树
* 3.输出当前结点 (父节点中间输出)
* 4.假如左子树遍历完毕,且当前结点的右子节点不为空,则递归遍历右子树
*/
public void inOrder() {
if (root == null) {
return;
}
inOrder(root);
}
/**
* 4_曹睿 > 7_曹芳 > 2_曹丕 > 5_曹协 > 1_曹操 > 3_曹植 > 6_曹志
*/
private void inOrder(TreeNode node) {
// 假如当前结点的左子结点不为空,继续遍历左子树
if (node.getLeft() != null) {
inOrder(node.getLeft());
}
// 中序遍历,中间打印父结点
System.out.println(node);
// 假如当前结点的右子结点不为空,继续遍历右子树
if (node.getRight() != null) {
inOrder(node.getRight());
}
}
递归后序遍历
后序遍历其实和前序遍历的代码90%是一样的,只是打印父节点的位置不同而已。
/**
* 后序遍历
*/
public void postOrder() {
if (root == null) {
return;
}
postOrder(root);
}
/**
* 7_曹芳 > 4_曹睿 > 5_曹协 > 2_曹丕 > 6_曹志 > 3_曹植 > 1_曹操
*/
public void postOrder(TreeNode node) {
// 假如当前结点的左子结点不为空,继续遍历左子树
if (node.getLeft() != null) {
postOrder(node.getLeft());
}
// 假如当前结点的右子结点不为空,继续遍历右子树
if (node.getRight() != null) {
postOrder(node.getRight());
}
// 后序遍历,最后打印父结点
System.out.println(node);
}
层次遍历
层次遍历,顾名思义,就是一层一层的遍历。
首先是第一层,也就是根节点。然后第二层,第三…….,每层从左至右遍历。
可以使用队列这种数据结构来实现二叉树的层次遍历。
思路:
-
判断根结点是否存在,存在则继续遍历。 -
创建一个队列数据结构的对象。 -
先将根结点入队。 -
进入循环,退出条件是队列为空。 -
队首出队。 -
假如当前结点的左子结点不为空,将左子结点入队。 -
假如当前结点的右子结点不为空,将右子结点入队。
文字看起来是很抽象,现在看一下图解:
代码:
/**
* 层次遍历
* 首先遍历第一层,再第二层... 从左至右
* 使用JDK自带的队列实现层次遍历
*
* 1_曹操 > 2_曹丕 > 3_曹植 > 4_曹睿 > 5_曹协 > 6_曹志 > 7_曹芳
*/
public void levelOrder() {
if (root == null) {
// 根为空,直接退出
return;
}
ArrayDeque<TreeNode<E>> deque = new ArrayDeque<>();
deque.offer(root); // 根节点入队
while (!deque.isEmpty()) {
// 队首出队
TreeNode<E> node = deque.remove();
System.out.println(node); // 打印当前结点
TreeNode<E> left = node.getLeft();
TreeNode<E> right = node.getRight();
if (left != null) {
deque.offer(left);
}
if (right != null) {
deque.offer(right);
}
}
}
非递归前序遍历
二叉树的遍历操作,使用递归来实现比较优雅,不过也可以用栈这种数据结构来实现。
思路: 后面的非递归中序思路差不多
1.初始时依次扫描根结点的所有左侧结点,访问它,并将它们一一进栈。
2.假如某个结点没有左子结点,出栈一个结点。
3.扫描该结点的右子结点,并将其进栈。
4.依次扫描右子结点的所有左侧结点,并一一进栈。
5.反复该过程直到栈为空为止。
public void preOrderStack() {
Stack<TreeNode<E>> stack = new Stack<>();
TreeNode<E> node = root;
while(node != null || !stack.isEmpty()){
if (node != null){
System.out.println(node); // 前序遍历
stack.push(node);
node = node.getLeft(); // 获取左子结点
} else {
// 弹栈是因为没找到当前结点的左子结点,来找当前结点的右子结点
node = stack.pop();
node = node.getRight(); // 获取右子结点
}
}
}
非递归中序遍历
public void inOrderStack() {
if(root == null) {
// 根结点为空则直接返回
return;
}
Stack<TreeNode<E>> stack = new Stack<>();
TreeNode<E> node = root;
// 循环退出条件 当前结点为空或栈为空
while(node != null || !stack.isEmpty()) {
if(node != null) {
stack.push(node);
node = node.getLeft();
} else {
node = stack.pop();
System.out.println(node); // 中序遍历
node = node.getRight();
}
}
}
非递归后序遍历
-
初始时依次扫描根结点的所有左侧结点,并将它们一一进栈。 -
查看栈顶元素的右结点。 -
假如该右结点为空,或者已经访问过,则弹出栈顶结点,并访问该结点。(ps.这样做就是为了后续打印父结点) -
假如该右节点不为空,或者未访问过,则依次扫描右子结点的所有左侧结点,并一一进栈。 -
反复该过程直到栈为空为止。
public void postOrderStack3(){
Stack<TreeNode<E>> stack = new Stack<TreeNode<E>>();
TreeNode<E> node = root;
TreeNode<E> preNode = null; // 记录之前遍历的右结点
while(node != null || !stack.isEmpty()){
if (node != null) {
stack.push(node);
node = node.getLeft();
} else {
node = stack.peek(); // 查看栈顶元素
// 如果右结点为空,或者右结点之前遍历过,打印根结点
if(node.getRight() == null || node.getRight() == preNode){
System.out.println(node);
node = stack.pop();
preNode = node;
node = null;
}
else{
node = node.getRight();
}
}
}
}
二叉树的创建
二叉树的创建可以使用补空法。
在得到补空之后的二叉树后,使用先序遍历得到一个序列,根据该序列来创建二叉树。
上面补空二叉树的前序遍历的序列为:ABD##E##CF##
。
代码:
BinaryTree
类的成员方法。
/**
* 创建二叉树 默认是字符串等基本类型
*
* 输入一个 补空二叉树 的前序序列 来创建二叉树
*/
public void createBinaryTree(String preOrderStr) {
// 创建一个扫描对象
String[] split = preOrderStr.split(",");
List<String> preOrderList = Arrays.stream(split).collect(Collectors.toList());
Iterator<String> it = preOrderList.iterator();
this.root = createBinaryTree(it);
}
private TreeNode<E> createBinaryTree(Iterator<String> it) {
if (!it.hasNext()) {
return null;
}
String temp = it.next();
if (Objects.equals(temp, "#")) {
return null;
}
// 生成父节点
TreeNode<E> node = new TreeNode<>((E)temp); // 针对基本数据类型类转换的
// 生成左子树
node.setLeft(createBinaryTree(it));
// 生成右子树
node.setRight(createBinaryTree(it));
return node;
}
测试:
public static void main(String[] args) {
/* ------ 开始测试 创建 ------*/
BinaryTree binaryTree = new BinaryTree();
binaryTree.createBinaryTree("A,B,D,#,#,E,#,#,C,F,#,#");
// 前序遍历创建的二叉树试下
// binaryTree.preOrder();
// binaryTree.inOrder();
// // 后序遍历
// binaryTree.postOrder();
// // 层次遍历
binaryTree.levelOrder(); // A B C D E F
顺序存储的二叉树
顺序存储二叉树一般针对的是完全二叉树,假如不是完全二叉树则可能会导致大量的空间浪费。
由完全二叉树的性质 假如根结点的索引为0,假如某个父节点的索引为n,若它的左右子结点存在的话,那么
-
左子结点的索引为2n+1。 -
右子结点的索引为2n+2。
要求:给一个数组,按照前序,中序,后序的遍历方式遍历数组。
public class ArrayBinaryTree<E> {
/**
* 存储元素的数组
*/
private Object[] elementData;
public ArrayBinaryTree(Object[] elementData) {
this.elementData = elementData;
}
}
给定的数组:
private static final String[] TREE_4 = {"A","B","C","D","E","F","G","H","I"};
前序遍历
/**
* 前序遍历
*/
public void preOrder() {
preOrder(0);
}
/**
* 前序遍历
*/
private void preOrder(int index) {
if (elementData == null || elementData.length <= 0) {
return;
}
// 先打印父结点
System.out.print(elementData[index]);
int left = 2 * index + 1;
int right = 2 * index + 2;
// 递归打印左子树的结点
if (left < elementData.length) {
preOrder(left);
}
if (right < elementData.length) {
preOrder(right);
}
}
中序遍历
/**
* 中序遍历
*/
public void inOrder() {
inOrder(0);
}
/**
* 中序遍历
*/
private void inOrder(int index) {
if (elementData == null || elementData.length <= 0) {
return;
}
int left = 2 * index + 1;
int right = 2 * index + 2;
// 递归打印左子树的结点
if (left < elementData.length) {
inOrder(left);
}
// 中间印父结点
System.out.print(elementData[index]);
if (right < elementData.length) {
inOrder(right);
}
}
后序遍历
/**
* 后续遍历
*/
public void postOrder() {
postOrder(0);
}
/**
* 后续遍历
*/
private void postOrder(int index) {
if (elementData == null || elementData.length <= 0) {
return;
}
int left = 2 * index + 1;
int right = 2 * index + 2;
// 递归打印左子树的结点
if (left < elementData.length) {
postOrder(left);
}
if (right < elementData.length) {
postOrder(right);
}
// 后打印父结点
System.out.print(elementData[index]);
}