树、二叉树、二叉搜索树

一、树

在这里插入图片描述在这里插入图片描述
比如上图中,A节点就是B节点的父节点,B节点是A节点的子节点。B、C、D这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫作根节点,也就是图中的节点E。我们把没有子节点的节点叫作叶子节点或者叶节点,比如图中的G、H、I、J、K、L都是叶子节点

树有三个比较相似的概念:高度深度

  • 节点的高度=节点到叶子节点的最长路径(边数)
  • 节点的深度=根节点到这个节点所经历的边的个数
  • 节点的层数=节点的深度+1
  • 树的高度=根节点的高度

在这里插入图片描述

二、二叉树

二叉树每个节点最多有两个子节点,分别是左子节点和右子节点
在这里插入图片描述
上图中,编号2的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树

编号3的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其它层的节点个数都要达到最大,这种二叉树叫做完全二叉树

在这里插入图片描述

三、二叉树的性质

1)在二叉树的第i层上,至多有 2 i 1 2^{i-1} 个节点( i > = 1 i>=1

2)深度为k的二叉树至多有 2 k + 1 1 2^{k+1}-1 个节点

3)对于任何一棵二叉树T,如果叶子节点数为n0,度为2的节点数为n2,则 n 0 = n 2 + 1 n0=n2+1 (叶子节点数=度为2的节点数+1)

推导:

假设度为1的节点数为n1,则二叉树的节点总数为 n = n 0 + n 1 + n 2 n=n0+n1+n2

连接数总是等于总结点数n-1,并且等于 n 1 + 2 n 2 n1+2*n2 ,即: n 1 = n 1 + 2 n 2 n-1=n1+2*n2

所以, n 0 + n 1 + n 2 1 = n 1 + n 2 + n 2 n0+n1+n2-1=n1+n2+n2 ,最后推导出 n 0 = n 2 + 1 n0=n2+1

例题:

一棵二叉树有7个度为1的结点,6个度为2的结点,则该二叉树共有个多少个结点?

题解:

n = n 0 + n 1 + n 2 = n 0 + 7 + 6 = 6 + 1 + 7 + 6 = 20 n=n0+n1+n2=n0+7+6=6+1+7+6=20

4)具有n个节点的完全二叉树的深度为 [ l o g 2 n ] [log_2 n] (向下取整)

5)如果对一棵有n个节点的完全二叉树的节点按层序编号,对任一节点i( 1 < = i < = n 1<=i<=n )有以下性质:

  • 如果 i = 1 i=1 ,则节点i是二叉树的根,无双亲;如果 i > 1 i>1 ,则其双亲是节点 [ i / 2 ] [i/2] (向下取整)
  • 如果 2 i > n 2i>n ,则节点i无左孩子;否则其左孩子是节点 2 i 2i
  • 如果 2 i + 1 > n 2i+1>n ,则节点i无右孩子;否则其右孩子是节点 2 i + 1 2i+1

四、二叉树的遍历

二叉树的遍历方法有三种:前序遍历中序遍历后序遍历。其中,前、中、后序表示的是节点与它的左右子树节点遍历打印的先后顺序

  • 前序遍历:对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树
  • 中序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树
  • 后序遍历:对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身

在这里插入图片描述

二叉树的前、中、后序遍历就是一个递归的过程

前序遍历的递推公式:
preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)

中序遍历的递推公式:
inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)

后序遍历的递推公式:
postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r

从前面的前、中、后序遍历的顺序图,可以看出来,每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数n成正比,也就是说二叉树遍历的时间复杂度是 O ( n ) O(n)

五、二叉查找树

二叉查找树是指一棵空树或者具有下列性质的二叉树:

  • 左子树上所有节点的值均小于它的根节点的值
  • 右子树上所有节点的值均大于它的根节点的值
  • 以此类推:左、右子树也分别为二叉查找树

二叉查找树的中序遍历后元素升序排列

二叉查找树的查找、插入、删除操作的时间复杂度均为 O ( l o g n ) O(logn)
在这里插入图片描述
二叉搜索树Demo:https://visualgo.net/zh/bst

1、查找操作

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

2、二叉查找树的插入操作

新插入的数据一般都是在叶子节点上,所以只需要从根节点开始,依次比较要插入的数据和节点的大小关系

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

