前端算法复习--二叉树

375 阅读11分钟

前言

     最近少更了,入职了新公司,熟悉各种环境和业务需求,短暂时间内都没有抽出时间去总结和分享了;今天给大家分享下,我是如何复习算法编程题来进行手撕代码

切入正题,认识算法

     谈起算法,它是解决某个问题的计算方法、步骤。为什么许多公司都喜欢让手撕代码呢,更多考察的是写代码的能力和思维吧;不过多赘述,开启我们的算法复习之途吧;

切入算法入口点

     也许对于一些人来说,是“学习”,因为自己在大学期间是有修过一些算法和数据结构的课程,切入点也是比较容易的;下面总结下如何切入到深入;

  • “学习”切入点
    • 先学习“数据结构”,因为后续的所有的算法实现等都是和它息息相关,在书籍中我们也是经常看到“算法和数据结构”
    • 多多练习,有了基础“数据结构”概念,更多的是去学习如何实现这些数据结构
  • “复习”切入点
    • “针对性”总结,同类型的涉及题目总结,比如“二叉树”总结相关概念和涉及到的编程题;
    • “同类型”练习,在code练习过程中,可能是无序的,所以我们要进行同类型题目复习,比如“我今天就看二叉树相关知识,实现二叉树相关代码”,在这个过程中不过多的去看“动态规划”知识,这样大脑的记忆力也是非常深刻的

如何练习

  • leetcode 大量同类型的题目,而且还有大量的优质解析
  • 牛客网 大量公司常见编程题
  • 自己写,先自己构造一个基础数据结构,然后在本地练习

今日复习 - 二叉树

其实我自己开始是在leetcode上进行大量的练习,典型的乱序练习,但是自己感觉还是记不住哈哈哈哈,老了,趁着元旦的三天假期,自己安排了复习计算,决定自己先本地总结一波,常见理论知识和常见题目,本地练习;

二叉树的知识脑图

二叉树.jpg

图1 二叉树知识脑图

这里就略过概念的介绍了直接进入我们常用到的算法编程题

二叉树编程

主要是自己进行本地练习,所以定义了前置的一些条件

二叉树结构

class NodeTree{ 
  constructor(val){
    this.left = null //左子树
    this.right = null //右子树
    this.value= val; //存储的值
    return this;
  }
} 

二叉树的创建

根据根节点创建 根据节点创建 粗略符合左右树的差异

function createTree(){
  this.root = null;
}
createTree.prototype.insertNode=function(node,newNode){
  //判断新节点和根节点的大小
  if(node.value > newNode.value){
    if(node.left ==null){
      node.left = newNode
    }else{
      //遍历左节点 进行查找
      this.insertNode(node.left,newNode)
    }
  }else{
    if(node.right == null){
      node.right = newNode
    }else{
      this.insertNode(node.right,newNode)
    }
  }
} 
createTree.prototype.insert=function(val){
    let node = new NodeTree(val); 
    //如果没有根节点 则直接插入到根节点中 
    if(!this.root){
      this.root = node;
    }else{  
      //否则插入到左右元素中
      this.insertNode(this.root,node)
    }
}

输入demo数据

let array = [10,7,6,8,9,13,5,11,14] 
let tree = new createTree();
array.forEach(item=>{
  tree.insert(item)
}) 

image.png

图2 树形结构图

二叉树遍历

前序遍历

DLR--前序遍历(根在前,从左往右,一棵树的根永远在左子树前面,左子树又永远在右子树前面 )

前序遍历的标准是,先遍历根节点,在遍历左子树,在遍历右子树,如果左子树中节点存在左节点,则存储当前左节点,继续遍历左节点的左子树则继续进行遍历;

递归

function preorderTraversal(root){ 
   //先遍历根节点 
   preOrderArray.push(root.value); 
   //遍历左子树
   root.left && preorderTraversal(root.left) 
   //遍历右子树
   root.right && preorderTraversal(root.right) 
}

非递归

  • 利用栈存储已经遍历的左子树
  • 然后在出栈 一个一个遍历右子树
  • 每次访问的都是最左边,同时对应最右端的右子树
function preorderTraversalNot(root){
  let  preOrderArrDir=[],stack =[]; 
  let curr = root;
  //先遍历根 左子树 左子树的子节点
  while(curr!=null || stack.length>0){  
    //当前节点存在 则压入栈中
    while(curr!=null){
      //压入当前元素
      stack.push(curr);
      //存储当前的元素
      preOrderArrDir.push(curr.value)
      //将curr指向当前的左节点
      curr = curr.left;
    }
    //判断是否为空,如果不为空 弹出已经遍历的元素,进行遍历右子树
    if(stack.length !=0){
      curr = stack.pop();
      curr = curr.right;
    }
  }
  return preOrderArrDir
}

