從JS角度直觀理解遞歸的本質

讓我們寫一個函數 pow(x, n),它可以計算 xn 次方。換句話說就是,x 乘以自身 n 次。

有兩種實現方式。

  1. 迭代思路:使用 for 循環:

    function pow(x, n) {let result = 1;// 在循環中,用 x 乘以 result n 次for (let i = 0; i < n; i++) {result *= x;}return result;
    }alert( pow(2, 3) ); // 8
    
  2. 遞歸思路:簡化任務,調用自身:

    function pow(x, n) {if (n == 1) {return x;} else {return x * pow(x, n - 1);}
    }alert( pow(2, 3) ); // 8
    

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

              if n==1  = x/
pow(x, n) =\else     = x * pow(x, n - 1)
  1. 如果 n == 1,所有事情都會很簡單,這叫做 基礎 的遞歸,因為它會立即產生明顯的結果:pow(x, 1) 等于 x
  2. 否則,我們可以用 x * pow(x, n - 1) 表示 pow(x, n)。在數學里,可能會寫為 x<sup>n</sup><span> </span>= x * x<sup>n-1</sup>。這叫做 一個遞歸步驟:我們將任務轉化為更簡單的行為(x 的乘法)和更簡單的同類任務的調用(帶有更小的 npow 運算)。接下來的步驟將其進一步簡化,直到 n 達到 1

這是一個很簡單的例子,我們從直覺上很容易理解遞歸,但是有很多人會有一種不可名狀的困惑,這個問題可以總結為“為什么會這樣?”

現在我們來研究一下遞歸調用是如何工作的。為此,我們會先看看函數底層的工作原理。

有關正在運行的函數的執行過程的相關信息被存儲在其 執行上下文 中。

執行上下文 是一個內部數據結構,它包含有關函數執行時的詳細細節:當前控制流所在的位置,當前的變量,this的值(此處我們不使用它),以及其它的一些內部細節。

一個函數調用僅具有一個與其相關聯的執行上下文。

當一個函數進行嵌套調用時,將發生以下的事兒:

  • 當前函數被暫停;
  • 與它關聯的執行上下文被一個叫做 執行上下文堆棧 的特殊數據結構保存;
  • 執行嵌套調用;
  • 嵌套調用結束后,從堆棧中恢復之前的執行上下文,并從停止的位置恢復外部函數。

讓我們看看 pow(2, 3) 調用期間都發生了什么。

pow(2, 3)

在調用 pow(2, 3) 的開始,執行上下文(context)會存儲變量:x = 2, n = 3,執行流程在函數的第 1 行。

我們將其描繪如下:

  • Context: { x: 2, n: 3, at line 1 } pow(2, 3)

這是函數開始執行的時候。條件 n == 1 結果為假,所以執行流程進入 if 的第二分支。

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

變量相同,但是行改變了,因此現在的上下文是:

  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

為了計算 x * pow(x, n - 1),我們需要使用帶有新參數的新的 pow 子調用 pow(2, 2)

pow(2, 2)

為了執行嵌套調用,JavaScript 會在 執行上下文堆棧 中記住當前的執行上下文。

這里我們調用相同的函數 pow,但這絕對沒問題。所有函數的處理都是一樣的:

  1. 當前上下文被“記錄”在堆棧的頂部。
  2. 為子調用創建新的上下文。
  3. 當子調用結束后 —— 前一個上下文被從堆棧中彈出,并繼續執行。

