Java实现二叉树和遍历

java

leetcode刷题需要经常用的二叉树,发现二叉树这种可以无限扩展知识点来虐别人的数据结构,很受面试官的青睐,这里记录一下Java定义二叉树和遍历。

一、什么是二叉树

1 .二叉树的性质

本身是有序树,树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2

 

 图 1 二叉树示意图

二叉树具有以下几个性质:

  1. 二叉树中,第 i 层最多有 2i-1 个结点。
  2. 如果二叉树的深度为 K,那么此二叉树最多有 2K-1 个结点。
  3. 二叉树中,终端结点数(叶子结点数)为 n0,度为 2 的结点数为 n2,则 n0=n2+1。

性质 3 的计算方法为:对于一个二叉树来说,除了度为 0 的叶子结点和度为 2 的结点,剩下的就是度为 1 的结点(设为 n1),那么总结点 n=n0+n1+n2
同时,对于每一个结点来说都是由其父结点分支表示的,假设树中分枝数为 B,那么总结点数 n=B+1。而分枝数是可以通过 n1 和 n2 表示的,即 B=n1+2*n2。所以,n 用另外一种方式表示为 n=n1+2*n2+1。
两种方式得到的 n 值组成一个方程组,就可以得出 n0=n2+1。

2 .满二叉树

如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树




图 2 满二叉树示意图

满二叉树除了满足普通二叉树的性质,还具有以下性质:

  1. 满二叉树中第 i 层的节点数为 2n-1 个。
  2. 深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1
  3. 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。
  4. 具有 n 个节点的满二叉树的深度为 log2(n+1)。

3 .完全二叉树

如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树



图 3 完全二叉树示意图

如图 3a) 所示是一棵完全二叉树,图 3b) 由于最后一层的节点没有按照从左向右分布,因此只能算作是普通的二叉树。
完全二叉树除了具有普通二叉树的性质,它自身也具有一些独特的性质,比如说,n 个结点的完全二叉树的深度为 ⌊log2n⌋+1。

⌊log2n⌋ 表示取小于 log2n 的最大整数。例如,⌊log24⌋ = 2,而 ⌊log25⌋ 结果也是 2。

对于任意一个完全二叉树来说,如果将含有的结点按照层次从左到右依次标号(如图 3a)),对于任意一个结点 i ,完全二叉树还有以下几个结论成立:

    1. 当 i>1 时,父亲结点为结点 [i/2] 。(i=1 时,表示的是根结点,无父亲结点)
    2. 如果 2*i>n(总结点的个数) ,则结点 i 肯定没有左孩子(为叶子结点);否则其左孩子是结点 2*i 。
    3. 如果 2*i+1>n ,则结点 i 肯定没有右孩子;否则右孩子是结点 2*i+1 。

二、二叉树的存储结构

二叉树的存储结构有两种,分别为顺序存储和链式存储。

1 .顺序存储

二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。满二叉树也可以使用顺序存储。要知道,满二叉树也是完全二叉树,因为它满足完全二叉树的所有特征。

普通二叉树转完全二叉树的方法很简单,只需给二叉树额外添加一些节点,将其"拼凑"成完全二叉树即可。如图 1 所示:



图 1 普通二叉树的转化

图 1 中,左侧是普通二叉树,右侧是转化后的完全(满)二叉树。
解决了二叉树的转化问题,接下来学习如何顺序存储完全(满)二叉树。
完全二叉树的顺序存储,仅需从根节点开始,按照层次依次将树中节点存储到数组即可。




图 2 完全二叉树示意图

例如,存储图 2 所示的完全二叉树,其存储状态如图 3 所示:


图 3 完全二叉树存储状态示意图

同样,存储由普通二叉树转化来的完全二叉树也是如此。例如,图 1 中普通二叉树的数组存储状态如图 4 所示:



图 4 普通二叉树的存储状态

