Wikipedia关于原地算法的描述:原地算法(in-place algorithm)基本上不需要额外辅助的数据结构,然而,允许少量额外的辅助变量来转换数据的算法。当算法运行时,输入的数据通常会被要输出的部分覆盖掉。
很容易看出,原地算法的特点是不需要辅助的数据结构而只需要辅助变量。通常,维护一个复杂的数据结构时间空间复杂度都是比较高的,原地算法的优越性正来自于不依赖数据结构,尽管它借鉴了数据结构的思想。
同时也不难看出,原地算法总体的实现思路就是用输出的数据在原地覆盖已经“没用”的输入的数据。
先做的是一条简单题:LeetCode 1047 删除字符串中所有相邻重复项
给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
示例:
输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
方法一:最容易想到的就是维护一个栈,因为本题删除字符的操作具有即时性。可以理解为“从第一个字符开始读字符串,遇到相同的字符立刻把它们从字符串中删除”,这个删除是即时生效的,会影响到后续的操作。比如“abbbaca”经过删除后不会变成“ca”,而是会变成“abaca”:在读到第三个“b”时前两个“b”已经被删去,所以不构成重复。读取字符并和前面的字符比较,如果相同则把前面的字符删去,这符合栈后进先出的特性。事实上,最后留在栈中的字符串就是结果。
1 class Solution { 2 public: 3 string removeDuplicates(string S) { 4 stack<char> s; 5 s.push(S[0]); 6 for(int i=1;i!=S.length();++i) 7 { 8 if(!s.empty()) 9 { 10 if(s.top()==S[i]) 11 s.pop(); 12 else 13 s.push(S[i]); 14 } 15 else 16 { 17 s.push(S[i]); 18 } 19 } 20 string res = ""; 21 while (!s.empty()) 22 { 23 res += s.top(); 24 s.pop(); 25 } 26 reverse(res.begin(),res.end()); 27 return res; 28 } 29 };View Code
维护一个栈是LeetCode官方给出的题解,也是最容易想到的,但只超过了60%的提交,在评论区找到了原地算法这个名词。
方法二:原地算法。原地算法借助一个辅助变量len来确定最终的经过删减的字符串的长度。并且在算法运行过程中,直接将删减后的字符赋给相应的位置。以题给的字符串“abbaca”为例,在算法运行过程中,它的变化情况是这样的:abbaca(a赋给S[0],len=1)->abbaca(b!=a,len=2)->abbaca(b==b,len=1)->abbaca(a==a,len=0)->cbbaca(c赋给S[0],len=1)->cabaca(c!=a,len=2)->ca(截取长度为len的字符串)。在读取到第二个a时,这时候实质上前面的字符已经确定要被删除,因此可以被覆盖。
1 class Solution { 2 public: 3 string removeDuplicates(string S) { 4 int len = 0; 5 for (char ch : S) { 6 if (len == 0 || S[len - 1] != ch) { 7 S[len] = ch; 8 ++len; 9 } else { 10 len--; 11 } 12 } 13 S.resize(len); 14 return S; 15 } 16 };View Code
方法三:还有一种方式也颇具原地算法的精神,即维护一个栈,而不实际构造一个栈,把字符串本身当作栈来用。顺序栈本身就是依靠数组实现的,因此在一个字符数组中模拟栈的操作并不违和。这样可以避免额外开辟栈的空间浪费。
1 class Solution { 2 public: 3 string removeDuplicates(string S) { 4 char top; 5 string res=""; 6 res+=S[0]; 7 top=S[0]; 8 for(int i=1;i!=S.length();++i) 9 { 10 if(top==S[i]) 11 { 12 res.erase(res.end()-1); 13 if(res!="") 14 top=res[res.length()-1]; 15 else if(res=="") 16 top=' '; 17 } 18 else if(top!=S[i]) 19 { 20 res+=S[i]; 21 top=S[i]; 22 } 23 } 24 return res; 25 } 26 };View Code
再看一道难一点的:LeetCode 289 生命游戏
根据百度百科,生命游戏,简称为生命,是英国数学家约翰·何顿·康威在1970年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞具有一个初始状态 live(1)即为活细胞, 或 dead(0)即为死细胞。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
根据当前状态,写一个函数来计算面板上细胞的下一个(一次更新后的)状态。下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。
示例:
输入:
[
[0,1,0],
[0,0,1],
[1,1,1],
[0,0,0]
]
输出:
[
[0,0,0],
[1,0,1],
[0,1,1],
[0,1,0]
]
方法一:看一眼就能想到的方法是,扫描一遍二维数组,建立一个标记数组记录细胞的死活,然后再遍历一遍改变细胞的状态,很简单也很浪费空间的方法。
1 class Solution { 2 public: 3 void gameOfLife(vector<vector<int>>& board) { 4 vector<vector<bool>> res; 5 int m = board.size(); 6 int n = board[0].size(); 7 for (int i = 0; i < m; ++i) 8 { 9 vector<bool> temp_res; 10 bool flag; 11 for (int j = 0; j < n; ++j) 12 { 13 int count = 0; 14 //上 15 if (i > 0) 16 count += board[i - 1][j]; 17 //下 18 if (i < m - 1) 19 count += board[i + 1][j]; 20 //左 21 if (j > 0) 22 count += board[i][j - 1]; 23 //右 24 if (j < n - 1) 25 count += board[i][j + 1]; 26 //左上 27 if (i > 0 && j > 0) 28 count += board[i - 1][j - 1]; 29 //右上 30 if (i > 0 && j < n-1) 31 count += board[i - 1][j + 1]; 32 //左下 33 if (i < m - 1 && j>0) 34 count += board[i + 1][j - 1]; 35 //右下 36 if (i<m-1 && j<n-1) 37 count += board[i + 1][j + 1]; 38 39 if (2 > count) 40 flag = 0; 41 if (2 == count) 42 { 43 if (board[i][j] == 0) 44 flag = 0; 45 else if(board[i][j]==1) 46 flag = 1; 47 } 48 if (count==3) 49 flag = 1; 50 if (3 < count) 51 flag = 0; 52 temp_res.push_back(flag); 53 } 54 res.push_back(temp_res); 55 } 56 for (int i = 0; i != res.size(); ++i) 57 { 58 for (int j = 0; j != res[0].size(); ++j) 59 { 60 if (0 == res[i][j]) 61 board[i][j] = 0; 62 else if (1 == res[i][j]) 63 board[i][j] = 1; 64 } 65 } 66 } 67 };View Code
方法二:原地算法。本题不能再原数组改变细胞状态的原因是细胞的生死是同时发生的,也就是说局部的改变不会影响到后面的细胞。用标记数组似乎是理所当然的做法,但是题目并没有限定为布尔数组,完全可以用其他数据来表示细胞的状态(或者说一种趋势)而同时不影响后续的判定。
1 void gameOfLife(vector<vector<int>>& board) 2 { 3 for(int i=0;i<board.size();i++) 4 for(int j=0;j<board[0].size();j++) 5 f(board,i,j); 6 for(int i=0;i<board.size();i++) 7 for(int j=0;j<board[0].size();j++) 8 { 9 if(board[i][j] == 2) 10 board[i][j] = 0; 11 if(board[i][j] == -1) 12 board[i][j] = 1; 13 } 14 15 } 16 void f(vector<vector<int>>& board,int x,int y) 17 { 18 int live = 0; 19 if(x>0) 20 { 21 if(board[x-1][y]>0) 22 live++; 23 if(y>0 && board[x-1][y-1]>0) 24 live++; 25 if(y<board[0].size()-1 && board[x-1][y+1]>0) 26 live++; 27 } 28 if(x<board.size()-1) 29 { 30 if(board[x+1][y]>0) 31 live++; 32 if(y>0 && board[x+1][y-1]>0) 33 live++; 34 if(y<board[0].size()-1 && board[x+1][y+1]>0) 35 live++; 36 } 37 if(y >0 && board[x][y-1]>0) 38 live++; 39 if(y < board[0].size()-1 && board[x][y+1]>0) 40 live++; 41 if(board[x][y] == 1) 42 { 43 if(live<2||live>3) 44 board[x][y] = 2; 45 } 46 else if(live == 3) 47 board[x][y] = -1; 48 }View Code