java二叉树

结点的深度(depth):根结点到当前结点唯一路径的结点树

结点的高度(height):当前结点到最远叶子结点的路径上的结点数

二叉树的性质:

  1. 非空二叉树的第i层,最多有2^(i-1)个结点
  2.  高度为h的二叉树上最多有2^h-1个结点
  3. 设叶子结点n0,度为1的结点为n1,度为2的结点为n2;n0 = n2 + 1;结点总数n = n0 + n1 + n2;因此二叉树的边数T = n1 + n2*2 = n - 1 = n0 + n1 + n2 - 1

真二叉树:结点的度只有0或2 

完全二叉树:从根结点到倒数第二层都是满二叉树,叶子结点只在倒数后两层出现,且最后一层叶子结点位于树的左侧

性质:完全二叉树从上到下、从左到右对结点从0开始编号,对任意i结点

  1. 如果i=0,它是根结点
  2. 如果i>0,它的父节点编号为 floor((i-1)/2)(floor()意为向下取整)
  3. 如果 2i+1<=n-1,它的左子结点编号为 2i+1
  4. 如果 2i+1>n-1,它没有左子结点
  5. 如果 2i+2<=n-1,它的右子结点编号为 2i+2

面试题:如果一棵完全二叉树有768个结点,求叶子结点的个数

假设叶子结点个数为n0,度为1的结点个数为n1,度为2的结点个数为n2

则总结点个数n = n0 + n1 + n2,且n0 = n2 + 1

所以 n = 2n0 + n1 - 1

因为完全二叉树的n1要么是0,要么是1

所以n1为1时,总结点数是偶数,n0 = n/2

反之n1为0时,总结点数是奇数,n0 = (n+1)/2

综上,n0 = floor(n+1/2) = 384

二叉搜索树

任意一个结点的值都大于其左子树所有结点的值,任意结点的值都小于其右子树所有结点的值,并且它的左右子树也为二叉搜索树

二叉搜索树的元素不能为空

添加结点:

  1. 找到父结点
  2. 创建新结点node
  3. 根据大小,使得 parent.left = node 或 parent.right = node
  4. (如果遇到值相等的结点,可以直接将旧的覆盖)
	public void add(E element) {
		ElementNotNullCheck(element);
		
		//添加第一个结点
		if (root == null) {
			root = new Node<E>(element,null);
			size++;
			return;
		}
		//不是第一个结点,找到父结点
		Node<E> parent = root;
		Node<E> node = root;
		int cmp = 0;
		while (node != null) {
			cmp = compare(element, node.element);
			parent = node;  //在向下传递之前保存父结点
			if (cmp > 0) {
				node = node.right;  //结点和右子结点继续比较
			}else if (cmp < 0) {
				node = node.left;  //结点和左子结点继续比较
			}else {
				return;  //相等
			}
		}
		
		Node<E> newNode = new Node<E>(element, parent);  //此结点位置为元素插入位置
		if (cmp < 0) {
			parent.left = newNode;  //成为左子结点
		}else if (cmp > 0) {
			parent.right = newNode;  //成为右子结点
		}
		size++;

	}

里面的比大小compare根据需求自己调节

二叉树的遍历

1、前序遍历 

根结点,前序遍历左子树,前序遍历右子树

private void preorderTraversal(Node<E> node){
    if (node == null) return;

    System.out.println(node.element);    
    preorderTraversal(node.left);
    preorderTraversal(node.right);
}

2、中序遍历

中序遍历左子树,根结点,中序遍历右子树

中序遍历的结果是升序或降序的

private void inorderTraversal(Node<E> node){
    if (node == null) return;
    
    inorderTraversal(node.left);
    System.out.println(node.element);
    inorderTraversal(node.right);
}

3、后序遍历

后序遍历右子树,根结点,后序遍历左子树 

private void postorderTraversal(Node<E> node){
    if (node == null) return;
    
    postorderTraversal(node.left);
    postorderTraversal(node.right);
    System.out.println(node.element);
}

