java-数据结构-二叉树

概述

二叉树是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);
	}

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值