笔试面试高频考点二叉树、树和二叉树(1)——数据结构

重点探讨的问题:

  • 树和二叉树相关概念
  • 二叉树的基本操作

1 树


1.1 是什么

  • 树是一种 非线性 的数据结构,其根朝下,叶子朝上,因为非常形似一颗倒过来的“树”,所以称为树。

1.2 相关概念

1)节点 / 树的度

  • 节点度:一个接节点含有子树的个数
  • 树度:所有节点中最大的度就是树的度

2)叶子 / 终端节点

  • 度为 0 的节点

3)双亲 / 父节点

  • 一个节点含有子节点,那么该节点就是子节点的双亲 / 父节点

4)子 / 孩子节点

  • 一个节点含有子树的根节点,就是该节点的子节点

5)根节点

  • 没有双亲节点的节点就是根节点

6)高度 / 深度

  • 从上到下,树的最大层次

1.3 如何表示

1)双亲/孩子/孩子双亲/孩子兄弟 表示法

  • 孩子兄弟表示法最常用

在这里插入图片描述

2 二叉树相关概念


2.1 是什么

  • 每个节点的度都不超过2的树
  • 树为空也为二叉树

2.2 满二叉树 完全二叉树

1)满二叉树

  • 一棵树的高度为k,树的节点个数为 2^k - 1

2)完全二叉树

  • 对满二叉树从左到右进行编号,完全二叉树的每个节点的编号都能和满二叉树一 一对应
  • 满二叉树是特殊的完全二叉树

在这里插入图片描述

2.3 相关性质

1)非空二叉树 i 层节点个数最多 2^(i - 1)

2)树的节点个数最多 2^i - 1

3)叶子节点个数n0,度为2的非叶节点个数为n2,则满足 n0 = n2 + 1

4)完全二叉树的节点个数为n,该二叉树的高度为 log(n + 1) 向上取整

5)节点个数为n的完全二叉树,从上到下从左到右 从0开始 给每个节点顺序编号,一个节点序号为i,那么父节点为(i - 1)/ 2;父节点

序号为i,子节点为 2i + 1 和 2i + 2 (子节点存在时)

6)树为完全二叉树,节点个数为偶数时,度为1的节点个数为 1;为奇数,度为1 节点个数为0

2.4 存储方式

  • 二叉树一般使用 孩子表示法 存储,孩子双亲多用于平衡树

在这里插入图片描述

static class TreeNode{
        public char val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(char val) {
            this.val = val;
        }
    }

2.5. 二叉树的遍历

1)NLR:前序遍历
2)LNR:中序遍历
3)LRN:后序遍历

3 二叉树的基本操作


3.1前/中/后 序遍历

1)思路:

  • 进入 左树 右树,根据 具体遍历 确定打印 或者 其他操作(如放入集合中) 时机

(2)解法:

  • 遍历法
 public void preOderTraveral(Node root){
        if(root == null){
            return;
        }

        System.out.print(root.val+" ");
        preOderTraveral(root.left);
        preOderTraveral(root.right);
    }

    public void inOderTraveral(Node root){
        if(root == null){
            return;
        }

        inOderTraveral(root.left);
        System.out.print(root.val+" ");
        inOderTraveral(root.right);
    }

    public void postOderTraveral(Node root){
        if(root == null){
            return;
        }

        postOderTraveral(root.left);
        postOderTraveral(root.right);
        System.out.print(root.val+" ");
    }
  • 子问题
 public List<Integer> preorderTraversal(TreeNode root) {
    //先创建list,如果root为空直接返回空list
    List<Integer> ret = new LinkedList<>();
    
    if(root == null) return ret;

    List<Integer> left = preorderTraversal(root.left);
    List<Integer> right = preorderTraversal(root.right);

    ret.addAll(left);
    ret.addAll(right);

    return ret;
}

//2中
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    if(root == null) return ret;

    List<Integer> leftTree = inorderTraversal(root.left);
    ret.addAll(leftTree);

    List<Integer> rightTree = inorderTraversal(root.right);
    ret.addAll(rightTree);

    return ret;

}