中序遍历

  • 先遍历左节点
  • 在遍历根节点
  • 在进行遍历右节点 预期结果:
[5,6,7,8,9,10,11,13,14]

递归遍历

let inOrderArray = []
function inorderTraversal(nodeTree){
  if(!nodeTree) return;
  //先遍历左节点
  nodeTree.left && inorderTraversal(nodeTree.left)
  //存储当前根节点
  inOrderArray.push(nodeTree.value)
  //在遍历右节点
  nodeTree.right && inorderTraversal(nodeTree.right)
}
inorderTraversal(tree.root)
console.log(inOrderArray)

非递归遍历

function inorderTraversalNot(root){
  let inOrderQueue = [];//存储遍历结果
  let curr = root,stack =[];
  while(curr!=null || stack.length>0){ 
    //先遍历左子树 到叶子节点
    while(curr!=null){
       //逐个压入左节点
       stack.push(curr); 
       curr = curr.left;  
    }  
    //进行出栈
    if(stack.length>0){
      //取出栈的最后一个元素 逐个是左子树排列的元素
      curr = stack.pop(); 
      //压入当前的元素,当前元素排列为左子树
      inOrderQueue.push(curr.value)
      //遍历当前节点的右子树
      curr = curr.right; 
    }
  }
  return inOrderQueue
}

实际输出

[ 5, 6, 7, 8, 9, 10, 11, 13, 14 ]
[ 5, 6, 7, 8, 9, 10, 11, 13, 14 ]

后序遍历

  • 先遍历左子树
  • 在遍历右子树
  • 在存入根节点 预期输出
[5,6,9,8,7,11,14,13,10]

递归

let postOrderTrversal=[]
function postorderTraversal(root){
  if(!root) return 
  //先左
  if( root.left )
  root.left && postorderTraversal(root.left)
  //后右 
  root.right && postorderTraversal(root.right)
  //在根
  postOrderTrversal.push(root.value)
}

非递归

定义两个栈,压入根元素,在压入左子树,在压入右子树,然后从后开始取出元素,后面的就是右子树,左子树和根元素

function postorderTraversalNot(root){
  let postOrderArray =[];
  let curr = root,stack1 = [],stack2=[];
  if(curr){
    stack1.push(curr)
  }
  // stack1 存储当前子树的目录结构
  while(stack1.length > 0){
    let curr = stack1.pop();//弹出当前元素
    //压入stack2中 stack2每次都存入的是根节点 然后是左子树的根节点 一直到叶子节点
    stack2.push(curr)
    //存储当前的左子树
    if(curr.left){ 
      stack1.push(curr.left)
    }
    //存储当前的右子树
    if(curr.right){
      stack1.push(curr.right)
    }
  }
  console.log(stack2)
  //遍历stack2的变量 此时stack 存储的数据结构正好是 节点的 根左右节点z
  while(stack2.length>0){
    let curr = stack2.pop();
    postOrderArray.push(curr || curr.value)
  }
  return postOrderArray;
}
let result = postorderTraversalNot(tree.root);
console.log(result)

后续遍历的这个过程不是很好理解,因此画了一个图

image.png

图3 后续遍历图

层次遍历

属于广度优先搜索BFS,一层一层遍历,遍历的顺序如下图 弹出一个节点,访问,若左子节点或右子节点不为空,将其压入队列。

image.png

图4 层次遍历
// 非递归
function bfsTree(root){ 
  let quque =[],bfsQueue=[]; //存储遍历的元素
  let curr = null;
  quque.push(root)
  while(quque.length>0){
      curr = quque.pop();//取出最后一个元素
      bfsQueue.push(curr.value)
     //查看当前左边是否存在元素 存在则压入栈前面
     if(curr.left) {
       quque.unshift(curr.left)
     }
     //查看当前元素右边是否存在元素
     if(curr.right){
       quque.unshift(curr.right)
     }
     //queue存储的元素都是当前层的左子树的子树和右子树的子树
  } 
  return bfsQueue;
}
let result =  bfsTree (tree.root);

基础实际使用

求二叉树中节点个数

给定一个二叉树,求节点的个数 求二叉树的个数,其实就是对二叉树的节点进行遍历的过程;

递归

采用先序中序 后序的都可以

let node = 1;
function comNodeSum(root){
    node ++ ;
    root.left && comNodeSum(root.left)
    root.right && comNodeSum(root.right)
}

非递归

层次遍历进行计算