由此,我们就实现了完全二叉树的顺序存储。
不仅如此,从顺序表中还原完全二叉树也很简单。我们知道,完全二叉树具有这样的性质,将树中节点按照层次并从左到右依次标号(1,2,3,...),若节点 i 有左右孩子,则其左孩子节点为 2*i,右孩子节点为 2*i+1。此性质可用于还原数组中存储的完全二叉树,也就是实现由图 3 到图 2、由图 4 到图 1 的转变。

2 .链式存储

其实二叉树并不适合用数组存储,因为并不是每个二叉树都是完全二叉树,普通二叉树使用顺序表存储或多或多会存在空间浪费的现象。

 



图 1 普通二叉树示意图

如图 1 所示,此为一棵普通的二叉树,若将其采用链式存储,则只需从树的根节点开始,将各个节点及其左右孩子使用链表存储即可。因此,图 1 对应的链式存储结构如图 2 所示:




图 2 二叉树链式存储结构示意图

由图 2 可知,采用链式存储二叉树时,其节点结构由 3 部分构成(如图 3 所示):

  • 指向左孩子节点的指针(Lchild);
  • 节点存储的数据(data);
  • 指向右孩子节点的指针(Rchild);

 



图 3 二叉树节点结构

其实,二叉树的链式存储结构远不止图 2 所示的这一种。例如,在某些实际场景中,可能会做 "查找某节点的父节点" 的操作,这时可以在节点结构中再添加一个指针域,用于各个节点指向其父亲节点,如图 4 所示:

 

 图 4 自定义二叉树的链式存储结构

这样的链表结构,通常称为三叉链表。

利用图 4 所示的三叉链表,我们可以很轻松地找到各节点的父节点。因此,在解决实际问题时,用合适的链表结构存储二叉树,可以起到事半功倍的效果。

三、java实现二叉树

1.Node节点的定义

我们需要自定义一个Node类

package com.hanwl.leetcode;

/**

* @Author hanwl

* @Date 2021-03-26 10:30

* @Version 1.0

* 定义二叉树的一个节点node

*/

public class Node {

private int value; //节点的值

private Node node; //此节点,数据类型为Node

private Node left; //此节点的左子节点,数据类型为Node

private Node right; //此节点的右子节点,数据类型为Node

public Node() {

}

public Node(int value) {

this.value = value;

this.left = null;

this.right = null;

}

public int getValue() {

return value;

}

public void setValue(int value) {

this.value = value;

}

public Node getNode() {

return node;

}

public void setNode(Node node) {

this.node = node;

}

public Node getLeft() {

return left;

}

public void setLeft(Node left) {

this.left = left;

}

public Node getRight() {

return right;

}

public void setRight(Node right) {

this.right = right;

}

}

2.数组转换二叉树

数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组

顺序二叉树通常只考虑完全二叉树

  第n个元素的左子节点为 2 * n + 1

  第n个元素的右子节点为 2 * n + 2

  第n个元素的父节点为 (n-1) / 2

  n : 表示二叉树中的第几个元素(按0开始编号,如图所示)

  对于具有n个节点的完全二叉树,如果按照从上至下和从左至右的顺序对所有节点序号从0开始顺序编号,则对于序号为i(0<=i< n)的节点有:
  如果i>0,则序号为i节点的双亲节点的序号为(i-1)/2(/为整除);如果i=0,则序号为i节点为根节点,无双亲节点 如果2i+1<n,则序号为i节点的左孩子节点的序号为2i+1;

  如果2i+1>=n,则序号为i节点无左孩子 如果2i+2<n,则序号为i节点的右孩子节点的序号为2i+2;如果2i+2>=n,则序号为i节点无右孩子

 

        

 

package com.hanwl.leetcode;

import java.util.ArrayDeque;

import java.util.ArrayList;

import java.util.List;

import java.util.Stack;

