前端算法複習--二叉樹

前言

     最近少更了,入職了新公司,熟悉各類環境和業務需求,短暫時間內都沒有抽出時間去總結和分享了;今天給你們分享下,我是如何複習算法編程題來進行手撕代碼java

切入正題,認識算法

     談起算法,它是解決某個問題的計算方法、步驟。爲何許多公司都喜歡讓手撕代碼呢,更多考察的是寫代碼的能力和思惟吧;不過多贅述,開啓咱們的算法複習之途吧;node

切入算法入口點

     也許對於一些人來講,是「學習」,由於本身在大學期間是有修過一些算法和數據結構的課程,切入點也是比較容易的;下面總結下如何切入到深刻;算法

  • 「學習」切入點
    • 先學習「數據結構」,由於後續的全部的算法實現等都是和它息息相關,在書籍中咱們也是常常看到「算法和數據結構」
    • 多多練習,有了基礎「數據結構」概念,更多的是去學習如何實現這些數據結構
  • 「複習」切入點
    • 「針對性」總結,同類型的涉及題目總結,好比「二叉樹」總結相關概念和涉及到的編程題;
    • 「同類型」練習,在code練習過程當中,多是無序的,因此咱們要進行同類型題目複習,好比「我今天就看二叉樹相關知識,實現二叉樹相關代碼」,在這個過程當中不過多的去看「動態規劃」知識,這樣大腦的記憶力也是很是深入的

如何練習

  • leetcode 大量同類型的題目,並且還有大量的優質解析
  • 牛客網 大量公司常見編程題
  • 本身寫,先本身構造一個基礎數據結構,而後在本地練習

今日複習 - 二叉樹

其實我本身開始是在leetcode上進行大量的練習,典型的亂序練習,可是本身感受仍是記不住哈哈哈哈,老了,趁着元旦的三天假期,本身安排了複習計算,決定本身先本地總結一波,常見理論知識和常見題目,本地練習;編程

二叉樹的知識腦圖

二叉樹.jpg

圖1 二叉樹知識腦圖

這裏就略過概念的介紹了直接進入咱們經常使用到的算法編程題markdown

二叉樹編程

主要是本身進行本地練習,因此定義了前置的一些條件數據結構

二叉樹結構

class NodeTree{ 
  constructor(val){
    this.left = null //左子樹
    this.right = null //右子樹
    this.value= val; //存儲的值
    return this;
  }
} 
複製代碼

二叉樹的建立

根據根節點建立 根據節點建立 粗略符合左右樹的差別oop

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--前序遍歷(根在前,從左往右,一棵樹的根永遠在左子樹前面,左子樹又永遠在右子樹前面 )post

前序遍歷的標準是,先遍歷根節點,在遍歷左子樹,在遍歷右子樹,若是左子樹中節點存在左節點,則存儲當前左節點,繼續遍歷左節點的左子樹則繼續進行遍歷;學習

遞歸

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
};
複製代碼

總結

本篇文章主要講述了在算法複習過程當中的知識方法和二叉樹的常見算法,學習方式因人而異,可是算法學習是一個鍛鍊的結果,共勉~

參考文檔

相關文章
相關標籤/搜索