大厂算法面试之leetcode精讲21.树

大厂算法面试之leetcode精讲21.树

视频讲解(高效学习):点击学习

目录:

1.开篇介绍

2.时间空间复杂度

3.动态规划

4.贪心

5.二分查找

6.深度优先&广度优先

7.双指针

8.滑动窗口

9.位运算

10.递归&分治

11剪枝&回溯

12.堆

13.单调栈

14.排序算法

15.链表

16.set&map

17.栈

18.队列

19.数组

20.字符串

21.树

22.字典树

23.并查集

24.其他类型题

树这种数据结构包括根节点root,左右节点,子树中又有父节点,子节点,兄弟节点,没有子节点的成为叶子节点,树分为二叉树和多叉树

ds_36

ds_37

List 就是特殊化的tree,Tree就是特殊化的Graph

二分搜索树

二分搜索树(英语:Binary Search Tree),也称为 有序二叉树或排序二叉树。满足以下几个条件:

  • 若它的左子树不为空,左子树上所有节点的值都小于它的根节点。
  • 若它的右子树不为空,右子树上所有的节点的值都大于它的根节点。
  • 它的左、右子树也都是二分搜索树。

二分搜索树的优势:不仅可以查找数据,还可以高效的插入、删除数据

ds_116

注意二分搜索树不一定是完全二叉树

ds_117

树的遍历

  • 深度优先
  • 广度优先

  1. 前序:根-左-右
  2. 中序:左-根-右
  3. 后序:左-右-根

ds_38

144. 二叉树的前序遍历(easy)

94. 二叉树的中序遍历 (easy)

145. 二叉树的后序遍历 (easy)

动画过大,点击查看

动画过大,点击查看

动画过大,点击查看

动画过大,点击查看

方法1.递归

js:

//时间复杂度O(n),n为节点个树,空间复杂度O(n),即递归的空间开销(树的高度),最坏的情况下树是链表,所以是O(n),平均空间复杂度O(logn)
//前序遍历:
var preorderTraversal = function(root, res = []) {
    if (!root) return res;
    res.push(root.val);
    preorderTraversal(root.left, res)
    preorderTraversal(root.right, res)
    return res;
};

//中序遍历:
var inorderTraversal = function(root, res = []) {
    if (!root) return res;
    inorderTraversal(root.left, res);
    res.push(root.val);
    inorderTraversal(root.right, res);
    return res;
};

//后序遍历:
var postorderTraversal = function(root, res = []) {
    if (!root) return res;
    postorderTraversal(root.left, res);
    postorderTraversal(root.right, res);
    res.push(root.val);
    return res;
};

Java:

//时间复杂度O(n),n为节点个树,空间复杂度O(n),即递归的空间开销(树的高度),最坏的情况下树是链表,所以是O(n),平均空间复杂度O(logn)
//前序遍历
class Solution {
    ArrayList<Integer> preOrderReverse(TreeNode root) {
        ArrayList<Integer> result = new ArrayList<Integer>();
        preOrder(root, result);
        return result;
    }

    void preOrder(TreeNode root, ArrayList<Integer> result) {
        if (root == null) {
            return;
        }
        result.add(root.val);
        preOrder(root.left, result);
        preOrder(root.right, result);
    }
}
// 中序遍历
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inorder(root, res);
        return res;
    }

    void inorder(TreeNode root, List<Integer> list) {
        if (root == null) {
            return;
        }
        inorder(root.left, list);
        list.add(root.val);
        inorder(root.right, list);
    }
}
// 后序遍历
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        postorder(root, res);
        return res;
    }

    void postorder(TreeNode root, List<Integer> list) {
        if (root == null) {
            return;
        }
        postorder(root.left, list);
        postorder(root.right, list);
        list.add(root.val); 
    }
}

方法2.迭代

js:

// 时间复杂度O(n),n为节点个树,空间复杂度O(n),显示栈的空间开销

// 前序遍历:中左右
// 压栈顺序:右左中
var preorderTraversal = function(root, res = []) {
    const stack = [];
    if (root) stack.push(root);
    while(stack.length) {
        const node = stack.pop();
        if(!node) {
            res.push(stack.pop().val);
            continue;
        }
        if (node.right) stack.push(node.right); // 右
        if (node.left) stack.push(node.left); // 左
        stack.push(node); // 中
        stack.push(null);
    }; 
    return res;
};


//  中序遍历:左中右
//  压栈顺序:右中左
var inorderTraversal = function(root, res = []) {
    const stack = [];
    if (root) stack.push(root);
    while(stack.length) {
        const node = stack.pop();
        if(!node) {
            res.push(stack.pop().val);
            continue;
        }
        if (node.right) stack.push(node.right); // 右
        stack.push(node); // 中
        stack.push(null);
        if (node.left) stack.push(node.left); // 左
    };
    return res;
};

// 后续遍历:左右中
// 压栈顺序:中右左
var postorderTraversal = function(root, res = []) {
    const stack = [];
    if (root) stack.push(root);
    while(stack.length) {
        const node = stack.pop();
        if(!node) {
            res.push(stack.pop().val);
            continue;
        }
        stack.push(node); // 中
        stack.push(null);
        if (node.right) stack.push(node.right); // 右
        if (node.left) stack.push(node.left); // 左
    };
    return res;
};

Java:

// 时间复杂度O(n),n为节点个树,空间复杂度O(n),显示栈的空间开销
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
class Solution {
  
