算法與數據結構(二十四)最優子結構原理和 dp 數組遍歷方向

注:此文只在個人總結 labuladong 動態規劃框架,僅限于學習交流,版權歸原作者所有;

本文是兩年前發的 動態規劃答疑篇open in new window 的修訂版,根據我的不斷學習總結以及讀者的評論反饋,我給擴展了更多內容,力求使本文成為繼 動態規劃核心套路框架 之后的一篇全面答疑文章。以下是正文。

這篇文章就給你講明白以下幾個問題:

1、到底什么才叫「最優子結構」,和動態規劃什么關系。

2、如何判斷一個問題是動態規劃問題,即如何看出是否存在重疊子問題。

3、為什么經常看到將 dp 數組的大小設置為 n + 1 而不是 n

4、為什么動態規劃遍歷 dp 數組的方式五花八門,有的正著遍歷,有的倒著遍歷,有的斜著遍歷。

注:

  1. 可以通過子問題推導出原問題就是證明有最優子結構,而最優子結構 + 重疊子問題 = 動態規劃;

一、最優子結構詳解

「最優子結構」是某些問題的一種特定性質,并不是動態規劃問題專有的。也就是說,很多問題其實都具有最優子結構,只是其中大部分不具有重疊子問題,所以我們不把它們歸為動態規劃系列問題而已。

我先舉個很容易理解的例子:假設你們學校有 10 個班,你已經計算出了每個班的最高考試成績。那么現在我要求你計算全校最高的成績,你會不會算?當然會,而且你不用重新遍歷全校學生的分數進行比較,而是只要在這 10 個最高成績中取最大的就是全校的最高成績。

我給你提出的這個問題就符合最優子結構:可以從子問題的最優結果推出更大規模問題的最優結果。讓你算每個班的最優成績就是子問題,你知道所有子問題的答案后,就可以借此推出全校學生的最優成績這個規模更大的問題的答案。

你看,這么簡單的問題都有最優子結構性質,只是因為顯然沒有重疊子問題,所以我們簡單地求最值肯定用不出動態規劃。

再舉個例子:假設你們學校有 10 個班,你已知每個班的最大分數差(最高分和最低分的差值)。那么現在我讓你計算全校學生中的最大分數差,你會不會算?可以想辦法算,但是肯定不能通過已知的這 10 個班的最大分數差推到出來。因為這 10 個班的最大分數差不一定就包含全校學生的最大分數差,比如全校的最大分數差可能是 3 班的最高分和 6 班的最低分之差。

這次我給你提出的問題就不符合最優子結構,因為你沒辦通過每個班的最優值推出全校的最優值,沒辦法通過子問題的最優值推出規模更大的問題的最優值。前文 動態規劃詳解 說過,想滿足最優子結,子問題之間必須互相獨立。全校的最大分數差可能出現在兩個班之間,顯然子問題不獨立,所以這個問題本身不符合最優子結構。

那么遇到這種最優子結構失效情況,怎么辦?策略是:改造問題。對于最大分數差這個問題,我們不是沒辦法利用已知的每個班的分數差嗎,那我只能這樣寫一段暴力代碼:

int result = 0;
for (Student a : school) {for (Student b : school) {if (a is b) continue;result = max(result, |a.score - b.score|);}
}
return result;

改造問題,也就是把問題等價轉化:最大分數差,不就等價于最高分數和最低分數的差么,那不就是要求最高和最低分數么,不就是我們討論的第一個問題么,不就具有最優子結構了么?那現在改變思路,借助最優子結構解決最值問題,再回過頭解決最大分數差問題,是不是就高效多了?

當然,上面這個例子太簡單了,不過請讀者回顧一下,我們做動態規劃問題,是不是一直在求各種最值,本質跟我們舉的例子沒啥區別,無非需要處理一下重疊子問題。

前文不 同定義不同解法 和 高樓扔雞蛋問題 就展示了如何改造問題,不同的最優子結構,可能導致不同的解法和效率。

再舉個常見但也十分簡單的例子,求一棵二叉樹的最大值,不難吧(簡單起見,假設節點中的值都是非負數):

int maxVal(TreeNode root) {if (root == null)return -1;int left = maxVal(root.left);int right = maxVal(root.right);return max(root.val, left, right);
}