/**

* @Author hanwl

* @Date 2021-03-26 10:36

* @Version 1.0

一般拿到的数据是一个int型的数组,那怎么将这个数组变成我们可以直接操作的树结构呢?

1、数组元素变Node类型节点

2、给N/2-1个节点设置子节点

3、给最后一个节点设置子节点【有可能只有左节点】

*/

public class TreeNode {

public static void main(String[] args) {

int[] array = {1, 2, 3, 4, 5, 6, 7};

List<Node> list = new ArrayList<>();

TreeNode treeNode = new TreeNode();

treeNode.create(array,list);

// nodeList中第0个索引处的值即为根节点

Node root = list.get(0);

System.out.println("先序遍历:");

preOrderTraverse(root);

System.out.println();

System.out.println("中序遍历:");

inOrderTraverse(root);

System.out.println();

System.out.println("后序遍历:");

postOrderTraverse(root);

System.out.println();

System.out.println("非递归先序遍历:");

preOrderTraversalbyLoop(root);

System.out.println();

System.out.println("非递归中序遍历:");

inOrderTraversalbyLoop(root);

System.out.println();

System.out.println("非递归后序遍历:");

postOrderTraversalbyLoop(root);

System.out.println();

System.out.println("广度优先遍历:");

levelOrderTraversal(root);

System.out.println();

System.out.println("深度优先遍历:");

depthTraversal(root);

}

//创建二叉树

public void create(int[] datas, List<Node> list){

// 将数组里面的东西变成节点的形式

for(int i=0;i<datas.length;i++) {

Node node=new Node(datas[i]);

list.add(node);

}

// 节点关联成树

for(int index=0;index<list.size()/2-1;index++){

//编号为n的节点他的左子节点编号为2*n 右子节点编号为2*n+1 但是因为list从0开始编号,所以还要+1

list.get(index).setLeft(list.get(index*2+1));

//与上同理

list.get(index).setRight(list.get(index*2+2));

}

// 最后一个父节点,因为最后一个父节点可能没有右孩子,所以单独拿出来处理 避免单孩子情况

int lastParentIndex=list.size()/2-1;

list.get(lastParentIndex).setLeft(list.get(lastParentIndex*2+1));

if (list.size()%2==1) {

// 如果有奇数个节点,最后一个父节点才有右子节点

list.get(lastParentIndex).setRight(list.get(lastParentIndex*2+2));

}

}

/**

* 先序遍历

*

* 这三种不同的遍历结构都是一样的,只是先后顺序不一样而已

*

* @param node

* 遍历的节点

*/

public static void preOrderTraverse(Node node) {

if (node == null)

return;

System.out.print(node.getValue() + " ");

preOrderTraverse(node.getLeft());

preOrderTraverse(node.getRight());

}

/**

* 中序遍历

*

* 这三种不同的遍历结构都是一样的,只是先后顺序不一样而已

*

* @param node

* 遍历的节点

*/

public static void inOrderTraverse(Node node) {

if (node == null)

return;

inOrderTraverse(node.getLeft());

System.out.print(node.getValue() + " ");

inOrderTraverse(node.getRight());

}

/**

* 后序遍历

*

* 这三种不同的遍历结构都是一样的,只是先后顺序不一样而已

*

* @param node

* 遍历的节点

*/

public static void postOrderTraverse(Node node) {

if (node == null)

return;

postOrderTraverse(node.getLeft());

postOrderTraverse(node.getRight());

System.out.print(node.getValue() + " ");

}

/**

* 非递归前序遍历

* 基本的原理就是当循环中的p不为空时,就读取p的值,并不断更新p为其左子节点,即不断读取左子节点,

* 直到一个枝节到达最后的子节点,再继续返回上一层进行取值

*

* 我这里使用了栈这个数据结构,用来保存不到遍历过但是没有遍历完全的父节点,之后再进行回滚。

* */

public static void preOrderTraversalbyLoop(Node node){

Stack<Node> stack = new Stack();

Node p = node;

while(p!=null || !stack.isEmpty()){

while(p!=null){

//当p不为空时,就读取p的值,并不断更新p为其左子节点,即不断读取左子节点

System.out.print(p.getValue()+" ");

stack.push(p); //将p入栈

p = p.getLeft();

}

if(!stack.isEmpty()){

p = stack.pop();

p = p.getRight();

}

}

}

/**

*非递归中序遍历

* 基本原理就是当循环中的p不为空时,就读取p的值,并不断更新p为其左子节点,但是切记这个时候不能进行输出,必须不断读取左子节点,

* 直到一个枝节到达最后的子节点,然后每次从栈中拿出一个元素,就进行输出,再继续返回上一层进行取值

* */

public static void inOrderTraversalbyLoop(Node node){

Stack<Node> stack = new Stack();

Node p = node;

while(p!=null || !stack.isEmpty()){

while(p!=null){

stack.push(p);

p = p.getLeft();

}

if(!stack.isEmpty()){

p = stack.pop();

System.out.print(p.getValue()+" ");

p = p.getRight();

}

}

}

/**

* 非递归后序遍历

* */

public static void postOrderTraversalbyLoop(Node node){

Stack<Node> stack = new Stack<Node>();

Node p = node, prev = node;

while(p!=null || !stack.isEmpty()){

while(p!=null){

stack.push(p);

p = p.getLeft();

}

if(!stack.isEmpty()){

Node temp = stack.peek().getRight();

//只是拿出来栈顶这个值,并没有进行删除

if(temp == null||temp == prev){

//节点没有右子节点或者到达根节点【考虑到最后一种情况】

p = stack.pop();

System.out.print(p.getValue()+" ");

prev = p;

p = null;

}

else{

p = temp;

}

}

}

}

// 广度优先遍历 参数node为根节点

public static void levelOrderTraversal(Node node){

if(node==null){

System.out.print("empty tree");

return;

}

ArrayDeque<Node> deque = new ArrayDeque<Node>();

deque.add(node);

while(!deque.isEmpty()){

Node rnode = deque.remove();

System.out.print(rnode.getValue()+" ");

if(rnode.getLeft()!=null){

deque.add(rnode.getLeft());

}

if(rnode.getRight()!=null){

deque.add(rnode.getRight());

}

}

}

// 深度优先遍历 参数node为根节点

public static void depthTraversal(Node node){

if(node==null){

System.out.print("empty tree");

return;

}

Stack<Node> stack = new Stack<Node>();

stack.push(node);

while(!stack.isEmpty()){

Node rnode = stack.pop();

System.out.print(rnode.getValue()+" ");

if(rnode.getLeft()!=null){

stack.add(rnode.getLeft());

}

if(rnode.getRight()!=null){

stack.add(rnode.getRight());

}

}

}

}