下面是進入子調用 pow(2, 2) 時的上下文堆棧:

  • Context: { x: 2, n: 2, at line 1 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

新的當前執行上下文位于頂部(粗體顯示),之前記住的上下文位于下方。

當我們完成子調用后 —— 很容易恢復上一個上下文,因為它既保留了變量,也保留了當時所在代碼的確切位置。

請注意:

在上面的圖中,我們使用“行(line)”一詞,因為在我們的示例中,每一行只有一個子調用,但通常一行代碼可能會包含多個子調用,例如 pow(…) + pow(…) + somethingElse(…)

因此,更準確地說,執行是“在子調用之后立即恢復”的。

pow(2, 1)

重復該過程:在第 5 行生成新的子調用,現在的參數是 x=2, n=1

新的執行上下文被創建,前一個被壓入堆棧頂部:

  • Context: { x: 2, n: 1, at line 1 } pow(2, 1)
  • Context: { x: 2, n: 2, at line 5 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

此時,有 2 個舊的上下文和 1 個當前正在運行的 pow(2, 1) 的上下文。

出口

在執行 pow(2, 1) 時,與之前的不同,條件 n == 1 為真,因此 if 的第一個分支生效:

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

此時不再有更多的嵌套調用,所以函數結束,返回 2

函數完成后,就不再需要其執行上下文了,因此它被從內存中移除。前一個上下文恢復到堆棧的頂部:

  • Context: { x: 2, n: 2, at line 5 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

恢復執行 pow(2, 2)。它擁有子調用 pow(2, 1) 的結果,因此也可以完成 x * pow(x, n - 1) 的執行,并返回 4

然后,前一個上下文被恢復:

  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

當它結束后,我們得到了結果 pow(2, 3) = 8

本示例中的遞歸深度為:3

從上面的解釋我們可以看出,遞歸深度等于堆棧中上下文的最大數量。

請注意內存要求。上下文占用內存,在我們的示例中,求 n 次方需要存儲 n 個上下文,以供更小的 n 值進行計算使用。

而循環算法更節省內存:

function pow(x, n) {let result = 1;for (let i = 0; i < n; i++) {result *= x;}return result;
}

迭代 pow 的過程中僅使用了一個上下文用于修改 iresult。它的內存要求小,并且是固定了,不依賴于 n

任何遞歸都可以用循環來重寫。通常循環變體更有效。

但有時重寫很難,尤其是函數根據條件使用不同的子調用,然后合并它們的結果,或者分支比較復雜時。而且有些優化可能沒有必要,完全不值得。

遞歸可以使代碼更短,更易于理解和維護。并不是每個地方都需要優化,大多數時候我們需要一個好代碼,這就是為什么要使用它。

既然循環更好,為什么要使用遞歸,接下來舉個例子。

遞歸的另一個重要應用就是遞歸遍歷。

假設我們有一家公司。人員結構可以表示為一個對象:

let company = {sales: [{name: 'John',salary: 1000}, {name: 'Alice',salary: 1600}],development: {sites: [{name: 'Peter',salary: 2000}, {name: 'Alex',salary: 1800}],internals: [{name: 'Jack',salary: 1300}]}
};

換句話說,一家公司有很多部門。

  • 一個部門可能有一 數組 的員工,比如,sales 部門有 2 名員工:John 和 Alice。
  • 或者,一個部門可能會劃分為幾個子部門,比如 development 有兩個分支:sitesinternals,它們都有自己的員工。
  • 當一個子部門增長時,它也有可能被拆分成幾個子部門(或團隊)。
    例如,sites 部門在未來可能會分為 siteAsiteB。并且,它們可能會被再繼續拆分。沒有圖示,腦補一下吧。

現在,如果我們需要一個函數來獲取所有薪資的總數。我們該怎么做?

迭代方式并不容易,因為結構比較復雜。首先想到的可能是在 company 上使用 for 循環,并在第一層部分上嵌套子循環。但是,之后我們需要更多的子循環來遍歷像 sites 這樣的二級部門的員工…… 然后,將來可能會出現在三級部門上的另一個子循環?如果我們在代碼中寫 3-4 級嵌套的子循環來遍歷單個對象, 那代碼得多丑啊。

我們試試遞歸吧。

我們可以看到,當我們的函數對一個部門求和時,有兩種可能的情況:

  1. 要么是由一個人員 數組 構成的“簡單”的部門 —— 這樣我們就可以通過一個簡單的循環來計算薪資的總和。
  2. 或者它是一個有 N 個子部門的 對象 —— 那么我們可以通過 N 層遞歸調用來求每一個子部門的薪資,然后將它們合并起來。

第一種情況是由人員數組構成的部門,這種情況很簡單,是最基礎的遞歸。

第二種情況是我們得到的是對象。那么可將這個復雜的任務拆分成適用于更小部門的子任務。它們可能會被繼續拆分,但很快或者不久就會拆分到第一種情況那樣。

這個算法從代碼來看可能會更簡單:

let company = { // 是同一個對象,簡潔起見被壓縮了sales: [{name: 'John', salary: 1000}, {name: 'Alice', salary: 1600 }],development: {sites: [{name: 'Peter', salary: 2000}, {name: 'Alex', salary: 1800 }],internals: [{name: 'Jack', salary: 1300}]}
};// 用來完成任務的函數
function sumSalaries(department) {if (Array.isArray(department)) { // 情況(1)return department.reduce((prev, current) => prev + current.salary, 0); // 求數組的和} else { // 情況(2)let sum = 0;for (let subdep of Object.values(department)) {sum += sumSalaries(subdep); // 遞歸調用所有子部門,對結果求和}return sum;}
}alert(sumSalaries(company)); // 7700

代碼很短也容易理解(希望是這樣?)。這就是遞歸的能力。它適用于任何層次的子部門嵌套。

下面是調用圖:

請添加圖片描述

我們可以很容易地看到其原理:對于對象 {...} 會生成子調用,而數組 [...] 是遞歸樹的“葉子”,它們會立即給出結果。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/20793.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/20793.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/20793.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Springboot中使用spel+自定義注解實現權限控制

使用spel+自定義注解實現權限控制的案例很多, 比如springsecurity,本文也是一同樣的方式實現權限校驗 定義注解 package com.example.demo.anno;import java.lang.annotation.ElementType; import java.lang.annotation.

opencv進階 ——(九)圖像處理之人臉修復祛馬賽克算法CodeFormer

算法簡介 CodeFormer是一種基于AI技術深度學習的人臉復原模型&#xff0c;由南洋理工大學和商湯科技聯合研究中心聯合開發&#xff0c;它能夠接收模糊或馬賽克圖像作為輸入&#xff0c;并生成更清晰的原始圖像。算法源碼地址&#xff1a;https://github.com/sczhou/CodeFormer…

如何快速找到 RCE

背景介紹 本文將分享國外白帽子在‘偵察’階段如何快速發現 RCE 漏洞的經歷。以Apache ActiveMQ 的 CVE-2023–46604 為特例&#xff0c;重點介紹如何發現類似此類的漏洞&#xff0c;讓我們開始吧。 快速發現過程 在‘偵察’階段&#xff0c;白帽小哥會保持每周更新一次目標…

1940java swing零售庫存管理系統myeclipse開發Mysql數據庫CS結構java編程

一、源碼特點 java swing 零售庫存管理系統 是一套完善的窗體設計系統&#xff0c;對理解SWING java 編程開發語言有幫助&#xff0c;系統具有完整的源代碼和數據庫&#xff0c;&#xff0c;系統主要采用C/S模式開發。 應用技術&#xff1a;javamysql 開發工具&#xff1a;…

適合技術小白學習的項目1863java在線視頻網站系統 Myeclipse開發mysql數據庫web結構java編程計算機網頁項目

一、源碼特點 java在線視頻網站系統 是一套完善的web設計系統&#xff0c;對理解JSP java編程開發語言有幫助采用了java設計&#xff0c;系統具有完整的源代碼和數據庫&#xff0c;系統采用web模式&#xff0c;系統主要采用B/S模式開發。 開發環境為TOMCAT7.0,Myeclipse8.5開發…

數據庫、數據表的基本操作

1.數據庫的基本操作 &#xff08;1&#xff09;創建數據庫 &#xff08;2&#xff09;刪除數據庫 &#xff08;3&#xff09;將數據庫的字符集修改為gbk gbk是漢字內碼擴展規范&#xff0c;是GB2312和GB13000的擴展&#xff0c;主要用于簡體中文。 &#xff08;4&#xff09;…

LabVIEW在高校電力電子實驗中的應用

概述&#xff1a;本文介紹了如何利用LabVIEW優化高校電力電子實驗&#xff0c;通過圖形化編程實現參數調節、實時數據監控與存儲&#xff0c;并與Simulink聯動&#xff0c;提高實驗效率和數據處理能力。 需求背景高校實驗室在進行電機拖動和電力電子實驗時&#xff0c;通常使用…

前端框架安全防范

前端框架安全防范 在現代Web開發中&#xff0c;前端框架如Angular和React已經成為構建復雜單頁面應用&#xff08;SPA&#xff09;的主流工具。然而&#xff0c;隨著應用復雜度的增加&#xff0c;安全問題也變得越來越重要。本文將介紹如何在使用Angular和React框架時&#xf…

Java中的synchronized關鍵字詳解

Java中的synchronized關鍵字詳解 1. 引言 在Java編程中&#xff0c;多線程是提高應用性能的重要手段之一。然而&#xff0c;多線程環境下共享資源的訪問控制成為必須面對的問題。synchronized關鍵字作為Java語言提供的一種同步機制&#xff0c;能夠有效地解決這一問題。本文將…

施耐德 BAS PLC 基本操作指南

CPU 型號 項目使用的 PLC 型號為&#xff1a;施耐德昆騰 Quantum 140 CPU 67160 P266 CPU &#xff0c;支持熱備冗余&#xff0c;內部存儲 1024K&#xff0c;支持 2 個 PCMCIA 擴展卡槽CPU 模塊自帶接口&#xff1a;MB 串口接口、MB 串口接口、USB 接口、以太網接口&#xff…

MATLAB算法實戰應用案例精講-【數模應用】聯合分析(附python和MATLAB代碼實現)

目錄 前言 算法原理 什么是聯合分析? 聯合分析的基本原理與步驟

【HarmonyOS】List組件多層對象嵌套ForEach渲染更新的處理

【HarmonyOS】List組件多層對象嵌套ForEach渲染更新的處理 問題背景&#xff1a; 在鴻蒙中UI更新渲染的機制&#xff0c;與傳統的Android IOS應用開發相比。開發會簡單許多&#xff0c;開發效率提升顯著。 一般傳統應用開發的流程處理分為三步&#xff1a;1.畫UI&#xff0c;…

TiDB-從0到1-分布式存儲

TiDB從0到1系列 TiDB-從0到1-體系結構TiDB-從0到1-分布式存儲TiDB-從0到1-分布式事務TiDB-從0到1-MVCC 一、TiDB-DML語句執行流程&#xff08;增刪改&#xff09; DML流程概要 1、協議驗證 用戶連接到TiDB Server后首先工作的是Protocol Layer模塊&#xff0c;該模塊會對用…

mysql表字段超過多少影響性能 mysql表多少效率會下降

一直有傳言說&#xff0c;MySQL 表的數據只要超過 2000 萬行&#xff0c;其性能就會下降。而本文作者用實驗分析證明&#xff1a;至少在 2023 年&#xff0c;這已不再是 MySQL 表的有效軟限制。 傳言 互聯網上有一則傳言說&#xff0c;我們應該避免單個 MySQL 表中的數據超過 …

內網滲透-在HTTP協議層面繞過WAF

進入正題&#xff0c;隨著安全意思增強&#xff0c;各企業對自己的網站也更加注重安全性。但很多web應用因為老舊&#xff0c;或貪圖方便想以最小代價保證應用安全&#xff0c;就只僅僅給服務器安裝waf。 本次從協議層面繞過waf實驗用sql注入演示&#xff0c;但不限于實際應用…

[數據集][目標檢測]輪胎檢測數據集VOC+YOLO格式439張1類別

數據集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路徑的txt文件&#xff0c;僅僅包含jpg圖片以及對應的VOC格式xml文件和yolo格式txt文件) 圖片數量(jpg文件個數)&#xff1a;439 標注數量(xml文件個數)&#xff1a;439 標注數量(txt文件個數)&#xff1a;439 標注類別…

mysql怎么部署雙機

MySQL的雙機部署是為了實現數據的高可用性和容錯性。以下是MySQL雙機熱備部署的基本步驟&#xff0c;我會盡量清晰地分點表示和歸納&#xff1a; 1. 環境準備 安裝MySQL&#xff1a;在兩臺服務器上分別安裝MySQL數據庫。確保版本兼容。 網絡配置&#xff1a;確保兩臺服務器之…

題目:判斷一個素數能被幾個9整除

題目&#xff1a;判斷一個素數能被幾個9整除 There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should …

顛仆流離學二叉樹2 (Java篇)

本篇會加入個人的所謂魚式瘋言 ??????魚式瘋言:??????此瘋言非彼瘋言 而是理解過并總結出來通俗易懂的大白話, 小編會盡可能的在每個概念后插入魚式瘋言,幫助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能說的不是那么嚴謹.但小編初心是能讓更多人…

泛型知識匯總

演示代碼&#xff1a; package exercise;import java.util.Arrays;public class MyArrayList<E> {Object[] obj new Object[10];int size;public boolean add(E e) {obj[size] e;size;return true;}public E get(int index) {return (E) obj[index];}//沒有這個函數&a…