二叉树BinaryTreeList

简介:本文介绍二叉树基本概念,并通过继承java集合List接口,java实现BinaryTreeList操作算法:插入、删除、前中后层次遍历、二叉树深度、最大小宽度等。

一、二叉树基本概念

1.二叉树是什么?

二叉树是树的一种,是一种节点的度不大于2的有序树。

2.为什么会出现二叉树?或者说二叉树为了解决什么问题而出现的?

为了解决链表中查找、插入最坏为O(n)的情况,数组中删除最坏移动元素O(n)的情况。

例如:数据库需要多次查找、更新操作,此时使用数组、链表这种数据结构会很慢。而二叉树则可以达到O(lgn)情况。

3.二叉树有哪些性质?

性质1:二叉树第i层上的结点数目最多为2^{i-1}  (i≥1)。
性质2:深度为k的二叉树至多有2^k-1个结点(k≥1)。
性质3:包含n个结点的二叉树的高度至少为log_2(n+1)
性质4:在任意一棵二叉树中,若终端结点的个数为n_0,度为2的结点数为n_2,则n_0=n_2+1

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;
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值