  public List<Integer> preorderTraversal(TreeNode root) {
      List<Integer> result = new LinkedList<>();
      Stack<TreeNode> st = new Stack<>();
      if (root != null) st.push(root);
      while (!st.empty()) {
          TreeNode node = st.peek();
          if (node != null) {
              st.pop();
              if (node.right!=null) st.push(node.right);
              if (node.left!=null) st.push(node.left);
              st.push(node);
              st.push(null);
              
          } else {
              st.pop();
              node = st.peek();
              st.pop();
              result.add(node.val);
          }
      }
      return result;
  }
}

// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {
  public List<Integer> inorderTraversal(TreeNode root) {
  	    List<Integer> result = new LinkedList<>();
      Stack<TreeNode> st = new Stack<>();
      if (root != null) st.push(root);
      while (!st.empty()) {
          TreeNode node = st.peek();
          if (node != null) {
              st.pop();
              if (node.right!=null) st.push(node.right);
              st.push(node);
              st.push(null);

              if (node.left!=null) st.push(node.left);
          } else {
              st.pop(); 
              node = st.peek();
              st.pop();
              result.add(node.val);
          }
      }
      return result;
  }
}


// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {

 public List<Integer> postorderTraversal(TreeNode root) {
      List<Integer> result = new LinkedList<>();
      Stack<TreeNode> st = new Stack<>();
      if (root != null) st.push(root);
      while (!st.empty()) {
          TreeNode node = st.peek();
          if (node != null) {
              st.pop();
              st.push(node);
              st.push(null);
              if (node.right!=null) st.push(node.right);
              if (node.left!=null) st.push(node.left);    
                             
          } else {
              st.pop();
              node = st.peek();  
              st.pop();
              result.add(node.val); 
          }
      }
      return result;
 }
}

98. 验证二叉搜索树 (medium)

方法1.递归

ds_61

  • 思路:利用二叉搜索树的性质,每个节点都大于它左子树所有节点,小于右子树上所有节点,并且每个节点左右子树不为空,那它的左右子树也是二叉搜索树。我们可以递归验证每个节点的左右子树。
  • 复杂度分析:时间复杂度 : O(n),n是二叉树节点的个树。空间复杂度 : O(n),n是递归的层数,最差的情况下二叉树是一条链,树高n,也就是总共n层

Js:

const helper = (root, lower, upper) => {
    if (root === null) {
        return true;
    }
    if (root.val <= lower || root.val >= upper) {//终止条件 不满足二叉搜索树的性质
        return false;
    }
  	//递归左右子树
    return helper(root.left, lower, root.val) && helper(root.right, root.val, upper);
}
var isValidBST = function(root) {
  	//定义low=-Infinity,让所有值都大于low
  	//定义upper=Infinity,让所有值都小于upper
    return helper(root, -Infinity, Infinity);
};

Java:

class Solution {
    public boolean isValidBST(TreeNode root) {
        return helper(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    public boolean helper(TreeNode root, long lower, long upper) {
        if (root == null) {
            return true;
        }
        if (root.val <= lower || root.val >= upper) {
            return false;
        }
        return helper(root.left, lower, root.val) && helper(root.right, root.val, upper);
    }
}

方法2:中序遍历

动画过大,点击查看

  • 思路:从二叉搜素树的性质可知,中序遍历二叉搜索树,得到的就是升序的数组
  • 复杂度分析:时间复杂度 : O(n),每个元素被访问一次。空间复杂度 : O(n),中序遍历用栈存储元素,需要额外的O(n)空间

Js:

//非递归版中序遍历
var isValidBST = function(root) {
    let stack = [];
    let inorder = -Infinity;

    while (stack.length || root !== null) {
        while (root !== null) {
            stack.push(root);
            root = root.left;
        }
        root = stack.pop();
        if (root.val <= inorder) {
            return false;
        }
        inorder = root.val;
        root = root.right;
    }
    return true;
};

//递归版中序遍历
var isValidBST = function (root) {
    let arr = [];
    const buildArr = (root) => {
        if (root) {
            buildArr(root.left);
            arr.push(root.val);
            buildArr(root.right);
        }
    }
    buildArr(root);
    for (let i = 1; i < arr.length; ++i) {
        if (arr[i] <= arr[i - 1])
            return false;
    }
    return true;
};

Java:

class Solution {
    public boolean isValidBST(TreeNode root) {
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        double inorder = -Double.MAX_VALUE;

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

236. 二叉树的最近公共祖先 (medium)

方法1.递归

ds_44

  • 思路:分四种情况,1.root是null或者root等于p或q,说明root是p,q的公共祖先,2.递归左右子树,如果左右子树递归函数返回的都不为空,则root就是p,q的公共祖先,3.左子树递归函数返回的值为空,则p,q都在右子树,4.右子树递归函数返回的值为空,则p,q都在左子树
  • 复杂度分析:时间复杂度 : O(n),n是二叉树节点的个树,空间复杂度 : O(n),n是递归的层

Js:

var lowestCommonAncestor = function(root, p, q) {
    // 1. 确定递归的函数
    const travelTree = function(root,p,q) {
        // 2. 确定递归终止条件
        if(root === null || root === p||root === q) {
            return root;
        }
        // 3. 确定递归单层逻辑
        let left = travelTree(root.left,p,q);
        let right = travelTree(root.right,p,q);
      	//如果在某一个节点的左右子树都能找到p和q说明这个节点就是公共祖先
        if(left !== null&&right !== null) {
            return root;
        }
        if(left ===null) {//如果左子树没找到就说明p,q都在右子树
            return right;
        }
        return left;//如果右子树没找到就说明p,q都在左子树
    }
   return  travelTree(root,p,q);//递归开始
};

Java:

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        return travelTree(root, p, q);
    }
    public TreeNode travelTree(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null || root == p || root == q) {
            return root;
        }
        TreeNode left = travelTree(root.left, p, q);
        TreeNode right = travelTree(root.right, p, q);
        if (left != null && right != null) {// 左右子树分别找到了,说明此时的root就是要求的结果
            return root;
        }
        if (left == null) {
            return right;
        }
        return left;
    }
}

235. 二叉搜索树的最近公共祖先 (easy)

方法1:从二叉搜索树中不断向左右子树寻找p或q的路径,最后寻找两个路径的第一个相同的节点,时间复杂度O(n),空间复杂度O(n)

方法2:递归

ds_45

