舉例說明你對尾遞迴的理解,有哪些應用場景

林恒發表於2024-05-28

一、遞迴

遞迴(英語:Recursion)

在數學與電腦科學中,是指在函式的定義中使用函式自身的方法

在函式內部,可以呼叫其他函式。如果一個函式在內部呼叫自身本身,這個函式就是遞迴函式

其核心思想是把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解

一般來說,遞迴需要有邊界條件、遞迴前進階段和遞迴返回階段。當邊界條件不滿足時,遞迴前進;當邊界條件滿足時,遞迴返回

下面實現一個函式 pow(x, n),它可以計算 xn 次方

使用迭代的方式,如下:

function pow(x, n) {
  let result = 1;

  // 再迴圈中,用 x 乘以 result n 次
  for (let i = 0; i < n; i++) {
    result *= x;
  }
  return result;
}

使用遞迴的方式,如下:

function pow(x, n) {
  if (n == 1) {
    return x;
  } else {
    return x * pow(x, n - 1);
  }
}

pow(x, n) 被呼叫時,執行分為兩個分支:

             if n==1  = x
             /
pow(x, n) =
             \
              else     = x * pow(x, n - 1)

也就是說pow 遞迴地呼叫自身 直到 n == 1

為了計算 pow(2, 4),遞迴變體經過了下面幾個步驟:

  1. pow(2, 4) = 2 * pow(2, 3)
  2. pow(2, 3) = 2 * pow(2, 2)
  3. pow(2, 2) = 2 * pow(2, 1)
  4. pow(2, 1) = 2

因此,遞迴將函式呼叫簡化為一個更簡單的函式呼叫,然後再將其簡化為一個更簡單的函式,以此類推,直到結果

二、尾遞迴

尾遞迴,即在函式尾位置呼叫自身(或是一個尾呼叫本身的其他函式等等)。尾遞迴也是遞迴的一種特殊情形。尾遞迴是一種特殊的尾呼叫,即在尾部直接呼叫自身的遞迴函式

尾遞迴在普通尾呼叫的基礎上,多出了2個特徵:

  • 在尾部呼叫的是函式自身
  • 可透過最佳化,使得計算僅佔用常量棧空間

在遞迴呼叫的過程當中系統為每一層的返回點、區域性量等開闢了棧來儲存,遞迴次數過多容易造成棧溢位

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

實現一下階乘,如果用普通的遞迴,如下:

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

factorial(5) // 120

如果n等於5,這個方法要執行5次,才返回最終的計算表示式,這樣每次都要儲存這個方法,就容易造成棧溢位,複雜度為O(n)

如果我們使用尾遞迴,則如下:

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

factorial(5, 1) // 120

可以看到,每一次返回的就是一個新的函式,不帶上一個函式的引數,也就不需要儲存上一個函式了。尾遞迴只需要儲存一個呼叫棧,複雜度 O(1)

二、應用場景

陣列求和

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

使用尾遞迴最佳化求斐波那契數列

function factorial2 (n, start = 1, total = 1) {
    if(n <= 2){
        return total
    }
    return factorial2 (n -1, total, total + start)
}

陣列扁平化

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
}

陣列物件格式化

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;
};

參考文獻

  • https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。

舉例說明你對尾遞迴的理解,有哪些應用場景

相關文章