function codeNodeNotSum(root){
  let node = 1;
  let queue =[],curr = root;
  queue.unshift(root) //存储根节点
  while(queue.length>0){
    curr = queue.pop(); //取出队列中的最后一个
    node++ ;//计数当前的节点数量
    if(curr.left){
      queue.unshift(curr.left); //将左节点存入到前面
    }
    if(curr.right){
      queue.unshift(curr.right); //右节点入队
    }
  }
  return node;
}
let result =codeNodeNotSum (tree.root);
console.log(result)

求二叉树的深度(高度)

  • 如果二叉树为空,二叉树的深度为0
  • 如果二叉树不为空,二叉树的深度 = max(左子树深度, 右子树深度) + 1

递归

//递归计算树的深度
function maxDepTree(root){
  let num = 0;
  if(!root) return 0;
  //取 左子树和右子树的最大高度 然后加上当前节点的1
  num = Math.max(maxDepTree(root.left),maxDepTree(root.right)) +1;
  return num;
} 
let result = maxDepTree(tree.root)
console.log(result)

非递归


//非递归 计算树的深度 层序遍历
function dpComTreeHeight(root){
  if(!root) return 0
  let currentNum = 1,//当前层的节点
      nextNum = 0,//下一层的节点数目
      depth = 0;//树的深度
  let queue = [],curr=null;
  queue.push(root); 
  while(queue.length >0){
    curr = queue.pop();
    currentNum --;
    //收集左节点
    if(curr.left){
      queue.unshift(curr.left)
      nextNum++;
    }   
    //收集右节点
    if(curr.right){
      queue.unshift(curr.right)
      nextNum ++ ;
    }
    //如果是当前该层的最后一个节点 
    if(currentNum == 0){
      depth++; 
      currentNum = nextNum;
      nextNum = 0;
    }
  }
  return depth;
}

let depth =dpComTreeHeight(tree.root);
console.log(depth)

求二叉树第k层的节点个数

思路

  • 采用层序遍历方法
  • 采用队列存储当前节点的左右节点,计数左右子树的节点个数;
  • 计数当前遍历的高度,当和第k层相等的时候,进行返回
/**
 * 层序遍历
 * @param {*} root  当前的树
 * @param {*} key   当前底基层
 */
function findKeyNumber(root,k){
  if(!root || k==0) return 0 
  let currentNum = 1,
      nextCurr =0,h=1;
  let queue = [],
      curr = null;
  queue.push(root);
  while(queue.length > 0){
    if(k == h) return currentNum;
    curr = queue.pop();
    currentNum -- 
    if(curr.left){
      queue.unshift(curr.left)
      nextCurr ++ ;
    }
    if(curr.right){
      queue.unshift(curr.right)
      nextCurr ++ 
    }
    //判断是否遍历完
    if(currentNum == 0){
       h++; //计数遍历的第几层
       currentNum = nextCurr;
       nextCurr = 0; //开始下一步步骤
    }
  }
  return 0
}
let re = findKeyNumber(tree.root,4)
console.log(re)

判断两棵二叉树是否是相同的树

思路:

  • 如果两棵树都为空 则返回true
  • 存在一个为空,则不相等
  • 否则比较两棵二叉树;根节点,左子树和右子树都相等;

递归

//递归
function isSametree(tree1,tree2){
  if(!tree1 && !tree2){
    return true;
  }
  if(!tree1 || !tree2){
    return false
  }
  //两个值不相同
  if(tree1.value != tree2.value){
    return false;
  }
  //比较左子树和右子树
  return  isSametree(tree1.left,tree2.left) && isSametree(tree2.right,tree2.right)
}

非递归


function isSametreeNot(tree1,tree2){
  if(!tree1 && !tree2){
    return true;
  }else if(!tree1 || !tree2){
    return false
  } 
  let stack1 = [],stack2 =[];
  let curr1 = tree1,curr2= tree2;
  stack2.push(tree2);
  stack1.push(tree1)
  while(stack1.length >0 && stack2.length >0 ){
    curr1 = stack1.pop() //取出对应节点1
    curr2 = stack2.pop()
    //比较是否相同
    if(curr1==null && curr2==null){
      continue
    }else if(curr2!=null && curr1!=null && curr1.value ==curr2.value){
      //当前节点相同,比较下一个
      stack2.push(curr2.left)
      stack2.push(curr2.right)
      stack1.push(curr1.left)
      stack1.push(curr1.right)      
    }else{
      return false;
    }
  } 
  return true;
}

let result3 = isSametreeNot(tree2,tree)
console.log(result3)
console.log(isSametreeNot(tree2,null))

判断二叉树是不是二叉平衡树(AVL)

它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树

思路

  • 如果是一棵空树 则是平衡
  • 计算左右子树高度,不能超过1,

