漢諾塔是經典的遞歸問題。本文對比 C 語言遞歸解法與 x86 匯編語言手動管理棧的實現,揭示函數調用的本質。
void move(char from, char to) {
printf("%c-->%c ", from, to);
}
void hanoi(int n, char from, char aux, char to) {
if (n == 1) {
move(from, to); // 基例:直接移動
} else {
hanoi(n-1, from, to, aux); // 步驟1: 將 n-1 個從 from 移到 aux (借用 to)
move(from, to); // 步驟2: 將第 n 個(最大)從 from 移到 to
hanoi(n-1, aux, from, to); // 步驟3: 將 n-1 個從 aux 移到 to (借用 from)
}
}
int main() {
hanoi(3, 'X', 'Y', 'Z'); // 3 階漢諾塔
return 0;
}
遞歸本質:每層調用將問題分解為兩個子問題 + 一個基本操作。編譯器自動管理調用棧(保存返回地址、參數、局部變量)。
核心思想:用棧模擬遞歸調用。棧幀結構:[返回地址] [n] [from] [aux] [to]。使用 BP 作為幀指針訪問參數。
; 關鍵數據段
DATA SEGMENT
n DB ?
msg DB 'Enter n: $'
msg1 DB 'HANOI Moves: $'
arrow DB '-->$'
count DW 0
DATA ENDS
CODE SEGMENT
ASSUME CS:CODE, DS:DATA
START:
; 初始化 DS, 輸入 n, 設置參數壓棧 (to, aux, from, n)
; 調用 HANOI_MOVE
; ...
HANOI_MOVE PROC NEAR
MOV BP, SP ; 保存棧幀基址
MOV AX, [BP+8] ; 取參數 n (假設棧布局: 返回地址, n, from, aux, to)
CMP AX, 1
JG MORE_THAN_ONE ; n > 1 跳轉
; === 基例 n == 1 ===
CALL PRINT_STEP ; 打印步驟號 [1] [2] ...
MOV DX, [BP+4] ; 取 from
CALL PRINT_CHAR
LEA DX, arrow
CALL PRINT_STR
MOV DX, [BP+10] ; 取 to
CALL PRINT_CHAR
CALL PRINT_SPACE
RET ; 返回
MORE_THAN_ONE:
; === 遞歸步驟 1: hanoi(n-1, from, to, aux) ===
; 參數入棧順序: aux, to, from, n-1 (逆序壓棧)
MOV AX, [BP+4] ; from
MOV BX, [BP+6] ; aux
MOV CX, [BP+8] ; to
MOV DX, [BP+10] ; n
DEC DX ; n-1
PUSH DX ; n-1
PUSH BX ; aux (新 to)
PUSH CX ; to (新 aux)
PUSH AX ; from
CALL HANOI_MOVE
ADD SP, 8 ; 清理棧參數
; === 步驟 2: move(from, to) ===
CALL PRINT_STEP
MOV DX, [BP+4] ; from
CALL PRINT_CHAR
LEA DX, arrow
CALL PRINT_STR
MOV DX, [BP+10] ; to
CALL PRINT_CHAR
CALL PRINT_SPACE
; === 遞歸步驟 3: hanoi(n-1, aux, from, to) ===
MOV AX, [BP+4] ; from
MOV BX, [BP+6] ; aux
MOV CX, [BP+8] ; to
MOV DX, [BP+10] ; n
DEC DX ; n-1
PUSH DX ; n-1
PUSH CX ; to (新 to)
PUSH AX ; from (新 aux)
PUSH BX ; aux (新 from)
CALL HANOI_MOVE
ADD SP, 8
RET
HANOI_MOVE ENDP
; ... PRINT_STEP, PRINT_CHAR, PRINT_STR, PRINT_SPACE 等輔助子程序 ...
CODE ENDS
END START
| 維度 | C 語言遞歸 | 匯編手動棧 |
|---|---|---|
| 代碼行數 | ~15 行 | ~100+ 行 |
| 參數傳遞 | 編譯器自動壓棧/出棧 | 程序員手動 PUSH/POP,嚴格遵守調用約定 |
| 返回地址 | 自動處理 | CALL 自動壓入,RET 自動彈出 |
| 局部變量 | 自動分配棧空間 | 需手動 SUB SP, size 分配,ADD SP, size 釋放 |
| 可讀性/維護性 | 極高 | 極低,易出現棧不平衡錯誤 |
| 性能 | 編譯器優化極好 | 可手工優化寄存器使用,但現代編譯器通常更強 |