春招來了,辭了職在家裡準備再找份實習工作。相信大家,尤其是大三、大四的同學都經常在招聘要求上看到這樣一條要求:熟悉常見的資料結構與演算法。常見的資料結構通常有:連結串列、二叉樹,如果要求再高點,可能會讓你實現紅黑樹、AVL樹這種高階的資料結構。由此可見,資料結構與演算法還是比較重要的,最近也是在複習這方面的知識。本篇為複習過程中遇到過的總結,同時也給各位跟我一樣準備面試的同學一份參考。另外,由於篇幅有限,本篇的重點在於二叉樹的常見演算法以及實現。
常見的二叉樹實現程式碼
之前寫過相關的文章,是關於如何建立及遍歷二叉樹的,這裡不再贅述。提供連結給各位感興趣的小夥伴,點此跳轉
翻轉二叉樹
對於一棵二叉樹,翻轉它的左右子樹,如下圖所示:
下面來分析具體的實現思路:- 對於根結點為空的情況
這種情況需要排除,因為null不是一個物件,不可能存在左右子樹並且可以翻轉的情況 - 對於一棵只有一個根結點的二叉樹
emmm,這種情況也可以翻轉,因為此時根結點左右子樹為null,交換左右子樹其實也就是在交換兩個null,理論上是翻轉了,但實際上我們看到的和沒有翻轉之前的結果是一樣的 - 對於一棵具有兩個或兩個以上結點的二叉樹,此時二叉樹可以表示為如下的影象:
可以看出,無論是隻有左子樹還是隻有右子樹都可以進行翻轉。這句話等價於,為空的子樹可以和不為空的子樹進行交換,也就是不對為空的子樹進行特殊處理
分析過程
其實這樣我們還是不知道二叉樹是如何翻轉的,我們可以用第一張圖的二叉樹為例子,看一下翻轉的具體過程。
- 首先我們需要對根結點進行判空處理,在根結點不為空的情況下存在左右子樹(即使左右子樹為空),然後交換左右子樹;
示例程式碼
根據上面的推理過程我們可以得出如下的程式碼:
function reverseTree(root){
if( root !== null){
[root.left, root.right] = [root.right, root.left]
reverseTree(root.left)
reverseTree(root.right)
}
}
複製程式碼
雖然推理過程比較複雜(也可能是寫的比較囉嗦。。),但是仔細觀察程式碼,這和遍歷的程式碼似乎也沒多大差別,只是把輸出結點變為了交換結點。
判斷二叉樹是否完全對稱
一棵左右完全對稱的二叉樹是這樣的:
那到底如何判斷呢?- 根結點為空時,此時為一棵空二叉樹,滿足對稱條件(-_-||)
- 只有一個根結點時,左右子樹都為null,滿足左右對稱條件
- 只有兩個結點時,此時左右子樹必定有一個為空,不可能存在對稱的情況
- 結點數在三個及三個以上時,二叉樹有對稱的可能。
按照我們正常的思維,看對稱與否,首先看左邊,然後看右邊,最後比較左右是否相等。同時我們注意到,在二叉樹深度比較大的時候,我們光是比較左右是不夠的。可以觀察到,我們比較完左右以後還需要比較左的左和右的右,比較左的右和右的左
分析過程
這麼看是比較繞,接下來我們來看圖分析:
- 先比較根結點左右孩子
- 將左子樹根結點的左孩子與右子樹根結點的右孩子進行比較
- 將左子樹根結點的右孩子與右子樹根結點的左孩子進行比較
- 重複以上過程...
示例程式碼
function isSymmetrical(pRoot)
{
// write code here
if(!pRoot){
return true
}
return funC(pRoot.left, pRoot.right)
}
function funC(left, right){
if(!left){
return right === null
}
if(!right){
return false
}
if(left.val !== right.val){
return false
}
return funC(left.right, right.left) && funC(left.left, right.right)
}
複製程式碼
求二叉樹的深度
分析過程
- 只有一個根結點時,二叉樹深度為1
- 只有左子樹時,二叉樹深度為左子樹深度加1
- 只有右子樹時,二叉樹深度為右子樹深度加1
- 同時存在左右子樹時,二叉樹深度為左右子樹中深度最大者加1
示例程式碼
function deep(root){
if(!root){
return 0
}
let left = deep(root.left)
let right = deep(root.right)
return left > right ? left + 1 : right + 1
}
複製程式碼
求二叉樹的寬度
二叉樹的寬度是啥?我把它理解為具有最多結點數的層中包含的結點數,比如下圖所示的二叉樹,其實它的寬度就是為4:
分析過程
根據上圖,我們如何算出二叉樹的寬度呢?其實有個很簡單的思路:
- 算出第一層的結點數,儲存
- 算出第二層的結點數,儲存一二層中較大的結點數
- 重複以上過程
示例程式碼
根據分析過程,我們可以利用佇列這種資料結構來實現這個演算法,程式碼如下:
function width(root){
if(!root){
return 0
}
let queue = [root], max = 1, deep = 1
while(queue.length){
while(deep--){
let temp = queue.shift()
if(temp.left){
queue.push(temp.left)
}
if(temp.right){
queue.push(temp.right)
}
}
deep = queue.length
max = max > deep ? max : deep
}
return max
}
複製程式碼
重建二叉樹
常見的遍歷
-
前序遍歷: 前序遍歷首先訪問根結點然後遍歷左子樹,最後遍歷右子樹。
-
中序遍歷: 中序遍歷首先訪問左子樹然後遍歷根節點,最後遍歷右子樹。
-
後序遍歷: 後序遍歷首先遍歷左子樹,然後遍歷右子樹,最後訪問根結點
題目描述
根據前序遍歷產生的序列和中序遍歷產生的序列生成一顆二叉樹
思路分析
假如有這麼一棵二叉樹:
可以看出它前序遍歷序列為:8 6 5 7 10 9 11,中序遍歷序列為:5 6 7 8 9 10 11 其中有個很明顯的特徵,根結點的值為前序遍歷序列的第一個值,而且我們在中序遍歷序列中很容易看出,根結點左右兩邊的結點分別為構成左子樹和右子樹的結點,所以我們可以得到一種解決問題的思路:- 獲取前序遍歷的第一個值,構建根結點
- 生成左子樹的前序遍歷序列和中序遍歷序列
- 生成右子樹的前序遍歷序列和中序遍歷序列
- 重複以上過程...
示例程式碼
function reConstructBinaryTree(pre, vin)
{
if(!pre || !vin || !pre.length || !vin.length){
return null
}
let root = new TreeNode(pre[0]),
tIndex = vin.indexOf(pre[0]),
leftIn = [],leftPre = [],rightIn = [],rightPre = []
for(let i = 0; i < tIndex; i++){
leftIn.push(vin[i])
leftPre.push(pre[i+1])
}
for(let i = tIndex+1; i < pre.length; i++){
rightIn.push(vin[i])
rightPre.push(pre[i])
}
//遞迴
root.left = reConstructBinaryTree(leftPre, leftIn)
root.right = reConstructBinaryTree(rightPre, rightIn)
return root
}
複製程式碼
以上思路、程式碼有錯漏請在評論區指出!
總結
程式碼部分來自牛客網--劍指offer,相應的題目也都可以在上面找到。不過在這期間,我也是找到了份實習工作,年後就要去搬磚了。既然找到了,春招就不參與了(春招難度比秋招難太多了)希望看這篇文章的同學們也能找到份合適的工作。