如何優雅的使用javascript遞迴畫一棵結構樹

徐小夕發表於2019-09-16

如何優雅的使用javascript遞迴畫一棵結構樹

遞迴和尾遞迴

簡單的說,遞迴就是函式自己呼叫自己,它做為一種演算法在程式設計語言中廣泛應用。其核心思想是把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解。一般來說,遞迴需要有邊界條件、遞迴前進階段和遞迴返回階段。當邊界條件不滿足時,遞迴前進;當邊界條件滿足時,遞迴返回。

但是作為一個合格的程式設計師,我們也因該知道,遞迴演算法相對常用的演算法如普通迴圈等,執行效率較低。因此,應該儘量避免使用遞迴,除非沒有更好的演算法或者某種特定情況,遞迴更為適合的時候。在遞迴呼叫的過程當中系統為每一層的返回點、區域性量等開闢了棧來儲存,遞迴次數過多容易造成棧溢位等。

這個時候,我們就需要用到尾遞迴,即一個函式中所有遞迴形式的呼叫都出現在函式的末尾,對於尾遞迴來說,由於只存在一個呼叫記錄,所以永遠不會發生"棧溢位"錯誤。

舉個例子,我們來實現一下階乘,如果用普通的遞迴,實現將是這樣的:

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120
複製程式碼

最多需要儲存n個呼叫棧,複雜度 O(n),如果我們使用尾遞迴:

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120
複製程式碼

此時只需要儲存一個呼叫棧,複雜度 O(1) 。通過這個案例,你是否已經慢慢理解其精髓了呢?接下來我將介紹幾個常用的遞回應用的案例,並在其後實現本文標題剖出的樹的實現。

遞迴的常用應用案例

1. 陣列求和

對於已知陣列arr,求arr各項之和。

function sumArray(arr, total) {
    if(arr.length === 1) {
        return total
    }
    return sum(arr, total + arr.pop())
}

let arr = [1,2,3,4];
sumArray(arr, arr[1]) // 10
複製程式碼

該方法給函式傳遞一個陣列引數和初始值,也就是陣列的第一項,通過迭代來實現陣列求和。

2. 斐波那且數列

斐波那契數列(Fibonacci sequence),又稱黃金分割數列,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……在數學上,斐波那契數列以如下被以遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在現代物理、準晶體結構、化學等領域,斐波納契數列都有直接的應用。接下來我們用js實現一個求第n個斐波那契數的方法:

// 斐波那契數列
function factorial1 (n) {
    if(n <= 2){
        return 1
    }
    return factorial1(n-1) + factorial1(n-2)
}

// 尾遞迴優化後
function factorial2 (n, start = 1, total = 1) {
    if(n <= 2){
        return total
    }
    return factorial2 (n -1, total, total + start)
}
複製程式碼

由尾遞迴優化後的函式可以知道,每一次呼叫函式自身,都會將更新後的初始值和最終的結果傳遞進去,通過回溯來求得最終的結果。

3. 階乘

階乘在上文以提到過,如想回顧,請向上翻閱。

4. 省市級聯多級聯動

省市級聯多級聯動的方法本質是生成結構化的資料結構,在element或antd中都有對應的實現,這裡就不做過多介紹了。

5. 深拷貝

深拷貝的例子大家也已經司空見慣了,這裡只給出一個簡單的實現思路:

function clone(target) {
   if (typeof target === 'object') {
       let cloneTarget = Array.isArray(target) ? [] : {};
       for (const key in target) {
           cloneTarget[key] = clone(target[key]);
       }
       return cloneTarget;
   } else {
       return target;
   }
};
複製程式碼

6. 爬梯問題

一共有n個臺階,每次只能走一個或兩個臺階,問要走完這個臺階,一共有多少種走法。

n =1; result = 1  --> 1
n =2; result = 2  --> 11 2
n =3; result = 3  --> 111 12 21
...
如果第一步走1個臺階,由以上規律可以發現剩下的臺階有n-1種走法;
如果第一步走2個臺階,由以上規律可以發現剩下的臺階有n-2種走法;
則一共有fn(n-1) + fn(n-2) 種走法
function steps(n) {
    if(n <= 1) {
        return 1
    }
    return steps(n-1) + steps(n-2)
}
複製程式碼

7. 物件資料格式化

這道題是本人曾經面試阿里的一道筆試題,問題是如果伺服器返回了巢狀的物件,物件鍵名大小寫不確定,如果統一讓鍵名小寫。

