js 簡單版本的二叉樹實現 https://github.com/DaiHangLin/js-binary-tree
概念
-
首先二叉樹是一顆樹,也就是每一個節點(除了root節點外)都存在其父節點,可能存在子節點,對於沒有子節點的節點稱之為葉子節點。
-
而二叉樹是樹的一種特殊情形,也就是每個節點最多只有2個子節點。
-
通常代表一棵樹的都是其跟節點
-
樹的節點: 包含一個數據,以及最多兩個子節點,一般稱之為左節點,右節點
-
節點層:跟節點的層定義為1,第一個子節點層定義為2,以此類推
-
深度:最大的節點層代表了樹的深度
-
滿二叉樹:除了最深一層(第三層)的葉子節點外,每個節點都有左右兩個葉子節點
root (1) / \ l r (2) /\ /\ l r l r (3)
-
完全二叉樹:第二層必須是滿二叉樹,在滿二叉樹的基礎上,新增加了子節點,但是不全
root (1) / \ l r (2) /\ l r (3)
如何用數據結構表示一個樹
- 樹由節點遞歸組成,所以首先需要定一個節點
這個就可以簡單的理解為一個樹的節點,其中value代表節點的數據,left代表左子節點,right代表右子節點。node = { value, left, right, }
- 如何生成一顆滿二叉樹
- 思路:
- 維護一個當前樹節點的鏈表中,初始值為跟節點。因為需要先進后出的概念,而對於js來說,可以用[]數組來直接實現鏈表,利用數據提供的方法
shift
從數組頭部移出數據unshift
從數據頭部插入數據push
從數組尾部插入數據pop
從數組尾部移出數據
- 從數組尾部取出第一個數據,也就是跟節點,判斷跟節點存不存在,如果不存在就創建,
- 繼續重復從數組中取出下一個節點,先判斷左節點是否存在,如果不存在,則創建其左節點,如果存在,則將左節點保存到數組頭部中,然后同樣的判斷其右節點是否存在,如果不存在,則創建其右節點,如果存在,則將右節點保存到數組頭部中。
- 每次新添加一個節點,則重復上面的兩個步驟
- 維護一個當前樹節點的鏈表中,初始值為跟節點。因為需要先進后出的概念,而對於js來說,可以用[]數組來直接實現鏈表,利用數據提供的方法
- 思路:
insertTreeNode = (root, v) => {
let queue = [root]
while (true) {
var node = queue.pop()
if (!node.value) {
node.value = v
break
}
if (!node.left) {
node.left = {value: v}
break
} else {
queue.unshift(node.left)
}
if (!node.right) {
node.right = {value: v}
break
} else {
queue.unshift(node.right)
}
}
console.log('tree', root)
}
如何對二叉樹進行遍歷
-
二叉樹的遍歷存在多種方式,常見的右
先序遍歷
,中序遍歷
,后序遍歷
1 / \ 2 3
- 先序遍歷 [1,2,3]
- 中序遍歷 [2, 1, 3]
- 后序遍歷 [3, 1, 2]
-
編碼實現的思路:一顆樹實際上是由很多的子節點的組成的,所以問題首先可以拆解為如何遍歷一個節點,一個二叉樹的節點可能存在 "無", "沒有任何子節點", “存在一個左節點”,“存在一個右節點”,“存在一個左節點和一個右節點”
function traverse(node) { if (node) { console.log(node.value) } if (node.left) { console.log(node.left.value) } if (node.right) { console.log(node.right.value) } }
-
對於一顆樹而言,實際上只是對一個節點的擴展,也就是在遍歷時,遇到其子節點時,需要對子節點進行同樣的操作
function traverseTree(root) {
function doTraverse(node) {
if (node) {
console.log(node.value)
}
node.left && doTraverse(node.left)
node.right && doTraverse(node.right)
}
doTraverse(root)
}
如何根據前序遍歷,后序遍歷的結果重新構建一顆樹
-
先來看一顆樹前序遍歷和中序遍歷結果的區別
1 / \ 2 3 / \ 4 5
- 前序遍歷 preOrder: [1, 2, 4, 5, 3]
- 中序遍歷 inOrder: [4, 2, 5, 1, 3]
-
分析;
- 分析前序遍歷: 前序遍歷的第一個數據,即為這顆樹的
跟節點
,但僅僅根據根節點
和前序遍歷的數據,是不足夠重構一顆樹的,因為你不能保證這個樹其中的某個節點是一個完全節點(也就是左節點和右節點同時存在)。 - 分析中序遍歷: 根據前序遍歷得到的結果,能直接推斷出 [4, 2, 5] 為 1 的
左節點
並且是中序遍歷
的結果, [3] 為 1 的右節點
並且是中序遍歷
的結果 - 再次根據上一步推斷的結果,我們能推斷出,
1
的左節點
有3個元素,按照先序遍歷
的結論,在preOrder
中,從頭部推出一個元素(unshift)后,接下來的3個節點,即為 1 的左節點
並且是先序遍歷
的結果 - 右節點的推論和左節點的推論一致,
- 分析前序遍歷: 前序遍歷的第一個數據,即為這顆樹的
-
實現: 這里的實現,完全是按照上面的思路完成,沒有任何的優化。
revert = (p, i) => {
if (!p || !i) {
return
}
function doRevert(preorder, inorder, node = {}) {
if (!preorder || !inorder || preorder.length == 0 || inorder.length == 0) {
return
}
const first = preorder[0]
if (!first) {
return
}
const nextLeftPre = []
const nextleftIn = []
let index;
for (index = 0; index < inorder.length; index ++) {
if (first == inorder[index]) {
break
}
nextleftIn.push(inorder[index])
}
const nextRightIn = inorder.slice(index + 1)
index = 1;
for (index; index < nextleftIn.length + 1; index ++) {
nextLeftPre.push(preorder[index])
}
const nextRightPre = preorder.slice(index)
node = {
value: first,
left: nextLeftPre.length != 0 ? doRevert(nextLeftPre, nextleftIn, {}) : null,
right: nextRightPre.length != 0 ? doRevert(nextRightPre, nextRightIn, {}) : null,
}
return node
}
let tree = {}
tree = doRevert(p, i, tree)
console.log('tree', tree)
}