递归算法整理合集
遞歸算法整理合集
?遞歸是常見的算法和編程思想,也是初學者幾乎最早接觸的算法思想之一。遞歸算法的優點是代碼簡潔清晰,邏輯簡單易懂;缺點一是算法運行復雜度較高,二是容易在具體代碼實現的時候調用棧的層次考慮不周,導致出現錯誤。但是如果僅僅想實現一個復雜的問題,而不是很關注復雜度的時候,遞歸算法無疑是一個很好的選擇。(實際上,現有的編譯器優化很多已經能夠自動將遞歸代碼優化為非遞歸結構實現,因此,如果實用中開啟編譯器優化,則大多數遞歸算法的復雜度其實也可以不必考慮)
?本文對遞歸算法的一些典型題目和例子做了梳理,代碼示例有的是用C++,有的是用python,之所以用不同的語言混雜,純屬是具體例題的方便起見,有些地方使用python而不用C++,只是為了更加方便閱讀和理解主要思路,而不是過多地關注底層的代碼細節。
例題1:斐波拉契數列
?斐波拉契數列是最容易想到的遞歸算法典例,但是直接遞歸復雜度為指數級別,很容易即超出時間限制,如果將每次計算的結果變成數組,其思想還是遞歸的思想,但更改了一種實現方式,則算法復雜度會降低為O(n)量級,代碼如下:
class Solution { public:int fib(int n) {int a[31]={0,1};for(int i=2;i<31;i++){a[i]=a[i-1]+a[i-2];}return a[n];} };例題2:全排列問題
?全排列問題是面試中經常會考察的算法題目,即給出一組不重復的數字,要求輸出所有的可能的排列。由基礎的組合數學知識,我們可以知道,一共有n!n!n!種可能的排列,但是如何通過計算機輸出呢?
?如果采取遞歸的思想,容易想到,每個數字作為第一個數字,都會對應(n?1)!(n-1)!(n?1)!種排列,即剩下的(n?1)(n-1)(n?1)個元素的排列數,這樣就可以構造遞歸的結構了,這里用python實現如下:
class Solution:def permute(self, nums: List[int]) -> List[List[int]]:if len(nums)==1:return [nums]if len(nums)==2:return [nums,nums[::-1]]ans = []for a in nums:tmp = copy.deepcopy(nums)# 這里用到deepcopy就是因為python的列表對象直接相等是淺復制,會不對,當然也可以用別的方法去除某元素,比如拼接tmp.remove(a)for i in self.permute(tmp):ans.append([a]+i)return ans例題3:N皇后問題
?N皇后問題是比較難的遞歸題目,通常用DFS算法實現,而DFS一般又通過遞歸實現。
?目前比較簡潔和高效的題解是這樣的:
?
?首先,我們是逐行掃描的,因此不需要考慮行的重合問題;其次,列的重合問題也比較容易考慮,只需要一個長度為N的數組來進行記錄即可;最后,需要考慮的是對角線元素,這里不能頭大,只需要觀察規律,就可以發現對角線元素的特點。對于每一條對角線而言,都是一條截距固定且斜率絕對值為1的直線,因此,i+yi+yi+y的值或者n?x+yn-x+yn?x+y的值必然是固定的,比如過(0,1)(0,1)(0,1)和(1,0)(1,0)(1,0)格子,即構成一個/型對角線,又比如(0,2)(0,2)(0,2)和(1,3)(1,3)(1,3),滿足4?0+2=4?1+34-0+2 = 4-1+34?0+2=4?1+3,因此也在一條\對角線上。這樣有什么好處呢?這樣的好處就是對角線是否有值了就可以直接一條對角線用一個a[i+j]a[i+j]a[i+j]或a[n?i?j]a[n-i-j]a[n?i?j]來記錄了,對應的記錄對角線的數組應該多大呢?只需要考慮一下邊界就可以了。對于/型對角線,可以知道最大的i+j=n?1+0=n?1i+j = n-1+0=n-1i+j=n?1+0=n?1,最小的i+j=1+0=1>0i+j = 1+0=1>0i+j=1+0=1>0所以只需要長度為nnn的數組就可以了;對于\型對角線,可以知道最大的n?i+j=n?0+0=nn-i+j = n-0+0=nn?i+j=n?0+0=n,最小的n?(n?1)+0=1+0=1>0n-(n-1)+0 = 1+0=1>0n?(n?1)+0=1+0=1>0所以也是只需要長度為nnn的數組就可以了。
?下面就可以遞歸了:
?從第一行開始,每一列進行搜索,然后當前這一列和對應的對角線(四角的元素只有一個,其實無所謂對角線)標記為訪問過,即為True,之后,就從第二行開始,如果沒有沖突,就繼續執行此擺放程序,即深度優先搜索DFS,但行數為下一行,如果有沖突,則continue,不再遞歸,即停止此路徑的搜索,相當于DFS剪枝。需要注意的是,由于遞歸調用了全局變量,因此,每次遞歸調用后,需要把函數值恢復為原來的值,這一點比較繞,需要注意!代碼如下:
class Solution { public:vector<vector<string>> solveNQueens(int n) {vector<vector<string>> ans;vector<string>board(n,string(n,'.'));vector<bool> col(n,false); //記錄列vector<bool> r(n,false); //記錄正對角線vector<bool> l(n,false); //記錄反對角線backtrace(ans,board,col,l,r,0,n);return ans;}void backtrace(vector<vector<string>> &ans,vector<string> &board,vector<bool> &col,vector<bool> &l,vector<bool> &r,int row,int n){if(row==n){ans.push_back(board);return;}for(int j=0;j<n;j++){if(col[j]||r[j+row]||l[n-j+row])continue;col[j]=r[j+row]=l[n-j+row] = true;board[row][j] = 'Q';backtrace(ans,board,col,l,r,row+1,n);//注意此處要恢復原來的值,保存?,F場col[j]=r[j+row]=l[n-j+row] = false;board[row][j] = '.';}} };例題4:島嶼數量問題
?島嶼數量問題也是比較常見的算法題,即給出一個01矩陣,要求其中的閉集數量,即1連成片可以構成一個“島嶼”,要求一共有多少個“島嶼”存在。
?這個就不同于上面的DFS了,這個是找到鄰居,標記,鄰居的鄰居也要標記,因此是廣度優先搜索,是BFS問題,通過BFS+遞歸,可以很漂亮地解決這個問題。具體思路是這樣的:
?按行按列遍歷矩陣的每一個元素,如果元素是0,那是水域,跳過即可;如果這個元素為1說明為陸地,記錄數量+1,這個點置為0,同時對此元素BFS遞歸,即任何于其相鄰的1元素在此時都會被搜索到,搜索到怎么辦呢?就是把搜索到的鄰域的1都置為0,這樣統計數量就很容易了。總體思想就是遞歸+BFS,還有一點并查集的思想。
?代碼實現如下:
class Solution:def numIslands(self, grid: List[List[str]]) -> int:n=len(grid)m=len(grid[0])count=0def dfs(x,y):for i,j in [[x+1,y],[x-1,y],[x,y+1],[x,y-1]]:if 0<=i<n and 0<=j<m and grid[i][j]=='1':# 只有搜索到陸地的時候才操作grid[i][j] = '0'dfs(i,j)for i in range(n):for j in range(m):if grid[i][j]=='1':grid[i][j]='0'dfs(i,j)count+=1return count例題5:二叉樹的前中后序遍歷
?二叉樹的前中后序遍歷問題是典型的遞歸實現算法,不再多言,如下所示:
struct TreeNode {int val;TreeNode *left;TreeNode *right;TreeNode(int x) : val(x), left(NULL), right(NULL) {} }; //前序遍歷 void preorder(TreeNode *root,vector<int>&path){if(root){path.push_back(root->val);preorder(root->left,path);preorder(root->right,path);} } //中序遍歷 void inorder(TreeNode *root,vector<int>&path){if(root){inorder(root->left,path);path.push_back(root->val);inorder(root->right,path);} } //后序遍歷 void postorder(TreeNode *root,vector<int>&path){if(root){postorder(root->left,path);postorder(root->right,path);path.push_back(root->val);} }例題6:N叉樹的后序遍歷
N叉樹的遍歷和二叉樹其實沒有本質的區別,只不過多了個for循環而已,這里以后序遍歷為例,看一下代碼實現:
/* // Definition for a Node. class Node { public:int val;vector<Node*> children;Node() {}Node(int _val) {val = _val;}Node(int _val, vector<Node*> _children) {val = _val;children = _children;} }; */ class Solution { public://這里必須要分開兩個函數調用,否則一個函數要返回值,無法遞歸vector<int> postorder(Node* root) {vector<int> ret;dfs(ret,root);return ret;}void dfs(vector<int>& ret,Node* root){if(root){for(int i=0;i<root->children.size();i++)dfs(ret,root->children[i]);ret.push_back(root->val);}} };例題7:快速冪算法
?著名的快速冪算法常用來計算大指數,廣泛應用于密碼學等領域??焖賰缢惴ㄒ矐昧诉f歸的基本思想,即每次冪為原來的1/2處理,從而以指數級別降冪運算,其實也是遞歸的思想,代碼如下:
def myPow(x, n):if x == 0.0: return 0.0res = 1if n < 0: x, n = 1 / x, -nif n==0:return 1elif n%2==1:return myPow(x,n-1)*xelse:temp = myPow(x,n//2)return temp ** 2?當然,平時應用的快速冪算法多是非遞歸位運算實現,但由于本文主要講解遞歸算法,就不在這里詳細說明了。
?以上就是梳理的一些經典的遞歸算法例題,供大家參考。
總結
- 上一篇: WaitForSingleObject的
- 下一篇: 基于python中jieba包的详细使用