本文示例代码已上传github,可直接点击查看
前言
前一阵子在学习HashMap的时候,知道了在java8之后的HashMap使用数组+链表+红黑树的结构来实现,看代码的时候百思不得其解。
因此想要学一下”树”这个数据结构,为学习红黑树打下基础,同时,二叉树的一些相关算法也是面试过程中的常问题目,提前学习以备不时之需。
本文主要写一些二叉树通用的操作,如遍历,求高度等,添加及删除节点等操作依赖于具体的二叉树实现,如排序二叉树和完全二叉树等的插入方式不同,这里不做实现,在后续文章中具体实现。
二叉树是什么?(摘自维基百科和百度百科)
1.定义
在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。
2.性质
- 在非空二叉树中,第i层的结点总数不超过 , i>=1;
- 深度为h的二叉树最多有 个结点(h>=1),最少有h个结点;
- 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
- 具有n个结点的完全二叉树的深度为 (注:[ ]表示向下取整)
- 有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:
若I为结点编号则 如果I>1,则其父结点的编号为I/2;
如果2I<=N,则其左孩子(即左子树的根结点)的编号为2I;若2I>N,则无左孩子;
如果2I+1<=N,则其右孩子的结点编号为2I+1;若2I+1>N,则无右孩子。 - 给定N个节点,能构成h(N)种不同的二叉树。
h(N)为卡特兰数的第N项。h(n)=C(2*n,n)/(n+1)。 - 设有i个枝点,I为所有枝点的道路长度总和,J为叶的道路长度总和J=I+2i
3.类型
- 满二叉树
- 完全二叉树
- 平衡二叉树
- 排序二叉树
- 红黑树
- 哈弗曼树
对各种二叉树的性质不再具体介绍,有兴趣可以自行百度。
Java实现
基本数据结构的实现
首先定义节点类,保存当前节点数据以及左右孩子信息。
class Node{
//节点保存的值,这里使用int
int val;
//左孩子
Node left;
//右孩子
Node right;
}
二叉树的类下所示:
public class BinaryTree {
//根节点
private Node root = null;
//构造方法
public BinaryTree() {
this.root = new Node(1, new Node(2, new Node(4, null, null), new Node(5, null, null)),
new Node(3, null, new Node(6, null, null)));
}
class Node {
int val;
//左孩子
Node left;
//右孩子
Node right;
Node(int val, Node left, Node right) {
this.val = val;
this.left = left;
this.right = right;
}
@Override
public String toString() {
return String.valueOf(val);
}
}
}
在图中的构造方法中,我用粗暴的方式构造了一个形如下图的二叉树,方便后续的测试。
二叉树的操作实现
遍历实现
二叉树的遍历可以说是面试过程中的重难点了,初,中甚至高级工程师的面试中都有可能碰到,而且大部分是让你白板编程写遍历算法,所以这一块一定要理解原理并加上多多实践。
二叉树的遍历思路有两种,深度遍历和广度遍历,其中深度遍历又分为前序,中序,后续三种,下面将对这几种逐个实现。
1.前序遍历的递归实现
/**
* 前序遍历(递归): 1、访问这个节点 2、调用自身来遍历节点的左子树 3、调用自身来遍历节点的右子树
*/
public List<Integer> preOrderTraversal() {
List<Integer> result = new ArrayList<>();
preOrder(root, result);
return result;
}
private void preOrder(Node root, List<Integer> result) {
if (null == root) {
return;
}
result.add(root.val);
preOrder(root.left, result);
preOrder(root.right, result);
}
二叉树的定义本身就是一个递归的过程,二叉树的根节点的左右孩子又分别是二叉树,所以在遍历的过程中,使用递归的思想十分简单。
首先访问当前节点,如果当前节点为空则返回,不为空则将当前节点的值放入结果列表,然后调用自身遍历自己的左孩子和右孩子。
2.前序遍历的非递归实现
/**
* 非递归先序遍历二叉树
* */
private List<Integer> prerderTraversal2() {
List<Integer> resultList=new ArrayList<>();
Stack<Node> treeStack=new Stack<>();
if(root==null) //如果为空树则返回
return resultList;
treeStack.push(root);
while(!treeStack.isEmpty()){
Node tempNode=treeStack.pop();
if(tempNode!=null){
resultList.add(tempNode.val);//访问根节点
treeStack.push(tempNode.right); //入栈右孩子
treeStack.push(tempNode.left);//入栈左孩子
}
}
return resultList;
}
-------------------------这一块写的很繁琐,理解了的同学可以不用看了直接跳过------------------------
在非递归的前序遍历中我们借助了栈,这块刚开始有些难理解,我们来一步一步试一下:
以上面图中的二叉树为例:
- 根节点也就是1入栈。此时栈从底到顶为:1,结果列表为空。
- 栈不为空,进入while循环,将1出栈并且添加到结果中,然后入栈1的右孩子,左孩子。此时栈从底到顶为:3,2,结果列表为1。
- 栈不为空,进入while循环,将2出栈并且添加到结果中,然后将入栈2的右孩子,左孩子。此时栈从底到顶为:3,5,4,结果列表为1,2。
- 栈不为空,进入while循环,将4出栈并加入结果中,然后将4的右孩子左孩子入栈(皆为空)。此时栈从底到顶为:3,5,结果列表为1,2,4。
- 栈不为空,进入while循环,将5出栈并加入结果,然后将5的右孩子,左孩子入栈(皆为空)。此时栈从底到顶为:3,结果列表为1,2,4,5。
- 栈不为空,进入while循环,将3出栈并加入结果,将3的右孩子(6),左孩子(空)入栈。此时栈从底到顶为:6,结果列表为1,2,4,5,3。
- 栈不为空,进入while循环,将6出栈并加入结果,然后将右孩子左孩子入栈(皆为空)。此时栈从底到顶为:空,结果列表为1,2,4,5,3,6。
- 栈为空,结束循环返回结果。
-------------------繁琐结束分割线----------------------------
以上两种遍历方式返回的结果都为:1,2,4,5,3,6
.
3.中序遍历的递归实现
/**
* 递归实现中序遍历
*/
private List<Integer> inrderTraversal() {
List<Integer> result = new ArrayList<>();
inOrder(root, result);
return result;
}
private void inOrder(Node root, List<Integer> result) {
if (null == root) {
return;
}
inOrder(root.left, result);
result.add(root.val);
inOrder(root.right, result);
}
这个的实现思路和递归实现前序遍历很相似,这里不再赘述。
4.中序遍历的非递归实现
/**
* 非递归实现中序遍历
*/
private List<Integer> inOrderTraversal2() {
List<Integer> list = new ArrayList<>();
Stack<Node> stack = new Stack<>();
Node cur = root;
while(cur!=null || !stack.empty()){
while(cur!=null){
stack.add(cur);
cur = cur.left;
}
cur = stack.pop();
list.add(cur.val);
cur = cur.right;
}
return list;
}
非递归实现的中序遍历思想:首先找到二叉树最左下角的节点,即从根节点一直沿着左孩子向前走,知道某一节点没有左孩子,然后将该节点添加到结果列表,继续以相同的思路遍历该节点的右孩子。之后会退一个节点,执行相同的操作,由于二叉树的实现中,节点并不保存父节点的信息,所以也需要借助栈来保存回退的信息。
具体的思路不再讲解,如果不很理解可以照着代码写一遍前序遍历中那种繁琐的流程就懂了。
以上中序遍历的结果为:4,2,5,1,3,6
.
5.后序遍历的递归实现
/**
* 递归实现后序遍历
*/
private List<Integer> postOrderTraversal() {
List<Integer> result = new ArrayList<>();
postOrder(root, result);
return result;
}
private void postOrder(Node root, List<Integer> result) {
if (null == root) {
return;
}
postOrder(root.left, result);
postOrder(root.right, result);
result.add(root.val);
}
原理与前面类似且十分简单,看代码即可。
6.后序遍历的非递归实现
/**
* 非递归实现后序遍历
*/
private List<Integer> postOrderTraversal2() {
Stack<Node> stack = new Stack<>();
stack.push(root);
List<Integer> resultList = new ArrayList<>();
while (!stack.isEmpty()) {
Node node = stack.pop();
if (node != null) {
resultList.add(node.val);
stack.push(node.left);
stack.push(node.right);
}
}
Collections.reverse(resultList);
return resultList;
}
其实后续遍历的实现有许多种,我自己也写了一种十分麻烦但是实现了功能的方法,但是后来在网上看到了这种取巧的办法,还是觉得应该写上这个方法,毕竟我们不是为了学习茴字的四种写法,而是为了学习一种足够好的方法去解决问题。
这种方法的思路是利用非递归实现的前序遍历,在前序遍历中,是根-左-右,那么我们将其稍微改一下,变成根-右-左,这样拿到结果之后翻转一下列表,就可以拿到后续遍历啦。
6.层次遍历
/**
* 层次遍历
*/
private List<Integer> levelIterator() {
List<Integer> resultList = new ArrayList<>();
if (root == null) {
return resultList;
}
LinkedList<Node> queue = new LinkedList<Node>();
Node current = null;
queue.offer(root);//将根节点入队
while (!queue.isEmpty()) {
current = queue.poll();//出队队头元素并访问
if (current != null) {
resultList.add(current.val);
queue.offer(current.left);
queue.offer(current.right);
}
}
return resultList;
}
层次遍历比较简单,借助了队列来实现,首先将根节点入队,然后从队首获取元素添加到结果中,并将其左孩子,右孩子依次入队,直到队列为空即可。
参考链接
https://www.jianshu.com/p/0190985635eb
https://blog.csdn.net/Double2hao/article/details/53286038
完。
ChangeLog
2018-11-04 完成以上皆为个人所思所得,如有错误欢迎评论区指正。
欢迎转载,烦请署名并保留原文链接。
联系邮箱:huyanshi2580@gmail.com
更多学习笔记见个人博客------>呼延十