3.深度优先和广度优先遍历详细步骤

深度优先遍历:
深度优先遍历(Depth First Search),简称DFS,其原则是,沿着一条路径一直找到最深的那个节点,当没有子节点的时候,返回上一级节点,寻找其另外的子节点,继续向下遍历,没有就向上返回一级,直到所有的节点都被遍历到,每个节点只能访问一次。

上图中的深度优先遍历结果是 {1,2,4,8,9,5,10,3,6,7 },遍历过程是首先访问1,然后是1的左节点2,然后是2的左节点4,再是4的左节点8,8没有子节点了,返回遍历8的父节点的4的另一个子节点9,9没有节点,再向上返回。(注意:这里的返回上一次并不会去重新遍历一遍)。

在算法实现的过程中,我们采用了栈(Stack)这种数据结构,它的特点是,最先压入栈的数据最后取出。

算法步骤:

1、首先将根节点1压入栈中【1】

2、将1节点弹出,找到1的两个子节点3,2,首先压入3节点,再压入2节点(后压入左节点的话,会先取出左节点,这样就保证了先遍历左节点),2节点再栈的顶部,最先出来【2,3】

3、弹出2节点,将2节点的两个子节点5,4压入【4,5,3】

4、弹出4节点,将4的子节点9,8压入【8,9,5,3】

