??腦抽的我上個文章爛尾,又開新坑,,,,我只能對自己前面開的爛坑說一句:青山不改,綠水長流,我們有緣再見!
??這次我想刷一刷算法題(對,我又叒叕換目標了),把常見的基礎算法做一個總結(千萬別又是起個頭就扔那里不管了,真的是廢人一個了。。。)
??好,話不多說,遞歸(Recursion)走起!
- 目錄:
算法:附錄
算法(1):遞歸
算法(2):鏈表
算法(3):數組
算法(4):字符串
算法(5):二叉樹
算法(6):二叉查找樹
算法(7):隊列和堆棧(附贈BFS和DFS)
算法(8):動態規劃
算法(9):哈希表
算法(10):排序
算法(11):回溯法
算法(12):位操作
概念理解:
??首先我們分析一下定義,遞歸是一種使用某種函數來解決問題的方法(當然你可以忽略這句廢話),特殊之處便在于該函數會不斷調用它自身來作為其子程序。
??那么,如何實現這么一個函數呢?它調用自己,又是干了些什么呢?
??這個小技巧便是該遞歸函數每一次調用的時候,它都能將給定的問題變成該問題的子問題,該函數會不知疲倦的一直調用它自己(覺得像影分身之術,但是確切點說的話,是那種分身又施展影分身術的 feel...),直到子問題被解決掉,不再產生遞歸為止(所以說,不是子子孫孫無窮匱哦,遞歸沒有我們的愚公爺爺厲害)。
??所以,遞歸函數是一定存在邊界的,也就是終止條件。在遇到某種情況時,遞歸函數將不再調用自身,而是輸出一個結果(當然也可以什么也不輸出,直接結束子程序)。所以寫遞歸函數的時候,一定不要忘了寫終止遞歸的條件(個人建議,先寫這些邊界條件,后面再寫程序處理邏輯)~
??扯了這么多,腦中揮散不去的還是大家對我爆喊 “talk is cheap, show me the code!"的場景,那么,代碼兄,該你登場了(以下題目均來自LeetCode)~
??附注:建議大家看看問題3,因為提到了一個遞歸經常遇到的問題,重復計算,而解決重復計算的方法也很簡單,加入一個記憶機制(Memoization),也就是儲存我們計算過的值即可。
問題1:字符串翻轉,要求,O(1)空間復雜度(看所給的例子輸入,題目應該叫列表翻轉才更貼切)
例子:
輸入: ['a','b','c']
輸出: ['c','b','a']
解決思路:
1.取出首尾兩個字符,并做交換;
2.遞歸調用該函數,來翻轉剩下的子串。
3.設計跳出遞歸的邊界條件,這里是begin >= end,即字符串遍歷完畢。
def reverseString(begin, end,s) -> None:
"""
Do not return anything, modify s in-place instead.
"""
if begin >= end:
return
s[begin], s[end] = s[end], s[begin]
reverseString(begin + 1, end - 1, s)
s = ['a','b','c']
reverseString(0, len(s) - 1, s)
print(s)
### output ###
# ['c', 'b', 'a']
問題2:給定一個鏈表,每兩個相鄰的節點交換位置,并返回頭節點。
例子:
輸入鏈表:1->2->3->4
輸出鏈表:2->1->4->3
解決思路:
1.交換前兩個節點的位置;
2.把剩下的鏈表傳遞給自身,遞歸調用。
3.當抵達鏈表尾部時結束遞歸。
class ListNode: #定義節點類
def __init__(self, x):
self.val = x
self.next = None
def swapPairs(head) -> ListNode: #實現功能的遞歸函數
if head == None or head.next == None:
return head
temp = head.next # 節點2,也就是temp,即為我們所要的head
head.next = swapPairs(temp.next) #將節點1 和后面的鏈表串拼接,也就是(4->3)
temp.next = head # 將節點2的后面接上節點1
return temp #返回節點2
def printListNode(node): #輔助函數,打印鏈表
while node:
print(node.val, end=' ')
if node.next:
print('->', end=' ')
node = node.next
print()
if __name__ == '__main__':
head = node = ListNode(1)
for i in range(2,5):
node.next = ListNode(i)
node = node.next
printListNode(head)
ans = swapPairs(head)
printListNode(ans)
問題3:楊輝三角形(聽說你不知道什么是楊輝三角形,鏈接附上,送給各位看官)
??在討論這個問題之前,為了讓大家看的更清晰,我們先提綱挈領一波,在此討論一下兩個概念,遞推關系和邊界條件。
??遞推關系說白了就是問題結果和子問題的結果之間的關系,而邊界條件我們前面提到過,便是遞歸到了終點,問題無法繼續分解為子問題,可以直接得到答案。
??那么通過這個例子,大家一起來看看這兩個概念的具現到底是何方神圣把~
??首先,我們定義一個函數,,
代表第
行,
代表第
列,那么,我們可以列出遞歸關系式:
其次,再列出邊界條件:
??這樣子,思路是不是就清晰明了一些了呢?當以后遇到復雜的遞歸問題的時候,可以先嘗試列出遞歸關系式和邊界條件,可以方便我們更清晰的整理思路呦~
當然,問題以及代碼如下:
輸入: 5
輸出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
def generate(numRows) -> list:
def helper(i, j):
if j == 0 or j == i:
return 1
return helper(i - 1, j - 1) + helper(i - 1, j)
ans = []
for i in range(0, numRows):
temp = []
for j in range(0, i + 1):
temp.append(helper(i, j))
ans.append(temp)
print(ans)
return ans
if __name__ == '__main__':
generate(5)
??上面我們的helper函數,完美的按照邊界條件和遞歸關系式要求所寫,結果也是沒有任何問題。但是,但是,但是!各位小伙伴試試把generate(5) 換成generate(30)試試(測測你電腦性能 /斜眼笑)?花費的時間多的讓你懷疑人生,哈哈~ 那么問題來了,到底是什么原因導致的呢?
??我們思考一下,當計算的時候,我們需要計算
和
,而這兩個都需要計算一次
,聰明的朋友,你是不是發現了問題所在?對,那就是重復計算(這個地方一定要加黑加粗,因為遞歸非常容易遇到這個問題,大家一定要留意才行)!當你需要顯示的行數愈多時,重復計算的量就會越大。
??所以,上面的程序也就逗小孩子玩玩,實用性幾乎為零(為什么用幾乎?因為它還能逗小孩子玩玩。所以這里充分體現了作者嚴謹的撰文態度)。那么,這種問題該如何解決呢?正解,加入記憶機制,將我們之前計算過的全部儲存下來即可~
??關門,上代碼!
def generate(numRows) -> list:
cache = {}
def helper(i, j):
if (i,j) in cache:
return cache[(i,j)]
if j == 0 or j == i:
return 1
result = helper(i - 1, j - 1) + helper(i - 1, j)
cache[(i,j)] = result
return result
ans = []
for i in range(0, numRows):
temp = []
for j in range(0, i + 1):
temp.append(helper(i, j))
ans.append(temp)
print(ans)
return ans
if __name__ == '__main__':
generate(5)
??這個時候,我們哪怕把5換成500,也是分分鐘的事情,不,秒秒鐘。
問題4:鏈表翻轉(又是一道可惡的鏈表題)
例子:
輸入: 1->2->3->4->5
輸出: 5->4->3->2->1
解決思路:
1.遞歸函數傳遞兩個參數,head 和 pre,head 保存的是原來的鏈表,pre 保存的是翻轉的鏈表。
2.在每次遞歸時,從head上取下來一個節點,然后把 pre 接在該節點后面。
3.在遞歸到最后時,head 節點為空,而 pre 里面則為翻轉好的鏈表。
class ListNode: #定義節點類
def __init__(self, x):
self.val = x
self.next = None
def reverseList( head, pre = None):
if not head: return pre
cur, head.next = head.next, pre
return reverseList(cur, head)
def printListNode(node): #輔助函數,打印鏈表
while node:
print(node.val, end=' ')
if node.next:
print('->', end=' ')
node = node.next
print()
if __name__ == '__main__':
head = node = ListNode(1)
for i in range(2,6):
node.next = ListNode(i)
node = node.next
printListNode(head)
ans = reverseList(head)
printListNode(ans)
問題5:鏈表相加(跟鏈表杠上了)
一個鏈表相當于存了一個數,如鏈表(2 -> 4 -> 3)相當于數字342,我們要做的就是把數讀出來,然后相加,再將結果寫成鏈表形式。
輸入: (2 -> 4 -> 3) + (5 -> 6 -> 4)
輸出: 7 -> 0 -> 8
解釋: 342 + 465 = 807.
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None
self.prev = None
def printListNode(node): #輔助函數,打印鏈表
while node:
print(node.val, end=' ')
if node.next:
print('->', end=' ')
node = node.next
print()
def addTwoNumbers( l1: ListNode, l2: ListNode) -> ListNode:
def toint(node):
return node.val + 10 * toint(node.next) if node else 0
def tolist(n):
node = ListNode(n % 10)
if n >= 10:
node.next = tolist(n // 10)
return node
return tolist(toint(l1) + toint(l2))
if __name__ == '__main__':
head1 = node1 = ListNode(1)
for i in range(2,10,2):
node1.next = ListNode(i)
node1 = node1.next
printListNode(head1)
ans = addTwoNumbers(head1,head1)
printListNode(ans)
??最后的最后,渴望變成天使,,,啊呸,最后的最后,我們來分析一下遞歸問題的時間復雜度和空間復雜度,這也是一個需要我們下功夫思考的地方,那么,Let’s begin!
-
時間復雜度:
??遞歸問題時間復雜度可以由一個公式來表示:
??其中,
為遞歸問題的時間復雜度,
表示為該問題共調用遞歸函數的次數(遞歸花時間,一般問題出在這里。重復計算導致計算機吐血而亡,GG前,面對電腦前的你,帶著疑惑和不甘,向你道出了最后的遺言:“我跟你...咳咳...什么仇什么怨,我如此強大的計算能力,咳咳,精通千萬種高級計算方法,你為什么要...咳咳...要讓我算1+1算到死?”。這時,你會如何回答?),而
表示單個遞歸函數的時間復雜度。很好理解吧~
??在此,我給大家列兩個例子,方便各位理解:
??例子1:我們來一起重溫一下問題1,字符串翻轉問題。每次我們取出兩個字符,剩下的繼續調用遞歸函數,那么我們需要調用n/2次(n代表字符串長度),那么可得。在每個遞歸函數當中,我們只做了一個交換動作,
s[begin], s[end] = s[end], s[begin]
,所以時間復雜度為,那么,該算法的復雜度便為:
是不是很簡單呢?
??例子2:大家可以回想一下斐波那契數列,如果我們用遞歸來表示的話,可以得到如下公式:??看起來,好像
的復雜度也是
,但是!但是!但是!他的復雜度為
!因為當它計算到第n個數時,需要調用該遞歸函數共
次!
??如下圖所示(為了表示方便,我們在該數列前面加上一個數字0),我們要得到f(4),那么需要計算f(2)和f(3),而計算f(3),我們又需要再算一遍f(2)!從圖中可以看出,通過這種方式計算,調用遞歸函數次數是指數級上升的!那么,當你回想問題3當中的楊輝三角形時,會不會發現了什么異曲同工之處?那么,如何解決這種指數爆炸問題,是不是各位老爺也心中有數了呢?(是的,就是加入記憶機制,把之前計算結果全部保存下來即可~這時你會驚奇的發現,時間復雜度變成了!
斐波那契數列計算.png
-
空間復雜度:
??空間復雜度我們也是分兩部分來考慮,一部分是遞歸相關的空間,另一部分是非遞歸相關的空間。
??遞歸相關空間:每次調用遞歸函數時,計算機都會從一個棧(stack)當中給該函數分配一些空間,如,當該函數執行結束時,需要返回原先運算的地方,這便需要一塊內存來存儲之前中斷的位置(計算機需要該地址,才知道該函數執行結束后,從哪里開始下一步運算),還有傳遞給該遞歸函數的參數,以及該函數的局部變量等等,這些都是跟遞歸相關的空間使用。
??如果只是一個普通的函數,那么當他運行完之后,該空間就會被釋放,但是對于遞歸調用來說,直到遇到邊界條件之前,所有被調用到的遞歸函數占用的空間都不會被釋放,相當于每調用一次遞歸函數,空間使用是累計增加的。所以如果不注意,便會遇到 “stack overflow”的場景。
??對于問題1當中的字符串翻轉問題來說,遞歸函數里只進行了一步交換元素位置的操作,所以需要的額外空間為,遞歸共進行了n次,所以該算法的遞歸相關空間的復雜度為
。
??非遞歸相關空間:這部分空間指的是不直接跟遞歸相關的那部分空間使用情況。諸如全局變量(常儲存在堆(heap)當中),如我們為了克服重復計算問題,所加入的記憶機制,還有算法程序的輸入以及輸出數據所占用的空間等。
尾遞歸(Tail Recursion)
??有一種遞歸較為特殊,叫尾遞歸。在此簡單講解一下:尾遞歸是一種遞歸,其中遞歸調用是遞歸函數中的最后一條指令。 并且函數中應該只有一個遞歸調用。說白了,其他地方不能出現遞歸調用,只能是最后一句是遞歸調用。那么,這么搞到底有什么好處呢?大家先來看兩個例子(功能很簡單,列表求和):
def sum_non_tail_recursion(ls):
"""
:type ls: List[int]
:rtype: int, the sum of the input list.
"""
if len(ls) == 0:
return 0
# not a tail recursion because it does some computation after the recursive call returned.
return ls[0] + sum_non_tail_recursion(ls[1:])
def sum_tail_recursion(ls):
"""
:type ls: List[int]
:rtype: int, the sum of the input list.
"""
def helper(ls, acc):
if len(ls) == 0:
return acc
# this is a tail recursion because the final instruction is a recursive call.
return helper(ls[1:], ls[0] + acc)
return helper(ls, 0)
??第一個便不是尾遞歸,雖然遞歸調用只出現了一次且出現在最后一句,即return語句里,但是,該return里面還包含了加法運算,相當于是先遞歸調用,再計算加法,然后把該加法結果返回。這樣子看,最后一步運算確實不是遞歸調用。
??那么大家明白了什么是尾遞歸,可能就有疑惑了,這么做到底有啥好處,代碼變得略復雜了,并且還多了個參數,那么這么做到底能得到什么呢?答案就是,極大的降低了空間復雜度!各位懵逼的少年,且聽我慢慢道來:
??從前有座山,山上呢有座廟,廟里呢,有個老和尚...(此處省略十萬八千字),于是,琦玉老師對杰諾斯說到,假設,我們的遞歸函數為,如果我們的遞歸調用如下:
??那么正常情況下,代碼運行結束前,每次調用需要開辟一塊空間,最終需要開辟n塊空間才行。即空間復雜度為
。但是如果是尾遞歸呢?我們只需要開辟兩塊空間,空間復雜度驟降為
!
??因為尾遞歸函數在遞歸結束時不需要再進行任何計算,直接將結果返回給上一層遞歸函數,那么也就意味著,遞歸到的時候,可以直接將返回值送給
,而不是
!那么,當我計算完
的時候,
函數所占用的內存便可以釋放掉,無需保留!
??杰諾斯趕緊拿小本本記下來,心中想道,這可能就是老師這么強的原因,我要趕快記下來才行。
??當然,還有一點需要提醒一下大家,那就是這種內存優化是需要看語言的。C 以及 C++ 都支持尾遞歸的這種優化方式,但是據我所知,Java 和 Python 好像還不行(當然不排除以后會支持)。所以當你所使用的語言不支持時,什么尾不尾的,那都是浮云一片~
總結:
接下來說三點遞歸算法使用小貼士:
- 毫無疑問,首先寫下遞歸關系式。
- 只要有可能,咱就用記憶機制。
- 當發生stack overflow 的時候,嘗試使用尾遞歸。
附注:
Answer 1. 你向瀕死的計算機冷笑道:“誰讓你只認識 0 和 1 兩個數呢”。
Answer 2. 你掏出了另一臺閃閃發光的計算機,“對不起,我有新歡了?!?br>
Answer 3. 你語重心長、態度堅定的說到:“為了實現中華民族偉大復興?!?/p>
各位看官老爺,希望可以不吝賜教,如有不妥之處還望指出~
也熱切希望大家可以給個小贊,或點個關注!
在此謝謝各位爺勒~