前言
本人是一個長期的數據分析愛好者,最近半年的時間的在網上學習了很多關於python、數據分析、數據挖掘以及項目管理相關的課程和知識,但是在學習的過程中,過於追求課程數量的增長,長時間關注於學習了多少多少門課程。事實上,學完一門課之後真正掌握的知識並不多,主要的原因是自己沒有認真學習和理解溫故而知新的這句話的真正含義。因此,從現在開始,我在學習《數據結構與算法——基於python》的課程內容之後,抽出固定的時間對每天學習的內容進行總結和分享,一方面有助於個人更好的掌握課程的內容,另一方面能和大家一起分享個人的學習歷程和相應的學習知識。
第三節 遞歸
## 基礎知識:
3.1 遞歸函數是一種自我調用的函數。
遞歸的兩個必要條件:基礎條件、F(n)和F(n-1) 的關係;
遞歸程序一般需要的時間複雜度和空間複雜度都比較大,但是對於for循環不適合的程序,就需要用遞歸來完成;
在python中定義了一個遞歸的最大深度,當遞歸深度超過定義的之後,遞歸算法將不再適用;
遞歸應用:
- *Simple Example 求和
傳統方式
n = 10
result = sum(range(n+1))
result
或者
def mysum(n):
result = 0
for i in range(n+1):
result += i
return result
result = mysum(10)
result
遞歸方法
def mysum_recursive(n):
if n == 0:
return 0
return n + mysum_recursive(n-1)
result = mysum_recursive(300)
result
當遞歸運算的次數太多,超過python規定的遞歸深度之後,程序就會報錯;
2. *Ex.2 階乘
傳統方式
def factorial(n):
result = 1
for i in range(1, n+1):
result *= i
return result
factorial(5)
遞歸方法
def factorial_recursive(n):
if n == 1:
return 1
return n * factorial_recursive(n - 1)
factorial_recursive(5)
- *Ex.3 斐波那契數
方法1
O(n)
def fibonacci1(n):
assert(n>=0)
a, b = 0, 1
for i in range(1, n+1):
a, b = b, a + b
return a
time fibonacci1(40)
方法2
O(2^n)
def fibonacci2(n):
assert(n>=0)
if (n <= 2):
return 1
return fibonacci2(n-1) + fibonacci2(n-2)
time fibonacci2(40)
方法3
def fibonacci3(n):
assert(n>=0)
if (n <= 1):
return (n,0)
(a, b) = fibonacci3(n-1)
return (a+b, a)
time fibonacci3(40)
通過上述三個不同方法之間的對比,從輸出結果都是一致且是正確的,但是從時間複雜度上來講,三種方法中,方法1最快,方法2其次,方法3最慢,我敲完這段話至少兩分鐘的時間,還沒有算出來!
爲什麼要計算斐波那契數列呢
計算一下每一項和前一項之間的比例;
def fibonaccis(n):
assert(n>=0)
result = [0, 1]
for i in range(2, n+1):
result.append(result[-2] + result[-1])
return result
fibos = fibonaccis(30)
r = []
for i in range(2, len(fibos)):
r.append(fibos[i] / fibos[i-1])
r
輸出結果,當數據運行的越來越大之後,兩項之比都接近一個固定的數字,1.618,這是我們所說的黃金比例;
事實上,自然界中很多的生物都具有黃金比例,肚臍到腳的距離/身高=0.618
4. Ex.4 畫尺子
先輸出如下所示的數字串
1
1 2 1
1 2 1 3 1 2 1
1 2 1 3 1 2 1 4 1 2 1 3 1 2 1
思路
根據上述情況可以看出,數據是一個遞歸的狀態;第n項等於第n-1項+n + 第n-1項;根據上述思路,可以寫出如下的三種代碼;
代碼
# O(2^n)
def ruler_bad(n):
assert(n>=0)
if (n==1):
return "1"
return ruler(n-1) + " " + str(n) + " " + ruler(n-1)
# O(n) 將前一個計算結果存儲起來,減少重複計算,降低時間複雜度
def ruler(n):
assert(n>=0)
if (n==1):
return "1"
t = ruler(n-1)
return t + " " + str(n) + " " + t
# O(n) 用for 循環的方式
def ruler2(n):
result = ""
for i in range(1, n+1):
result = result + str(i) + " " + result
return result
ruler_bad(3)
ruler(3)
ruler2(3)
輸出結果
根據上述思路可以寫出如下圖所示的尺子的遞歸程序;
代碼
def draw_line(tick_length, tick_label=''):# 畫中間的最高線
line = '-' * tick_length
if tick_label:
line += ' ' + tick_label
print(line)
def draw_interval(center_length): # 畫中間的刻度
if center_length > 0:
draw_interval(center_length - 1)
draw_line(center_length)
draw_interval(center_length - 1)
def draw_rule(num_inches, major_length): # 畫尺子程序
draw_line(major_length, '0')
for j in range(1, 1 + num_inches):
draw_interval(major_length - 1)
draw_line(major_length, str(j))
draw_interval(2)
draw_rule(3,3)
輸出結果
5. Ex.5 數學表達式
給定兩個等於a≤b的整數,編寫一個程序,以最小的遞增(加1)和展開(乘以2)操作序列將a轉換爲b。
例如,
23 =((5 * 2 +1)* 2 +1)
113 =(((((11 +1)+1)+1)* 2 * 2 * 2 +1)
思路
根據題目要求,a≤b,且只能用+1,和X2的操作,使算式相等。可分爲以下三種情況;
1、b<2a, 只能做加法
2、b<2a,將a乘以2,然後再做加1;
3、b=2a,或者b=a,這是兩種基礎情況;
根據第1、2 兩種情況判斷是做+1還是X2的操作。再判斷奇偶性。
按照上述思路,可以寫出如下代碼;
def intSeq(a, b):
if (a == b): # base
return str(a)
if (b % 2 == 1): # 說明b是一個奇數,可先將b變成b-1,求得b-1和a的關係,再+1
return "(" + intSeq(a, b-1) + " + 1)"
# b是偶數的情況
if (b < a * 2): # 如果b<2a,只能一個一個做+1操作。
return "(" + intSeq(a, b-1) + " + 1)"
# 剩下的情況就是b>2a,且b爲偶數。
return intSeq(a, b/2) + " * 2";
a = 5;
b = 101;
print(str(b) + " = " + intSeq(a, b))
輸出結果如下所示;
6. Ex.6 漢諾塔問題
思路
根據規則要求,可以自己推斷出3階漢諾塔需要7步,四階漢諾塔需要15步,5階需要31步,可以分析出規律。f(n)=2^n-1次;需要程序輸出每一步移動的過程,說明從哪兒到哪兒。
對於三界漢諾塔,需要下列的步驟;
同時需要輸出f(n)的步驟,根據遞歸思想。
base: f(1)就是L到R;
遞歸:f(n),假設知道f(n)的答案,推導f(n+1)。f(n)和f(n+1)之間的關係如下圖所示;
由於需要表明移動的位置,應該在函數中定義初始位置,中間位置,末尾位置;
按照上述思路,可以寫出如下代碼;
def hanoi(n, start, end, by):
if (n==1):
print("Move from " + start + " to " + end)
else:
hanoi(n-1, start, by, end)
hanoi(1, start, end, by)
hanoi(n-1, by, end, start)
n = 3
hanoi(n, "START", "END", "BY")
輸出結果;
7. Ex.7 格雷碼
每一次和上一次有一個數字是不一樣的,0000變爲0001,按照這樣的規律改變;
給定兩個等於a≤b的整數,編寫一個程序,以最小的遞增(加1)和展開(乘以2)操作序列將a轉換爲b。當第i個位置由0變成1之後,表示爲enter i,由1變成0之後,表示爲exit i,再進行enter 操作,如此循環。
思路:
通過圖片中的move 那一列的數據:121312141213121,和前面的畫尺子那道題是類似的,對於121312141213121中,每一個數字第一次出現的時候,是enter。第二次出現之後變成exit,然後不再變化。
f(1)的時候,enter 1;f(2)的時候,enter 1,enter 2,exit 1;f(3)的時候,enter 1,enter 2,exit 1,enter 3,enter 1,exit 2,exit 1;首先得有enter,然後再有exit。先enter,然後調用下一級的code,enter 1,然後調用本身自己的exit,enter 自己,enter 下一級,exit 自己。
按照上述思路;可以寫出如下程序;
程序
def moves_ins(n, forward): # forward 的不同的值來表示輸出enter 還是exit
if n == 0:
return
moves_ins(n-1, True)
print("enter ", n) if forward else print("exit ", n)
moves_ins(n-1, False)
moves_ins(3, True) #初始化情況都爲true
輸出結果!
8. Ex.8 子集
子集的定義:子集是一個數學概念,如果集合A的任意一個元素都是集合B的元素(任意a∈A則a∈B),那麼集合A稱爲集合B的子集。
在n個元素中,對於每個元素,存在兩種狀態,選擇或者不選擇,因此,對於含有n個元素的集合,共有2^n個子集。編寫一個程序,輸出該集合的所有子集。
對於含有a、b、c三個元素的集合;首先取出空集,成爲一個子集,對於a,先取出來,然後空集加上a,得到兩個子集,空集和a。對於b,把之前的兩個集合加上b,空集中加上b,a裏面加上b,形成了四個集合,然後對於c,將c分別加到上述四個集合裏面,形成了4個新的子集,一共形成了8個子集。具體如下圖所示;
根據上述思路,可以寫出如下的程序。
def subsets(nums):
result = [[]] # 一個二維數組
for num in nums:
for element in result[:]: # 建立一個result的copy 不能在原始數據上進行操作
x=element[:] # 建立一個element的copy 不能在原始數據上進行操作
x.append(num)
result.append(x)
return result
nums = [1, 2, 3]
print(subsets(nums))
輸出結果;
根據輸出結果,可以判定該程序是正確的。
留下兩個問題:
1、 result[:] 這裏爲什麼要make a copy
2、element[:] 這裏爲什麼要make a copy
另外一種方法:
具有通用性,對於其他語言也可以按照這種方式;
還是用之前a、b、c的三個例子,構建一個搜索樹;
剛開始有一個空集,對於a,如果使用了a,b和c還沒有使用,可以得到一個a。繼續使用b,得到ab,在ab的基礎上,考慮c,得到abc。使用c,得到ac;得到 空集、a、ab、abc、ac這五種選項,這時,a這條路走完了。對於b這條搜索路,可以得到b、bc這兩種。對於c這個搜索樹,得到c;一共得到了8個子集;
在上述過程中,如果路走不通了,就往回走,和深度搜索一樣,在往回追溯的過程中,應當恢復到第一次訪問的狀態,如下圖所示;
按照上述思路可以寫出下列程序;
# 最好能把這個程序背下來,面試被抽到的機率很大
def subsets_recursive(nums):
lst = [] # 初始化一個空集
result = [] # 最好返回的結果,將list放入result裏面
# helper 輔助性的函數。
subsets_recursive_helper(result, lst, nums, 0);
return result;
def subsets_recursive_helper(result, lst, nums, pos):
result.append(lst[:]) # 先將result加進來,create了一個list_copy
for i in range(pos, len(nums)): #考察未被放入的元素,剛開始將a放進去;
lst.append(nums[i])
subsets_recursive_helper(result, lst, nums, i+1) # a放進去之後,不用再加入a了,所有pos需要+1操作,
# 等所有的元素都進入到result裏面之後,開始回溯,回溯的要求是和第一次訪問的狀態相同,此時是abc,應該將c去掉,變成ab,
lst.pop() # c在最上面,進行出棧操作,ab也已經出現了。繼續出棧,變成a,繼續探索ac
nums = ['a', 'b', 'c']
print(subsets_recursive(nums))
程序結合圖片理解更好!
輸出結果;
9. **Ex.2 子集 II **
給定一組可能包含重複項的整數,則返回所有可能的子集,返回子集的集合裏面,不能有重複的元素。
思路
對於 a、b、b,建立一個數組,當a[n]= a[n-1]的時候,可以認爲該元素已經出現過,和上面的例子相比,增加一個排序的功能,a[n]= a[n-1]的時候,就跳出循環。
根據上述思路;可以寫成如下程序;
def subsets_recursive2(nums):
lst = []
result = []
nums.sort() #對input進行排序。
#print(nums)
subsets2_recursive_helper(result, lst, nums, 0);
return result;
def subsets2_recursive_helper(result, lst, nums, pos):
result.append(lst[:])
for i in range(pos, len(nums)):
if (i != pos and nums[i] == nums[i-1]):
continue; # 當前的數字和之前的數字相等時,跳出此次循環
lst.append(nums[i])
subsets2_recursive_helper(result, lst, nums, i+1)
lst.pop()
nums = [1, 2, 3])
print(subsets_recursive2(nums))
- **Ex.10 排列組合 **
Given abc:
Output: bca cba cab acb bac abc
輸出n個集合的所有的排列組合,n個元素共有n的階乘個子集。
思路
對於 a、b、c,首先搜索a,然後剩下b和c,可以選擇b,接着選擇c,變成abc,此時沒有其他選擇了。先選擇,或者先選擇c,按照和a同樣的規則即可。
按照上述思路,可以寫出一下程序;
def perm(result, nums):
if (len(nums)==0): # 當數組大小爲零時(nums爲空,表示所有的元素已經排列完),就將數組打印出來。
print(result)
for i in range(len(nums)):
perm(result+str(nums[i]), nums[0:i]+nums[i+1:]) # nums[0:i]+nums[i+1:] 表示除第i個元素後剩餘的元素。
nums = [1, 2, 3]
perm('', nums)
輸出結果
11. **Ex.11 唯一排列組合 **
輸出n個集合的所有的唯一的排列組合,n個元素共有n的階乘個子集,
思路
和唯一子集中的元素方法詳細,先對每個元素進行排序,然後對已經操作過的元素不在進行操作。根據此思路,得出的代碼如下;
代碼
def permUnique(result, nums):
nums.sort() # 首先進行排序
if (len(nums)==0):
print(result)
for i in range(len(nums)):
if (i != 0 and nums[i] == nums[i-1]):
continue; # 對於重複的元素,進行剪枝,已經出現的元素不再進行操作。
permUnique(result+str(nums[i]), nums[0:i]+nums[i+1:])
nums = [1, 2, 3]
permUnique('', nums)
nums = [3, 2, 3]
permUnique('', nums)
對比運行結果;對於3,2,3.這個集合沒有輸出重複的子集。
12. **Ex.12 K個元素的排列組合 **
取兩個參數n和k,並打印出所有P(n,k)= n! /(n-k)! 恰好包含n個元素中的k個的排列。 當k = 2且n = 4時。
ab ac ad ba bc bd ca cb cd da db dc
思路
和子集一樣的思路,只是當k=0 的時候選擇結束,表示裏面的元素已經夠兩個了;
代碼
def permSizeK(result, nums, k):
if k == 0: # 當k=0時,表示已經選出了講個元素。
print(result)
for i in range(len(nums)):
permSizeK(result+str(nums[i]), nums[0:i] + nums[i+1:], k - 1)
nums = [1, 2, 3, 4]
k = 2
permSizeK('', nums, k)
輸出結果
13. **Ex.13 字母大小寫排序 **
枚舉輸入中指定的任何字母的所有大寫/小寫排列;
For example,
word = “medium-one”
Rule = “io” i和o都有大寫和小寫,然後做一個排列組合
solutions = [“medium-one”, “medIum-one”, “medium-One”, “medIum-One”]
14 Ex.14 組合總和
給定一組候選編號(候選)(無重複)和目標編號(目標),在候選編號總和爲目標的候選中找到所有唯一組合。在一個數組中間,找到一個子集,使得子集的和目標值相等。
可以從候選人無限制的次數中選擇相同的重複數。
思路
base:每次找到一個數字之後,用目標值減掉這個數字,剩餘的數值是下面要搜索的路徑。當剩餘值爲零的時候,就找到了。
剩下的思路和尋找子集題目的思路一致,根據上述思路可以寫出下列程序。
代碼
def comb(nums, t):
result = []
tmp = []
combHelper(result, tmp, nums, t, 0)
return result
def combHelper(result, tmp, nums, remains, start):
if remains < 0: return
if remains == 0: # 當remains爲零的時候,表示結束
result.append(tmp[:])
else:
for i in range(start, len(nums)):
tmp.append(nums[i])
combHelper(result, tmp, nums, remains - nums[i], i)
tmp.pop()
candidates = [2,3,6,7]
t = 7
comb(candidates, t)
輸出結果
14 Ex.14 組合總和 II
給定候選編號(候選)和目標編號(目標)的集合,找到候選編號總和爲目標的候選中的所有唯一組合。
候選中的每個數字在組合中只能使用一次。
注意:
所有數字(包括目標)將爲正整數。
解決方案集不得包含重複的組合。
思路
在上一題的基礎上加了一個唯一值的限制,和前面的唯一值的思路一樣。先對元素進行排序,然後對於當前值和前面的值相等的直接跳過循環。
代碼
def comb2(nums, t):
result = []
tmp = []
nums.sort() # 先排序
combHelper2(result, tmp, nums, t, 0)
return result
def combHelper2(result, tmp, nums, remains, start):
if remains < 0: return
if remains == 0:
result.append(tmp[:])
else:
for i in range(start, len(nums)):
if(i > start and nums[i] == nums[i-1]): continue; # skip duplicates 對於相同的直接跳過
tmp.append(nums[i])
combHelper2(result, tmp, nums, remains - nums[i], i + 1)
tmp.pop()
candidates = [10,1,2,7,6,1,5]
t = 3
comb2(candidates, t)
對比輸出結果,根據輸出結果,可以看出,可以當加unique的限制之後,結果中沒有出現兩個連續的相同的結果;
15 Ex.15 括號
給定n對括號,編寫一個函數以生成格式正確的括號的所有組合。
思路
何謂好的括號配對,首先出現左括號,一共n對括號,有n個左括號和n個右括號,當用了一個左括號之後,還剩下n-1個左括號,n個右括號。
再打一個左括號,剩下n-2個左括號,n個右括號,可以打印完n個左括號,然後再打印n個右括號。
每一次先使用左括號;
具體過程如下圖所示;
根據上述思路寫出以下程序;
# str 不需要進行pop操作
# list 需要進行pop操作
def generateParenthesis(n):
def generate(prefix, left, right, parens=[]):
if right == 0: parens.append(prefix) #當右括號用完了,說明左括號也用完了。此時加到結果集裏面去。
if left > 0: generate(prefix + '(', left-1, right)# 當左括號還有的時候,繼續遞歸調用,
if right > left: generate(prefix + ')', left, right-1)# 當右括號大於左括號之後,說明左括號用完,上一條路已經走完,加上右括號,右括號數量減一。
return parens
return generate('', n, n)
generateParenthesis(4)
輸出結果;