//递归
function isAsl (root){
 if(!root) return true;
 //计算左子树和右子树高度
 let leftH = maxDepTree(root.left)
 let rightH = maxDepTree(root.right) 
 //左右子树的因子大于1即树的高度过高
 if(Math.abs(leftH-rightH)>1) return false;
 //在进行比较左右子树
 return isAsl(root.left) && isAsl(root.right)
}

求二叉树的镜像

  • 如果树为空,则返回
  • 如果树存在,将左右子树进行替换
function createMirrorTree(root){
  if(!root) return;
  let newNode = new NodeTree(root.value);
  //新节点左子树等于右子树
  newNode.left = createMirrorTree(root.right)
  //新节点右子树等于左子树
  newNode.right = createMirrorTree(root.left)
  return newNode
}

判断两个二叉树是否是互相镜像

  • 两棵树都为null ,则是镜像
  • 不为空则比较一棵树的左子树和右子树

递归

function isMirrorTree(root1,root2){ 
  if(root1==null && root2==null ) return true;
  if(!root2 || !root1 ) return false;
  if(root1.value != root2.value) return false;
  return isMirrorTree(root1.left,root2.right) && isMirrorTree(root1.right,root2.left)
 }

非递归

  • 选取两个栈,存储两个二叉树的节点
  • 然后取出栈的元素进行比较,不相同则返回false;
function isMirrorTreeNot(root1,root2){
  if(!root1 && !root2){
    return true;
  }else if(!root1 || !root2){
    return false
  } 
  let stack1=[],stack2 =[];
  let curr1 = null,curr2= null;
  stack1.push(root1);
  stack2.push(root2)
  while(stack2.length > 0 && stack1.length >0){
    curr1 = stack1.pop();//取出当前节点的数据
    curr2 = stack2.pop();   
    if(!curr1 && !curr2){
      continue ;
    }else if(curr2 &&  curr1 && curr1.value == curr2.value){
      stack1.push(curr1.left) //现存root1左
      stack1.push(curr1.right) //存root1 右
      stack2.push(curr2.right) //先存root2 左
      stack2.push(curr2.left) //存root2 左
    }else{
      return false;
    }
    //存栈的数据
  }
  return true;
}

判断是否为二分查找树BST

二分查找树特点 就是中序遍历是一个有序的列表

  • 进行中序遍历
  • 遍历出的结果是自增的

function isValidBST(root,pre){
  //进行中序遍历
  if(!root) return true;
  let verLeft = isValidBST(root.left,pre)
  if(!verLeft){
    return false
  }
  //当前节点比前一个小,则不是递增的,则错误
  if(root.value <=pre){
    return false;
  }
  pre = root.value;
  let verRight = isValidBST(root.right,pre)
  if(!verRight) return false;
  return true; 
}

判断是否是子树结构

给定两个树,判断B是否是A的子树

  • 思路
    • 比较A树的左子树是否有B
    • 比较A树的右子树是否有B
    • 比较以当前A的是否存在B
function hasSubTree(root1,root2){
  if(!root1 || !root2) return false;
  return isSubTree(root1,root2) || isSubTree(root1.left,root2)  || isSubTree(root1.right,root2) 
}
function isSubTree(root1,root2){
  if(!root2) return true;
  if(!root1) return false;
  return root1.value !=root2.value  ? false:
  isSubTree(root1.left,root2.left) && isSubTree(root1.right,root2.right)  
}
console.log(JSON.stringify(tree.root))
let result = hasSubTree(tree.root,null)
console.log(result)

最近的公共节点

/*
 * function TreeNode(x) {
 *   this.val = x;
 *   this.left = null;
 *   this.right = null;
 * }
 */

/**
 * 
 * @param root TreeNode类 
 * @param o1 int整型 
 * @param o2 int整型 
 * @return int整型
 */
function lowestCommonAncestor( root ,  o1 ,  o2 ) {
    // write code here
    if(!root) return -1;
    /**
    关键还是找到最近公共节点的特征:
    1. 如果该节点不是O1也不是O2,那么O1与O2必然分别在该节点的左子树和右子树中
    2. 如果该节点就是O1或者O2,那么另一个节点在它的左子树或右子树中
    稍微可以优化的一点就是,遇到O1或者O2节点就不往下递归了,把O1或者O2节点一层层往上传。
    */
    if(o1==root.val || o2==root.val) return root.val;
    let left = lowestCommonAncestor(root.left,o1,o2);
    let right = lowestCommonAncestor(root.right,o1,o2); 
    if(left==-1) return right;
    if(right==-1) return left;
    return root.val;
}
module.exports = {
    lowestCommonAncestor : lowestCommonAncestor
};

总结

本篇文章主要讲述了在算法复习过程中的知识方法和二叉树的常见算法,学习方式因人而异,但是算法学习是一个锻炼的结果,共勉~

参考文档