1. 程式人生 > >【079】利用“剪葉子”演算法實現樹形結構的搜尋功能,用Vue.js實現

【079】利用“剪葉子”演算法實現樹形結構的搜尋功能,用Vue.js實現

業務場景

工作中碰到這樣的一個場景:需要對一個樹形結構進行搜尋,凡是匹配的節點都要保留。如果這個匹配的節點存在父節點,那麼不論這個父節點是否匹配搜尋內容,都要保留,並按照樹形結構展示出來。如果一個節點既不匹配搜尋內容,同時也沒有匹配搜尋內容的子節點,那麼該節點就不再保留。

效果如下面這個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

演算法思路

  1. 利用樹的遍歷演算法,找到每個葉子節點及其父節點。
  2. 判斷葉子節點是否匹配搜尋內容,如果匹配則保留;如果不匹配,就把這個葉子節點從它的父節點 children 陣列中刪除。
  3. 這麼做,一次只能搜尋一層葉子節點,如果資料結構裡有多層節點沒有匹配,就需要重複步驟 1 和步驟 2 。重複的次數應該是樹的深度減去1。要減一是因為根節點沒有父節點,自然也不能執行【從父節點 children 陣列中刪除】的操作。
  4. 最後對根節點做判斷,如果根節點有子節點,那麼什麼也不做。如果樹形結構只有一個根節點,若根節點不匹配,則刪除;若根節點匹配,則保留。

下面的圖片演示了演算法的過程:

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

程式碼實現

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>