  • 思路:分为三种情况,1.root节点大于p并且大于q,说明p和q都在root的左子树, 2.root节点小于p并且小于q,说明p和q都在root的右子树,3.其他情况比如root等于p或q,说明root就是公共祖先,前两种情况直接递归左右子树,第3种情况直接返回root
  • 复杂度分析:时间复杂度 : O(n),n是二叉树节点的个树,空间复杂度 : O(n),n是递归的层数,最坏情况和n相等,此时的树是一个链表

Js:

var lowestCommonAncestor = function(root, p, q) {
    if(root === null) {//递归终止条件
        return root;
    }
  	//如果root节点大于p并且大于q,说明p和q都在root的左子树
    if(root.val>p.val&&root.val>q.val) {
        let left = lowestCommonAncestor(root.left,p,q);
        return left !== null&&left;
    }
  	//如果root节点小于p并且小于q,说明p和q都在root的右子树
    if(root.val<p.val&&root.val<q.val) {
        let right = lowestCommonAncestor(root.right,p,q);
        return right !== null&&right;
    }
    return root;//如果上述条件都不满足说明root就是公共祖先
};

Java:

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
        if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
        return root;
    }
}

方法3:迭代

ds_45

  • 思路:和递归的情况一样,只不过用root变量不断迭代左右子树
  • 复杂度分析:时间复杂度 : O(n),空间复杂度 : O(1),n是二叉树节点的个树

js:

var lowestCommonAncestor = function(root, p, q) {
    while(root) {
        if(root.val>p.val&&root.val>q.val) {
            root = root.left;
        }else if(root.val<p.val&&root.val<q.val) {
            root = root.right;
        }else {
            return root;
        }
        
    }
    return null;
};

java:

//java
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while (true) {
            if (root.val > p.val && root.val > q.val) {
                root = root.left;
            } else if (root.val < p.val && root.val < q.val) {
                root = root.right;
            } else {
                break;
            }
        }
        return root;
    }
}

102. 二叉树的层序遍历 (medium)

方法1.广度优先遍历

动画过大,点击查看

  • 思路:准备一个队列,将根节点加入队列,当队列不为空的时候循环队列,每次循环拿到当前队列的大小,在循环当前层的每个元素,然后加入输出数组ret中,如果这个元素存在左右节点则将左右节点加入队列
  • 复杂度分析:时间复杂度 O(n),每个点进队出队各一次,故渐进时间复杂度为 O(n)。空间复杂度O(n),队列中元素的个数不超过 n 个

Js:

var levelOrder = function(root) {
    const ret = [];
    if (!root) {
        return ret;
    }

    const q = [];
    q.push(root);//初始队列
    while (q.length !== 0) {
        const currentLevelSize = q.length;//当前层节点的数量
        ret.push([]);//新的层推入数组
        for (let i = 1; i <= currentLevelSize; ++i) {//循环当前层的节点
            const node = q.shift();
            ret[ret.length - 1].push(node.val);//推入当前层的数组
            if (node.left) q.push(node.left);//检查左节点,存在左节点就继续加入队列
            if (node.right) q.push(node.right);//检查左右节点,存在右节点就继续加入队列
        }
    }
        
    return ret;
};

Java:

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        if (root == null) {
            return ret;
        }

        Queue<TreeNode> q = new LinkedList<TreeNode>();
        q.offer(root);
        while (!q.isEmpty()) {
            List<Integer> level = new ArrayList<Integer>();
            int currentLevelSize = q.size();
            for (int i = 1; i <= currentLevelSize; ++i) {
                TreeNode node = q.poll();
                level.add(node.val);
                if (node.left != null) {
                    q.offer(node.left);
                }
                if (node.right != null) {
                    q.offer(node.right);
                }
            }
            ret.add(level);
        }
        
        return ret;
    }
}
方法2:深度优先遍历

动画过大,点击查看

  • 思路:从根节点开始不断递归左右子树,透传step层数和res输出数组。
  • 复杂度分析:时间复杂度O(n),n是节点的个数。空间复杂度O(n),n是树的高度。

js:

var levelOrder = function(root) {
    if(!root) return []
    let res = []
    dfs(root, 0, res)
    return res
};

function dfs(root, step, res){//每层透传当前节点,层数,和输出数组
  if(root){
    if(!res[step]) res[step] = []//初始化当前层数组
    res[step].push(root.val)//当前节点加入当前层数组
    dfs(root.left, step + 1, res)//step+1,递归左节点	
    dfs(root.right, step + 1, res)//step+1,递归右节点	
  }
}

Java:

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res  = new ArrayList<>();
        if(root != null){
            dfs(res, root, 0);
        }
        return res;
    }
    
    private void dfs(List<List<Integer>> res, TreeNode node, int step){
        if(res.size() - 1 < step){
            res.add(new ArrayList<Integer>());
        }
        res.get(step).add(node.val);
        if(node.left!=null){
            dfs(res, node.left, step + 1);
        }
        if(node.right!=null){
            dfs(res, node.right, step + 1);
        }
    }
}

107. 二叉树的层序遍历 II (medium)

时间复杂度O(n),空间复杂度O(n)

