1. 程式人生 > >從“漢諾塔”看遞迴演算法

從“漢諾塔”看遞迴演算法

  遞迴演算法是《資料結構與演算法》中最簡潔的演算法之一,它可以非常簡明地描述“減而治之”(decrease and conquer)和“分而治之”(divide and conquer)這兩種演算法思想。遞迴演算法雖然從程式碼角度來看非常簡單,但對於新手理解起來卻不那麼簡單。本文我將結合《資料結構與演算法》的專業描述和《程式設計師的數學》的通俗描述,並以“漢諾塔”為例來講解我對“遞迴”演算法的理解,並給出 Python 程式碼描述。

一、漢諾塔迷題

  “漢諾塔”是由數學家 Edouard Lucas 於1883 年發明的遊戲,具體內容可以看 wiki - 漢諾塔 ,下面給出 3 層漢諾塔的移動示意圖:
          這裡寫圖片描述


  從上圖可以觀察到,過程 1、2、3 和過程 5、6、7是非常相似的,前者將 2 個圓盤從 A 移到 C ;後者將 2 個圓盤從 C 移到 B 。總結如下:
        這裡寫圖片描述

二、遞迴思維

  從上面對“漢諾塔”迷題的觀察和分析可以清楚的反映“遞迴”的思維方式:當我們碰到“簡單問題易解(一層漢諾塔),複雜問題難解(多層漢諾塔)”時,嘗試問自己“能將複雜的問題轉化為較為簡單的同類問題嗎?”
  這就是遞迴的思維方式,對於漢諾塔來說,就是將 n 層漢諾塔轉換為 n-1 層漢諾塔問題,即在問題中找出遞迴結構
        這裡寫圖片描述
  如果找到了遞迴結構,接下來就是根據遞迴結構建立遞迴公式

{H(n)=H(n1)+1+H(n1)H(1)=1  上面的式子就是遞推方程,而遞推方程實際上是一種微分方程,它的特點是:只給出一個隱式的方程式和一個邊界條件(又稱初始條件),然後求解出顯示方程式,即求出微分方程的解析式
H(n)=2n1  這就是 n 層漢諾塔移動的步數。

  換成專業術語來描述:遞迴演算法的關鍵就是找到遞迴基(base case)和遞推公式(recursion relation, recurrence)。其中,遞迴基一般比較容易找到,而遞推公式往往隱藏在大量的細節中,需要我們去分析提取。一般來說,分析提出遞迴公式,先要歸納出遞迴例項

(同類問題,規模不同),比如 n 層漢諾塔問題和 n-1 層漢諾塔問題。有了遞迴例項的概念,然後需要逆向思維(倒推),即觀察構成更大規模問題的最後幾步。比如:6 層漢諾塔 A –> B 移動完成前,最後一步(一大步)必然是將一個 5 層漢諾塔 C –> B 移動,而倒數第二步必然是最大的圓盤 A –> B 移動。很明顯,這兩步都可以概括為一個遞迴例項。倒推出了這兩步,你也就明白了前面的所有步驟無非就是將一個 5 層漢諾塔 A –> B 移動。至此,就找到了 6 層漢諾塔問題的遞迴結構,然後再推廣到 n 層漢諾塔問題。

三、程式碼實現

  下面通過程式碼來體會漢諾塔問題的遞迴演算法

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

r'''
tower_of_hanoi.py

Usage:

python3 tower_of_hanoi.py
'''

def move(n, source, target, auxiliary):
    # base case
    if n == 1:
        print(source, '-->', target)
        return

    # first step: move n - 1 disks from source to auxiliary, so they are out of the way
    move(n-1, source, auxiliary, target)

    # second step: move the nth disk from source to target
    move(1, source, target, auxiliary)

    # last step: move the n - 1 disks that we left on auxiliary onto target
    return move(n-1, auxiliary, target, source)


if __name__ == '__main__':
    # initiate call from source A to target C with auxiliary B
    move(3, 'A', 'C', 'B')

  從上面的程式碼可以看出:遞迴演算法的確很簡潔,真個演算法,除了 base-case 外,就剩三次遞迴呼叫,這三次遞迴呼叫也清晰的描述了該問題的遞迴結構。