1.什么是二叉树
在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。
2.二叉树的性质
(1)二叉树具有唯一根结点
(2)二叉树中每个结点多只有2个孩子
(3)二叉树每个结点最多只有一个父节点
(4)二叉树具有天然递归结构,表现为:
①每个节点的左子树(若存在),也是二叉树
②每个结点的右子树(若存在),也是二叉树
(5) 在非空二叉树中,第i层的结点总数不超过 (2^i-1) , i>=1;
(6)深度为h的二叉树最多有(2^h) - 1 个结点(h>=1),最少有h个结点;
(7)对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;
(8) 具有N个结点的完全二叉树的深度为[log2N] + 1(注:[ ]表示向下取整)
3.二叉树的代码结构(Java实现)
class Node{
E e;//表示二叉树结点元素的值
Node left;//表示左子树
Node right;//表示右子树
}
4.二叉树的遍历(递归与非递归)-Java语言实现
递归方式:https://blog.csdn.net/hoji_james/article/details/80601375
非递归方式:https://blog.csdn.net/hoji_james/article/details/80601720
5.二叉树的扩展-二分搜索树Binary Search Tree(BST)
二分搜索树是一种特殊的二叉树,它的特殊之处在于:它的每个结点的值大于其左子树(若存在)的所有结点的值,而小于其右子树(若存在)的所有结点的值。
它的代码实现如下(Java语言实现)(注意:这里考虑的是整棵树中无重复元素的情况)
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class BST<E extends Comparable<E>> {//类型E需要具有可比较性
//私有内部类(二分搜索树的结点具体是什么样子,对用户是屏蔽的,所在在这里为私有内部类)
private class Node{//结点类
public E e;//存放的元素
public Node left;//左孩子
public Node right;//右孩子
public Node(E e){
this.e = e;
left = null;
right = null;
}
}
public Node root;//根结点
private int size;//当前二分搜索数存储了多少个元素
public BST(){
root = null;
size = 0;
}
//获得二分搜索树含有的元素个数
public int size(){
return size;
}
//判断二叉树是否为空
public boolean isEmpty(){
return size == 0;
}
//向二分搜索树中添加新的元素e
public void add(E e){
//如果当前树为空则新加的结点作为根结点
if(root == null){
root = new Node(e);
size++;
}else{
//否则向二分搜索树从根结点开始,去添加一个新的元素
add(root, e);//尝试从根结点开始,插入元素e
}
}
//递归函数
//向以node为根的二分搜索树中插入元素e
//返回插入新结点后二分搜索树的根
private void add(Node node, E e){
if(e.equals(node.e)){
//相当于要插入的元素在二分搜索树中已经有了
return;
}else if(e.compareTo(node.e) < 0 && node.left == null){
//左子树为空时直接把e插入node的左子树
node.left = new Node(e);
size ++;
return;
}else if(e.compareTo(node.e) > 0 && node.right == null){
//右子树为空时直接把e插入node的右子树
node.right = new Node(e);
size ++;
return;
}
//若左子树或右子树不为空则需要递归调用插入元素的过程
if(e.compareTo(node.e) < 0){
add(node.left, e);
}else{ //e.compareTo(node.e) > 0
add(node.right, e);
}
}
//对于查询元素来说,我们只需要不停地看每一个node里面存的元素就好了
//看二分搜索树中是否包含元素e
public boolean contains(E e){
//递归实现
//和我们的添加元素一样,由于我们是要递归地进行实现,那么在递归过程中,
//就要从这个二分搜索树的根开始
//逐渐地转移,在新的二分搜索树的子树中缩小我们的问题规模,
// 也就是缩小我们查询的树的规模,知道找到或找不到这个元素e为止
return contains(root, e);
}
//看以node为根的二分搜索树中是否包含元素e,递归算法
private boolean contains(Node node, E e){
if(node == null){
return false;//递归出口
}
if(e.compareTo(node.e) == 0){
return true;
}else if(e.compareTo(node.e) < 0){
//在左子树中寻找
return contains(node.left, e);
}else{
//在右子树中寻找
return contains(node.right, e);
}
}
//用户进行调用的前序遍历方法(根,左子树,右子树)
public void preOrder(){
preOrder(root);
}
//递归前序遍历
//前序遍历以node为根的二分搜索树,递归算法
private void preOrder(Node node){
if(node == null){
return;
}
System.out.println(node.e);//先遍历根节点
preOrder(node.left);//然后遍历左子树(递归)
preOrder(node.right);//最后遍历右子树(递归)
}
//二分搜索树的非递归前序遍历
//借助栈,栈用于帮助我们记录下面需要访问的结点
public void preOrderNR(){
Stack<Node> stack = new Stack<>();
//因为根结点是我们第一个访问到的结点,所以首先将根结点压栈
stack.push(root);//将根结点压栈
while(stack.isEmpty() == false){
//不为空的时候,则栈顶记录了下面要访问哪个结点,相应地我们就要去访问这个结点
Node cur = stack.pop();
System.out.println(cur.e);
//打印完成之后,我们相应地要记录当前访问结点的左子树和右子树(压栈)
//由于栈是后入先出的,所以先压的是右子树
if(cur.right != null) {
stack.push(cur.right);
}
if(cur.left != null) {
stack.push(cur.left);
}
}
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
generateBSTString(root, 0, res);//根结点,深度
return res.toString();
}
//生成以node为根结点,深度为depth的描述二叉树的字符串
private void generateBSTString(Node node, int depth, StringBuilder res) {
if(node == null){
res.append(generateDepthString(depth) + "null\n");
return;
}
res.append(generateDepthString(depth) + node.e + "\n");
generateBSTString(node.left,depth + 1, res);
generateBSTString(node.right, depth + 1,res);
}
private String generateDepthString(int depth) {
StringBuilder res = new StringBuilder();
for(int i = 0; i < depth; i++){
res.append("--");
}
return res.toString();
}
//二分搜索树的中序遍历
public void inOrder(){
inOrder(root);
}
//中序遍历以node为根的二分搜索树,递归算法
private void inOrder(Node node) {
if(node == null){
return;
}
inOrder(node.left);//先遍历左子树
System.out.println(node.e);//然后遍历根节点
inOrder(node.right);//最后遍历右子树
}
//二分搜索树的后续遍历
public void postOrder(){
postOrder(root);
}
//后续遍历以node为根的二分搜索树,递归算法
private void postOrder(Node node){
if(node == null){
return;
}
inOrder(node.left);//先遍历左子树
inOrder(node.right);//然后遍历右子树
System.out.println(node.e);//最后遍历根结点
}
//二分搜索树的层序遍历(广度优先遍历)
//借助于队列这种数据结构
public void levelOrder(){
Queue<Node> queue = new LinkedList<>();
queue.add(root);//根结点添加进队列
while(queue.isEmpty() == false){
Node cur = queue.remove();//当前遍历的结点
System.out.println(cur.e);
if(cur.left != null) {
queue.add(cur.left);
}
if(cur.right != null) {
queue.add(cur.right);
}
}
}
//寻找二分搜索树的最小元素
public E miniNum(){
if(size == 0){
throw new IllegalArgumentException("树为空");
}
return miniNum(root).e;
}
//返回以node为根的二分搜索树的最小键值所在的节点
private Node miniNum(Node node){
if(node.left == null){//向左走再也走不动了
return node;
}
return miniNum(node.left);
}
//寻找二分搜索树的最大元素
public E maxNum(){
if(size == 0){
throw new IllegalArgumentException("树为空");
}
return maxNum(root).e;
}
//返回以node为根的二分搜索树的最大键值所在的节点
private Node maxNum(Node node){
if(node.right == null){//向右再也走不动了
return node;
}
return maxNum(node.right);
}
//从二分搜索树中删除最小值所在结点,返回最小值
public E removeMin(){
E ret = miniNum();
root = removeMin(root);//从根结点开始,尝试删除最小值所在的结点
return ret;
}
//删除掉以node为根的二分搜索树中的最小结点
//返回删除结点后新的二分搜索树的根
private Node removeMin(Node node){
//递归到底(叶子结点),删除最小元素
if(node.left == null){//不能再向左走了,我们要删的就是这个结点
Node rightNode = node.right;//保存当前结点的右子树
node.right = null;
size --;//这一步不要忘了
return rightNode;
}
//未递归到底(非叶子结点),删除左子树中的最小元素
node.left = removeMin(node.left);//这样才能真正改变整个二分搜索树的结构
return node;
}
//从二分搜索树中删除最大值所在结点,返回最大值
public E removeMax(){
E ret = maxNum();
root = removeMax(root);
return ret;
}
//删除以node为根的二分搜索树中的最大结点
//返回删除结点后新的二分搜索树的根
private Node removeMax(Node node){
if(node.right == null){//叶子结点
Node leftNode = node.left;//保存当前结点的左子树
node.left = null;
size --;//不要忘记维护size
return leftNode;
}
node.right = removeMax(node.right);
return node;
}
//从二分搜索树中删除元素为e的结点
public void remove(E e){
root = remove(root,e);//将删除元素后的二叉树的根节点
}
//删除以node为根的二分搜索树中值为e的结点,递归算法
//返回删除结点后新的二分搜索树的根
private Node remove(Node node,E e){
if(node == null){
return null;
}
if(e.compareTo(node.e) < 0){//e小于当前node的e
//要去node的左子树找到待删除的元素
node.left = remove(node.left, e);
return node;
}else if(e.compareTo(node.e) > 0){
//要删除的元素e比当前node上的元素e要大
//到当前node的右子树中尝试将e删除
node.right = remove(node.right, e);
return node;
}else{
//node.e == e
//此时要删除node这个结点
//待删除结点左子树为空的情况
if(node.left == null){
Node rightNode = node.right;//保存一下node的右子树
node.right = null;//将node和这棵树断开关系
size --;
return rightNode;//返回右子树的根节点
}
//待删除结点右子树为空的情况
if(node.right == null){
Node leftNode = node.left;//保存当前node的左孩子
node.left = null;//把node和二叉树断掉关系
size --;
return leftNode;
}
//待删除结点左右子树均不为空
//思路:找到比待删除结点大的最小结点,即待删除结点右子树的最小结点
//用这个结点顶替待删除结点的位置
Node successor = miniNum(node.right);
successor.right = removeMin(node.right);
size ++;//?? 6-12
successor.left = node.left;
node.left = node.right = null;//让node结点与二分搜索树脱离关系
size --;//?? 6-12
return successor;
}
}
}
测试类:
public class Main {
public static void main(String[] args) {
BST<Integer> bst = new BST<>();
int[] nums = {5,3,4,6,8,4,2};
for(int num : nums){
//每次将nums中的一个数取出来,插入到bst中
bst.add(num);
}
////////////////////
// 5 //
// / \ //
// 3 6 //
// / \ \ //
// 2 4 8 //
///////////////////
//测试前序遍历
bst.preOrder();
/*
遍历结果:
5
3
2
4
6
8
*/
System.out.println();
System.out.println(bst);
/*
* 5
--3
----2
------null
------null
----4
------null
------null
--6
----null
----8
------null
------null
*/
//测试中序遍历
bst.inOrder();
System.out.println();
/*
* 遍历结果
* 2
3
4
5
6
8
正好是排好序的
*/
//测试后序遍历
bst.postOrder();
System.out.println();
/*
遍历结果:
* 2
3
4
6
8
5
*/
//测试非递归方式下的前序遍历
bst.preOrderNR();
System.out.println();
/*
遍历结果:
5
3
2
4
6
8
*/
//测试二分搜索树的层序遍历
bst.levelOrder();
System.out.println();
/*
遍历结果:
5
3
6
2
4
8
*/
}
}