js:

const levelOrderBottom = (root) => {
    if (root == null) {
        return [];
    }
    const queue = [];
    queue.push(root);
    const res = [];

    while (queue.length) {
        const subRes = [];
        const levelSize = queue.length;
        for (let i = 0; i < levelSize; i++) {
            const cur = queue.shift();
            subRes.push(cur.val);
            if (cur.left) {
                queue.push(cur.left);
            }
            if (cur.right) {
                queue.push(cur.right);
            }
        }
        res.unshift(subRes);//和102不一样的地方 推入res中的方向正好相反
    }
    return res;
}



java:

class Solution {
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        List<List<Integer>> res = new LinkedList<List<Integer>>();
        if (root == null) {
            return res;
        }
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            List<Integer> subRes = new ArrayList<Integer>();
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode cur = queue.poll();
                subRes.add(cur.val);
                TreeNode left = cur.left, right = cur.right;
                if (left != null) {
                    queue.offer(left);
                }
                if (right != null) {
                    queue.offer(right);
                }
            }
            res.add(0, subRes);
        }
        return res;
    }
}

104. 二叉树的最大深度 (easy)

方法1.dfs

动画过大,点击查看

  • 思路:一个节点的深度等于1加左节点和有节点深度的较大者,用公式表示 h(r)=Math.max(h(r.left), h(right)) + 1,所以可以深度遍历左右子树,返回左右子树的最大深度。
  • 复杂度分析:时间复杂度O(n), 其中 n 为二叉树节点的个数。空间复杂度O(n), 其中n 表示二叉树的高度,最坏的情况下和树的节点数相同

js:

var maxDepth = function(root) {
    if(!root) {
        return 0;
    } else {
        const left = maxDepth(root.left);//递归左子树
        const right = maxDepth(root.right);//递归右子树
        return Math.max(left, right) + 1;//1加左节点和有节点深度的较大者
    }
};

Java:

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null) {
            return 0;
        } else {
            int left = maxDepth(root.left);
            int right = maxDepth(root.right);
            return Math.max(left, right) + 1;
        }
    }
}

方法2.bfs

动画过大,点击查看

  • 思路:层序遍历二叉树,每层结束的时候depth加1.
  • 复杂度分析:时间复杂度O(n),n 为二叉树的节点个数,每个节点只会被访问一次。空间复杂度O(n),表示队列中存放的元素,最坏情况下和二叉树节点个数一样

Js:

const maxDepth = (root) => {
    if (root == null) return 0;
    const queue = [root];
    let depth = 1;
    while (queue.length) {
        // 当前层的节点个数
        const levelSize = queue.length;          
        // 逐个让当前层的节点出列
        for (let i = 0; i < levelSize; i++) {    
            // 当前出列的节点
            const cur = queue.shift();            
            // 左右子节点入列
            if (cur.left) queue.push(cur.left);
            if (cur.right) queue.push(cur.right); 
        }
        // 当前层所有节点已经出列,如果队列不为空,说明有下一层节点,depth+1
        if (queue.length) depth++;
    }
    return depth;
};

Java:

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null){
            return 0;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int depth = 0;
        while(!queue.isEmpty()){
            int levelSize = queue.size();
            for(int i = 0; i < levelSize; i++){
                TreeNode node = queue.poll();
                if(node.left != null){
                    queue.offer(node.left);
                }
                if(node.right != null){
                    queue.offer(node.right);
                }
            }
            depth++;
        }
        return depth;
    }
}

111. 二叉树的最小深度 (easy)

方法1.dfs

ds_64

  • 思路:深度优先遍历左右子树,分解成子问题,树的最小深度等于左右子树最小深度+1
  • 复杂度分析:时间复杂度O(n), 其中 n 为二叉树节点的个数。空间复杂度O(n), 其中n 表示二叉树的高度,最坏的情况下树呈现链状,和树的节点数相同,平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logn)

js:

var minDepth = function(root) {
    if(root == null) {
        return 0;
    }
    if(root.left == null && root.right == null) {//遍历到叶子结点终止
        return 1;
    }
    let ans = Number.MAX_SAFE_INTEGER;
    if(root.left != null) {
        ans = Math.min(minDepth(root.left), ans);//递归左子树,找到左子树的最小深度
    }
    if(root.right != null) {
        ans = Math.min(minDepth(root.right), ans);//递归右子树,找到右子树的最小深度
    }
    return ans + 1;//最小深度等于左右子树最小深度+1
};

Java:

class Solution {
    public int minDepth(TreeNode root) {
        if(root == null) {
            return 0;
        }
        if(root.left == null && root.right == null) {
            return 1;
        }
        int ans = Integer.MAX_VALUE;
        if(root.left != null) {
            ans = Math.min(minDepth(root.left), ans);
        }
        if(root.right != null) {
            ans = Math.min(minDepth(root.right), ans);
        }
        return ans + 1;
    }
}

方法2.bfs

动画过大,点击查看

  • 思路:广度优先遍历二叉树,每一层深度+1,遇到第一个叶子结点终止,此时的深度就是最小深度
  • 复杂度分析:时间复杂度O(n)n 为二叉树的节点个数,每个节点只会被访问一次。空间复杂度O(n),表示队列中存放的元素,最坏情况下和二叉树节点个数一样

js:

var minDepth = function(root) {
    if(root == null) return 0;
    let q = [root];
    //root本身就是一层,所以深度初始化为1
    let depth = 1;
    while(q.length){
        let len = q.length;
        for(let i=0; i<len; i++){
            let curr = q.shift();
            //判断是当前否是叶子节点
            if(curr.left == null && curr.right == null){
                return depth;
            }
            //如果不是叶子结点,就将其相邻节点加入队列
            if(curr.left){
                q.push(curr.left);
            }
            if(curr.right){
                q.push(curr.right);
            }
        }
        depth++;
    }
    return depth;

};