4、层序遍历(重点)

使用队列,先将根结点入队,再进行循环

循环包括:先将队头结点出队,然后将该结点的左子结点入队,再将该结点的右子结点入队

public void levelOrderTraversal(){

    if(root == null)  return;
    
    Queue<Node<E>> queue = new LinkedList<>();   //官方队列
    queue.offer(root);   //根结点入队

    while(!queue.isEmpty()){
        Node<E> node = queue.poll();   //头结点出队
        System.out,println(node);

        if(node.left != null){
            queue.offer(node.left);   //结点左子结点入队
        }

        if(node.right != null){
            queue.offer(node.right);  //结点右子结点入队
        }
    }
}

5、利用前序遍历树状打印二叉树

这个树状图大致为

根结点

L---左子树根结点

L---L---左子树左子结点

L---R---左子树右子结点

R---右子树根结点

R---L---右子树左子结点

R---R---右子树右子结点

	public String toString() {
		StringBuilder sb = new StringBuilder();
		toString(root, sb, "");   //打印根结点
		return sb.toString();
	}
	
	private void toString(Node<E> node, StringBuilder sb, String prefix) {
		if (node == null) return;

		sb.append(prefix).append(node.element).append("\n");  //输出内容为前缀+元素+换行
		toString(node.left, sb, prefix + "L---");   //输出左子结点
		toString(node.right, sb, prefix + "R---");   //输出右子结点
	}

练习:

1、计算二叉树高度

二叉树高度从叶子结点算起,叶子结点高度为一,根结点高度最大

结点的高度 = 它的左子结点与右子结点高度最大的那个 + 1

所以利用递归,从根结点一层层直到叶子结点,算高度

public int height(){
    return height(root);
}

private int height(){
    if(node == null)  return 0;  //如果结点为空,那这个二叉树肯定是空的,高度为0

    return 1 + Math.max(height(node.left),height(node.right));  //max函数选出最大的那个   
}

非递归,使用层序遍历,每遍历完一层,就让高度+1

层序遍历使用队列,通过观察可以发现,当上一层元素全部出队时,队列的长度等于一层元素的个数,因此设定一个变量levelSize,在while循环中,levelSize不断递减,每当levelSize = 0时,即为一层元素全部出队,这时这个变量的值被赋上队列长度,继续循环,此时记录一层

	public int height() {
		if (root == null) return 0;
		
		int height = 0;
		int levelSize = 1;   // 存储着每一层的元素数量
		Queue<Node<E>> queue = new LinkedList<>();
		queue.offer(root);
		
		while (!queue.isEmpty()) {
			Node<E> node = queue.poll();
			levelSize--;
			
			if (node.left != null) {
				queue.offer(node.left);
			}
			
			if (node.right != null) {
				queue.offer(node.right);
			}

			if (levelSize == 0) { // 意味着即将要访问下一层
				levelSize = queue.size();
				height++;
			}
		}
		
		return height;
	}

2、判断一棵树是否为完全二叉树

使用层序遍历,判断每一个结点的左右子结点情况

如果左右子结点都有,就继续入队

如果有右子结点,但是左子结点为空,那么这不是完全二叉树,返回false

如果有左子结点,但是右子结点为空,那么要看他后面的结点是不是都是叶子结点,如果都是,说明是完全二叉树,反之就不是完全二叉树

