二叉树
什么是二叉树?
二叉树是一种很简单的结构,根节点延伸出左子节点和右子节点.这些子节点又有自己的子节点,自己当成子节点的根节点,不断延伸,可以无穷无尽.
二叉树的作用有很多,比如在一些数据的查找上设计得很巧妙,我们规定左节点存放的值小于根节点,右节点存放的值大于根节点,这样子在进行查找的时候,就可以让该传入的值直接与最开始的根节点进行比较,从判断去哪个子节点寻找,不用遍历所有节点.
遍历
二叉树的遍历又分为前序遍历,中序遍历,后序遍历,这些遍历往往会让人很乱,且看小编给你分析得头头是道.
前序遍历
前序遍历就是以根节点为起点,然后开始遍历其左子树,和右子树,每个子树又以这样的规则遍历,就得到了前序遍历.
比如上面图示,先遍历根节点,然后是左子树,之后是右子树,每个子树又以该规则为例子
我们可以遍历出一个路径14->12->11->13(左子树完毕)->15->8->16(右子树完毕)
中序遍历
中序遍历的遍历顺序是左子树->根节点->右子树,每个子树里面也以这种规则是遍历.
比如还是前序遍历的子树结构,先是以12为根节点的左子树,12又有左子树11,所以先输出11,之后是12,然后是13,这个时候12这棵左子树遍历完毕,然后才遍历到14,之后是14的右子树15,15也有左子树,所以显示8,然后是15,之后才是16.
所以整体的遍历顺序是11->12->13->14->8->15->16
后序遍历
后序遍历的顺序是 左子树->右子树->根节点,每个子树也要以这种规则遍历.
比如还是上面的图:
14的左子树是12,12的左子树是11,所以先输出11,之后是12的右子树13,然后才是12,12遍历完毕后,到14的右子树15,15的左子树是8,所以先输出左子树,然后是右子树16,之后才是根节点15,然后才是15的父节点14.
所以,该顺序数值是: 11->13->12->8->16->15->14
代码分析
代码其实很简单,我们从上面的分析可以知道,这些都可以用递归去实现,只是递归的位置不一样,才造就了遍历顺序的不同:
public class Search {
private List<Integer> list=new ArrayList<>();
// 树结构
public static class Node{
int value;
Node left;
Node right;
public Node(int v,Node l,Node r){
this.value=v;
this.left=l;
this.right=r;
}
}
//前序遍历
public List<Integer> forwardFind(Node node){
list.add(node.value);
if (node.left!=null){
forwardFind(node.left);
}
if (node.right!=null){
forwardFind(node.right);
}
return this.list;
}
//中序遍历
public List<Integer> midleFind(Node node){
if (node.left!=null){
midleFind(node.left);
}
list.add(node.value);
if (node.right!=null){
midleFind(node.right);
}
return list;
}
//后序遍历
public List<Integer> reverFind(Node node){
if (node.left!=null){
reverFind(node.left);
}
if (node.right!=null){
reverFind(node.right);
}
list.add(node.value);
return list;
}
}
class Test{
public static void main(String[] args) {
Search.Node nodel1 = new Search.Node(2,null,null);
Search.Node nodel2 = new Search.Node(31,null,null);
Search.Node noder3 = new Search.Node(35,null,null);
Search.Node noder4 = new Search.Node(47,null,null);
Search.Node nodel = new Search.Node(30,nodel1,nodel2);
Search.Node noder = new Search.Node(36,noder3,noder4);
Search.Node node = new Search.Node(32, nodel, noder);
Search search = new Search();
List<Integer> integers = search.reverFind(node);
for (Integer i :
integers) {
System.out.println(i);
}
}
}
存放节点
public void put(int value){
if (headNode==null){
System.out.println("根节点为空");
return;
}
put(headNode,value);
modCount=0;
}
private void put(Node node, int value) {
if (node.value==value || modCount==1)
return;
if (node.left==null && value<node.value){
node.left= new Node(value, null, null);
modCount++;
}
if (node.right==null && value> node.value){
node.right=new Node(value,null,null);
modCount++;
}
if (node.left!=null){
put(node.left,value);
}
if (node.right!=null){
put(node.right,value);
}
}
删除节点
代码分析
删除节点其实听起来是挺简单的,但是你会被绕乱,因为我们涉及到递归,所以要考虑到底是先删除根节点还是先删除子节点.
我们先来看看错误版本
//删除节点
public void delNode(Node node,int value) {
if (node.value==value){
node=null;
return ;
}
if (node.left!=null){
delNode(node.left,value);
}
if (node.right!=null){
delNode(node.right,value);
}
}
这样子你会不会觉得很完美,一个节点进来,先判断当前节点是否与要删除的值相等,如果不是,直接判断左右子节点进行递归.
但是这样的删除逻辑你倒是要注意了:
这里假设传入的node是根节点root,需要删除的值为30,而我们的树结构是上一小节的树结构,数据也一样,
接下来我们直接来debug:
- 首先是root节点是2,该不与30相等,所以进入下一步,判断node.left是否为空,不为空,则直接进入delNode(node.left,value)这个递归函数,注意看在进入回调之前的node怎样的:
- 进入后,再看该node是怎样的:
你会发现node改变了,跳到了30所在节点,也就是此刻root节点变为了30.
之后就执行代码逻辑,将root置于null,然后return,乍一看没啥问题,可是往往大问题就出现return上
- 因为此刻return是返回上一级函数,也就是最开始那个函数执行完delNode(node.left,value)之后,这个时候你会发现,跳出该函数后node变量又变为:
这回问题了吧?node跳回根节点是32的时候,发现其30的那个子节点还在,并没有删除,这是什么情况?
这样bug就出来了,你会发现无论你怎么下去,30这个节点一直都删除不了,但是逻辑感觉是没有什么错误的.
- 为什么会这样的问题?
首先你得想一下,我们删除节点其实并不是真实将数据给删除了,这个数据还是会存在内存里,只不过是将指向该数据的引用给删除.所以当前在进入递归的时候,这个形参node其实只是一个引用,如果你直接判断该node是为想要删除的节点,那么只是将指向该节点的引用node指向了空.而没有将该节点的父节点的子节点引用消除,所以就造成了等你返回的时候,父节点的子节点引用还是存在.
比如我举个例子:
你看上面这张图,如果是按照先判断当前节点来说,我们要删除11,那么这个时候,虽然可以指向了null,但是只是将承接11的变量引用指向了node,而没有真实的将12的左子节点引用删除,这就导致了返回的时候12的左子节点引用一种存在.也就是11没有删除.
- 所以小伙伴大概都猜到,我们要删除的11的话,其实要把12的左子节点引用删除,也就是12指向11的箭头,这个时候,才真正的删除11在该树里的引用,如果遍历树的时候是找不到11了.
上面的场景可以模拟一下: 如果递归跳到12这个节点,不是判断12节点本身,而是将12节点的左右节点判断,也就是将12.left删除,就可以取消指向11的引用.
- 所以真正的代码是这样的:
//
public void del(Node node,int value){
if (node.left!=null){
if (node.left.value==value) {
node.left = null;
return;
}
del(node.left,value);
}
if (node.right!=null){
if (node.right.value==value){
node.right=null;
return;
}
del(node.right,value);
}
}
顺序存储二叉树
如果你存储二叉树时是按照数组来存储的,那么需要你将数组按照二叉树的方式遍历出来,也就是前序后序中序遍历
这里的n是下标
//前序遍历二叉树数组
private List<Integer> ArraySearch(int index,int[] a){
if (a.length == 0){
return list;
}
list.add(a[index]);
if (index*2+1<a.length){
//左子树遍历
ArraySearch(index*2+1,a);
}
if (index*2+2<a.length){
ArraySearch(index*2+2,a);
}
return list;
}
经典问题(折纸问题)
要求一个纸张向上对折,然后在平摊出来,若是折痕往下,记为下,折痕往上,记为上:
要求输入一个整合n,然后输出相应折痕的中序遍历.
其实这个问题就可以演化成二叉树问题,你会发现,第一次折痕是down,若是看成root节点的话,那么以后每次对折,左节点就是down折痕,右节点就是up折痕.
而且,一次对折产生一个层次节点,比如两次对折就产生两个层次的节点,一共是三个节点.
那么这个问题就变得简单了.
import sun.misc.Queue;
import java.util.List;
public class PageTree {
private static Node head;
public static class Node<T>{
T item;
Node<T> left;
Node<T> right;
public Node(T item,Node<T> left,Node<T>right){
this.item=item;
this.left=left;
this.right=right;
}
}
public static Node<String> create(int n) throws InterruptedException {
Node<String> root=null;
for (int i = 0; i < n; i++) {
if (i==0){
root=new Node<>("down",null,null);
continue;
}
Queue<Node<String>> nodeQueue = new Queue<>();
nodeQueue.enqueue(root);
while (!nodeQueue.isEmpty()){
Node<String> deNode = nodeQueue.dequeue();
if (deNode.left!=null){
nodeQueue.enqueue(deNode.left);
}
if (deNode.right!=null){
nodeQueue.enqueue(deNode.right);
}
if (deNode.left==null&&deNode.right==null){
deNode.left=new Node<>("down",null,null);
deNode.right=new Node<>("up",null,null);
}
}
}
return root;
}
public static void print(Node<String> node){
System.out.println(node.item);
if (node.left!=null){
print(node.left);
}
if (node.right!=null){
print(node.right);
}
}
public static void main(String[] args) throws InterruptedException {
Node<String> stringNode = PageTree.create(3);
PageTree.print(stringNode);
}
}