Java:

class Solution {
    public int minDepth(TreeNode root) {
       if (root == null) {
           return 0;
       }
       int depth = 0;
       Queue<TreeNode> q = new LinkedList<>();
       q.offer(root);

       while (!q.isEmpty()) {
           int sz = q.size();
           depth++;
           for (int i = 0; i < sz; i++) {
               TreeNode cur = q.poll();
               if (cur.left == null && cur.right == null) {
                   return depth;
               }
               if (cur.left != null) {
                   q.offer(cur.left);
               }
               if (cur.right != null) {
                   q.offer(cur.right);
               }
           }
       }
       return depth;
    }
}

127. 单词接龙 (hard)

ds_108

  • 思路:bfs,循环beginWord,对每个字符替换26个小写字符,看新生成的单词是否在wordList里面,如果存在就是一个合法的路径,直到新生成的单词是endWord,循环完成还是没到达endWord就返回0。双向bfs思路一样,只不过是从两边向中间靠拢,判断新生成的单词是否在另一个方向走过的路径中。
方法1:bfs

js:

const ladderLength = (beginWord, endWord, wordList) => {
  const wordSet = new Set(wordList);
  const queue = [];
  queue.push([beginWord, 1]);//开始单词和层级加入队列

  while (queue.length) {
    const [word, level] = queue.shift()//出队 进行bfs
    if (word == endWord) {//和endword相等返回层级
      return level;
    }
    for (let i = 0; i < word.length; i++) { //循环单词列表
      for (let c = 97; c <= 122; c++) { //循环26个小写字符
        //得到新的单词
        const newWord = word.slice(0, i) + String.fromCharCode(c) + word.slice(i + 1);
        if (wordSet.has(newWord)) { //检查wordset包不包括新生成的单词 避重复入列
          queue.push([newWord, level + 1]); //新单词加入队列
          wordSet.delete(newWord);  //避死循环
        }
      }
    }
  }
  return 0; // bfs结束,始终没有遇到终点
};
方法2:双向bfs

js:

var ladderLength = function(beginWord, endWord, wordList) {
    let wordSet = new Set(wordList)
    if (!wordSet.has(endWord)) return 0
    //从beginWord到endWord
    let begSet = []
		//从endWord到beginWord
    let endSet = []
    begSet.push(beginWord)
    endSet.push(endWord)

    let n = 1//层级
    while(begSet.length > 0){//开始遍历单词
        const nextBegins = []//bfs下一层级的单词数组
        //步数少数对换一下,让走的慢的在走几步
        if(begSet.length > endSet.length){
            let q = begSet
            begSet = endSet
            endSet = q
        }
        //循环begSet
        for(let k = 0; k < begSet.length;k++){
            let m = begSet[k]
            for(let i = 0; i < m.length; i++){
                for (let c = 97; c <= 122; c++){
                    //生成新词
                    let newm = m.slice(0, i) + String.fromCharCode(c) + m.slice(i + 1);
                    if(endSet.includes(newm)){//相遇
                        return n + 1
                    }
                    if( wordSet.has(newm)){
                        nextBegins.push(newm)	//下一层bfs的单词数组
                        wordSet.delete(newm)  //防止重复
                    } 
                }
            }
        }
        begSet = nextBegins
        n++ //层级+1
    }
    return 0
   
};

297. 二叉树的序列化与反序列化 (hard)

方法1.递归dfs
ds_110
  • 思路:深度优先遍历,按根,左,右 返回字符串,方便反序列化的时候从根节点开始构建,递归左右子树,直到遇见了null节点。
  • 复杂度:时间复杂度O(n),每个节点访问一次,n是树的节点个数。空间复杂度O(n),最坏情况下递归深度是n

js:

const serialize = (root) => {
    if (root == null) {                  //遇到null 返回‘X’进行标示
        return 'X';
    }
    const left = serialize(root.left);   //序列化左子树
    const right = serialize(root.right); //序列化右子树
    return root.val + ',' + left + ',' + right; //按根,左,右 返回字符串
};

const deserialize = (data) => {
    const list = data.split(',');   //字符串转数组

    const buildTree = (list) => {  	//构建树
        const rootVal = list.shift(); //第一个元素
        if (rootVal == "X") {         //如果是X,返回null
            return null;
        }
        const root = new TreeNode(rootVal); //如果不是X就创建节点
        root.left = buildTree(list);        //构建左子树
        root.right = buildTree(list);       //构建右子树
        return root;                        //返回构建的节点
    };

    return buildTree(list);
};


方法2:bfs

动画过大,点击查看

  • 思路:广度优先遍历节点,不断子节点不断入队,按照根左右的顺序序列化和反序列化
  • 复杂度:时间复杂度O(n),每个节点访问一次,n是树的节点个数。空间复杂度O(n),队列的空间

js:

const serialize = (root) => {
    const queue = [root];
    let res = [];
    while (queue.length) {
        const node = queue.shift(); //出队
        if (node) {                 //node存在 推入根左右
            res.push(node.val);
            queue.push(node.left);
            queue.push(node.right);
        } else {                    //如果不存在 推入‘x’
            res.push('X');
        }
    }
    return res.join(',');  //数组转成字符串
}

