数据结构学习笔记——二叉树(1)【Java代码】

在学习二叉树之前,要先了解一下二叉树的父类——树。

(一)树

1. 树的定义

树是一种非线性的数据结构。树是n(n ≥ 0) 个结点的有限集。在一棵非空树中:

(1)有且仅有一个称为根的结点

(2)当n > 1时,其余结点可以分为m (m > 0) 个互不相交的有限集,其中每一个有限集又是一棵树,并且称为根的子树。

2. 树的专业术语

结点:包含一个数据元素以及若干指向其子树的分支。

结点的度: 结点拥有的子树数。

树的度:树内结点的度的最大值。

有序树:树的结点从左至右是有顺序的,无序树则没有顺序。

(二)二叉树

1. 二叉树的定义

二叉树是一个抽象的数据类型,每个节点至多只有两棵子树,并且子树有左右之分,其次序不能颠倒。

2. 二叉树的性质

性质1 : 二叉树的第i层至多有 2i-1 个结点(i ≥ 1)。

性质2: 深度为k的二叉树至多有2k - 1个结点。

性质3: 任何一棵二叉树T,如果其终端结点(叶子结点)数为你n0,度为2的结点数为n2,则 n0 = n2 + 1。

设二叉树T的总结点数为n,度为1的结点数为n1
因为二叉树的结点度只能为0, 1, 2,所以 n = n0 + n1 + n2
又因为除了根节点外的每一个结点都有一个分支进入,所以设 B 为分支总数,可知 n = B + 1
分支都是由度为1或2的结点射出,所以 B = n1 + 2 * n2 ⇨ n = n1 + 2 * n2 + 1
n0+n1+n2 = n1+2*n2+1 ⇨ n0 = n2 + 1

性质4: 具有 n 个结点的完全二叉树的最大深度为 ⌊log2n⌋ + 1。(⌊⌋ 为向下取整符号,⌊3.5⌋ = 3,⌊-1.2⌋ = -2)

满二叉树:深度为 k,且结点数为 2k - 1的二叉树
在这里插入图片描述
完全二叉树: 深度为 k,结点数为 n 的二叉树,当且仅当每一位结点都与深度为 k 的满二叉树中编号从1 至 n的结点一一对应。
在这里插入图片描述
性质解释: 因为深度为 k 的完全二叉树的结点数范围为 2k-1 ≤ n ≤ 2k - 1(由性质2可知)
所以 k - 1 ≤ log2n ⇨ k ≤ ⌊log2n⌋ + 1

性质5: 如果对一颗有 n 个结点的完全二叉树的结点按照层序编号(从第一层到最后一层,从左到右),对任意结点 i (1 ≤ i ≤ n)有
(1) 如果 i 是1,则结点 i 是二叉树的根,无双亲;如果对 i > 1,则其双亲是结点⌊i / 2⌋ ;
(2) 如果 2i > n,则结点 i 无左孩子;否则其左孩子结点为2i;
(3) 如果 2i + 1 > n,则结点 i 无右孩子;否则其右孩子结点为2i + 1。

由上文完全二叉树定义的图中可知,左孩子的编号为根节点的2倍,右孩子的编号为左孩子加1。
所以(2)中,当左孩子的编号大于树的结点数说明左孩子不存在,(3)的证明同理。

3. 二叉树的存储结构

  1. 顺序存储结构:用一组地址连续的存储单元自上而下依次存储完全二叉树上的元素,结点为空使用 0 或其他空元素表示。一个深度为 k 的二叉树,结果得到一个长度为2k - 1的一维数组。
  2. 链式存储结构:至少包含数据域以及左右指针域,为了方便找到双亲也可以增加一个双亲结点的指针域。
// 链式存储结点最简洁的结构体
tydedef struct BiTNode{
	TelemType data;
	struct BiTNode *lchild, *rchild;// *parent 双亲结点
}BiTNode, *BiTree;

4. 遍历二叉树

  1. 先序遍历:先访问根节点,再访问左孩子,最后遍历右子数。
// 力扣 144
public static void preOrderRecur(TreeNode head) {
    if (head == null) {
        return;
    }
    System.out.print(head.value + " ");
    preOrderRecur(head.left);
    preOrderRecur(head.right);
}
  1. 中序遍历:先遍历左子树,再访问根节点,最后遍历右子数。
// 力扣 94
class Solution {
	//  递归
    public static void inorderTraversal(TreeNode root) {
        if (root == null) {
            return;
        }
        inorderTraversal(root.left);
        System.out.print(root.value + " ");
        inorderTraversal(root.right);
    }
    // 非递归
    public static void inorderTraversal2(TreeNode root) {
    	// 使用栈存储结点
        Deque<TreeNode> stk = new LinkedList<TreeNode>();
        while (root != null || !stk.isEmpty()) {
        	// 一直遍历左子树,直到左子树为空
            while (root != null) {
                stk.push(root);
                root = root.left;
            }
            // 弹出栈内结点
            root = stk.pop();
            Sysyem.out.println(root.val);
            // 判断弹出的节点是否有右子树,右子树存在继续遍历右子树的左子树
            root = root.right;
        }
        return res;
    }
}
  1. 后序遍历:先遍历左子树,再遍历右子数,最后访问根节点。
