概述
二叉树是n个有限元素的集合,由一个根及两个不相交的左、右子树组成,子树也是二叉树,是有序树
1.每个节点至多有两个子结点,因此二叉树节点的度小于等于2
2.第n层上,最多有2^n-1个节点
构建二叉树
1.构建一颗二叉树的数据结构
@AllArgsConstructor
@Data
private static class Node<T> {
private T t;
private Node<T> left;
private Node<T> right;
@Override
public String toString() {
return t.toString();
}
}
2.将一个数据集构建为一个Node集合
Node<T> create(List<T> data) {
List<Node<T>> nodes = new ArrayList<>();
data.forEach(t -> nodes.add(new Node<T>(t, null, null)));
//...
}
3.观察如下二叉树
任何节点记为i的左孩子j,右孩子k,都有j=2*i, k=2*i + 1
最后一个子节点记为n,其父节点记为m,都有m/n=2(这里的2,指的是商,注意在计算机中,商和余数的概念)
那么我们的算法大致就有了:
遍历步骤2中的nodes,为每个节点的左右节点附值,遍历的终点为nodes.size()/2 - 1, 因为nodes的起始索引为0开始,因此要减一
i <= nodes.size() / 2 - 1
于是可得到如下代码:
for (int i = 0; i < nodes.size() / 2; i++) {
nodes.get(i).left = nodes.get(i * 2);
nodes.get(i).right = nodes.get(i * 2 + 1);
}
上述代码,似乎已经构建出了一颗以nodes.get(0)为根节点的二叉树了,但是测试发现有问题。
问题就出在当i=0时,i*2=i=0,那怎么解决呢,思考如下:
当我们取nodes.get(0)时,值为1
当我们取nodes.get(1)时,值为2
当我们取nodes.get(2)时,值为3
当我们取nodes.get(3)时,值为4
当我们取nodes.get(4)时,值为5
......
通过以上类推发现,只要我们将如上代码稍加调整如下,即可:
for (int i = 0; i < nodes.size() / 2; i++) {
nodes.get(i).left = nodes.get(i * 2 + 1);
if ((i * 2 + 1 + 1) < nodes.size()) { //避免索引下标越界
nodes.get(i).right = nodes.get(i * 2 + 1 + 1);
}
}
稍加优化,得到最终版代码如下:
for (int i = 0; i < nodes.size() / 2; i++) {
nodes.get(i).left = nodes.get(i * 2 + 1);
if ((i * 2 + 2) < nodes.size()) {
nodes.get(i).right = nodes.get(i * 2 + 2);
}
}
最后拿到nodes.get(o)即是一颗二叉树的根节点
中序遍历
中序遍历的思想是左根右,如下图:
1.递归版:
从根节点出发,递归到最后一个左孩子节点后,下一次node为null退出,返回上一层递归停止的地方println(node),并且递归其右孩子,如果没有右孩子,则返回上上一层递归停止的地方println(node)...... 如此往复直至最后一个右孩子。
void iterator(Node<T> node) {
if (node == null) {
return;
}
iterator(node.left);
println(node);
iterator(node.right);
}
2.循环版:
循环版,不是真正意义上的循环,只是用循环的代码来完成递归的思想,首先我们有如下思考:
肯定需要一个集合来存储二叉树中的元素,并按某种顺序自由取出,那么该集合最好是一个双端队列,于是有如下代码:
LinkedList<Node<T>> queue = new LinkedList<>();
接下来,基于上图的逻辑,把node的左孩子递归到queue中,使用push是压栈,等价于addFirst直到最后一个,退出循环
while (node != null) {
queue.push(node);
node = node.left;
}
紧接着,我们得思考,如何取出队列中的左孩子,上一步骤中,最后一个入队的元素肯定是8,因为在队头,直接pop出来即可
if (!queue.isEmpty()) {
Node<T> pop = queue.pop();
println(pop);
}
同时,我们需要判断该节点是否有右孩子,如果有,还是用递归的思想,于是得到大致代码如下:
while (!queue.isEmpty()) {
Node<T> pop = queue.pop();
println(pop);
if (pop.right != null) {
node = pop.right;
}
}
经过测试,我们发现,这样的代码,只能把根节点递归出来的左孩子打印出来,我们的想法是,如果当前打印的左孩子有右子节点,需要将其入队列,那就需要代码再次进入到while(node != null) 这个循环里去,这样是可以联想到while(node != null)上层应该还有一个循环,立刻改造一下代码:
while (true) {
while (node != null) {
queue.push(node);
node = node.left;
}
if (!queue.isEmpty()) {
Node<T> pop = queue.pop();
println(pop);
if (pop.right != null) {
node = pop.right;
}
}
}
那么外层的循环条件是什么呢?最终的结束标志是node=null,于是有如下代码:
while (node != null) {
while (node != null) {
queue.push(node);
node = node.left;
}
if (!queue.isEmpty()) {
Node<T> pop = queue.pop();
println(pop);
if (pop.right != null) {
node = pop.right;
}
}
}
再次经过测试,我们发现,队列queue还有元素没有被取出来,代码就执行结束了,于是为了,让队列不为空时,循环继续,于是有如下代码:
while (node != null || !queue.isEmpty()) {
while (node != null) {
queue.push(node);
node = node.left;
}
if (!queue.isEmpty()) {
Node<T> pop = queue.pop();
println(pop);
if (pop.right != null) {
node = pop.right;
}
}
}
反复测试,以上代码就是中序遍历的用循环改造递归的代码
前序遍历
前序遍历的思想是根左右,如下图:
1.递归版:
与中序遍历一样从根节点出发,先左后右,区别是每次递归先获取当前元素。
void beforeIterator(Node<T> node) {
if (node == null) {
return;
}
println(node);
beforeIterator(node.left);
beforeIterator(node.right);
}
2.循环版:
与中序遍历的分析思路一模一样,就不写了,区别在于:获取元素是在节点入队的时候
void beforeLoop(Node<T> node) {
LinkedList<Node<T>> queue = new LinkedList<>();
while(node != null || !queue.isEmpty()) {
while(node != null) {
queue.push(node);
println(node);
node = node.left;
}
if(!queue.isEmpty()) {
Node<T> pop = queue.pop();
node = pop.right;
}
}
}
后序遍历
后序遍历的思想是左右根,如下图:
1.递归版:
与中序遍历一样,只是获取元素放在了最后。
void afterIterator(Node<T> node) {
if (node == null) {
return;
}
afterIterator(node.left);
afterIterator(node.right);
println(node);
}
2.循环版:
后序遍历递归改造循环较为繁琐一些,大致思想是在左孩子入队列的基础上,从队列中取出的节点的右孩子为空,才获取元素,于是很快有如下代码:(注意此处没有用栈的思想,此处使用的是队列)
void afterLoop(Node<T> node) {
LinkedList<Node<T>> queue = new LinkedList<>();
while(node != null || !queue.isEmpty()) {
while(node != null) {
queue.add(node); //入队尾
node = node.left;
}
if(!queue.isEmpty()) {
Node<T> last = queue.pollLast(); //弹出队尾
if(last.right != null) {
node = last.right;
}else {
println(last);
}
}
}
}
经过断点调试我们发现:
if(last.right != null) {
node = last.right;
}else {
println(last);
}
这段代码导致了有右孩子的节点被从队列弹出后,并没有被获取而丢失掉了,于是我们设想加入一个容器来存放这些被丢失掉的节点,并且在适当的时机获取,继续改造代码如下:
void afterLoop(Node<T> node) {
LinkedList<Node<T>> queue = new LinkedList<>();
List<Node<T>> flagNodes = new ArrayList<>();
while(node != null || !queue.isEmpty()) {
while(node != null) {
queue.add(node);
node = node.left;
}
if(!queue.isEmpty()) {
Node<T> last = queue.pollLast();
if(last.right != null) {
flagNodes.add(last);
node = last.right;
}else {
println(last);
}
}
}
}
然而,以上代码只是把原本丢失掉的元素存储了起来flagNodes.add(last);并没有被获取到,要获取丢失元素的时机是,先获取丢失元素的右孩子,在获取丢失元素本身,那么我们的思路应该如下:
首先从队列queue中get出丢失元素(并不是弹出),然后判断元素是否存在与容器中,如果不存在才从队列中弹出该元素,并获取该元素,反之如果不存在,则将get出的该元素放入容器中,这样就可以避免上述的问题:只获取丢失元素的右孩子,而丢失了元素本身,根据这个思路我们改造代码如下:
void afterLoop(Node<T> node) {
LinkedList<Node<T>> queue = new LinkedList<>();
List<Node<T>> flagNodes = new ArrayList<>();
while(node != null || !queue.isEmpty()) {
while(node != null) {
queue.add(node);
node = node.left;
}
if(!queue.isEmpty()) {
Node<T> last = queue.getLast();
if(last.right != null && !flagNodes.contains(last)) {
flagNodes.add(last);
node = last.right;
}else {
println(queue.pollLast());
}
}
}
}
经测试,以上就是后序遍历循环版的代码
完整代码如下
package cn.qu.data.structure;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.AllArgsConstructor;
import lombok.Data;
public class BinaryTree<T> {
final AtomicInteger COUNTER = new AtomicInteger();
/*
* 构建二叉树
*/
public Node<T> create(List<T> data) {
List<Node<T>> nodes = new ArrayList<>();
data.forEach(t -> nodes.add(new Node<T>(t, null, null)));
for (int i = 0; i < nodes.size() / 2; i++) {
nodes.get(i).left = nodes.get(i * 2 + 1);
if ((i * 2 + 2) < nodes.size()) {
nodes.get(i).right = nodes.get(i * 2 + 2);
}
}
return nodes.get(0);
}
/*
* 循环中序遍历
*/
public void loop(Node<T> node) {
LinkedList<Node<T>> queue = new LinkedList<>();
while (node != null || !queue.isEmpty()) {
while (node != null) {
queue.push(node);
node = node.left;
}
if (!queue.isEmpty()) {
Node<T> pop = queue.pop();
println(pop);
if (pop.right != null) {
node = pop.right;
}
}
}
}
/*
* 递归中序遍历
*/
public void iterator(Node<T> node) {
if (node == null) {
return;
}
iterator(node.left);
println(node);
iterator(node.right);
}
/*
* 循环前序遍历
*/
public void beforeLoop(Node<T> node) {
LinkedList<Node<T>> queue = new LinkedList<>();
while(node != null || !queue.isEmpty()) {
while(node != null) {
queue.push(node);
println(node);
node = node.left;
}
if(!queue.isEmpty()) {
Node<T> pop = queue.pop();
node = pop.right;
}
}
}
/*
* 递归前序遍历
*/
public void beforeIterator(Node<T> node) {
if (node == null) {
return;
}
println(node);
beforeIterator(node.left);
beforeIterator(node.right);
}
/*
* 后续循环遍历
*/
public void afterLoop(Node<T> node) {
LinkedList<Node<T>> queue = new LinkedList<>();
List<Node<T>> flagNodes = new ArrayList<>();
while(node != null || !queue.isEmpty()) {
while(node != null) {
queue.add(node);
node = node.left;
}
if(!queue.isEmpty()) {
Node<T> last = queue.getLast();
if(last.right != null && !flagNodes.contains(last)) {
flagNodes.add(last);
node = last.right;
}else {
println(queue.pollLast());
}
}
}
}
/*
* 后续递归遍历
*/
public void afterIterator(Node<T> node) {
if (node == null) {
return;
}
afterIterator(node.left);
afterIterator(node.right);
println(node);
}
public static void main(String[] args) {
BinaryTree<Integer> binaryTree = new BinaryTree<>();
ArrayList<Integer> list = new ArrayList<>();
for(int i = 1; i <= 16; i ++) {
list.add(i);
}
Node<Integer> node = binaryTree.create(list);
binaryTree.afterLoop(node);
}
@AllArgsConstructor
@Data
private static class Node<T> {
private T t;
private Node<T> left;
private Node<T> right;
@Override
public String toString() {
return t.toString();
}
}
private void println(Node<T> node) {
System.out.println(COUNTER.incrementAndGet() + ") : " + node);
}
}