const deserialize = (data) => {
    if (data == 'X') return null;

    const list = data.split(',');  //字符串转数组

    const root = new TreeNode(list[0]); //从队首开始构建
    const queue = [root];          //根节点加入队列
    let cursor = 1;                //遍历到了第几个节点

    while (cursor < list.length) { //当队列没遍历完时
        const node = queue.shift();  //出队

        const leftVal = list[cursor];      //左节点的值
        const rightVal = list[cursor + 1]; //右节点的值

        if (leftVal != 'X') {              //不是空节点
            const leftNode = new TreeNode(leftVal); //构建左节点
            node.left = leftNode;                   //左节点挂在父节点的left下
            queue.push(leftNode);                   //自己入列 构建以自己为根的子树
        }
        if (rightVal != 'X') {
            const rightNode = new TreeNode(rightVal);
            node.right = rightNode;
            queue.push(rightNode);
        }
        cursor += 2; //构建的节点数+2
    }
    return root;  //返回根
};

226. 翻转二叉树 (easy)

方法1:递归
  • 思路:递归左右子树,反转左右节点
  • 复杂度:时间复杂度O(n),树的每个节点都会遍历一遍。空间复杂度O(n),递归栈空间,最坏的情况下,和节点的个数n相同

js:

var invertTree = function(root) {
    if (root === null) {//递归终止条件
        return null;
    }
    const left = invertTree(root.left);//递归左子树
    const right = invertTree(root.right);//递归右子树
  	//交换左右节点
    root.left = right;
    root.right = left;
    return root;
};

java:

class Solution {
    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;
    }
}

100. 相同的树(easy)

方法1.dfs递归
  • 思路:递归比较左右子树
  • 复杂度:时间复杂度O(n),n是节点较少的树中的节点个数,递归有一个节点为null,另一个不为null就停止递归,空间复杂度O(n),递归深度不会超过节点个数

js:

var isSameTree = function(p, q) {
    if(p == null && q == null) //都是null表示相同
        return true;
    if(p == null || q == null) //只有一个是null表示不同
        return false;
    if(p.val != q.val) //节点的值不同
        return false;
    return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);//递归左右子树
};


java:

class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if(p == null && q == null) 
            return true;
        if(p == null || q == null) 
            return false;
        if(p.val != q.val) 
            return false;
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
    }
}

方法2bfs:
  • 复杂度:时间复杂度O(n),n是节点较少的树中的节点个数,空间复杂度O(n),队列的空间不会超过节点较少的树中的节点个数

js:

var isSameTree = function(p, q) {
    let pQ = [p], qQ = [q], res = true

    while (pQ.length) {
        pNode = pQ.shift()
        qNode = qQ.shift()

        if (pNode === null && qNode === null) {
            res = true
        } else if (pNode === null || qNode === null) {
            res = false
            break 
        } else {
            if (pNode.val !== qNode.val) {
                res = false
                break 
            } else {
                pQ.push(pNode.left)
                pQ.push(pNode.right)

                qQ.push(qNode.left)
                qQ.push(qNode.right)
            }
        }
    }

    return res
};

java:

class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        Queue<TreeNode> pQ = new LinkedList<TreeNode>();
        Queue<TreeNode> qQ = new LinkedList<TreeNode>();
        pQ.offer(p);
        qQ.offer(q);
        boolean res = true;

        while (!pQ.isEmpty()) {
            TreeNode pNode = pQ.poll();
            TreeNode qNode = qQ.poll();

            if (pNode == null && qNode == null) {
                res = true;
            } else if (pNode == null || qNode == null) {
                res = false;
                break ;
            } else {
                if (pNode.val != qNode.val) {
                    res = false;
                    break ;
                } else {
                    pQ.offer(pNode.left);
                    pQ.offer(pNode.right);

                    qQ.offer(qNode.left);
                    qQ.offer(qNode.right);
                }
            }
        }
        return res;
    }
}

101. 对称二叉树 (easy)

方法1.dfs递归
  • 复杂度:时间复杂度O(n),每个节点遍历一次,空间复杂度O(n),递归栈深度,最深不超过n

js:

var isSymmetric = function(root) {
    if(!root) return true
    const isMirror = (l, r) => {
        if(!l && !r) return true; //两个空节点也为镜像
        if(
            l && r && l.val === r.val &&  //左节点和右节点相同,左子树和右子树也对称则返回true
            isMirror(l.left, r.right) && 
            isMirror(l.right, r.left)
        ) {
            return true;
        }
        return false;
    }
    return isMirror(root.left, root.right)
};

java:

class Solution {
    public boolean isSymmetric(TreeNode root) {
        return isMirror(root, root);
    }

    public boolean isMirror(TreeNode l, TreeNode r) {
        if (l == null && r == null) {
            return true;
        }
        if (l == null || r == null) {
            return false;
        }
        return l.val == r.val && isMirror(l.left, r.right) && isMirror(l.right, r.left);
    }
}
方法2.bfs
  • 复杂度:时间复杂度O(n),每个节点遍历一次,空间复杂度O(n),队列的空间

js:

function isSymmetric(root) {
    const isMirror = (l, r) => {
        const queue = [l, r];
        while (queue.length) {
            const u = queue.shift();
            const v = queue.shift();
            if (!u && !v) continue; //两个空节点也为镜像
            //左右节点只有一个节点为空,或者值不相同返回false
            if (!u || !v || u.val !== v.val) return false;
            queue.push(u.left, v.right); //加入左节点的左子树,右节点的右子树
            queue.push(v.left, u.right); //加入右节点的左子树,左节点的右子树
        }
        return true;
    };
    return isMirror(root.left, root.right);
}

java:

class Solution {
    public boolean isSymmetric(TreeNode root) {
        return isMirror(root, root);
    }