你看這個問題也符合最優子結構,以 root 為根的樹的最大值,可以通過兩邊子樹(子問題)的最大值推導出來,結合剛才學校和班級的例子,很容易理解吧。

當然這也不是動態規劃問題,旨在說明,最優子結構并不是動態規劃獨有的一種性質,能求最值的問題大部分都具有這個性質;但反過來,最優子結構性質作為動態規劃問題的必要條件,一定是讓你求最值的,以后碰到那種惡心人的最值題,思路往動態規劃想就對了,這就是套路。

動態規劃不就是從最簡單的 base case 往后推導嗎,可以想象成一個鏈式反應,以小博大。但只有符合最優子結構的問題,才有發生這種鏈式反應的性質。

找最優子結構的過程,其實就是證明狀態轉移方程正確性的過程,方程符合最優子結構就可以寫暴力解了,寫出暴力解就可以看出有沒有重疊子問題了,有則優化,無則 OK。這也是套路,經常刷題的讀者應該能體會。

這里就不舉那些正宗動態規劃的例子了,讀者可以翻翻歷史文章,看看狀態轉移是如何遵循最優子結構的,這個話題就聊到這,下面再來看其他的動態規劃迷惑行為。

注:

  1. 最優子結構只是動態規劃問題的必要條件,比如求二叉樹最大值,但是能求最值問題的大都具有最優子結構;
  2. 動態規劃問題一定是讓求最值的;
  3. 找最優子結構的過程,其實就是證明狀態轉移方程正確性的過程,方程符合最優子結構就可以寫暴力解了,寫出暴力解就可以看出有沒有重疊子問題了,有則優化,無則 OK。這也是套路,經常刷題的讀者應該能體會;

二、如何一眼看出重疊子問題

經常有讀者說:

看了前文 動態規劃核心套路,我知道了如何一步步優化動態規劃問題;

看了前文 動態規劃設計:數學歸納法,我知道了利用數學歸納法寫出暴力解(狀態轉移方程)。

但就算我寫出了暴力解,我很難判斷這個解法是否存在重疊子問題,從而無法確定是否可以運用備忘錄等方法去優化算法效率。

對于這個問題,其實我在動態規劃系列的文章中寫過幾次,在這里再統一總結一下吧。

首先,最簡單粗暴的方式就是畫圖,把遞歸樹畫出來,看看有沒有重復的節點

比如最簡單的例子,動態規劃核心套路 中斐波那契數列的遞歸樹:

在這里插入圖片描述

這棵遞歸樹很明顯存在重復的節點,所以我們可以通過備忘錄避免冗余計算。

但畢竟斐波那契數列問題太簡單了,實際的動態規劃問題比較復雜,比如二維甚至三維的動態規劃,當然也可以畫遞歸樹,但不免有些復雜。

比如在 最小路徑和問題 中,我們寫出了這樣一個暴力解法:

int dp(int[][] grid, int i, int j) {if (i == 0 && j == 0) {return grid[0][0];}if (i < 0 || j < 0) {return Integer.MAX_VALUE;}return Math.min(dp(grid, i - 1, j), dp(grid, i, j - 1)) + grid[i][j];
}

你不需要讀過前文,光看這個函數代碼就能看出來,該函數遞歸過程中參數 i, j 在不斷變化,即「狀態」是 (i, j) 的值,你是否可以判斷這個解法是否存在重疊子問題呢?

假設輸入的 i = 8, j = 7,二維狀態的遞歸樹如下圖,顯然出現了重疊子問題:

在這里插入圖片描述

但稍加思考就可以知道,其實根本沒必要畫圖,可以通過遞歸框架直接判斷是否存在重疊子問題

具體操作就是直接刪掉代碼細節,抽象出該解法的遞歸框架:

int dp(int[][] grid, int i, int j) {dp(grid, i - 1, j), // #1dp(grid, i, j - 1)  // #2
}

可以看到 i, j 的值在不斷減小,那么我問你一個問題:如果我想從狀態 (i, j) 轉移到 (i-1, j-1),有幾種路徑?

顯然有兩種路徑,可以是 (i, j) -> #1 -> #2 或者 (i, j) -> #2 -> #1,不止一種,說明 (i-1, j-1) 會被多次計算,所以一定存在重疊子問題。

