【079】利用“剪葉子”演算法實現樹形結構的搜尋功能,用Vue.js實現
阿新 • • 發佈:2019-02-15
業務場景
工作中碰到這樣的一個場景:需要對一個樹形結構進行搜尋,凡是匹配的節點都要保留。如果這個匹配的節點存在父節點,那麼不論這個父節點是否匹配搜尋內容,都要保留,並按照樹形結構展示出來。如果一個節點既不匹配搜尋內容,同時也沒有匹配搜尋內容的子節點,那麼該節點就不再保留。
效果如下面這個gif動畫所示:
資料結構
場景中的資料結構類似這種形式:
export default function(){
let arr =[
{
name:'學習',
children:[
{ name: "雜誌" },
{ name: "紙質書" },
{
name:'電子書',
children:[
{
name:'文學',
children:[
{name:'茶館' },
{name:'紅與黑'},
{name: "傅雷家書"}
]
}
]
}
]
},
{
name:'電影',
children:[
{ name:'美國電影' },
{ name:'日本電影' }
]
}
]
return arr;
} // end function
演算法思路
- 利用樹的遍歷演算法,找到每個葉子節點及其父節點。
- 判斷葉子節點是否匹配搜尋內容,如果匹配則保留;如果不匹配,就把這個葉子節點從它的父節點 children 陣列中刪除。
- 這麼做,一次只能搜尋一層葉子節點,如果資料結構裡有多層節點沒有匹配,就需要重複步驟 1 和步驟 2 。重複的次數應該是樹的深度減去1。要減一是因為根節點沒有父節點,自然也不能執行【從父節點 children 陣列中刪除】的操作。
- 最後對根節點做判斷,如果根節點有子節點,那麼什麼也不做。如果樹形結構只有一個根節點,若根節點不匹配,則刪除;若根節點匹配,則保留。
下面的圖片演示了演算法的過程:
程式碼實現
blog079
├─ src
│ ├─App.vue
│ ├─data.js
│ ├─home.vue
│ ├─main.js
│ ├─router.js
│ └─treeNode.vue
│
├─.babelrc
├─index.template.html
├─package.json
├─webpack.config.js
└─yarn.lock
本文只說明一些主要程式碼。
資料來源:data.js
export default function(){
let arr =[
{
name:'學習',
children:[
{
name: "雜誌",
children:[
{
name:"電腦雜誌",
children:[
{name:"大眾軟體"}
]
}
]
},
{
name: "紙質書"
},
{
name:'電子書',
children:[
{
name:'文學',
children:[
{
name:'茶館'
},
{
name:'紅與黑'
},
{
name: "傅雷家書"
}
]
}
]
}
]
},
{
name:'電影',
children:[
{
name:'美國電影3'
},
{
name:'日本電影'
},
{
name:"23"
}
]
}
]
return arr;
} // end function
展示樹形結構的元件 treeNode.vue
<template>
<li>
<!-- 顯示當前節點名稱 -->
<span v-html="nodeName"></span>
<!-- 如果存在孩子節點,迴圈子節點陣列,並遞迴呼叫本元件。 -->
<ul v-if="isHasChildren">
<tree-node v-for="(item,index) in node.children" :key="index" :node="item"
:search-text="searchText">
</tree-node>
</ul>
</li>
</template>
<script>
export default {
name:"tree-node",
props:["node", "searchText"],
// data(){
// return {};
// }
computed:{
// 判斷當前節點是否存在孩子節點
isHasChildren(){
let flag = false;
if (this.node.children && this.node.children.length > 0) {
flag = true;
}
return flag;
},
// 如果當前節點名稱,有文字和搜尋內容匹配,就把匹配的文字標紅。
// 反之,則正常顯示節點名稱。
nodeName(){
if (this.searchText == undefined || this.searchText == "" || this.searchText == null) {
return this.node.name;
}
if (this.node.name.indexOf(this.searchText) <= -1) {
return this.node.name;
}
return this.replaceAll(this.node.name, this.searchText,
"<span style='color:red;'>" + this.searchText + "</span>");
}
},
methods:{
/**
* 替換掉原字串中所有的子字串。不使用正則表示式的實現。
* 當遇到特殊字元的時候,不用輸入適應正則的轉義。
* @param String str 原字串
* @param String substr 要被替換的子串
* @param String replacement 新的子串
*/
replaceAll(str, substr, replacement){
if (!str) {
return "";
}
return str.split(substr).join(replacement);
}
}
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped></style>
主頁面 home.vue。包含搜尋功能。
<template>
<!--
功能:
1.利用遞迴元件展示樹形結構。
2.利用“剪葉子”的演算法搜尋樹的節點。
作者:張超
-->
<div>
<p>這裡是首頁</p>
<p><input type="text" placeholder="搜尋" @keyup.enter="search($event)"></p>
<ul v-if="nodeList && nodeList.length > 0">
<tree-node v-for="(item,index) in nodeList" :key="index" :node="item"
:search-text="searchText"></tree-node>
</ul>
<div v-else>沒有搜尋到相應結果</div>
</div>
</template>
<script>
import nodeListFunc from "./data.js";
import treeNode from "./treeNode.vue";
export default {
data(){
let list = nodeListFunc();
return {
nodeList: list,
searchText: ""
};
},
components:{
treeNode
},
methods:{
// 對子節點進行搜尋。
searchEach(node, value){
let depth = this.getTreeDepth(node);
let self = this;
for (let i = 0; i < depth - 1; i++) {
// 記錄【刪除不匹配搜尋內容的葉子節點】操作的次數。
// 如果這個變數記錄的操作次數為0,表示樹形結構中,所有的
// 葉子節點(不包含只有根節點的情況)都匹配搜尋內容。那麼就沒有必要再
// 在迴圈體裡面遍歷樹了.
let spliceCounter = 0;
// 遍歷樹形結構
this.traverseTree(node, n=>{
if (self.isHasChildren(n)) {
let children = n.children;
let length = children.length;
// 找到不匹配搜尋內容的葉子節點並刪除。為了避免要刪除的元素在陣列中的索引改變,從後向前迴圈,
// 找到匹配的元素就刪除。
for (let j = length - 1; j >= 0; j--) {
let e3 = children[j];
if (!self.isHasChildren(e3) && e3.name.indexOf(value) <= -1) {
children.splice(j,1);
spliceCounter++;
}
} // end for (let j = length - 1; j >= 0; j--)
}
}); // end this.traverseTree(node, n=>{
// 所有的葉子節點都匹配搜尋內容,沒必要再執行迴圈體了。
if (spliceCounter == 0) {
break;
}
}
},
// 搜尋框回車事件響應
search(e){
let self = this;
// 把樹形結構還原成搜尋以前的。
this.nodeList = nodeListFunc();
if (e.target.value=="") {
this.searchText = "";
return;
}
if (this.nodeList && this.nodeList.length > 0) {
this.nodeList.forEach((n,i,a)=>{
self.searchEach(n, e.target.value);
});
// 沒有葉子節點的根節點也要清理掉
let length = this.nodeList.length;
for (let i = length - 1; i >= 0; i--) {
let e2 = this.nodeList[i];
if (!this.isHasChildren(e2) && e2.name.indexOf(e.target.value) <= -1) {
this.nodeList.splice(i, 1);
}
}
this.searchText = e.target.value;
}
},
// 判斷樹形結構中的一個節點是否具有孩子節點
isHasChildren(node){
let flag = false;
if (node.children && node.children.length > 0) {
flag = true;
}
return flag;
},
// 通過傳入根節點獲得樹的深度,是 calDepth 的呼叫者。
getTreeDepth(node){
if (undefined == node || null == node) {
return 0;
}
// 返回結果
let r = 0;
// 樹中當前層節點的集合。
let currentLevelNodes = [node];
// 判斷當前層是否有節點
while(currentLevelNodes.length > 0){
// 當前層有節點,深度可以加一。
r++;
// 下一層節點的集合。
let nextLevelNodes = new Array();
// 找到樹中所有的下一層節點,並把這些節點放到 nextLevelNodes 中。
for(let i = 0; i < currentLevelNodes.length; i++) {
let e = currentLevelNodes[i];
if (this.isHasChildren(e)) {
nextLevelNodes = nextLevelNodes.concat(e.children);
}
}
// 令當前層節點集合的引用指向下一層節點的集合。
currentLevelNodes = nextLevelNodes;
}
return r;
},
// 非遞迴遍歷樹
// 作者:張超
traverseTree(node, callback) {
if (!node) {
return;
}
var stack = [];
stack.push(node);
var tmpNode;
while (stack.length > 0) {
tmpNode = stack.pop();
callback(tmpNode);
if (tmpNode.children && tmpNode.children.length > 0) {
for (let i = tmpNode.children.length - 1; i >= 0; i--) {
stack.push(tmpNode.children[i]);
}
}
}
}
}
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped></style>