    public boolean isMirror(TreeNode u, TreeNode v) {
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(u);
        queue.offer(v);
        while (!queue.isEmpty()) {
            u = queue.poll();
            v = queue.poll();
            if (u == null && v == null) {
                continue;
            }
            if ((u == null || v == null) || (u.val != v.val)) {
                return false;
            }

            queue.offer(u.left);
            queue.offer(v.right);

            queue.offer(u.right);
            queue.offer(v.left);
        }
        return true;
    }
}

112. 路径总和 (easy)

ds_128

  • 思路:递归左右子树,不断让sum减去当前节点的值。左右子树有一个返回true就找到了一条这样的路径
  • 复杂度:时间复杂度O(n),n是节点个数,每个节点遍历一次。空间复杂度O(n),取决于递归栈空间,最坏的情况下是O(n)

js:

const hasPathSum = (root, sum) => {
    if (root == null) {//递归终止条件
        return false;
    }
    if (root.left == null && root.right == null) {//遍历到叶子节点
        return sum - root.val == 0;                 //sum正好减少到了0 返回ture 否则返回false
    }
    //递归左右子树,有一个返回true就找到了一条这样的路径
    return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
}

java:

class Solution {
    public boolean hasPathSum(TreeNode root, int sum) {
        if (root == null) {
            return false;
        }
        if (root.left == null && root.right == null) {
            return sum == root.val;
        }
        return hasPathSum(root.left, sum - root.val) || hasPathSum(root.right, sum - root.val);
    }
}

437. 路径总和 III (medium)

方法1:dfs
  • 思路:递归左右子树,在递归的子阶段中,继续以该节点为根节点继续进行路径的寻找
  • 复杂度:时间复杂度O(n^2),所有节点都要遍历一边,还要寻找以该节点为根的子树中是否存在符合条件的路径。空间复杂度O(n),递归空间

js:

var pathSum = function(root, targetSum) {
    if (root == null) {
        return 0;
    }
    
    let ret = rootSum(root, targetSum);//以root为根节点的树中寻找路径
    ret += pathSum(root.left, targetSum);//递归左子树,在左子树中寻找路径
    ret += pathSum(root.right, targetSum);//递归右子树,在左子树中寻找路径
    return ret;
};

const rootSum = (root, targetSum) => {
    let ret = 0;

    if (root == null) {
        return 0;
    }
    const val = root.val;
    if (val === targetSum) {
        ret++;
    } 

  	//以root.left为根节点,targetSum - val为寻找的路径和,继续寻找路径
    ret += rootSum(root.left, targetSum - val);
  	//以root.right为根节点,targetSum - val为寻找的路径和,继续寻找路径
    ret += rootSum(root.right, targetSum - val);
    return ret;
}

java:

class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        if (root == null) {
            return 0;
        }

        int ret = rootSum(root, targetSum);
        ret += pathSum(root.left, targetSum);
        ret += pathSum(root.right, targetSum);
        return ret;
    }

    public int rootSum(TreeNode root, int targetSum) {
        int ret = 0;

        if (root == null) {
            return 0;
        }
        int val = root.val;
        if (val == targetSum) {
            ret++;
        } 

        ret += rootSum(root.left, targetSum - val);
        ret += rootSum(root.right, targetSum - val);
        return ret;
    }
}

方法2:前缀和

ds_130,png

  • 思路:记录从根节点到该节点的路径和curr,在递归左右子树,看之前遍历过的路径中是否存在以curr - targetSum为 路径和的路径,如果存在,那么从根节点到当前节点的路径 减去 这条路径 就是符合条件的路径之一,看图
  • 复杂度:时间复杂度O(n),二叉树中的节点只遍历一遍。空间复杂度O(n),递归栈深度。

js:

var pathSum = function(root, targetSum) {
    const prefix = new Map();//存放前缀和 和它的路径数量
    prefix.set(0, 1);//初始化前缀和是0的个数
    return dfs(root, prefix, 0, targetSum);
}

const dfs = (root, prefix, curr, targetSum) => {//curr指到达当前节点的路径上的和
    if (root == null) {//空节点就没有路径了,返回0
        return 0;
    }

    let ret = 0;
    curr += root.val;//加上当前节点

    ret = prefix.get(curr - targetSum) || 0;//获取以curr - targetSum的前缀和的数量
    prefix.set(curr, (prefix.get(curr) || 0) + 1);//在prefix中增加curr的前缀和的数量
    ret += dfs(root.left, prefix, curr, targetSum);//递归左子树 加上左子树前缀和为targetSum的路径数
    ret += dfs(root.right, prefix, curr, targetSum);//递归右子树 加上右子树前缀和为targetSum的路径数
    prefix.set(curr, (prefix.get(curr) || 0) - 1);//节点递归完毕,回溯curr的前缀和数量 避免重复计算

    return ret;
}

java:

class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        HashMap<Long, Integer> prefix = new HashMap<>();
        prefix.put(0L, 1);
        return dfs(root, prefix, 0, targetSum);
    }

    public int dfs(TreeNode root, Map<Long, Integer> prefix, long curr, int targetSum) {
        if (root == null) {
            return 0;
        }

        int ret = 0;
        curr += root.val;

        ret = prefix.getOrDefault(curr - targetSum, 0);
        prefix.put(curr, prefix.getOrDefault(curr, 0) + 1);
        ret += dfs(root.left, prefix, curr, targetSum);
        ret += dfs(root.right, prefix, curr, targetSum);
        prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);

        return ret;
    }
}

257. 二叉树的所有路径 (easy)

方法1:dfs
  • 思路:递归左右子树,直到叶子节点,递归的过程中不断透传path,递归的每一层连接上当前节点的值
  • 复杂度:时间复杂度O(n),每个节点遍历一次。空间复杂度O(n),递归栈空间

