從N-1到N——理解遞歸的一種思路

從面向過程中的循環控制語句,轉變到函數式編程中無處不在的遞歸,往往需要有一個適應的過程。

本文以求一個列表的全排列為例,描述一種從數學歸納法的角度來理解遞歸的方式,使用這種思路可以更加容易地寫出正確的遞歸來。

數學歸納法的原理在于:首先證明在某個起點值時命題成立,然后證明從一個值到下一個值的過程有效。同樣地,如果我們能求得某一個起點的值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]的全排列中每一個結果的頭部。

例如,如果X1,除了1之外的子列表[L--X]的全排列為[ [2,3], [3,2] ],那么把1加到每個結果元素的頭部,得到[ [1,2,3], [1,3,2] ];同理,分別將X對應為23,執行相同的過程就可以得到最終的結果[ [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)].
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容