HuKai's Blog HuKai's Blog
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)

HuKai

梦想成为全栈的保安
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)
  • 计算机网络

  • 操作系统

  • 数据结构与算法

    • 数据结构概述
    • 算法概述
    • 时间空间复杂度
    • 线性表的顺序存储结构
    • 链表
    • 栈
    • 队列
    • 排序算法上篇
    • 查找算法
    • 哈希表
    • 树
    • 二叉树
    • 遍历二叉树
      • 二叉树的建立
      • 二叉树的遍历
      • 非递归方式实现二叉树遍历
    • 线索二叉树
    • 排序算法下篇
    • 赫夫曼树及其应用
    • 二叉排序树
    • 平衡二叉树(AVL树)
    • 多路查找树
    • 图
    • 图的遍历
  • 设计模式

  • 基本功
  • 数据结构与算法
HuKai
2022-03-20
目录

遍历二叉树

# 二叉树的建立

在讲遍历二叉树之前我们先来看看怎么用Java建立二叉树,前面说过二叉树的存储结构分为顺序存储结构(数组)和二叉链表,下面我们分别用这两种结构建立二叉树。

# 1. 顺序存储结构实现

import java.util.Arrays;

/**
 * 二叉树顺序存储实现
 * @author hukai
 *
 */
public class ArrayBinaryTree<E> {
	
	private static final int DEFAULT_DEEP = 8;
	
	// 使用数组来记录该树的所有节点
    private Object[] elementData;
    
    // 保存该树的深度
    private int deep;
    
    // 数组长度
    private int arraySize;
    
    // 以默认的深度创建二叉树
    public ArrayBinaryTree() {
        this.deep = DEFAULT_DEEP;
        // 二叉树的性质-深度为k的二叉树至多有 2^k -1个结点(k≥1)
        this.arraySize = (int) (Math.pow(2, deep) - 1);
        elementData = new Object[arraySize];
    }
    
    // 以指定深度创建二叉树
    public ArrayBinaryTree(int deep) {
    	if (deep < 0) throw new IllegalArgumentException("树的深度不能小于0");
        this.deep = deep;
        this.arraySize = (int) Math.pow(2, deep) - 1;
        elementData = new Object[arraySize];
    }
    
    // 以指定深度、指定根节点创建二叉树
    public ArrayBinaryTree(int deep, E data) {
    	if (deep<=0) throw new IllegalArgumentException("树的深度必须大于0");
    	if (data == null) throw new NullPointerException("根结点不能为空");
        this.deep = deep;
        this.arraySize = (int) Math.pow(2, deep) - 1;
        elementData = new Object[arraySize];
        elementData[0] = data;
    }
    