再舉個稍微復雜的例子,后文 正則表達式問題 的暴力解代碼:

bool dp(string& s, int i, string& p, int j) {int m = s.size(), n = p.size();if (j == n)  return i == m;if (i == m) {if ((n - j) % 2 == 1) return false;for (; j + 1 < n; j += 2) {if (p[j + 1] != '*') return false;}return true;}if (s[i] == p[j] || p[j] == '.') {if (j < n - 1 && p[j + 1] == '*') {return dp(s, i, p, j + 2)|| dp(s, i + 1, p, j);} else {return dp(s, i + 1, p, j + 1);}} else if (j < n - 1 && p[j + 1] == '*') {return dp(s, i, p, j + 2);}return false;
}

代碼有些復雜對吧,如果畫圖的話有些麻煩,但我們不畫圖,直接忽略所有細節代碼和條件分支,只抽象出遞歸框架:

bool dp(string& s, int i, string& p, int j) {dp(s, i, p, j + 2);     // #1dp(s, i + 1, p, j);     // #2dp(s, i + 1, p, j + 1); // #3
}

和上一題一樣,這個解法的「狀態」也是 (i, j) 的值,那么我繼續問你問題:如果我想從狀態 (i, j) 轉移到 (i+2, j+2),有幾種路徑?

顯然,至少有兩條路徑:(i, j) -> #1 -> #2 -> #2(i, j) -> #3 -> #3,這就說明這個解法存在巨量重疊子問題。

所以,不用畫圖就知道這個解法也存在重疊子問題,需要用備忘錄技巧去優化。

注:重疊子問題:最笨的方法是腦補一個遞歸樹,看有沒有重疊子問題;進一步可以通過遞歸框架判斷是否有子問題,有子問題就可以考慮使用備忘錄技巧去優化;

三、dp 數組的大小設置

比如說后文 編輯距離問題,我首先講的是自頂向下的遞歸解法,實現了這樣一個 dp 函數:

int minDistance(String s1, String s2) {int m = s1.length(), n = s2.length();// 按照 dp 函數的定義,計算 s1 和 s2 的最小編輯距離return dp(s1, m - 1, s2, n - 1);
}// 定義:s1[0..i] 和 s2[0..j] 的最小編輯距離是 dp(s1, i, s2, j)
int dp(String s1, int i, String s2, int j) {// 處理 base caseif (i == -1) {return j + 1;}if (j == -1) {return i + 1;}// 進行狀態轉移if (s1.charAt(i) == s2.charAt(j)) {return dp(s1, i - 1, s2, j - 1);} else {return min(dp(s1, i, s2, j - 1) + 1,dp(s1, i - 1, s2, j) + 1,dp(s1, i - 1, s2, j - 1) + 1);}
}

然后改造成了自底向上的迭代解法:

int minDistance(String s1, String s2) {int m = s1.length(), n = s2.length();// 定義:s1[0..i] 和 s2[0..j] 的最小編輯距離是 dp[i+1][j+1]int[][] dp = new int[m + 1][n + 1];// 初始化 base case for (int i = 1; i <= m; i++)dp[i][0] = i;for (int j = 1; j <= n; j++)dp[0][j] = j;// 自底向上求解for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {// 進行狀態轉移if (s1.charAt(i-1) == s2.charAt(j-1)) {dp[i][j] = dp[i - 1][j - 1];} else {dp[i][j] = min(dp[i - 1][j] + 1,dp[i][j - 1] + 1,dp[i - 1][j - 1] + 1);}}}// 按照 dp 數組的定義,存儲 s1 和 s2 的最小編輯距離return dp[m][n];
}

這兩種解法思路是完全相同的,但就有讀者提問,為什么迭代解法中的 dp 數組初始化大小要設置為 int[m+1][n+1]?為什么 s1[0..i]s2[0..j] 的最小編輯距離要存儲在 dp[i+1][j+1] 中,有一位索引偏移?

能不能模仿 dp 函數的定義,把 dp 數組初始化為 int[m][n],然后讓 s1[0..i]s2[0..j] 的最小編輯距離要存儲在 dp[i][j] 中?