3、二叉查找树的删除操作

针对要删除节点的子节点个数的不同,需要分三种情况来处理

第一种情况是,如果要删除的节点没有子节点,我们只需要直接将父节点中,指向要删除节点的指针置为null。比如下图中的删除节点55

第二种情况是,如果要删除的节点只有一个子节点(只有左子节点或者右子节点),我们只需要更新父节点中,指向要删除节点的指针,让它指向要删除节点的子节点就可以了。比如下图中的删除节点13

第三种情况是,如果要删除的节点有两个子节点,我们需要找到这个节点的右子树中的最小节点,把它替换到要删除的节点上。然后再删除这个最小节点,因为最小节点肯定没有左子节点(如果有左子节点,那就不是最小节点了),所以,我们可以应用上面两条规则来删除这个最小节点。比如下图中的删除节点18
在这里插入图片描述

4、二叉查找树的时间复杂度分析

在这里插入图片描述
上图中第一种二叉查找树,根节点的左右子树极度不平衡,已经退化成了链表,所以查找的时间复杂度就变成了 O ( n ) O(n)

最理想的情况,二叉查找树是一棵完全二叉树(或满二叉树),时间复杂度其实都跟树的高度成正比,也就是 O ( h e i g h t ) O(height) 。完全二叉树的层数小于等于 l o g 2 n + 1 log_2n+1 ,也就是说,完全二叉树的高度小于等于 l o g 2 n log_2n 。平衡二叉查找树的高度接近于 l o g n logn ,所以插入、删除、查找操作的时间复杂度也比较稳定,是 O ( l o g n ) O(logn)

5、有了如此高效的散列表,为什么还需要二叉树?

散列表的插入、删除、查找操作的时间复杂度可以做到常量级的 O ( 1 ) O(1) ,非常高效。而二叉查找树在比较平衡的情况下,插入、删除、查找时间复杂度才是 O ( l o g n ) O(logn) ,相对于散列表,好像并没有什么优势,那我们为什么还要用二叉查找树呢?

1)、散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。而对于二叉查找树来说,只需要中序遍历,就可以在 O ( n ) O(n) 的时间复杂度内,输出有序的数据序列

2)、散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定,尽管二叉查找树的性能不稳定,但是在工程中,我们最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在 O ( l o g n ) O(logn)

3)、笼统地来说,尽管散列表的查找等操作的时间复杂度是常量级的 ,但因为哈希冲突的存在,这个常量不一定比 l o g n logn 小,所以实际的查找速度可能不一定比 O ( l o g n ) O(logn) 快。加上哈希函数的耗时,也不一定就比平衡二叉树的效率高

4)、散列表的构造比二叉查找树要复杂,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定

六、树相关题目

1、LeetCode94:二叉树的中序遍历

1)递归:

    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        helper(root, result);
        return result;
    }

    private void helper(TreeNode root, List<Integer> result) {
        if (root != null) {
            if (root.left != null) {
                helper(root.left, result);
            }
            result.add(root.val);
            if (root.right != null) {
                helper(root.right, result);
            }
        }
    }

2)基于栈的中序遍历:

    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        TreeNode current = root;
        while (current != null || !stack.empty()) {
            if (current != null) {
                stack.push(current);
                current = current.left;
            } else {
                current = stack.pop();
                result.add(current.val);
                current = current.right;
            }
        }
        return result;
    }

2、LeetCode98:验证二叉搜索树

1)递归中序遍历:

    double last = -Double.MAX_VALUE;

    public boolean isValidBST(TreeNode root) {
        if (root == null) return true;
        if (isValidBST(root.left)) {
            if (last < root.val) {
                last = root.val;
                return isValidBST(root.right);
            }
        }
        return false;
    }

2)基于栈的中序遍历:

    Stack<TreeNode> stack = new Stack<>();
    double last = -Double.MAX_VALUE;

    public boolean isValidBST(TreeNode root) {
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            if (root.val <= last) return false;
            last = root.val;
            root = root.right;
        }
        return true;
    }

3、LeetCode226:翻转二叉树

1)递归:

    public TreeNode invertTree(TreeNode root) {
        if (root == null) return null;
        TreeNode left = invertTree(root.left);
        TreeNode right = invertTree(root.right);
        root.left = right;
        root.right = left;
        return root;
    }