js:

var binaryTreePaths = function(root) {
    const paths = [];
    const dfs = (root, path) => {//传入递归的节点和路径数组
        if (root) {
            path += root.val.toString();//加入当前节点
          	//叶子结点就将所有连接起来的节点字符串加入paths中 也就是其中一条路径
            if (root.left === null && root.right === null) { 
                paths.push(path); 
            } else {
                path += "->"; //不是叶子节点继续递归左右子树
                dfs(root.left, path);
                dfs(root.right, path);
            }
        }
    }
    dfs(root, "");
    return paths;
};

java:

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> paths = new ArrayList<String>();
        dfs(root, "", paths);
        return paths;
    }

    public void dfs(TreeNode root, String path, List<String> paths) {
        if (root != null) {
            StringBuffer path1 = new StringBuffer(path);
            path1.append(Integer.toString(root.val));
            if (root.left == null && root.right == null) {
                paths.add(path1.toString());
            } else {
                path1.append("->");
                dfs(root.left, path1.toString(), paths);
                dfs(root.right, path1.toString(), paths);
            }
        }
    }
}
方法2:bfs

动画过大,点击查看

  • 思路:用队列辅助进行广度优先遍历,不断将左右子节点加入队列,直到叶子节点
  • 复杂度:同方法1

js:

var binaryTreePaths = function(root) {
    const res = [];
    if (root === null) {
        return res;
    }
    const nodes = [root];
    const paths = [root.val.toString()];

    while (nodes.length) {
        const node = nodes.shift(); 
        const path = paths.shift();

        if (node.left === null && node.right === null) {//叶子节点
            res.push(path);
        } else {
            if (node.left !== null) {//左节点不为空 加入队列
                nodes.push(node.left);
                paths.push(path + "->" + node.left.val.toString());
            }

            if (node.right !== null) {//右节点不为空 加入队列
                nodes.push(node.right);
                paths.push(path + "->" + node.right.val.toString());
            }
        }
    }
    return res;
};


java:

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> res = new ArrayList<String>();
        if (root == null) {
            return res;
        }
        Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>();
        Queue<String> pathQueue = new LinkedList<String>();

        nodeQueue.offer(root);
        pathQueue.offer(Integer.toString(root.val));

        while (!nodeQueue.isEmpty()) {
            TreeNode node = nodeQueue.poll(); 
            String path = pathQueue.poll();

            if (node.left == null && node.right == null) {
                res.add(path);
            } else {
                if (node.left != null) {
                    nodeQueue.offer(node.left);
                    pathQueue.offer(new StringBuffer(path).append("->").append(node.left.val).toString());
                }

                if (node.right != null) {
                    nodeQueue.offer(node.right);
                    pathQueue.offer(new StringBuffer(path).append("->").append(node.right.val).toString());
                }
            }
        }
        return res;
    }
}

199. 二叉树的右视图 (medium)

ds_165

方法1:dfs
  • 思路:深度优先遍历,先递归右节点 让它在下一层先被处理,当res长度和step相等时 当前节点就是这一层的右节点,加入数组中
  • 复杂度:时间复杂度O(n),每个节点遍历一次。空间复杂度O(n),递归栈空间

js:

var rightSideView = function (root) {
    function dfs(root, step, res) {//传入根节点 层数 右视的节点的数组
        if (root) {
            if (res.length === step) {
                res.push(root.val); //当res长度和step相等时 当前节点就是这一层的右节点 加入数组中
            }
            dfs(root.right, step + 1, res); //先递归右节点 让它在下一层先被处理
            dfs(root.left, step + 1, res);
        }
    }
    if (!root) return [];
    let arr = [];
    dfs(root, 0, arr);
    return arr;
};

java:

class Solution {
    List<Integer> res;
    public List<Integer> rightSideView(TreeNode root) {
        res = new LinkedList<>();
        if (root == null) {
            return res;
        }
        dfs(root, 0);
        return res;
    }

    private void dfs(TreeNode root, int depth) {
        if (root == null) {
            return;
        }
        if (depth == res.size()) {
            res.add(root.val);
        }
        dfs(root.right, depth+1);
        dfs(root.left, depth+1);
    }
}

方法2:bfs
  • 思路:广度优先遍历,记录每一层的节点个数,出队之后让长度减1,当当前层长度等于1时 说明是最边的节点。
  • 复杂度:时间复杂度O(n),每个节点遍历一次。空间复杂度O(n),队列的空间

js:

var rightSideView = function (root) {
    if (!root) return [];
    let queue = [root]; //广度优先遍历的队列 首先加入root
    let arr = []; //存放右视的节点
    while (queue.length > 0) {
        let len = queue.length;
        while (len) {
            let node = queue.shift(); //取出队列第一个元素
            if (len === 1) arr.push(node.val); //当当前层长度等于1时 说明是最边的节点
            if (node.left) queue.push(node.left); //继续向队列中添加左右节点
            if (node.right) queue.push(node.right);
            len--;//出队之后 当前层长度减1
        }
    }
    return arr;
};

java:

class Solution {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> res = new LinkedList<>();
        if (root == null) {
            return res;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            TreeNode curr = null;
            for (int i = 0; i < size; ++i) {
                curr = queue.poll();
                if (curr.left != null) {
                    queue.offer(curr.left);
                }
                if (curr.right != null) {
                    queue.offer(curr.right);
                }
            }
            res.add(curr.val);//或者在换一种思路,循环完当前层之后,最后出队的就是当前层最右边的一个
        }
        return res;
    }
}

posted on 2021-12-06 08:46  全栈潇晨  阅读(43)  评论(0编辑  收藏  举报

导航