public boolean isComplete(){
    if (root == null)  return;

    Queue<Node<E>> queue = new LinkedList<>();
    queue.offer(root);   //根结点入队

    if(leaf && node.left != null && node.right != null){
        return false;    //如果要求node是叶子结点但却不是
    }

    boolean leaf = false;   //看node是否为叶子结点
    while(queue.size != 0){
        Node<E> node = queue.poll();  //node存放出队元素
        
        if(node.left != null && node.right != null){   //都不为空,入队,继续遍历
            queue.offer(node.left);
            queue.offer(node.right);
        }else if(node.left == null && node.right != null){
            return false;
        }else{
            leaf = true;   //要不就是只有左子结点,要不就是叶子结点
            if (node.left != null){
                queue.offer(node.left);   //只有左子结点也入队,继续遍历
            }
        }
    }
    return true;    //以上都不符合,说明是完全二叉树

}

 第二种写法:判断方式改为左右子结点分开单独成条件判断

	public boolean isComplete() {
		if (root == null) return false;
		
		Queue<Node<E>> queue = new LinkedList<>();
		queue.offer(root);

		boolean leaf = false;
		while (!queue.isEmpty()) {
			Node<E> node = queue.poll();
			if (leaf && node.left != 0 && node.right != 0) return false;
			
			if (node.left != null) {
				queue.offer(node.left);
			} else if (node.right != null) { //node.left == null && node.right != null
				return false;
			}
			
			if (node.right != null) {
				queue.offer(node.right);
			} else { //node.right == null
				leaf = true;
			}
		}
		
		return true;
	}

前驱结点:中序遍历时的前一个结点 

如果是二叉搜索树,前驱结点就是比这个结点小一点的那个结点 

查找位置,在左子树找最右边的那个结点

如果左子树为空,但是父结点不是空,且父结点比它小,那么父结点就是它的前驱结点;如果父结点比它大,那就找父结点的父结点,直到找到比它小的结点(结点在这个父结点的右子树中)

 如果左子树为空,父结点也为空,说明这个结点没有前驱结点

	private Node<E> predecessor(Node<E> node) {
		if (node == null) return null;
		
		//前驱结点在左子树当中(left.right.right.right....)
		Node<E> p = node.left;
		if (p != null) {
			while (p.right != null) {
				p = p.right;
			}
			return p;
		}
		
		// 从父结点、祖父结点中寻找前驱结点
		while (node.parent != null && node == node.parent.left) {  
        //父结点不为空且结点是父结点的左子树
			node = node.parent;
		}

		//剩下父结点为空或者结点是右子树
		return node.parent;
	}

后继结点同理

删除结点

1、叶子结点直接删除

2、度为1的结点,用子结点替代原结点的位置

child.parent = node.parent;

node.parent.left(right) = child;

3、如果要删除的结点是根结点,让root指向子结点即可

root = child;

child.parent = null;

4、度为2的结点,用它的前驱结点或后继结点取代它,再删除原来的前驱结点或后继结点

如果一个结点度为2,那它的前驱或后继结点的度只能是1或0,那么删除过程也就转换成上面两种情况

public void remove(E element){  //调用函数一般直接删内容,但是要找结点
    remove(node(element));
}

public void remove(Node<E> node){
    if(node == null) return;

    size--;   //树长度减1
    if(node.left != null && node.right != null){
        Node<E> s = successor(node);   //找到后继结点
        node.element = s.element;    //后继结点覆盖原结点
        node = s;   //删除原结点
    }

    //此时只剩下度为0或1的情况
    Node<E> replacement = node.left != null ? node.left : node.right;   //找到代替值
    if(replacement != null){   //度为1
        replacement.parent = node.parent;   //更换父结点
        if(node.parent == null){   //结点度为1且为根结点
            root = replacement;
        }else if(node == node.parent.left){   //是左结点
            node.parent.left = replacement;
        }else{
            node.parent.right = replacement;   //右结点
        }
    }else if(node.parent == null){   //这个树只有这一个结点
        root = null;
    }else{   //叶子结点
        if(node == node.parent.left){  //是左结点
            node.parent.left = null;
        }else{
            node.parent.right = null;   //是右结点
        }
    }

}

private Node<E> node(E element){  //通过元素找结点
    Node<E> node = root;
    int cmp = compare(element,node.element);   //比大小,看往哪里遍历
    if(cmp == 0) return node;
    if(cmp > 0){
        node = node.right;
    }else{
        node = node.left;
    }
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值