    /**
     * 为指定节点添加子节点
     * @param parentIndex  双亲结点索引
     * @param data  数据
     * @param left  是否为左节点
     */
    public void add(int parentIndex, E data, boolean left) {
    	// 添加根结点
    	if (parentIndex == -1) {
    		elementData[0] = data;
    		return;
		}
    	if (elementData[parentIndex] == null) {
            throw new RuntimeException(parentIndex + "处节点为空,无法添加子节点");
        }
    	// 获取节点索引
    	int index = left ? 2 * parentIndex + 1 : 2 * parentIndex + 2;
    	if (index >= arraySize) {
			throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + arraySize);
		}
    	elementData[index] = data;
    }
    
    // 判断二叉树是否为空
    public boolean empty() {
        // 根据根元素判断二叉树是否为空
        return elementData[0] == null;
    }
    
    // 返回根节点
    public E root() {
        return elementData(0);
    }
    
    // 返回指定节点(非根结点)的父节点
    public E parent(int index) {
    	if (index <= 0) return null;
    	return elementData((index - 1) / 2);
    }
    
    // 返回指定节点(非叶子)的左子节点,当左子节点不存在时返回null
    public E leftChild(int index) {
        if (2 * index + 1 >= arraySize) {
            throw new RuntimeException("该节点为叶子节点,无子节点");
        }
        return elementData(index * 2 + 1);
    }

    // 返回指定节点(非叶子)的右子节点,当右子节点不存在时返回null
    public E rightChild(int index) {
        if (2 * index + 1 >= arraySize) {
            throw new RuntimeException("该节点为叶子节点,无子节点");
        }
        return elementData(index * 2 + 2);
    }
    
    
    // 返回指定节点的位置
    public int position(E data) {
        // 该循环实际上就是按广度遍历来搜索每个节点
        for (int i = 0; i < arraySize; i++) {
            if (elementData[i].equals(data)) {
                return i;
            }
        }
        return -1;
    }
    
    
    /**
	 * 返回此列表中指定位置的元素
	 */
	@SuppressWarnings("unchecked")
	private E elementData(int index) {
		return (E) elementData[index];
	}
	
	@Override
	public String toString() {
		return Arrays.toString(elementData);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

# 2. 二叉链表的实现

/**
 * 二叉链表的实现
 * 
 * @author hukai
 *
 */
public class LinkedBinaryTree<E> {

	// 根结点
	private TreeNode<E> root;

	// 以默认的构造器创建二叉树
	public LinkedBinaryTree() {
		this.root = new TreeNode<E>();
	}

	// 以指定根元素创建二叉树
	public LinkedBinaryTree(E element) {
		this.root = new TreeNode<E>(element);
	}

	/**
	 * 为指定节点添加子节点
	 * 
	 * @param parent
	 *            需要添加子节点的父节点
	 * @param data
	 *            新子节点的数据
	 * @param isLeft
	 *            是否为左节点
	 * @return
	 */
	public TreeNode<E> addNode(TreeNode<E> parent, E data, boolean isLeft) {
		if (parent == null) {
			throw new RuntimeException("父节点为null, 无法添加子节点");
		}
		if (isLeft && parent.leftChild != null) {
			throw new RuntimeException("父节点已有左子节点,无法添加左子节点");
		}
		if (!isLeft && parent.rightChild != null) {
			throw new RuntimeException("节点已有右子节点,无法添加右子节点");
		}

		TreeNode<E> newNode = new TreeNode<E>(data);
		if (isLeft) {
			// 让父节点的left引用指向新节点
			parent.leftChild = newNode;
		} else {
			// 让父节点的left引用指向新节点
			parent.rightChild = newNode;
		}
		return newNode;
	}

	// 判断二叉树是否为空
	public boolean empty() {
		// 根据元素判断二叉树是否为空
		return root.element == null;
	}

	// 返回根节点
	public TreeNode<E> root() {
		if (empty()) {
			throw new RuntimeException("树为空,无法访问根节点");
		}
		return root;
	}

	// 返回该二叉树的深度
	public int deep() {
		// 获取该树的深度
		return deep(root);
	}

	// 这是一个递归方法:每一棵子树的深度为其所有子树的最大深度 + 1
	private int deep(TreeNode<E> node) {
		if (node == null)
			return 0;
		// 没有子树
		if (node.leftChild == null && node.rightChild == null) {
			return 1;
		} else {
			int leftDeep = deep(node.leftChild);
			int rightDeep = deep(node.rightChild);
			int max = Math.max(leftDeep, rightDeep);
			// 返回其左右子树中较大的深度 + 1
			return max + 1;
		}
	}

	/**
	 * 内部结点类
	 * 
	 * @author hukai
	 *
	 */
	public static class TreeNode<E> {
		// 数据元素
		E element;
		// 左孩子
		TreeNode<E> leftChild;
		// 右孩子
		TreeNode<E> rightChild;

		public TreeNode() {
		}

		public TreeNode(E element) {
			this.element = element;
		}

		public TreeNode(E element, TreeNode<E> left, TreeNode<E> right) {
			this.element = element;
			this.leftChild = left;
			this.rightChild = right;
		}
	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

上次还提到过三叉链表,其实对比二叉链表只是内部结点类增加了一个指向父节点的指针,在此不做赘述。

# 二叉树的遍历

# 1. 前序遍历

规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。

按照前序遍历地方式,遍历如下二叉树,则访问顺序为:A、B、D、H、I、E、J、C、F、G。

以上面实现的二叉链表为例,前序遍历算法如下:

/**
 * 前序遍历
 * @param node
 */
public void PreOrderTraverse(TreeNode<E> node){
	if (node == null) return;
	// 先访问根节点
	System.out.println(node.element);
	// 前序遍历左子树
	PreOrderTraverse(node.leftChild);
	// 前序遍历右子树
	PreOrderTraverse(node.rightChild);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2. 中序遍历

规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。

按照中序遍历地方式,遍历如下二叉树,则访问顺序为:H、D、I、B、J、E、A、F、C、G。

中序遍历算法和前序遍历类似:

/**
 * 中序遍历
 * @param node
 */
public void inOrderTraverse(TreeNode<E> node){
	if (node == null) return;
	// 中序遍历左子树
	inOrderTraverse(node.leftChild);
	// 访问根节点
	System.out.println(node.element);
	// 中序遍历右子树
	inOrderTraverse(node.rightChild);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3. 后序遍历

规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。

按照后序遍历地方式,遍历如下二叉树,则访问顺序为:H、D、I、B、J、E、A、F、C、G。

后序遍历算法:

/**
 * 后序遍历
 * @param node
 */
public void postOrderTraverse(TreeNode<E> node){
	if (node == null) return;
	// 后序遍历左子树
	postOrderTraverse(node.leftChild);
	// 后序遍历右子树
	postOrderTraverse(node.rightChild);
	// 后访问根节点
	System.out.println(node.element);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 4. 层序遍历

规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。

按照层序遍历地方式,遍历如下二叉树,则访问顺序为:A、B、C、D、E、F、G、H、I、J。

不难发现,在顺序存储结构实现的二叉树中,层序遍历实际上就是遍历数组,在此不做赘述。

# 非递归方式实现二叉树遍历

上面实现二叉树的三种遍历方式(前序、中序、后序)都采用了递归的方式,能否不采用递归方式实现这三种遍历呢?答案是肯定的,我们可以利用栈来实现。

# 1. 非递归方式实现前序遍历

具体步骤:

  1. 首先创建一个新的栈,记为stack;
  2. 将头结点head压入stack中;
  3. 每次从stack中弹出栈顶节点,记为cur,然后打印cur值;
  4. 如果cur右孩子不为空,则将右孩子压入栈中;如果cur的左孩子不为空,将其压入stack中;
  5. 重复步骤3,4,直到stack为空

过程模拟:

代码实现:

public void preOrderTraverse(ThreadedNode<T> node){
	if (node == null) return;
	// 将头结点head压入stack中
	Stack<ThreadedNode<T>> stack = new Stack<ThreadedNode<T>>();
	stack.push(node);
	
	// 从stack中弹出栈顶节点,记为cur,然后打印cur值
	do {
		ThreadedNode<T> curNode = stack.pop();
		System.out.print(curNode.getData() + "   ");
		// 如果cur右孩子不为空,则将右孩子压入栈中
		if (curNode.getRightNode() != null) {
			stack.push(curNode.getRightNode());
		}
		// 如果cur的左孩子不为空,将其压入stack中
		if (curNode.getLeftNode() != null) {
			stack.push(curNode.getLeftNode());
		}
	} while (!stack.isEmpty());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2. 非递归方式实现中序遍历

具体步骤:

  1. 创建一个新栈,记为stack,申请一个变量cur,初始时令cur为头节点;
  2. 先把cur节点压入栈中,对以cur节点为头的整棵子树来说,依次把整棵树的左子树压入栈中,即不断令cur=cur.left,然后重复步骤2;
  3. 不断重复步骤2,直到发现cur为空,此时从stack中弹出一个节点记为temp,打印temp的值,并让cur = temp.right,然后继续重复步骤2;
  4. 当stack为空并且cur为空时结束;

过程模拟:

public void inOrderTraverse(ThreadedNode<T> node){
	if (node == null) return;
	Stack<ThreadedNode<T>> stack = new Stack<ThreadedNode<T>>();
	
	// 申请一个变量cur,初始时令cur为头节点
	ThreadedNode<T> curNode = node;
	
	do {
		// 先把cur节点压入栈中,对以cur节点为头的整棵子树来说,依次把整棵树的左子树压入栈中
		while (curNode!= null) {
			stack.push(curNode);
			curNode = curNode.getLeftNode();
		}
		
		// 此时cur为空,从stack中弹出一个节点记为temp,打印node的值,并让cur = temp.right
		ThreadedNode<T> tempNode = stack.pop();
		System.out.print(tempNode.getData() + "   ");
		curNode = tempNode.getRightNode();
	} while (!stack.isEmpty() || curNode != null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3. 非递归方式实现后序遍历

具体步骤:

  1. 申请两个栈stack1,stack2,然后将头结点压入stack1中;
  2. 从stack1中弹出的节点记为cur,然后先把cur的左孩子压入stack1中,再把cur的右孩子压入stack1中;
  3. 在整个过程中,每一个从stack1中弹出的节点都放在第二个栈stack2中;
  4. 不断重复步骤2和步骤3,直到stack1为空,过程停止;
  5. 从stack2中依次弹出节点并打印,打印的顺序就是后序遍历的顺序;

过程模拟:

代码实现:

public void postOrderTraverse(ThreadedNode<T> node){
	if (node == null) return;
	// 创建两个栈stack1,stack2
	Stack<ThreadedNode<T>> stack1 = new Stack<ThreadedNode<T>>();
	Stack<ThreadedNode<T>> stack2 = new Stack<ThreadedNode<T>>();
	// 将头结点压入stack1中
	stack1.push(node);
	
	do {
		// 从stack1中弹出的节点记为cur,然后先把cur的左孩子压入stack1中,再把cur的右孩子压入stack1中;
		ThreadedNode<T> curNode = stack1.pop();
		if (curNode.getLeftNode() != null) {
			stack1.push(curNode.getLeftNode());
		}
		if (curNode.getRightNode() != null) {
			stack1.push(curNode.getRightNode());
		}
		// 从stack1中弹出的节点都放在第二个栈stack2中
		stack2.push(curNode);
		
	} while (!stack1.isEmpty());
	
	// 从stack2中依次弹出节点并打印,打印的顺序就是后序遍历的顺序
	while(!stack2.isEmpty()){
		ThreadedNode<T> tempNode = stack2.pop();
		System.out.print(tempNode.getData() + "   ");
	}
	
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

其实,使用一个栈也能实现非递归后序遍历,可以思考一下,再次不详述。

编辑 (opens new window)
#数据结构
上次更新: 2022/03/20, 16:39:15
二叉树
线索二叉树

← 二叉树 线索二叉树→

最近更新
01
MyBatisPlus
03-20
02
MyBatis源码剖析-延迟加载
03-20
03
MyBatis源码剖析-二级缓存
03-20
更多文章>
Theme by Vdoing | Copyright © 2021-2022 HuKai | 赣ICP备17016768号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式