let obj = {
    a: '1',
    b: {
        c: '2',
        D: {
            E: '3'
        }
    }
}
轉化為如下:
let obj = {
    a: '1',
    b: {
        c: '2',
        d: {
            e: '3'
        }
    }
}

// 程式碼實現
function keysLower(obj) {
    let reg = new RegExp("([A-Z]+)", "g");
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            let temp = obj[key];
            if (reg.test(key.toString())) {
                // 將修改後的屬性名重新賦值給temp,並在物件obj內新增一個轉換後的屬性
                temp = obj[key.replace(reg, function (result) {
                    return result.toLowerCase()
                })] = obj[key];
                // 將之前大寫的鍵屬性刪除
                delete obj[key];
            }
            // 如果屬性是物件或者陣列,重新執行函式
            if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
                keysLower(temp);
            }
        }
    }
    return obj;
};
複製程式碼

具體過程和思路在程式碼中已經寫出了註釋,感興趣可以自己研究一下。

8. 遍歷目錄/刪除目錄

我們這裡使用node來實現刪除一個目錄,用現有的node API確實有刪除目錄的功能,但是目錄下如果有檔案或者子目錄,fs.rmdir && fs.rmdirSync 是不能將其刪除的,所以要先刪除目錄下的檔案,最後再刪除資料夾。

function deleteFolder(path) {
    var files = [];
    if(fs.existsSync(path)) { // 如果目錄存在
        files = fs.readdirSync(path);
        files.forEach(function(file,index){
            var curPath = path + "/" + file;
            if(fs.statSync(curPath).isDirectory()) { // 如果是目錄,則遞迴
                deleteFolder(curPath);
            } else { // 刪除檔案
                fs.unlinkSync(curPath);
            }
        });
        fs.rmdirSync(path);
    }
}
複製程式碼

9. 繪製分形圖形

通過遞迴,我們可以在圖形學上有更大的自由度,但是請記住,並不是最好的選擇。

如何優雅的使用javascript遞迴畫一棵結構樹

如何優雅的使用javascript遞迴畫一棵結構樹
我們可以藉助一些工具和遞迴的思想,實現如上的分形圖案。

10. 扁平化陣列Flat

陣列拍平實際上就是把一個巢狀的陣列,展開成一個陣列,如下案例:

let a = [1,2,3, [1,2,3, [1,2,3]]]
// 變成
let a = [1,2,3,1,2,3,1,2,3]
// 具體實現
function flat(arr = [], result = []) {
    arr.forEach(v => {
        if(Array.isArray(v)) {
            result = result.concat(flat(v, []))
        }else {
            result.push(v)
        }
    })
    return result
}

flat(a)
複製程式碼

當然這只是筆者實現的一種方式,更多實現方式等著你去探索。

用遞迴畫一棵自定義風格的結構樹

通過上面的介紹,我想大家對遞迴及其應用已經有一個基本的概念,接下來我將一步步的帶大家用遞迴畫一棵結構樹。 效果圖:

如何優雅的使用javascript遞迴畫一棵結構樹

如何優雅的使用javascript遞迴畫一棵結構樹
該圖形是根據目錄結構生成的目錄樹圖,在很多應用場景中被廣泛使用,接下來我們就來看看他的實現過程吧:

const fs = require('fs')
const path = require('path')
// 遍歷目錄/生成目錄樹
function treeFolder(path, flag = '|_') {
    var files = [];
    
    if(fs.existsSync(path)) {
        files = fs.readdirSync(path);
        files.forEach(function(file,index){
            var curPath = path + "/" + file;
            if(fs.statSync(curPath).isDirectory()) { // recurse
                // obj[file] = treeFolder(curPath, {});
                console.log(flag, file)
                treeFolder(curPath, '   ' + flag)
            } else {
                // obj['--'] = file
                console.log(flag, file)
            }
        })
        // return obj
    }
}

treeFolder(path.resolve(__dirname, './test'))
複製程式碼

test為我們建的測試目錄,如下:

如何優雅的使用javascript遞迴畫一棵結構樹
我們通過短短10幾行程式碼就實現了一個生成結構樹的小應用,是不是感覺遞迴有點意思呢?在這個函式中,第一個引數是目錄的絕對路徑,第二個是標示符,標示符決定我們生成的樹枝的樣式,我們可以自定義不同的樣式。

歡迎大家相互學習交流,一起探索前端的邊界。

如何優雅的使用javascript遞迴畫一棵結構樹

更多推薦

相關文章