简介:本文介绍二叉树基本概念,并通过继承java集合List接口,java实现BinaryTreeList操作算法:插入、删除、前中后层次遍历、二叉树深度、最大小宽度等。
一、二叉树基本概念
1.二叉树是什么?
二叉树是树的一种,是一种节点的度不大于2的有序树。
2.为什么会出现二叉树?或者说二叉树为了解决什么问题而出现的?
为了解决链表中查找、插入最坏为O(n)的情况,数组中删除最坏移动元素O(n)的情况。
例如:数据库需要多次查找、更新操作,此时使用数组、链表这种数据结构会很慢。而二叉树则可以达到O(lgn)情况。
3.二叉树有哪些性质?
性质1:二叉树第i层上的结点数目最多为
(i≥1)。
性质2:深度为k的二叉树至多有个结点(k≥1)。
性质3:包含n个结点的二叉树的高度至少为。
性质4:在任意一棵二叉树中,若终端结点的个数为,度为2的结点数为
,则
。
4.二叉树节点如何定义?
属性:父节点(可选)、左孩子、右孩子、值
二、二叉树操作算法介绍
1.添加元素
思路:根据二叉树特性,找到待插入元素合适位置,插入即可。
2.遍历
前序:根左右。思路:辅助栈,访问节点打印并入栈,出栈再访问右节点。
中序:左根右。思路:辅助栈,访问到最左节点,出栈打印,再访问右节点。
后序:左右根。思路:辅助栈,访问到最左节点,出栈,当节点左右为空,或者是第二次访问时打印,否则继续入栈,访问右节点。
层次:一层一层。辅助队列,节点出队打印,节点左右节点分别入队列。
3.删除元素
思路:找到待删除的节点,然后:
1)叶子节点直接删除
2)含有左或右子树,将其子树作为其父节点的左/右子树
3)包含左、右子树,找到右子树最左节点(或左子树最右节点)y替换待删除节点。
- 如果是待删除节点右节点,直接替换。
- 如果不是待删除节点右节点,需要y.right替换y,然后y替换待删除节点
4.二叉树高度(深度)
思路:借助层次遍历思想。辅助队列,入队一层后根据队列当前大小全部出队代表一层节点全部出完,每层代表深度+1。
5.二叉树最大宽度(最小宽度)
思路:借助层次遍历思想。辅助队列,每层包含元素最大、最小值。
6.查找元素
思路:根据二叉树特性,直接寻找该值。
7.遍历转换
中序、后续转前序
三、二叉树操作算法java实现
1.实现思路
参照java集合ArrayList实现一个BinaryTreeList,并增加二叉树特有算法。
2.为什么要通过实现BinaryTreeList来实现二叉树操作算法?
1)二叉树仅是一种数据结构,只有和算法结合并封装后才能实际使用。
2)参照java集合实现一个可用的BinaryTreeList,能够学习到java集合原码中的编程、算法实现风格。
3.BinaryTreeList类如何布局?
除了普通的二叉树外,还存在进阶版的平衡二叉树。因此,将二叉树需要独立操作如添加、删除等方法在BinaryTreeList类中实现;将是否为空、是否包含某个元素等和平衡二叉树公有方法放到AbstractTreeList中。
四、AbstractTreeList类java实现
1.定义Node节点类
static class Node<E> {
E value;
Node<E> parent;
Node<E> leftChild;
Node<E> rightChild;
Node(Node<E>parent,Node<E> left,Node<E> right,E value){
this.parent = parent;
this.leftChild = left;
this.rightChild = right;
this.value = value;
}
Node(Node<E> left,Node<E> right,E value){
this(null,left,right,value);
}
Node(){}
}
2.为什么Node节点类作为内部静态类存放于AbstractTreeList抽象类中?
静态内部类和一般内部类都可以存在于抽象类中。两者区别在于:
- 一般内部类对象中包含一个外部类对象的this指针的引用用于访问外部类对象,这容易导致内存泄漏。
- 静态内部类对象不存在外部类对象的this指针,不易内存泄露。
3.AbstractTreeList抽象类结构和属性
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.Queue;
public abstract class AbstractTreeList<E> implements List<E>{
int size = 0;//表示TreeList元素个数
Node<E> root;//表示根节点
static class Node<E> {//节点静态类
E value;
Node<E> parent;
Node<E> leftChild;
Node<E> rightChild;
Node(Node<E>parent,Node<E> left,Node<E> right,E value){
this(parent,left,right,value,0);
}
Node(Node<E> left,Node<E> right,E value){
this(null,left,right,value);
}
Node(){}
}
//各种实现的方法 ...
}
4.二叉树遍历算法实现
先序遍历:根左右。辅助栈,入栈前打印,遍历到最左节点,出栈后遍历其右子树。
/**
* 先序遍历:根左右。辅助栈,入栈前打印,遍历到最左节点,出栈后遍历其右子树
* @return
*/
public List<E> asListPre() {
Node<E> r;
ArrayList<E> resultList = new ArrayList<E>();
if((r=root) == null) return resultList;
LinkedList<Node<E>> stack = new LinkedList<Node<E>>();
while(r!=null || !stack.isEmpty()) {
while (r!=null) {
resultList.add(r.value);
stack.push(r);
r = r.leftChild;
}
r = stack.pop();
r = r.rightChild;
}
return resultList;
}
中序遍历:左根右。辅助栈,遍历到最左节点全部入栈,出栈打印并入右子树。
/**
* 先序:左根右。辅助栈,根全部入栈,出栈打印。
* @return
*/
public List<E> asListOrder() {
Node<E> r;
ArrayList<E> resultList = new ArrayList<E>();
if((r=root) == null) return resultList;
LinkedList<Node<E>> stack = new LinkedList<Node<E>>();
while(r!=null || !stack.isEmpty()) {
while (r!=null) {
stack.push(r);
r = r.leftChild;
}
r = stack.pop();
resultList.add(r.value);
r = r.rightChild;
}
return resultList;
}
后续遍历:左右根。辅助栈,遍历到最左节点全部入栈,出栈栈顶节点左右孩子为空、且是第二次遍历时打印,否则再次入栈。
/**
* 后续遍历:左右根。辅助栈,遍历到最左节点全部入栈,出栈栈顶节点左右孩子为空、且是第二次遍历时打印,否则再次入栈。
* @return
*/
public List<E> asListLast() {
Node<E> cur,pre = null;
ArrayList<E> resultList = new ArrayList<E>();
if((cur =root) == null) return resultList;
LinkedList<Node<E>> stack = new LinkedList<Node<E>>();
while(cur!=null || !stack.isEmpty()) {
while (cur!=null) {
stack.push(cur);
cur = cur.leftChild;
}
cur = stack.pop();
if(cur.rightChild == null || cur.rightChild==pre) {//当前cur节点是叶子节点;当前cur.right节点是上次访问的节点
resultList.add(cur.value);
pre = cur;
cur = null;//已经访问了cur的左子节点,不需要再次访问
}else {
stack.push(cur);
cur = cur.rightChild;
}
}
return resultList;
}
层次遍历:辅助队列,将所有节点入队,出对后将节点左、右子树入队,直到队空
/**
* 层次遍历:辅助队列,将所有节点入队,出对后将节点左、右子树入队,直到队空
* @return
*/
public List<E> asListLevel() {
Node<E> r;
ArrayList<E> resultList = new ArrayList<E>();
if((r =root) == null) return resultList;
Queue<Node<E>> queue = new LinkedList<Node<E>>();
queue.offer(r);
while(!queue.isEmpty()) {
r = queue.poll();
resultList.add(r.value);
if(r.leftChild != null) queue.offer(r.leftChild);
if(r.rightChild != null) queue.offer(r.rightChild);
}
return resultList;
}
5.二叉树最大、最小宽度,最大深度
最大深度:借助层次遍历思想,每次遍历一层所有值(个数使用queue.size计算),出队节点左右子树入队,一层出完deep++。
/**
* 二叉树最大深度。思路:层次遍历,每次遍历一层所有值(个数使用queue.size计算),然后deep++
* @return
*/
public int maxDeep() {
return maxDeep(root);
}
public int maxDeep(Node<E> r) {
if(r == null) return 0;
int deep = 0;
Queue<Node<E>> queue = new LinkedList<Node<E>>();
queue.offer(r);
while(!queue.isEmpty()) {
deep++;
int queueSize = queue.size();
while(queueSize > 0) {
queueSize--;
r = queue.poll();
if(r.leftChild != null) queue.offer(r.leftChild);
if(r.rightChild != null) queue.offer(r.rightChild);
}
}
return deep;
}
最大、最小宽度:和最大深度思想一致。
public int maxWidth() {
Node<E> r;
if((r = root) == null) return 0;
int width = 0;
Queue<Node<E>> queue = new LinkedList<Node<E>>();
queue.offer(r);
while(!queue.isEmpty()) {
int queueSize = queue.size();
width = (queueSize > width) ? queueSize : width;
//width = (queueSize < width) ? queueSize : width;//最小宽度用此替换
while(queueSize > 0) {
queueSize--;
r = queue.poll();//出队列同时,将下一层节点入队列
if(r.leftChild != null) queue.offer(r.leftChild);
if(r.rightChild != null) queue.offer(r.rightChild);
}
}
return width;
}
6.查找等其他操作算法
@Override
public boolean contains(Object o) {
if(o == null) throw new NullPointerException("输入元素为空");
Node<E> r;
if((r = root) == null) return false;
while (r != null) {
if(compare((E) o, r.value) < 0) r = r.leftChild;
else if(compare((E) o, r.value) > 0) r = r.rightChild;
else return true;
}
return false;
}
@Override
public boolean isEmpty() {
return (size == 0)?true:false;
}
@Override
public int size() {
return (root == null) ? 0 : size;
}
int compare(E e1, E e2) {
return ((Comparable<E>) e1).compareTo(e2);
}
五、BinaryTreeList类java实现
BinaryTreeList类主要实现二叉树的插入、删除操作。
1.BinaryTreeList类结构
public class BinaryTreeList<E> extends AbstractTreeList<E> implements List<E>{
public BinaryTreeList(){
super();
}
//其他方法...
}
2.二叉树插入操作
思路:根据插入值e的大小,找到树中合适的插入位置,然后插入,设置size++。
@Override
public boolean add(E e) {
addVal(e);
return true;
}
void addVal(E e) {
if (e == null) throw new NullPointerException();
Node<E> r = root;
if(r == null) root = new Node<E>(null,null, null, e);
else if(contains(e)) return ;
else {
//根据r和e找到null节点,就是待插入的节点.需要保留该null节点的父节点
Node<E> parent = root;
while(r != null) {
parent = r;
if(compare(e, r.value)<0) r = r.leftChild;
else if(compare(e, r.value) > 0) r = r.rightChild;
}
Node<E> ele = new Node<E>(parent,null, null, e);
if (compare(e,parent.value) < 0) parent.leftChild = ele;
else if(compare(e,parent.value) > 0)parent.rightChild = ele;
else return;
}
size++;
}
3.二叉树删除操作
思路:先根据删除元素e先找到对应节点z,判断待删除节点结构。
1)叶子节点:直接删除。
2)只有一个孩子的节点:用其子树替换待删除节点。
3)有两个孩子的节点:寻找z的后继节点y,让y占据z的位置。z原来的左、右子树分别称为y的左右子树。其中y一定没有左孩子。
- y是z的右孩子,直接用y替换z,并拼接上z的左孩子到y。
- y不是z的右孩子,先用y的右孩子替换y,再用y替换z。
/**
* 先找到节点。根据节点结构,做不同操作。
*/
@Override
public boolean remove(Object o) {
if(o == null) throw new NullPointerException("删除元素为null");
return removeVal(o);
}
boolean removeVal(Object o) {
//删除值o,需要先寻找到o所在的节点z,然后判断z是叶子节点、只有一个孩子的节点、两个孩子节点
Node<E> z=root;
if(z== null) return false;
while(z!=null) {
if(compare((E)o, z.value) < 0) z = z.leftChild;
else if(compare((E)o, z.value) > 0) z = z.rightChild;
else {//找到了
if(z.leftChild == null) transplant(z, z.rightChild);
else if(z.rightChild == null) transplant(z, z.leftChild);
else {
Node<E>y = z.rightChild;
while(y.leftChild!=null) y = y.leftChild;
if(y != z.rightChild) {
transplant(y, y.rightChild);//替换y和y右孩子父节点
//拼接y节点右孩子
y.rightChild = z.rightChild;
z.rightChild.parent = y;
}
//y替换z,设置了y和z.parent的关系
transplant(z, y);
//拼接y节点的左孩子
y.leftChild = z.leftChild;
y.leftChild.parent = y;
}
return true;
}
}
return false;
}
/**
* 用newNode为根的子树移植到oldNode为根的子树。注意oldNode的左孩子,右孩子并没有拼接到newNode
* @param oldNode 待替换子树节点根
* @param newNode 替换oldNode子树新子树的根
*/
void transplant(Node<E>oldNode,Node<E>newNode) {
if (oldNode.parent == null) root = newNode;
else if(oldNode == oldNode.parent.leftChild) oldNode.parent.leftChild = newNode;
else oldNode.parent.rightChild = newNode;
if(newNode != null) newNode.parent = oldNode.parent;
}