5,弹出8,8没有子节点,不压入【9,5,3】

6、弹出9,9没有子节点,不压入【5,3】

7、弹出5,5有一个节点,压入10,【10,3】

8、弹出10,10没有节点,不压入【3】

9、弹出3,压入3的子节点7,6【6,7】

10,弹出6,没有子节点【7】

11、弹出7,没有子节点,栈为空【】,算法结束

我们来看一看节点的出栈顺序【1,2,4,8,9,5,10,3,6,7】,刚好就是我们深度遍历的顺序。下面看Java代码

/**

* 二叉树的深度优先遍历

* @param root

* @return

*/

public List<Integer> dfs(Node root){

Stack<Node> stack=new Stack<Node>();

List<Integer> list=new LinkedList<Integer>();

if(root==null)

return list;

//压入根节点

stack.push(root);

//然后就循环取出和压入节点,直到栈为空,结束循环

while (!stack.isEmpty()){

Node t=stack.pop();

if(t.getRight()!=null)

stack.push(t.getRight());

if(t.getLeft()!=null)

stack.push(t.getLeft());

list.add(t.getValue());

}

return list;

}

广度优先遍历:
广度优先遍历(Breadth First Search),简称BFS;广度优先遍历的原则就是对每一层的节点依次访问,一层访问结束后,进入下一层,直到最后一个节点,同样的,每个节点都只访问一次。

上图中,广度优先遍历的结果是【1,2,3,4,5,6,7,8,9,10】,遍历顺序就是从上到下,从左到右。

在算法实现过程中,我们使用的队列(Queue)这种数据结构,这种结构的特点是先进先出,在java中Queue是一个借口,而LinkedList实现了这个接口的所有功能。

算法过程:

1、节点1,插入队列【1】

2、取出节点1,插入1的子节点2,3 ,节点2在队列的前端【2,3】

3、取出节点2,插入2的子节点4,5,节点3在队列的最前端【3,4,5】

4、取出节点3,插入3的子节点6,7,节点4在队列的最前端【4,5,6,7】

5、取出节点4,插入3的子节点8,9,节点5在队列的最前端【5,6,7,8,9】

6、取出节点5,插入5的子节点10,节点6在队列的最前端【6,7,8,9,10】

7、取出节点6,没有子节点,不插入,节点7在队列的最前端【7,8,9,10】

8、取出节点7,没有子节点,不插入,节点8在队列的最前端【8,9,10】

9、取出节点8,没有子节点,不插入,节点9在队列的最前端【9,10】

10、取出节点9,没有子节点,不插入,节点10在队列的最前端【10】

11、取出节点10,队列为空,算法结束

我们看一下节点出队的顺序【1,2,3,4,5,6,7,8,9,10】,就是广度优先遍历的顺序,下面看java代码

/**

* 二叉树的广度优先遍历

* @param root

* @return

*/

public List<Integer> bfs(Node root) {

Queue<Node> queue = new LinkedList<Node>();

List<Integer> list=new LinkedList<Integer>();

if(root==null)

return list;

queue.add(root);

while (!queue.isEmpty()){

Node t=queue.remove();

if(t.getLeft()!=null)

queue.add(t.getLeft());

if(t.getRight()!=null)

queue.add(t.getRight());

list.add(t.getValue());

}

return list;

}

 

参考地址:

https://blog.csdn.net/weixin_42636552/article/details/82973190

https://blog.csdn.net/XTAOTWO/article/details/83625586

 

 

 

以上是 Java实现二叉树和遍历 的全部内容, 来源链接: utcz.com/z/389861.html

回到顶部