二叉树

本文介绍了二叉树的概念,包括满二叉树和完全二叉树。详细阐述了二叉树的链式存储法和顺序存储法,以及前序、中序、后序遍历和按层遍历的方法。此外,重点讨论了二叉查找树(二叉搜索树)的特性,包括其查找、插入和删除操作。还对比了二叉树与散列表的优劣,强调了平衡二叉查找树在保持高效性能的同时提供有序数据序列输出的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述
在这里插入图片描述
       叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树。
       叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树。

       存储一颗二叉树,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。
在这里插入图片描述

       链式存储法,每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。大部分二叉树都是通过这种结构。
       基于数组的顺序存储法,如果节点存储在数组中下标为i的位置,小标为2i的位置存储的就是左子节点,小标为2i+1的位置存储的就是右子节点。同理,也可反推出父节点。如果不是完全二叉树就会浪费空间。

       遍历
       前序遍历:根左右
       中序遍历:左根右
       后序遍历:左右根
在这里插入图片描述


//前序遍历
void preOrder(Node* root) {
  if (root == null) return;
  print root // 此处为伪代码,表示打印 root 节点
  preOrder(root->left);
  preOrder(root->right);
}
//中序遍历
void inOrder(Node* root) {
  if (root == null) return;
  inOrder(root->left);
  print root // 此处为伪代码,表示打印 root 节点
  inOrder(root->right);
}
//后序遍历
void postOrder(Node* root) {
  if (root == null) return;
  postOrder(root->left);
  postOrder(root->right);
  print root // 此处为伪代码,表示打印 root 节点
}

       按层遍历

package LeetCode;

import java.util.LinkedList;

public class LevelIterator {
    public static class BiTree{
        int val;
        BiTree left=null;
        BiTree right=null;
        public BiTree(int val) {
            this.val = val;
        }
    }

    public void LevelIterator(BiTree root){
        if(root ==null){
            return ;
        }
        LinkedList<BiTree> queue = new LinkedList<BiTree>();
        BiTree current = null;
        queue.offer(root);//将根节点入队
        while (!queue.isEmpty()){
            current = queue.poll(); //出队对头元素并访问
            System.out.println(current.val + "-->");
            if(current.left != null){  //如果当前节点的左节点不为空入队
                queue.offer(current.left);
            }
            if(current.right != null){  //如果当前节点的右节点不为空,把右节点入队
                queue.offer(current.right);
            }
        }
    }

    public static void main(String[] args) {



    }
}

       二叉查找树最大的特点就是,支持动态数据集合的快速插入,删除,查找操作。
       二叉查找树是二叉树中最常用的一种类型,也叫二叉搜索树,二叉查找树是为了实现快速查找而生的,不仅支持快速查找一个数据,还支持快速插入,删除一个数据。
       二叉查找树要求,在书中任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。

二叉查找树的查找

       我们先取根节点,如果它等于我们要查找的数据,那就返回。如果要查找的数据比根节点的值小,那就在左子树中递归查找;如果要查找的数据比根节点的值大,那就在右子树中递归查找。BinarySearchTree

二叉查找树的插入操作

       如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点数值小,并且节点的左子树为空,就将新数据插入到左子节点的位置;如果不为空,就再递归遍历左子树,查找插入位置。

二叉查找树的删除操作

       第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为 null。比如图中的删除节点 55。
第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如图中的删除节点 13。
第三种情况是,如果要删除的节点有两个子节点,这就比较复杂了。我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除掉这个最小节点,因为最小节点肯定没有左子节点(如果有左子结点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如图中的删除节点 18。

package LeetCode;

public class BinarySearchTree {
    public static class Node{
        private int data;
        private Node left;
        private Node right;
        public Node(int data){
            this.data = data;
        }
    }

    private Node tree;
    public Node find(int data){
        Node p = tree;
        while(p != null){
            if(data < p.data) p = p.left;
            else if (data > p.data) p = p.right;
            else return p;
        }
        return null;
    }
    public void insert(int data) {
        if (tree == null) {
            tree = new Node(data);
            return;
        }

        Node p = tree;
        while (p != null) {
            if (data > p.data) {
                if (p.right == null) {
                    p.right = new Node(data);
                    return;
                }
                p = p.right;
            } else { // data < p.data
                if (p.left == null) {
                    p.left = new Node(data);
                    return;
                }
                p = p.left;
            }
        }
    }

    public void delete(int data) {
        Node p = tree; // p 指向要删除的节点,初始化指向根节点
        Node pp = null; // pp 记录的是 p 的父节点
        while (p != null && p.data != data) {
            pp = p;
            if (data > p.data) p = p.right;
            else p = p.left;
        }
        if (p == null) return; // 没有找到

        // 要删除的节点有两个子节点
        if (p.left != null && p.right != null) { // 查找右子树中最小节点
            Node minP = p.right;
            Node minPP = p; // minPP 表示 minP 的父节点
            while (minP.left != null) {
                minPP = minP;
                minP = minP.left;
            }
            p.data = minP.data; // 将 minP 的数据替换到 p 中
            p = minP; // 下面就变成了删除 minP 了
            pp = minPP;
        }

        // 删除节点是叶子节点或者仅有一个子节点
        Node child; // p 的子节点
        if (p.left != null) child = p.left;
        else if (p.right != null) child = p.right;
        else child = null;

        if (pp == null) tree = child; // 删除的是根节点
        else if (pp.left == p) pp.left = child;
        else pp.right = child;
    }


}

       中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度为O(n),非常高效。

       有了高效的散列表还是用二叉树的原因
       第一,散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序,而对于二叉查找树来说,我们只需要中序遍历,就可以在O(n)的时间复杂度,输出有序的数据序列
       第二,散列表扩容耗时多,当遇到散列冲突时,性能不稳定,但是平衡二叉树的性能非常稳定
       第三,尽管散列表的查找等操作的时间复杂度是常量级别的,但因为哈希冲突的存在,这个常量不一定比logn小,所以实际的查找速度不一定比O(logn)快,加上哈希函数耗时,不一定就比平衡二叉查找树的效率高。
       第四,散列表的构造比二叉查找树复杂,考虑的东西多。平衡二叉查找树只需要考虑平衡性这一个问题。
       第五,为了避免过多的散列冲突,散列表装载因子不能过大,特别是基于开放寻址法解决冲突的散列表,不然会浪费空间。

       平衡二叉树的严格定义,二叉树中任意一个节点的左右子树的高度相差不能高于1,所以完全二叉树,满二叉树都是平衡二叉树。
       平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。

       散列表:插入删除查找都是O(1), 是最常用的,但其缺点是不能顺序遍历以及扩容缩容的性能损耗。适用于那些不需要顺序遍历,数据更新不那么频繁的。
        跳表:插入删除查找都是O(logn), 并且能顺序遍历。缺点是空间复杂度O(n)。适用于不那么在意内存空间的,其顺序遍历和区间查找非常方便。
        红黑树:插入删除查找都是O(logn), 中序遍历即是顺序遍历,稳定。缺点是难以实现,去查找不方便。其实跳表更佳,但红黑树已经用于很多地方了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值