首發於微信公衆號《前端成長記》,寫於 2019.12.06javascript
本文記錄刷題過程當中的整個思考過程,以供參考。主要內容涵蓋:前端
題目地址java
給定兩個二叉樹,編寫一個函數來檢驗它們是否相同。node
若是兩個樹在結構上相同,而且節點具備相同的值,則認爲它們是相同的。算法
示例:數組
輸入: 1 1 / \ / \ 2 3 2 3 [1,2,3], [1,2,3] 輸出: true 輸入: 1 1 / \ 2 2 [1,2], [1,null,2] 輸出: false 輸入: 1 1 / \ / \ 2 1 1 2 [1,2,1], [1,1,2] 輸出: false
題目直接說了是二叉樹,而二叉樹的遍歷方式有兩種:深度優先和廣度優先,我就從這兩個思路來做答。微信
Ⅰ.深度優先函數
代碼:優化
/** * @param {TreeNode} p * @param {TreeNode} q * @return {boolean} */ var isSameTree = function(p, 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) };
結果:code
O(n)
,n
爲節點個數Ⅱ.廣度優先
代碼:
/** * @param {TreeNode} p * @param {TreeNode} q * @return {boolean} */ var isSameTree = function(p, q) { if (p === null && q === null) return true if (p === null || q === null) return false let pQ =[p] // 左側比較隊列 let qQ =[q] // 右側比較隊列 let res = true while(true) { if (!pQ.length || !qQ.length) { res = pQ.length === qQ.length break } // 當前比較節點 let curP = pQ.shift() let curQ = qQ.shift() if ((curP && !curQ) || (!curP && curQ) || (curP && curQ && curP.val !== curQ.val)) { res = false break } else { let pL = curP ? curP.left : null let pR = curP ? curP.right : null if (pL || pR) { // 至少一個存在纔有意義 pQ.push(pL, pR) // 依次推入比較數組,實際上就是廣度優先 } let qL = curQ ? curQ.left : null let qR = curQ ? curQ.right : null if (qL || qR) { // 至少一個存在纔有意義 qQ.push(qL, qR) // 依次推入比較數組,實際上就是廣度優先 } } } return res };
結果:
O(n)
,n
爲節點個數思路基本上都是這兩種,未發現方向不一樣的解法。
通常碰到二叉樹的題,要麼就深度遍歷,要麼就廣度遍歷。深度優先,也叫先序遍歷。
給定一個二叉樹,檢查它是不是鏡像對稱的。
例如,二叉樹 [1,2,2,3,4,4,3]
是對稱的。
示例:
1 / \ 2 2 / \ / \ 3 4 4 3
可是下面這個 [1,2,2,null,3,null,3]
則不是鏡像對稱的:
1 / \ 2 2 \ \ 3 3
說明:
若是你能夠運用遞歸和迭代兩種方法解決這個問題,會很加分。
仍是一道二叉樹的題,因此常規思路就是遍歷操做,深度優先或廣度優先均可。鏡像對稱能夠觀察到很明顯的特色是有相同的根節點值,且每一個樹的右子樹與另外一個樹的左字數對稱相等。深度優先的方式,其實就是遞歸的思路,符合題目的說明。
Ⅰ.深度優先
代碼:
/** * @param {TreeNode} root * @return {boolean} */ var isSymmetric = function(root) { function isMirror (l, 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) } return isMirror(root, root) };
結果:
O(n)
,n
爲節點個數Ⅱ.廣度優先
代碼:
/** * @param {TreeNode} root * @return {boolean} */ var isSymmetric = function(root) { if (root === null) return true // 初始隊列 let q = [root.left, root.right] // 依次將同級push進隊列,每次取兩個對稱節點進行判斷 while(q.length) { let l = q.shift() let r = q.shift() if (l === null && r === null) continue if (l === null || r === null) return false if (l.val !== r.val) return false q.push(l.left, r.right, l.right, r.left) } return true };
結果:
O(n)
,n
爲節點個數看到一個有意思的思路,將樹按照左中右的順序輸入到數組,加上層數,該數組也是對稱的。
Ⅰ.左中右順序輸出數組
代碼:
/** * @param {TreeNode} root * @return {boolean} */ var isSymmetric = function(root) { if (root === null) return true // 輸出數組 let arr = [] search(arr, root, 1); // 入參分別爲輸出,節點和層級 function search(output, n, k) { if (n.left !== null) { search(output, n.left, k+1) } if (n.right !== null) { search(output, n.right, k + 1); } } //判斷是否對稱 let i = 0, j = arr.length - 1 while (i < j) { if (arr[i] != arr[j]) { return false } i++ j-- } return true };
結果:
O(n)
,n
爲節點個數這道題的大體解法都是遍歷節點或者利用隊列,只是在遞歸的細節上會有些差別。左中右輸出數組的思路很清奇,雖然效率明顯會更低下,可是不失爲一種思路。
給定一個二叉樹,找出其最大深度。
二叉樹的深度爲根節點到最遠葉子節點的最長路徑上的節點數。
說明: 葉子節點是指沒有子節點的節點。
示例:
給定二叉樹 [3,9,20,null,null,15,7]
,
3 / \ 9 20 / \ 15 7
返回它的最大深度 3 。
這道題最基本的思路就是計算出每條子節點的深度,再進行比較。爲了提高效率,能夠增長同級比對,去除不多是最長節點的葉節點計算。
因此這裏我就用如下幾種思路來實現深度優先算法。
Ⅰ.遞歸
代碼:
/** * @param {TreeNode} root * @return {number} */ var maxDepth = function(root) { if (root === null) return 0 // 左側子樹的最大高度 let l = maxDepth(root.left) // 右側子樹的最大高度 let r = maxDepth(root.right) return Math.max(l, r) + 1 };
結果:
O(n)
,n
爲節點個數Ⅱ.利用隊列
代碼:
/** * @param {TreeNode} root * @return {number} */ var maxDepth = function(root) { if (root === null) return 0 // 隊列 let q = [root] let dep = 0 while(q.length) { let size = q.length dep++ while(size > 0) { let node = q.shift() if (node.left !== null) q.push(node.left) if (node.right !== null) q.push(node.right) size-- } } return dep };
結果:
O(n)
,n
爲節點個數這裏看到一個用棧的角度來實現的,取棧高度的最大值,其餘的基本都是循環的細節差別,大致思路一致。
Ⅰ.利用棧
代碼:
/** * @param {TreeNode} root * @return {number} */ var maxDepth = function(root) { if (root === null) return 0 // 棧 let s = [{ node: root, dep: 1 }] let dep = 0 while(s.length) { // 先進後出 var cur = s.pop() if (cur.node !== null) { let curDep = cur.dep dep = Math.max(dep, curDep) if (cur.node.left !== null) s.push({node: cur.node.left, dep: curDep + 1}) if (cur.node.right !== null) s.push({node: cur.node.right, dep: curDep + 1}) } } return dep };
結果:
O(n)
,n
爲節點個數二叉樹的操做,通常就是深度優先和廣度優先,因此基本上就朝這兩個方向上去解,而後進行優化就能夠了。
給定一個二叉樹,返回其節點值自底向上的層次遍歷。 (即按從葉子節點所在層到根節點所在的層,逐層從左向右遍歷)
例如:
給定二叉樹 [3,9,20,null,null,15,7]
,
3 / \ 9 20 / \ 15 7
返回其自底向上的層次遍歷爲:
[ [15,7], [9,20], [3] ]
這道題在我看來仍是兩種方式,深度優先和廣度優先。
Ⅰ.深度優先
代碼:
/** * @param {TreeNode} root * @return {number[][]} */ var levelOrderBottom = function(root) { // 當前層級標識 let idx = 0 let res = [] function levelOrder(node, floor, arr) { if (node === null) return arr if(arr[floor]) { arr[floor].push(node.val) } else { arr[floor] = [node.val] } levelOrder(node.left, floor + 1, arr) levelOrder(node.right, floor + 1, arr) return arr } return levelOrder(root, idx, res).reverse() };
結果:
O(n)
,n
爲節點個數Ⅱ.廣度優先
代碼:
/** * @param {TreeNode} root * @return {number[][]} */ var levelOrderBottom = function(root) { if (root === null) return [] // 初始隊列 let q = [root] let res = [] while(q.length) { // 當前層節點數量 const count = q.length let curArr = [] for(let i = 0; i < count;i++) { const node = q.shift() curArr.push(node.val) // 將子節點依次推入隊列 if (node.left) q.push(node.left) if (node.right ) q.push(node.right ) } res.push(curArr) } return res.reverse() };
結果:
O(n)
,n
爲節點個數沒有看到什麼特別的解法,主要都是按 BFS 和 DFS 來處理,要麼迭代,要麼遞歸等等。
這裏就介紹下別的吧,在第一種解法中咱們使用的是前序優先,固然用中序優先或後序優先也能夠,下面代碼能夠說明區別:
// 先序,順序爲 根 -> 左 -> 右 function levelOrder(node, floor, arr) { if(arr[floor]) { arr[floor].push(node.val) } else { arr[floor] = [node.val] } levelOrder(node.left, floor + 1, arr) levelOrder(node.right, floor + 1, arr) return arr } // 中序,順序爲 左 -> 根 -> 右 function levelOrder(node, floor, arr) { levelOrder(node.left, floor + 1, arr) if(arr[floor]) { arr[floor].push(node.val) } else { arr[floor] = [node.val] } levelOrder(node.right, floor + 1, arr) return arr } // 後序,順序爲 左 -> 右 -> 根 function levelOrder(node, floor, arr) { levelOrder(node.left, floor + 1, arr) levelOrder(node.right, floor + 1, arr) if(arr[floor]) { arr[floor].push(node.val) } else { arr[floor] = [node.val] } return arr }
二叉樹的題目就根據狀況在深度優先和廣度優先中擇優選擇便可,基本不會有太大的問題。
將一個按照升序排列的有序數組,轉換爲一棵高度平衡二叉搜索樹。
本題中,一個高度平衡二叉樹是指一個二叉樹每一個節點 的左右兩個子樹的高度差的絕對值不超過 1。
示例:
給定有序數組: [-10,-3,0,5,9], 一個可能的答案是:[0,-3,9,-10,null,5],它能夠表示下面這個高度平衡二叉搜索樹: 0 / \ -3 9 / / -10 5
這裏有兩點要注意的:高度平衡二叉樹要求每一個節點的左右兩個子樹的高度差的絕對值不超過 1;而二叉搜索樹要求左子樹上全部節點值小於根節點,右子樹上全部節點值大於根節點。
而題目給出的是一個有序的數組,因此能夠直接考慮二分後進行處理,我這就直接遞歸做答:找到根節點,遞歸生成左右子樹。
Ⅰ.遞歸
代碼:
/** * @param {number[]} nums * @return {TreeNode} */ var sortedArrayToBST = function(nums) { if (!nums.length) return null // 中位數,用偏移避免溢出 const mid = nums.length >>> 1 const root = new TreeNode(nums[mid]) root.left = sortedArrayToBST(nums.slice(0, mid)) root.right = sortedArrayToBST(nums.slice(mid + 1)) return root };
結果:
O(n)
這裏看到另一種解法,先建立一個平衡二叉樹,而後中序遍歷樹同時遍歷數組便可,由於中序遍歷出來的恰好是有序數組。
Ⅰ.建立樹後中序遍歷數組賦值
代碼:
/** * @param {number[]} nums * @return {TreeNode} */ var sortedArrayToBST = function(nums) { if (!nums.length) return null // 節點總數 let len = nums.length let root = new TreeNode(-1); let q = [root] // 已經建立了根節點 len-- while(len) { const node = q.shift() // 左子樹 const l = new TreeNode(-1) q.push(l) node.left = l len-- if (len) { // 右子樹 const r = new TreeNode(-1) q.push(r) node.right = r len-- } } let i = 0 inorder(root) function inorder(node) { if (node === null) return inorder(node.left) node.val = nums[i++] inorder(node.right) } return root };
結果:
O(n)
這裏實際上是個逆向思惟,以前是二叉樹輸出數組,如今變成數組轉成二叉樹。恰好能夠翻一下前序中序和後序的區別,這裏中序就能夠了。不過這道題我仍是更推薦遞歸二分求解。
(完)
本文爲原創文章,可能會更新知識點及修正錯誤,所以轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
若是能給您帶去些許幫助,歡迎 ⭐️star 或 ✏️ fork (轉載請註明出處:https://chenjiahao.xyz)