// 力扣 145
class Solution {
    public static void postorderTraversal(TreeNode root) {
        if (root == null) {
            return;
        }
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        System.out.print(root.value + " ");
    }
}

以上文的满二叉树为例
在这里插入图片描述
先序遍历:1,2,4,5,3,6,7
中序遍历:4,2,5,1,6,3,7
后序遍历:4,5,2,6,7,3,1

5. 遍历二叉树的代码应用

1. 二叉树的最大深度(后序遍历)(力扣 104)

// 力扣 104
class Solution {
    public int maxDepth(TreeNode root) {
        // 递归求二叉树
        // 要求最后遍历的结点为根节点,所以使用后序遍历
        if(root == null)
            return 0;
        int l = maxDepth(root.left);
        int r = maxDepth(root.right);
        // depth表示当前结点的最大深度
        int depth = Math.max(l, r) + 1;
        return depth;
    }
}

2. 二叉树的复制(后序遍历)

public class TreeNode {
	int val;
	TreeNode left;
	TreeNode right;
	TreeNode(int x) { val = x; }
}
class Solution {
	// 为了得到新的树,需要先得到根的左右子树在建立,所以是后序遍历构造树
    public TreeNode CopyTree(TreeNode root) {
        if(!root)
        	return null;
        TreeNode newLeft = CopyTree(root.left);
        TreeNode newRight = CopyTree(root.right);
        return Create(root.val, newLeft, newRight);
    }
    // 创造一个新节点
    public TreeNode Create(int val, TreeNode left, TreeNdoe right){
    	TreeNode tn = new TreeNode();
    	tn.val = val;
    	tn.left = left;
    	tn.right = right;
    	return tn;
    }
}

3. 从前序与中序遍历序列构造二叉树(力扣 105)

class Solution {
	// 递归一:代码逻辑简单易懂,时间复杂度高 
    public TreeNode buildTree(int[] preorder, int[] inorder) {
    	// 空数组就返回空
        if(preorder.length == 0)
            return null;
        // 先序遍历的数组第一个为根节点
        TreeNode root = new TreeNode(preorder[0]);
        // i存储中序遍历中根节点的位置
        int i = 0;
        // 寻找根节点在中序遍历的位置去分割左右子树
        for(i = 0; i < inorder.length; i++){
            if(inorder[i] == root.val){
                break;
            }
        }
        // Arrays.copyOfRange(arr,i,j);可以对arr数组进行分割,不包含第j位,下标从0开始记录。时间复杂度为O(n)
        root.left = buildTree(Arrays.copyOfRange(preorder, 1, 1 + i), Arrays.copyOfRange(inorder, 0, i));
        root.right = buildTree(Arrays.copyOfRange(preorder, 1 + i, preorder.length), Arrays.copyOfRange(inorder, 1 + i, inorder.length));
        return root;
    }
	
	// 递归二:利用变量避免了对数组的分割,减少时间复杂度,推荐做法,时间复杂度为O(n);
    private Map<Integer, Integer> indexMap;

    public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return null;
        }
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = indexMap.get(preorder[preorder_root]);
        
        // 先把根节点建立出来
        TreeNode root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
        root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
        root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int n = preorder.length;
        // 构造哈希映射,帮助我们快速定位根节点
        indexMap = new HashMap<Integer, Integer>();
        for (int i = 0; i < n; i++) {
            indexMap.put(inorder[i], i);
        }
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
}

4. 从中序与后序遍历序列构造二叉树(力扣 106)

// 方法同题3
// 方法一只是改变数组的切割位置以及初始根节点的位置变为后序数组的最后一位
// root.left = buildTree(Arrays.copyOfRange(inorder, 0, i), Arrays.copyOfRange(postorder, 0, i));
// root.right = buildTree(Arrays.copyOfRange(inorder, i + 1, n), Arrays.copyOfRange(postorder, i, n - 1));
class Solution {
	// 方法二
    int post_idx;
    int[] postorder;
    int[] inorder;
    Map<Integer, Integer> idx_map = new HashMap<Integer, Integer>();

    public TreeNode helper(int in_left, int in_right) {
        // 如果这里没有节点构造二叉树了,就结束
        if (in_left > in_right) {
            return null;
        }

        // 选择 post_idx 位置的元素作为当前子树根节点
        int root_val = postorder[post_idx];
        TreeNode root = new TreeNode(root_val);

        // 根据 root 所在位置分成左右两棵子树
        int index = idx_map.get(root_val);

        // 下标减一
        post_idx--;
        // 构造右子树
        root.right = helper(index + 1, in_right);
        // 构造左子树
        root.left = helper(in_left, index - 1);
        return root;
    }

    public TreeNode buildTree(int[] inorder, int[] postorder) {
        this.postorder = postorder;
        this.inorder = inorder;
        // 从后序遍历的最后一个元素开始
        post_idx = postorder.length - 1;

        // 建立(元素,下标)键值对的哈希表
        int idx = 0;
        for (Integer val : inorder) {
            idx_map.put(val, idx++);
        }
        
        return helper(0, inorder.length - 1);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值