//3后
public List<Integer> postorderTraversal(TreeNode root) {
    List<Integer> ret = new LinkedList<>();

    if(root == null) return ret;

    List<Integer> left = postorderTraversal(root.left);
    ret.addAll(left);

    List<Integer> right = postorderTraversal(root.right);
    ret.addAll(right);

    return ret;
}

3.2 求节点个数

1)解法:

  • 遍历法
public static int size = 0;
    public int size1(TreeNode root){
        if(root == null) return size;

        size++;

        size1(root.left);
        size1(root.right);

        return size;
    }
  • 子问题
public int size2(TreeNode root){
        int size = 0;
        if(root == null) return size;

        int leftSize = size2(root.left);
        int rightSize = size2(root.right);

        return leftSize + rightSize + 1;
    }

3.3 求子叶个数

1)解法:

  • 遍历法
public static int leafSize = 0;
    public int getLeafNodeCount1(TreeNode root){
        if(root == null) return leafSize;

        if(root.left == null && root.right == null) leafSize++;

        getLeafNodeCount1(root.left);
        getLeafNodeCount1(root.right);

        return leafSize;
    }
  • 子问题
public int getLeadNodCount2(TreeNode root){
    int leafSize = 0;

    if (root == null) return leafSize;

    if(root.left == null && root.right == null) leafSize++;

    int leftLeafSize = getLeadNodCount2(root.left);
    int rightLeafSize = getLeadNodCount2(root.right);

    return leafSize + leftLeafSize + rightLeafSize;
}

3.4 第k层节点个数

1)解法:

  • 遍历法
public int getLevelNodeCount(TreeNode root, int k){
    if(root == null) return 0;

    if(k == 1) return 1;

    return getLevelNodeCount(root.left, k - 1) + getLevelNodeCount(root.right, k - 1);
}

3.5 查找val所在的节点

1)思路:

  • 使用子问题思路,递归中止条件就是root.val == val / null

  • 左右树分别递归,递归完成后使用ret接收,同时进行判空 返回

2)代码:

public TreeNode find(TreeNode root, int val){
    if(root == null) return  null;

    if(root.val == val) return root;

    TreeNode ret = find(root.left, val);
    if(ret != null) return ret;


    ret = find(root.right, val);
    if(ret != null) return ret;

    return null;
}

3.6 求树的高度

1)思路:

  • 使用子问题思路递归,中止条件为root == null

  • 左右树分别进行递归,记录左右树的高度。递归都完成后,返回两树高度较大值+1

  • tip:如果使用三目运算符进行嵌套可能会导致递归次数太多,而超时错误

2)时间复杂度:

  • O(n),每个节点都要进行求树的高度

3)解法:

  • 子问题
public int getHeight(TreeNode root){
    if(root == null) return 0;
    
    //提前保存左右树的高度,如果直接在在三目运算符中嵌套,可能会因为超时而报错
    int leftHeight = getHeight(root.left);
    int rightHegiht = getHeight(root.right);

    return leftHeight > rightHegiht ? leftHeight + 1 : rightHegiht + 1;
    
    //return (getHeight(root.left) > getHeight(root.right)) ? getHeight(root.left) + 1 : getHeight(root.right) + 1;
}

3.7 二叉树层序遍历

1)返回类型为void

(1)思路:

  • 根节点判空,创建队列,传入root

  • 保存出队根节点,将该 根节点的非空子节点入队,依次循环 直到队列为空(一次循环出队一个节点)

(2)解法

public void leverOrder(TreeNode root){
    if(root == null) return;

    Queue<TreeNode> queue= new LinkedList<>();
    queue.offer(root);

    while(!queue.isEmpty()){
        TreeNode tmp = queue.poll();
        System.out.print(tmp.val + " ");

        if(tmp.left != null) queue.offer(tmp.left);

        if(tmp.right != null) queue.offer(tmp.right);
    }

    System.out.println();
}

2)返回类型为 List<List>
(1)思路:

  • 总:出上层,入下层,尾插上层

    • 创建 List<List> ret保存层序遍历的结果
    • 创建Queue queue储存本层的节点,来源于上次外循环的入队
    • 创建List list保存本层的节点,内循环结束尾插到ret中
  • 一次外循环表示处理完一层的元素,包括各自保存本层和下层的非空元素

  • 一次内循环出队size个节点,出队节点 传入临时list,同时入队 出队节点 的非空子节点,一次循环结束将list尾插到ret。

    • szie = queue.size(),是queue保存的本层节点个数
    • list 在内循环中初始化
  • tip:先初始化 list,当root == null 时返回ret