2)迭代:

    public TreeNode invertTree(TreeNode root) {
        if (root == null) return null;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode current = queue.poll();
            TreeNode temp = current.left;
            current.left = current.right;
            current.right = temp;
            if (current.left != null) queue.add(current.left);
            if (current.right != null) queue.add(current.right);
        }
        return root;
    }

4、LeetCode111:二叉树的最小深度

给定一个二叉树,找出其最小深度

最小深度是从根节点到最近叶子节点的最短路径上的节点数量

说明:叶子节点是指没有子节点的节点

示例:

给定二叉树[3,9,20,null,null,15,7]

	3
   / \
  9  20
    /  \
   15   7

返回它的最小深度2

题解:

    public int minDepth(TreeNode root) {
        if (root == null) return 0;
        return helper(root);
    }

    private int helper(TreeNode root) {
        //到达叶子节点就返回1
        if (root.left == null && root.right == null) return 1;
        //左孩子为空,只考虑右孩子的方向
        if (root.left == null) return helper(root.right) + 1;
        //右孩子为空,只考虑左孩子的方向
        if (root.right == null) return helper(root.left) + 1;
        //既有左孩子又有右孩子,那么就选一个较小的
        return Math.min(helper(root.left), helper(root.right)) + 1;
    }

5、LeetCode429:N叉树的层序遍历

在这里插入图片描述
题解:

    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> levelOrder(Node root) {
        helper(root, 0);
        return result;
    }

    private void helper(Node node, int depth) {
        if (node == null) return;
        if (depth >= result.size()) {
            result.add(new ArrayList<>());
        }
        result.get(depth).add(node.val);
        for (int i = 0; i < node.children.size(); ++i) {
            helper(node.children.get(i), depth + 1);
        }
    }

6、LeetCode235、236:二叉搜索树的最近公共祖先二叉树的最近公共祖先

1)、利用二叉查找树的性质:左子树上所有节点的值均小于它的根节点的值、右子树上所有节点的值均大于它的根节点的值(235)

1)递归:

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if ((root.val - p.val) * (root.val - q.val) <= 0) {
            return root;
        } else if (root.val - p.val > 0 && root.val - q.val > 0) {
            return lowestCommonAncestor(root.left, p, q);
        } else {
            return lowestCommonAncestor(root.right, p, q);
        }
    }

2)迭代:

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while (root != null) {
            if (p.val - root.val > 0 && q.val - root.val > 0) {
                root = root.right;
            } else if (p.val - root.val < 0 && q.val - root.val < 0) {
                root = root.left;
            } else {
                return root;
            }
        }
        return null;
    }

2)、递归(235、236)

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null || root == p || root == q) return root;
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //return left == null ? right : right == null ? left : root;
        if (left == null) {
            return right;
        } else if (right == null) {
            return left;
        } else {
            return root;
        }
    }

解析:

两个节点p、q分为两种情况:

  • p和q在相同子树中
  • p和q在不同子树中

从根节点遍历,递归向左右子树查询节点信息

递归终止条件:如果当前节点为空或等于p或q,则返回当前节点

递归遍历左右子树,如果左右子树查到节点都不为空,则表明p和q分别在左右子树中,因此,当前节点即为最近公共祖先

如果左右子树其中一个不为空,则返回非空节点

7、LeetCode105:从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树

注意:可以假设树中没有重复的元素

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

返回如下的二叉树:

	3
   / \
  9  20
    /  \
   15   7

题解:

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return helper(preorder, 0, inorder, 0, inorder.length);
    }

    private TreeNode helper(int[] preorder, int p, int[] inorder, int i, int j) {
        if (i >= j) return null;
        TreeNode root = new TreeNode(preorder[p]);
        int k = 0;
        while (inorder[k] != root.val) k++;
        root.left = helper(preorder, p + 1, inorder, i, k);
        root.right = helper(preorder, p + 1 + k - i, inorder, k + 1, j);
        return root;
    }

题解
在这里插入图片描述
https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/qian-xu-zhong-xu-bian-li-gou-zao-er-cha-shu-mo-ni-/

常用数据结构的时间、空间复杂度:

在这里插入图片描述

https://www.bigocheatsheet.com/

发布了177 篇原创文章 · 获赞 407 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/102890123