理論上,你怎么定義都可以,只要根據定義處理好 base case 就可以

你看 dp 函數的定義,dp(s1, i, s2, j) 計算 s1[0..i]s2[0..j] 的編輯距離,那么 i, j 等于 -1 時代表空串的 base case,所以函數開頭處理了這兩種特殊情況。

再看 dp 數組,你當然也可以定義 dp[i][j] 存儲 s1[0..i]s2[0..j] 的編輯距離,但問題是 base case 怎么搞?索引怎么能是 -1 呢?

所以我們把 dp 數組初始化為 int[m+1][n+1],讓索引整體偏移一位,把索引 0 留出來作為 base case 表示空串,然后定義 dp[i+1][j+1] 存儲 s1[0..i]s2[0..j] 的編輯距離。

注:dp 數組大小到底如何定義,依據 base case 來定,比如編輯距離問題中,base case dp[0][0] 代表空串情況,因此 dp 數組需要設置為 dp[m+1][n+1],把索引 0 留出來表示 base case 空串,做題時候,可以考慮下空串之類的情況再考慮設置 dp 數組大小;

四、dp 數組的遍歷方向

我相信讀者做動態規問題時,肯定會對 dp 數組的遍歷順序有些頭疼。我們拿二維 dp 數組來舉例,有時候我們是正向遍歷:

int[][] dp = new int[m][n];
for (int i = 0; i < m; i++)for (int j = 0; j < n; j++)// 計算 dp[i][j]

有時候我們反向遍歷:

for (int i = m - 1; i >= 0; i--)for (int j = n - 1; j >= 0; j--)// 計算 dp[i][j]

有時候可能會斜向遍歷:

// 斜著遍歷數組
for (int l = 2; l <= n; l++) {for (int i = 0; i <= n - l; i++) {int j = l + i - 1;// 計算 dp[i][j]}
}

甚至更讓人迷惑的是,有時候發現正向反向遍歷都可以得到正確答案,比如我們在 團滅股票問題 中有的地方就正反皆可。

如果仔細觀察的話可以發現其中的原因,你只要把住兩點就行了:

1、遍歷的過程中,所需的狀態必須是已經計算出來的

2、遍歷結束后,存儲結果的那個位置必須已經被計算出來

注:

  1. 遍歷開始時候,所需狀態必須已經計算出來;

  2. 遍歷結束時候,存儲結果的位置必須已經計算出來;

下面來具體解釋上面兩個原則是什么意思。

比如編輯距離這個經典的問題,詳解見后文 編輯距離詳解,我們通過對 dp 數組的定義,確定了 base case 是 dp[..][0]dp[0][..],最終答案是 dp[m][n];而且我們通過狀態轉移方程知道 dp[i][j] 需要從 dp[i-1][j], dp[i][j-1], dp[i-1][j-1] 轉移而來,如下圖:

在這里插入圖片描述

那么,參考剛才說的兩條原則,你該怎么遍歷 dp 數組?肯定是正向遍歷:

for (int i = 1; i < m; i++)for (int j = 1; j < n; j++)// 通過 dp[i-1][j], dp[i][j - 1], dp[i-1][j-1]// 計算 dp[i][j]

因為,這樣每一步迭代的左邊、上邊、左上邊的位置都是 base case 或者之前計算過的,而且最終結束在我們想要的答案 dp[m][n]

再舉一例,回文子序列問題,詳見后文 子序列問題模板,我們通過過對 dp 數組的定義,確定了 base case 處在中間的對角線,dp[i][j] 需要從 dp[i+1][j], dp[i][j-1], dp[i+1][j-1] 轉移而來,想要求的最終答案是 dp[0][n-1],如下圖:

在這里插入圖片描述

這種情況根據剛才的兩個原則,就可以有兩種正確的遍歷方式:

在這里插入圖片描述

要么從左至右斜著遍歷,要么從下向上從左到右遍歷,這樣才能保證每次 dp[i][j] 的左邊、下邊、左下邊已經計算完畢,得到正確結果。

現在,你應該理解了這兩個原則,主要就是看 base case 和最終結果的存儲位置,保證遍歷過程中使用的數據都是計算完畢的就行,有時候確實存在多種方法可以得到正確答案,可根據個人口味自行選擇。

注:dp 遍歷方向的本質原則就是 以已知推未知,同時需要保證遍歷結束時候,存儲結果的位置必須已經計算出來;

五、參考文獻

  1. 最優子結構原理和 dp 數組遍歷方向

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

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

相關文章

【STM32】高效開發工具CubeMonitor快速上手

工欲善其事必先利其器。擁有一個輔助測試工具&#xff0c;能極大提高開發項目的效率。STM32CubeMonitor系列工具能夠實時讀取和呈現其變量&#xff0c;從而在運行時幫助微調和診斷STM32應用&#xff0c;類似于一個簡單的示波器。它是一款基于流程的圖形化編程工具&#xff0c;類…

面試題:線程池的底層工作原理

線程池的幾個重要的參數&#xff1a; 1、corePoolSize&#xff1a;線程池的核心線程數&#xff08;也是默認線程數&#xff09; 2、maximumPoolSize&#xff1a;最大線程數 3、keepAliveTime&#xff1a;允許的線程最大空閑時間&#xff08;單位/秒&#xff09; 線程池內部是…

鏈表之第二回

歡迎來到我的&#xff1a;世界 該文章收入欄目&#xff1a;鏈表 希望作者的文章對你有所幫助&#xff0c;有不足的地方還請指正&#xff0c;大家一起學習交流 ! 目錄 前言第一題&#xff1a;反轉一個鏈表第二題&#xff1a;鏈表內指定區間反轉第三題&#xff1a;判斷一個鏈表…

opencv+ffmpeg+QOpenGLWidget開發的音視頻播放器demo

前言 本篇文檔的demo包含了 1.使用OpenCV對圖像進行處理&#xff0c;對圖像進行置灰&#xff0c;旋轉&#xff0c;摳圖&#xff0c;高斯模糊&#xff0c;中值濾波&#xff0c;部分區域清除置黑&#xff0c;背景移除&#xff0c;邊緣檢測等操作&#xff1b;2.單純使用opencv播放…

一個案例:Vue2組件化開發組件從入門到入土

1. 環境搭建 1.1. 創建項目 npm install -g vue/clivue create vue_study_todolist1.2. 清空項目代碼 清楚HelloWorld.Vue代碼中的內容。 1.3. 啟動空項目 1.4 項目目標 項目組件實現以下效果 2. 組件拆分代碼 Vue是一個基于組件的框架&#xff0c;允許您將界面拆分成小的…

open cv學習 (五) 圖像的閾值處理

圖像的閾值處理 demo1 # 二值化處理黑白漸變圖 import cv2 img cv2.imread("./img.png", 0) # 二值化處理 t1, dst cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) cv2.imshow("img", img) cv2.imshow("dst", dst) cv2.waitKey() cv2.des…

Golang使用MinIO

最近在使用Golang做了一個網盤項目&#xff08;學習&#xff09;&#xff0c;文件存儲一直保存在本地&#xff08;各廠商提供的oss貴&#xff09;&#xff0c;所以就在思考怎么來處理這些文件&#xff0c;類似的方案很對hdfs、fastdfs&#xff0c;但這其中MinIO是最近幾年比較火…

生信豆芽菜-差異基因富集分析的圈圖

網址&#xff1a;http://www.sxdyc.com/visualsEnrichCirplot 1、數據準備 準備一個基因集的文件 2、選擇富集分析的數據庫&#xff0c;同時輸入展示top幾的條目&#xff0c;選擇顏色&#xff0c;如果是GO的話選擇三個顏色&#xff0c;如果是KEGG選擇一個&#xff0c;如果是G…

神經網絡論文研讀-多模態方向-綜述研讀(上)

翻譯以機翻為主 原文目錄 前言 圖1&#xff1a;LMU印章&#xff08;左&#xff09;風格轉移到梵高的向日葵繪畫&#xff08;中&#xff09;并與提示混合 - 梵高&#xff0c;向日葵 -通過CLIPVGAN&#xff08;右&#xff09;。在過去的幾年中&#xff0c;自然語言處理&#xff…

微信小程序實現拖拽的小球

目錄 前言 js 獲取微信小程序中獲取系統信息 觸摸移動事件的處理函數 觸摸結束事件的處理函數 用于監聽頁面滾動事件 全局參數 html CSS 前言 小程序開發提供了豐富的API和事件處理函數&#xff0c;使得開發者可以方便地實現各種交互功能。其中&#xff0c;拖拽功能…

無涯教程-Perl - tell函數

描述 此函數返回指定FILEHANDLE中讀取指針的當前位置(以字節為單位)。如果省略FILEHANDLE,則它將返回上次訪問的文件中的位置。 語法 以下是此函數的簡單語法- tell FILEHANDLEtell返回值 此函數以字節為單位返回當前文件位置。 例 以下是顯示其基本用法的示例代碼,要檢…

leetcode473. 火柴拼正方形(回溯算法-java)

火柴拼正方形 leetcode473 火柴拼正方形題目描述回溯算法 上期經典算法 leetcode473 火柴拼正方形 難度 - 中等 原題鏈接 - leetcode473 火柴拼正方形 題目描述 你將得到一個整數數組 matchsticks &#xff0c;其中 matchsticks[i] 是第 i 個火柴棒的長度。你要用 所有的火柴棍…

BC119 小樂樂與字符串

描述 在慶祝祖國母親70華誕之際&#xff0c;老師給小樂樂出了一個問題。大家都知道China的英文縮寫是CHN&#xff0c;那么給你一個字符串s&#xff0c;你需要做的是統計s中子序列“CHN”的個數。子序列的定義&#xff1a;存在任意下標a < b < c&#xff0c;那么“s[a]s[b…

微服務—Eureka注冊中心

eureka相當于是一個公司的管理人事HR,各部門之間如果有合作時&#xff0c;由HR進行人員的分配以及調度&#xff0c;具體選哪個人&#xff0c;全憑HR的心情&#xff0c;如果你這個部門存在沒有意義&#xff0c;直接把你這個部門撤銷&#xff0c;全體人員裁掉&#xff0c;所以不想…

計算機網絡筆記

TCP有連接可靠服務 TCP特點&#xff1a; 1.TCP是面向連接的傳輸層協議&#xff1b; 2.每條TCP連接只能有兩個端點&#xff0c;每條TCP連接是一對一的&#xff1b; 3.TCP提供可靠交付&#xff0c;保證傳送數據無差錯&#xff0c;不丟失&#xff0c;不重復且有序&#xff1b; 4.…

Android Studio瀑布流實現

效果&#xff1a; ImageDetail class package com.example.waterfallflow; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.widget.ImageView;public class ImageDetail extends Activity{Overrideprotected void …

DNNGP、DeepGS 和 DLGWAS模型構成對比

一、DNNGP DNNGP 是基于深度卷積神經網絡&#xff0c;這個結構包括一個輸入層&#xff0c;三個卷積層&#xff0c;一個批標準化層&#xff0c;兩個dropout層&#xff0c;一個平坦化層&#xff0c;一個 dense層。 dropout層&#xff1a;在神經網絡中,dropout層是一個非常有效的正…

信息與通信工程面試準備——數學知識|正態分布|中心極限定理

目錄 正態分布 正態分布的參數 正態分布的第一個參數是均值 正態分布的第二個參數是標準差SD 所有正態分布的共同特征 標準正態分布&#xff1a;正態分布的特例 中心極限定理 理解定義 示例# 1 示例# 2 知道樣本均值總是正態分布的實際含義是什么&#xff1f; 正態分…

Scala 如何調試隱式轉換--隱式轉換代碼的顯示展示

方法1 在需要隱式轉換的地方&#xff0c;把需要的參數顯示的寫出。 略方法2&#xff0c;查看編譯代碼 在terminal中 利用 scalac -Xprint:typer xxx.scala方法打印添加了隱式值的代碼示例。 對于復雜的工程來說&#xff0c;直接跑到terminal執行 scalac -Xprint:typer xxx.…

JVM——類文件結構

文章目錄 一 概述二 Class 文件結構總結2.1 魔數2.2 Class 文件版本2.3 常量池2.4 訪問標志2.5 當前類索引,父類索引與接口索引集合2.6 字段表集合2.7 方法表集合2.8 屬性表集合 一 概述 在 Java 中&#xff0c;JVM 可以理解的代碼就叫做字節碼&#xff08;即擴展名為 .class …