The Nature of Code閱讀心得與Python實作:8.2 Recursion

更新 發佈閱讀 21 分鐘

除了自我相似性之外,碎形的另一個基本組成部分是遞迴(recursion)。遞迴指的是,利用稱為製作規則(production rule)的同一套規則,不斷地進行迭代,而且每次迭代時,都會把上一次迭代的結果,作為這次迭代的起點。

自從德國數學家Georg Cantor在1883年介紹了後來被稱為Cantor集(Cantor set)的無限集合(infinite set)後,儘管那時fractal這個字還沒被造出來,但碎形的種子就此在現代數學中生根、發芽。Cantor用來製作Cantor集的方法非常簡單,就只是運用遞迴的方式,不斷地把線段三等分並挖掉中間那部分而已;如下圖所示:

raw-image

從上圖可以看出來,製作Cantor集時,所使用的製作規則是:把線段三等分並挖掉中間那部分。製作的過程則是從一段線段開始,遵循製作規則處理完這條線段之後,會得到兩條比較短的線段,然後又再遵循製作規則來處理這兩條比較短的線段,進而得到四條更短的線段。接下來,再繼續遵循製作規則來處理那四條線段,並得到八條更短的線段,然後再遵循製作規則來處理那八條線段;如此不斷地繼續下去,最後就可以得到Cantor集。很明顯地,這就是一個遞迴的過程。

Cantor在製造Cantor集時,是假設遞迴過程不斷地進行下去永不止歇,這樣才能得到無限集合。當然啦,這種無限的過程,就只存在數學家的抽象思維中,在真實世界是不存在的。所以,在寫程式以遞迴來處理問題時,必須設定好,在特定的條件下,遞迴必須終止,不再繼續下去;這個特定的條件就叫做基底情況(base case),因為已經做到底了,夠了!不能再繼續下去了,該停了!那如果不設定基底情況會怎樣?程式會像無窮迴圈一樣,一直一直的跑下去嗎?當然不會!遞迴程式最後不是把系統的資源給吃光,就是會因為超過能夠允許的遞迴深度上限值而出現錯誤。

Implementing Recursive Functions

遞迴函數指的是會呼叫自己的函數;通常長這樣:

def some_function():
:
some_function()

最常被用來說明遞迴函數運算方式的,應該就是數學上的階乘(factorial)了。對於正整數n而言,其階乘記為n!,並定義為:

n! = n × (n−1) × (n−2) × ⋯ × 3 × 2 × 1

另外,0的階乘定義為

0! = 1

從定義就可以推導出,對於正整數n而言

n! = n × (n−1)!

這條式子挺簡單的,不過也就是這條式子,讓階乘成為解釋遞迴函數時的最佳代言人。怎麼說呢?把式子再詳細拆解下去就知道了:

    n! = n × (n−1)! 
(n−1)! = (n−1) × (n−2)!
(n−2)! = (n−2) × (n−3)!
:
:
3! = 3 × 2!
2! = 2 × 1!
1! = 1

上面這一大串式子,不就是遞迴的過程嗎?寫成程式,可以這樣寫:

def factorial(n):
# 基底情況
if n in (0, 1):
return 1

return n*factorial(n-1)

從實際的例子來看,會更容易了解這個遞迴函數的運作方式。假設我們要計算$3!$,呼叫factorial(3)之後,整個運作過程是這樣的:

factorial(3)                      6
↓ ↑
return 3*factorial(2) 3*2
↓ ↑
return 2*factorial(1) 2*1
↓ ↑
return 1 -----> 1

從這裡也可以看出來,遞迴就是一層一層的呼叫下去;越來越深、越來越深,深到抵達基底情況才停止。所以說,基底情況就是整個遞迴大廈的地基,是最底部的結構,再下去就沒東西了。沒有基底情況這個地基,遞迴大廈是蓋不起來的;這或許就是為什麼要用「base」這個字來描述這個地基的原因吧。

計算階乘除了用遞迴來寫之外,當然也可以用迴圈來寫:

def factorial(n):
if n in (0, 1):
return 1

f = n
for i in range(n-1, 1, -1):
f *= i

return f

