從面向過程中的循環控制語句,轉變到函數式編程中無處不在的遞歸,往往需要有一個適應的過程。
本文以求一個列表的全排列為例,描述一種從數學歸納法的角度來理解遞歸的方式,使用這種思路可以更加容易地寫出正確的遞歸來。
數學歸納法的原理在于:首先證明在某個起點值時命題成立,然后證明從一個值到下一個值的過程有效。同樣地,如果我們能求得某一個起點的值f(1),那么,只要我們知道如何在f(N-1)的基礎上求得f(N),遞歸的問題就可以解決了。以此為思路,遞歸語句就不那么難理解了,也不那么難寫了。
以求一個列表的全排列為例說明這個過程。比如,對于[1,2,3]
這樣的序列,要求返回 [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
。
遞歸的起點是只有一個元素的情況,也就是:
perms([X]) -> [X];
對于列表L
中的每一個元素X
,如果除X
之外的子列表[L--X]
的全排列已經求出來了,那么整個列表L
的全排列就可以求出來了;它們之間的關系是:把X
添加在[L--X]
的全排列中每一個結果的頭部。
例如,如果X
是1
,除了1
之外的子列表[L--X]
的全排列為[ [2,3], [3,2] ]
,那么把1
加到每個結果元素的頭部,得到[ [1,2,3], [1,3,2] ]
;同理,分別將X
對應為2
和3
,執行相同的過程就可以得到最終的結果[ [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1] ]
。
用Erlang代碼表達就是這樣:
perms([X]) -> [X];
perms(L) -> [ [X|T] || X<-L, T<-perms(L--[X])].
對于這個問題,還可以有另外一種解決的思路。對于一個列表L
,如果除第一個元素H
之外的子列表的全排列集已經求出來了,再將H
插入到此集中每一個元素的每一個位置就可以了。
例如:[1,2,3]
的子列表[2,3
]的全排列是[ [2,3], [3,2] ]
,則將1
插入 [2,3]
和[3,2]
中的每一個位置,分別得到[ [1,2,3],[2,1,3],[2,3,1] ]
和[ [1,3,2], [3,1,2],[3,2,1] ]
,然后將這兩個列表合并即可得到最終的結果。
用代碼表示就是:
perms([X]) -> [X];
perms([H|T]) -> lists:append([interleave(H, R) || R <- perms(T)]).
其中interleave的實現如下:
interleave(X, []) -> [X];
interleave(X, [H|T]) ->
[X|[H|T] | [H|R] || R <- interleave(X, T)].