(2)解法:

public List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> ret = new LinkedList<>();

    if(root == null ) return ret;

    Queue<TreeNode> queue = new LinkedList<>();
    queue.offer(root);

    while(!queue.isEmpty()){
        List<Integer> list = new LinkedList<>();
        int size = queue.size();

        while(size != 0){
            TreeNode node = queue.poll();
            list.add(node.val);

            if(node.left != null){
                queue.offer(node.left);
            }

            if(node.right != null){
                queue.offer(node.right);
            }

            size--;
        }

        ret.add(list);

    }
    return ret;
}

3.8 二叉树前中后序遍历-非递归

1) 前序遍历

(1)思路

  • 总:栈保存节点,根为空出栈找到右节点,循环至栈空

  • 初始化栈 stack,创建 cur = root 遍历节点

  • 外循环 限制条件为 cur 非空 或者 栈非空

  • 内循环 限制条件cur 非空,先入栈cur打印cur,再cur左移。跳出内循环,左节点打印完毕,出栈根节点top,cur = top.right进入右节点 ,完成一次外循环。

    • 进入右节要将在栈中的根节点出掉,才能到下一次右节点为空的时候 找到要遍历的另一棵树的根节点(打印路径中的下一个右节点)

(2)解法:

public void preOrderTravelNor(Node root){
    if(root == null) return;
    Stack<Node> stack = new Stack<>();
    Node cur = root;

    while(!stack.empty() || cur != null){
        while(cur != null){
            stack.push(cur);
            System.out.print(cur.val + " ");
            cur = cur.left;
        }

        Node top = stack.pop();
        cur = top.right;
    }

}

2)中序遍历

(1)思路:

  • 大体和前序遍历类似,因为中序遍历是 左 根 右,所以等cur到达 要打印的树 的最左边元素后,出栈根节点时再打印

(2)解法:

public void inOrderTravelNor(Node root){
    if(root == null) return;
    Stack<Node> stack = new Stack<>();
    Node cur = root;

    while(cur != null || !stack.empty()){
        while(cur != null){
            stack.push(cur);
            cur = cur.left;
        }

        Node top = stack.pop();
        System.out.print(top.val + " ");
        cur = cur.right;
    }
}

3)后序遍历

(1)思路:

  • 总:将第一个要打印的元素看作一个主体节点,该主体节点可能是父节点cur的左节点或者右节点,如果是左节点直出栈打印,如果是右节点则要加上判断 cur.right == pre打印过的节点,避免陷入死循环。
  • a 内循环到最左边的节点,判断该节点的右节点。右节点分空和非空两种情况,cur.right == null空直接将该节点出栈打印,同时将cur 赋值为空下次循环直接回到根节点;非空cur = cur.right 将其视为新的根节点继续循环
  • b 如果是非空右节点,循环至要打印的根节点(即该节点的子字节点都为空)进行出栈打印,打印完成后,我们要退回该节点的根节点将其打印
  • 我们的目标是只打印该根节点(出栈操作),不打印已经打印过的右节点
  • 仅仅依靠原有(a)中的cur.right == null判空代码,又会重新进入根节点的右节点,陷入打印的死循环
  • 所以我们 加上cur.right == pre(上次打过的的节点),直接出栈中元素,不进入已打印过的右节点
public void postOrderTravelNor(Node root){
    if(root == null) return;
    Stack<Node> stack = new Stack<>();
    Node cur = root;
    Node pre = null;

    while(cur != null || !stack.empty()){
        while(cur != null){
            stack.push(cur);
            cur = cur.left;
        }

        //取出堆顶元素,不出队,因为可能右节点非空
        cur = stack.peek();

        //cur.right == pre 操作防止回到根节点后再进入右节点
        if(cur.right == null || cur.right == pre){
            Node popNode = stack.pop();
            System.out.print(popNode.val + " ");
            pre = cur;

            //cur = null 不会重复进入左节点,直接回到根节点
            cur = null;
        }else{
            cur = cur.right;
        }
    }
}
  • 41
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 41
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值