事實上,用遞迴寫的東西,改用迴圈寫也可以,只不過用遞迴來寫會比較直覺,程式的可讀性也比較好;但是,有一好沒兩好,遞迴的效率通常會比迴圈差,也會佔用比較多的記憶體。

在結束階乘這個話題前必須提醒一下,上面計算階乘的函數factorial(),不管是遞迴版或迴圈版,因為主要是要說明遞迴是什麼,所以就都沒有去判斷使用者給的n是不是0或正整數,以免不相干的程式碼影響說明及閱讀。比較完整的寫法,這部分還是應該要加上去。

接下來,就用遞迴的方式來畫些漂亮的圖案。

Example 8.1: Recursive Circles Once

raw-image
def draw_circles(surface, x, y, radius):
# 基底情況
if radius < 4:
return

pygame.draw.circle(surface, (0, 0, 0), (x, y), radius, 1)

radius *= 0.75
# 呼叫自己來畫圓心位置相同,但半徑比較小的圓
draw_circles(surface, x, y, radius)


# python version 3.10.9
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 8.1: Recursive Circles Once")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

screen.fill(WHITE)

draw_circles(screen, WIDTH//2, HEIGHT//2, WIDTH//2)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

pygame.display.update()
frame_rate.tick(FPS)

Example 8.2: Recursive Circles Twice

遞迴時,可以呼叫自己不止一次。在這個例子中是呼叫自己兩次,在左、右畫出比較小的圓。

raw-image
def draw_circles(surface, x, y, radius):
# 基底情況
if radius < 4:
return

pygame.draw.circle(surface, (0, 0, 0), (x, y), radius, 1)

# 呼叫自己,在左、右畫半徑比較小的圓
draw_circles(surface, x-radius/2, y, radius/2)
draw_circles(surface, x+radius/2, y, radius/2)


# python version 3.10.9
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 8.2: Recursive Circles Twice")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

screen.fill(WHITE)

draw_circles(screen, WIDTH//2, HEIGHT//2, WIDTH//2)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

pygame.display.update()
frame_rate.tick(FPS)

Example 8.3: Recursive Circles Four Times

呼叫自己四次,在左、右、上、下畫出比較小的圓。

raw-image
def draw_circles(surface, x, y, radius):
# 基底情況
if radius < 16:
return

pygame.draw.circle(surface, (0, 0, 0), (x, y), radius, 1)

# 呼叫自己,在左、右、上、下畫半徑比較小的圓
draw_circles(surface, x-radius/2, y, radius/2)
draw_circles(surface, x+radius/2, y, radius/2)
draw_circles(surface, x, y-radius/2, radius/2)
draw_circles(surface, x, y+radius/2, radius/2)


# python version 3.10.9
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 8.3: Recursive Circles Four Times")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 240
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

screen.fill(WHITE)

draw_circles(screen, WIDTH//2, HEIGHT//2, WIDTH//2)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

pygame.display.update()
frame_rate.tick(FPS)

Drawing the Cantor Set with Recursion

先前提到過Cantor集,現在就來寫個畫Cantor集的遞迴函數。

製作Cantor集的基本動作,也就是製作規則,是把一條線段三等分,然後挖掉中間那部分;不斷地針對剩下的線段重複這個基本動作,就可以得到Cantor集了。不過,在寫程式畫Cantor集時,因為我們是要把每一次做完基本動作之後的結果都畫出來,所以並不需要那麼麻煩,先把線段畫出來,然後再塗掉中間那部分;我們可以就只畫出左、右兩條線段就可以了,反正結果都一樣。

原理很簡單,那程式要怎麼寫呢?第一步,當然是要畫出一條線段囉。程式可以這樣寫:

def cantor(surface, x, y, length):
pygame.draw.line(surface, (0, 0, 0), (x, y), (x+length, y))

這樣就可以呼叫cantor()來畫出一條起點為(x, y),長度為length的水平線段了。

畫好一條線段之後,再來就是要畫左、右兩條長度為length/3的線段。在這裡要注意一下,這兩條線段要畫在原來那條線段下面一點的地方,要不然所有的線都疊在一起,看起來就跟原來那條線段沒兩樣;有畫等於沒畫。假設我們要把這兩條線段畫在原來那條線段下面20個像素的位置,那這兩條線段的起點位置會在(x, y+20)(x+2*length/3, y+20)。加入這部分程式的cantor(),會變成

def cantor(surface, x, y, length):
pygame.draw.line(surface, (0, 0, 0), (x, y), (x+length, y))

cantor(surface, x, y+20, length/3)
cantor(surface, x+2*length/3, y+20, length/3)

當呼叫cantor()之後,會用pygame.draw.line()畫出一條線段,接著又呼叫cantor(),除了在下方畫出較短的線段之外,還會再呼叫cantor()。這時候,除了在更下方畫出更短的線段外,還會再呼叫cantor()。如此這般,就這樣不斷地輪迴下去,展現出遞迴的本色。

處理完畫線的部分後,最後在cantor()中,還得加上基底情況,以免無窮地遞迴下去。假設我們希望線段的長度小於2個像素時就停止遞迴,不再繼續畫出任何線段;加上這個基底情況後,完整的cantor()長這樣:

def cantor(surface, x, y, length):
# 基底情況
if length < 2:
return

pygame.draw.line(surface, (0, 0, 0), (x, y), (x+length, y))

cantor(surface, x, y+20, length/3)
cantor(surface, x+2*length/3, y+20, length/3)

下面這個例子,就是利用cantor()來畫出Cantor集。

Example 8.4: The Cantor Set

raw-image

主程式如下:

# python version 3.10.9
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Example 8.4: The Cantor Set")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 120
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

screen.fill(WHITE)

cantor(screen, 10, 10, 620)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

pygame.display.update()
frame_rate.tick(FPS)

Exercise 8.1

在H字形圖案兩條垂直線的4個端點,畫上比較小的H字形圖案;不斷地重複這個步驟就可以了。

raw-image
def draw_Hs(surface, x, y, width, height):
# 基底情況
if width < 4:
return

# 畫H字形圖案
hx, hy = width/2, height/2
pygame.draw.line(surface, (0, 0, 0), (x-hx, y), (x+hx, y))
pygame.draw.line(surface, (0, 0, 0), (x-hx, y-hy), (x-hx, y+hy))
pygame.draw.line(surface, (0, 0, 0), (x+hx, y-hy), (x+hx, y+hy))

# 在H字形圖案的垂直線段端點畫小一點的H字形圖案
width *= 0.45
height *= 0.45
draw_Hs(surface, x-hx, y-hy, width, height)
draw_Hs(surface, x-hx, y+hy, width, height)
draw_Hs(surface, x+hx, y-hy, width, height)
draw_Hs(surface, x+hx, y+hy, width, height)


# python version 3.10.9
import sys

import pygame # version 2.3.0


pygame.init()

pygame.display.set_caption("Exercise 8.1")

WHITE = (255, 255, 255)

screen_size = WIDTH, HEIGHT = 640, 320
screen = pygame.display.set_mode(screen_size)

FPS = 30
frame_rate = pygame.time.Clock()

screen.fill(WHITE)

draw_Hs(screen, WIDTH/2, HEIGHT/2, WIDTH/2, HEIGHT/2)

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

pygame.display.update()
frame_rate.tick(FPS)


留言
avatar-img
留言分享你的想法!
avatar-img
ysf的沙龍
19會員
157內容數
寫點東西自娛娛人
你可能也想看
Thumbnail
題目敘述 Combination Sum II_Leetcode #40 給定一個整數陣列candidates 和 目標值target。 每個陣列元素只能選擇一次,請問有多少種組合方法,可以使得組合數總和 = target? 請把滿足組合數總和 = target的組合方法以陣列的形式返回答案
Thumbnail
題目敘述 Combination Sum II_Leetcode #40 給定一個整數陣列candidates 和 目標值target。 每個陣列元素只能選擇一次,請問有多少種組合方法,可以使得組合數總和 = target? 請把滿足組合數總和 = target的組合方法以陣列的形式返回答案
Thumbnail
題目敘述 Distinct Subsequences 給定一個字串s和目標t,請問有多少個s的子序列可以完美匹配目標t ? 也就是說,有多少個s的子序列和目標t相等? 測試範例 Input: s = "rabbbit", t = "rabbit" Output: 3
Thumbnail
題目敘述 Distinct Subsequences 給定一個字串s和目標t,請問有多少個s的子序列可以完美匹配目標t ? 也就是說,有多少個s的子序列和目標t相等? 測試範例 Input: s = "rabbbit", t = "rabbit" Output: 3
Thumbnail
Continuous Subarray Sum 給定一個整數陣列,請問是否存在一段區間和能夠整除k的連續區間,而且區間長度≥2? 如果存在,返回True。 無果無解,返回False。 例如[2,5,3,1,8,6], k = 6, 其中[3,1,8]是區間和能夠整除6的連續區間,而且區間長度≥2
Thumbnail
Continuous Subarray Sum 給定一個整數陣列,請問是否存在一段區間和能夠整除k的連續區間,而且區間長度≥2? 如果存在,返回True。 無果無解,返回False。 例如[2,5,3,1,8,6], k = 6, 其中[3,1,8]是區間和能夠整除6的連續區間,而且區間長度≥2
Thumbnail
給定兩個輸入整數陣列, 若在兩個陣列遇到相同的數字可以連成一線, 但是有規定連線不可和別的連線有交叉, 請問最多可以形成幾條連線? 解答中探討了演算法化簡的技巧和DP模型, 可以透過演算法化簡的技巧, 把這題映射到原本已經學會的Longest Common Subsequence的DP模型來解開。
Thumbnail
給定兩個輸入整數陣列, 若在兩個陣列遇到相同的數字可以連成一線, 但是有規定連線不可和別的連線有交叉, 請問最多可以形成幾條連線? 解答中探討了演算法化簡的技巧和DP模型, 可以透過演算法化簡的技巧, 把這題映射到原本已經學會的Longest Common Subsequence的DP模型來解開。
Thumbnail
題目敘述 題目會給我們一個輸入陣列nums,要求我們判斷輸入陣列nums內部是否存在長度為三的遞增子序列? 題目的原文敘述 測試範例 Example 1: Input: nums = [1,2,3,4,5] Output: true Explanation: Any triplet wh
Thumbnail
題目敘述 題目會給我們一個輸入陣列nums,要求我們判斷輸入陣列nums內部是否存在長度為三的遞增子序列? 題目的原文敘述 測試範例 Example 1: Input: nums = [1,2,3,4,5] Output: true Explanation: Any triplet wh
Thumbnail
題目敘述 題目會給兩個陣列nums1和nums2。 題目要求我們從中同步選擇長度為k的子序列,並且最大化子序列的分數, 回傳最高的分數值。 分數的定義: 分數 = (nums1[i0] + nums1[i1] +...+ nums1[ik - 1]) * min(nums2[i0] ,
Thumbnail
題目敘述 題目會給兩個陣列nums1和nums2。 題目要求我們從中同步選擇長度為k的子序列,並且最大化子序列的分數, 回傳最高的分數值。 分數的定義: 分數 = (nums1[i0] + nums1[i1] +...+ nums1[ik - 1]) * min(nums2[i0] ,
Thumbnail
題目敘述 題目會給我們一個參數k 和 目標值n。 請問我們從1~9內挑k個相異的數字,使得他們的總和為n 的組合數有多少? 挑選時,每個數字必須相異,而且每個數字只能選一次。 題目的原文敘述 測試範例 Example 1: Input: k = 3, n = 7 Output: [
Thumbnail
題目敘述 題目會給我們一個參數k 和 目標值n。 請問我們從1~9內挑k個相異的數字,使得他們的總和為n 的組合數有多少? 挑選時,每個數字必須相異,而且每個數字只能選一次。 題目的原文敘述 測試範例 Example 1: Input: k = 3, n = 7 Output: [
Thumbnail
題目敘述 題目的情境是設計並且實現一個包含所有正整數的數據流,以set集合的方式存在。 數據流 = {1, 2, 3, 4, ..., ∞} 要求我們去實現定義好的function介面: SmallestInfiniteSet()建構子,初始化這個包含所有正整數的數據流。 int po
Thumbnail
題目敘述 題目的情境是設計並且實現一個包含所有正整數的數據流,以set集合的方式存在。 數據流 = {1, 2, 3, 4, ..., ∞} 要求我們去實現定義好的function介面: SmallestInfiniteSet()建構子,初始化這個包含所有正整數的數據流。 int po
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News