數據結構第六講: 樹
第六講 樹
樹是一種分層數據的抽象模型。最常見的樹是家譜。(圖來自網絡)
在明代世系表這棵樹中,所有的皇帝都被稱為節點。朱元璋稱為根節點。後代是皇帝的節點,稱為內部節點。沒有子元素的節點比如明思宗朱由檢稱為外部節點或葉節點。朱棣及其後代節點稱為朱元璋的子樹。
以明宣宗朱瞻基為例子,他擁有三個祖先節點。因此他的深度為3。
樹的高度取決於節點深度的最大值。根節點出於第0層。朱棣屬於第二層。以此類推。整個世系表中,他的高度為12。
二叉樹
二叉樹最多只能有·2個子節點。
如:B為A的左側子節點。E為A的右側子節點。
二叉搜索樹(BST)是一種特殊的節點。左側子節點存放比父節點小的值。右側子節點存放大於等於父節點的值、
功能的逐步實現
js創建一棵二叉樹(BinarySearchTree),可以借鑒鏈表的思路
還記得鏈表(linkList)嗎,可以通過指針來表示節點之間的關系。同時,還可以用對象來實現這個二叉樹,
實現以下功能:
- insert(key):在樹中插入一個新鍵
- search(key):在樹中查找一個鍵,存在則返回true,否則為false
- inOderTraverse,preOderTraverse,postOderTraverse:中序/先序/後序遍歷所有節點
- min/max:返回樹中最小/最大的鍵值
- remove:從樹中移除某個鍵。
插入節點
// 樹 class BinarySearchTree{ constructor(){ this.Node=function(key){ this.key=key; this.left=null; this.right=null; } this.root=null this.insertNode=this.insertNode.bind(this) } insertNode(_root,_node){ if(_root.key>_node.key){ if(_root.left==null){ _root.left=_node; }else{ this.insertNode(_root.left,_node); } }else{ if(_root.right==null){ _root.right=_node; }else{ this.insertNode(_root.right,_node) } } } // 插入 insert(key){ let Node=this.Node; let node=new Node(key); if(this.root==null){ this.root=node; }else{ this.insertNode(this.root,node) } } }
跑一下測試用例:
let a=new BinarySearchTree();
a.insert(11)
a.insert(7)
a.insert(15)
a.insert(5)
a.insert(3)
a.insert(9)
a.insert(8)
a.insert(10)
a.insert(13)
a.insert(12)
a.insert(14)
a.insert(20)
a.insert(18)
a.insert(25)
輸出結果轉化之後:
樹的遍歷
遍歷一棵樹,應當從頂層,左層還是右層開始?
遍歷的方法需要以訪問者模式(回調函數)體現。
樹方法最常用的就是遞歸。那麽應如何設計?
中序遍歷:從最小到最大
中序遍歷的順序是“從最小到最大”。
- 每次遞歸前,應檢查傳入的節點是否為null。這是遞歸停止的條件。
- 調用相同的函數訪問左側子節點。直到找到最小的。
- 訪問完了,再訪問最近的右側節點,直到不可訪問。
// 中序遍歷
inOrderTraverse(callback){
// 中序遍歷所需的必要方法
const inOrderTraverseNode=(_root,_callback=()=>{})=>{
// 從頂層開始遍歷
if(_root!==null){
inOrderTraverseNode(_root.left,_callback);
_callback(_root.key);
inOrderTraverseNode(_root.right,_callback);
}
}
inOrderTraverseNode(this.root,callback);
}
打印結果發現,其實這個遍歷實現了樹的key值從小到大排列。
a.inOrderTraverse((key)=>{console.log(key)})
// 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
先序遍歷:如何打印一個結構化的數據結構
先序遍歷的過程:
先把左側子節點全部訪問完了,再尋找一個距此時位置(“親緣關系”)最近的右側節點。
preOrderTraverse(callback){
// 中序遍歷所需的必要方法
const preOrderTraverseNode=(_root,_callback=()=>{})=>{
// 從頂層開始遍歷
if(_root!==null){
_callback(_root.key);
preOrderTraverseNode(_root.left,_callback);
preOrderTraverseNode(_root.right,_callback);
}
}
preOrderTraverseNode(this.root,callback);
}
所以,所謂先序遍歷就是把callback的位置提前了。
後序遍歷:從左到右先遍歷子代
後續遍歷是先訪問一個樹的後代節點。最後才訪問本身。
那麽後序遍歷的方法是不是把callback放到最後執行呢?
是的。簡直無腦。
// 後序遍歷
postOrderTraverse(callback){
// 中序遍歷所需的必要方法
const postOrderTraverseNode=(_root,_callback=()=>{})=>{
// 從頂層開始遍歷
if(_root!==null){
postOrderTraverseNode(_root.left,_callback);
postOrderTraverseNode(_root.right,_callback);
_callback(_root.key);//我在後面
}
}
postOrderTraverseNode(this.root,callback);
}
搜索特定值
//是否存在
search(_key,_root){
if(!_root){
_root=this.root
}
if(!_root){
return false;
}else if(_root.key==_key){
return true;
}
if(_root.key>_key){
if(_root.left==null){
return false;
}else{
if(_root.left.key==_key){
return true
}else{
return this.search(_key,_root.left)
}
}
}else{
if(_root.right==null){
return false
}else{
if(_root.right.key==_key){
return true
}else{
return this.search(_key,_root.right)
}
}
}
}
查找最大/最小值
// 工具函數
find(_root,side){
if(!_root[side]){
return _root.key
}else{
return this.find(_root[side],side)
}
}
// 最大值,不斷查找右邊
max(){
return this.find(this.root,'right')
}
// 最小值
min(){
return this.find(this.root,'left')
}
會發現這是個非常輕松的事。
移除一個節點
Bst最麻煩的方法莫過於此。
首先,你得找到這個節點=>遞歸終止的條件
其次,判斷這個節點(_root)的父節點(parentNode)和這個節點的子節點(_root.left、_root.right)判斷:
- 如果
_root
沒有子節點,那麽直接把父節點對應的side值設為null
?
- 如果
_root
擁有一個子節點,跳過這個節點,直接把父節點的指針指向這個子節點。
如果兩個都有:
- 找到
_root
右邊子樹的最小節點_node
,然後令parentNode的指針指向這個節點 - _node的父節點刪除指向_node的指針。
- 找到
- 如果
_remove(_node,_key,parentNode,side){
if(_key<_node.key){
return this._remove(_node.left,_key,_node,'left')
}else if(_key>_node.key){
return this._remove(_node.right,_key,_node,'right')
}else if(_node.key==_key){
// 頂層:移除根節點
if(!parentNode){
this.root=null;
return this.root;
}else{
if(!_node.left&&!_node.right){
// 刪除的如果是葉節點
parentNode[side]=null
}else if(_node.left&&!_node.right){
let tmp=_node.left;
parentNode[side]=tmp
}else if(_node.right&&!_node.left){
let tmp=_node.right;
parentNode[side]=tmp
}else{
let tmpRight=_node.right;
// 找到右側子樹的最小節點。__node
let __node=this.find(tmpRight,'left');
// 刪除這個節點。
this._remove(tmpRight,__node.key);
// 重新賦值
parentNode[side]=__node.key;
}
return this.root
}
}
}
remove(key){
if(this.search(key)){
return this._remove(this.root,key)
}else{
console.log('未找到key')
return false;
}
}
a.remove(15)
打印結果如下
測試通過。
做一道練習
在實際工作生活中,比如一本書常分為第一講,第1-1節,第2-1節...,第二講:第2-1節...
如果後端發給你一個這樣的數據:
let data = [{
id: '1',
children: [{
id: `1-1`,
children: [{
id: '1-1-1',
children: [{
id: '1-1-1-1'
},{
id:'1-1-1-2'
}]
},{
id:'1-1-2',
children: [{
id: '1-1-2-1'
},{
id:'1-1-2-2'
}]
}]
},{
id:'2',
children:[{
id:'2-1'
},{
id:'2-2',
children:[{
id:'2-2-1'
},{
id:'2-2-2',
children: [{
id: '2-2-2-1'
},{
id:'2-2-2-2'
}]
}]
}]
}]
}]
如何扁平化如下的json對象?
const flatJson=(_data,arr)=>{
if(!arr){
arr=[]
}
for(let i=0;i<_data.length;i++){
console.log(_data[i].id)
arr.push(_data[i].id);
if(_data[i].children){
flatJson(_data[i].children,arr)
}
}
return arr;
}
console.log(flatJson(data))
測試用例結果通過:
可以進一步思考:這裏arr.push()在判斷前執行。如果是在判斷後執行,會是什麽結果呢?
數據結構第六講: 樹