1.1 Word Search 给定一个二维平板和一个单词,请找出这个单词是否在二维平板中出现。
单词可以由平板中的邻接单元组成,这里的“邻接”定义为上下左右四个方向。
同一个单元上的字母最多只能使用一次。
在深度优先搜索中,最重要的就是考虑好搜索顺序。
我们先枚举单词的起点,然后依次枚举单词的每个字母。
过程中需要将已经使用过的字母改成一个特殊字母,以避免重复使用字符。
时间复杂度分析:单词起点一共有 n2n2 个,单词的每个字母一共有上下左右四个方向可以选择,但由于不能走回头路,所以除了单词首字母外,仅有三种选择。所以总时间复杂度是 O(n2*3k)。
class Solution {
public:
bool exist(vector<vector<char>>& board, string str) {
for (int i = 0; i < board.size(); i ++ )
for (int j = 0; j < board[i].size(); j ++ )
if (dfs(board, str, 0, i, j))
return true;
return false;
}
bool dfs(vector<vector<char>> &board, string &str, int u, int x, int y) {
//不匹配直接返回false,进入下一步循环,匹配则继续执行
if (board[x][y] != str[u]) return false;
//字符串全部匹配成果则返回true
if (u == str.size() - 1) return true;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
char t = board[x][y];
board[x][y] = '*';
for (int i = 0; i < 4; i ++ ) {//每个字母都在四个方向去遍历
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < board.size() && b >= 0 && b < board[a].size()) {
//不匹配则回溯回该字母的下一个方向
if (dfs(board, str, u + 1, a, b)) return true;
}
}
board[x][y] = t;//四个方向都不匹配,则回溯回上一个字母的下一个方向,并恢复现场
return false;
}
};
1.2 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
(递归) O(4^l)
class Solution {
public:
vector<char> digit[10];
vector<string> res;
void init() {
char cur = 'a';
for (int i = 2; i < 10; i++) {
for (int j = 0; j < 3; j++)
digit[i].push_back(cur++);
if (i == 7 || i == 9)
digit[i].push_back(cur++);
}
}
void solve(string digits, int d, string cur) {
if (d == digits.length()) {
res.push_back(cur);
return;
}
int cur_num = digits[d] - '0';
for (int i = 0; i < digit[cur_num].size(); i++)
solve(digits, d + 1, cur + digit[cur_num][i]);
}
vector<string> letterCombinations(string digits) {
if (digits == "")
return res;
init();
solve(digits, 0, "");
return res;
}
};
1.3 给定一个字符串,只包含数字。请解码出所有合法的IP地址。
(暴力搜索) O(C3n−1)
直接暴力搜索出所有合法方案。
合法的IP地址由四个0到255的整数组成。我们直接枚举四个整数的位数,然后判断每个数的范围是否在0到255。
时间复杂度分析:一共 n个数字,n−1个数字间隔,相当于从 n−1个数字间隔中挑3个断点,所以计算量是 O(C3n−1)
class Solution {
public:
vector<string> ans;
vector<int> path;
vector<string> restoreIpAddresses(string s) {
dfs(0, 0, s);
return ans;
}
// u表示枚举到的字符串下标,k表示当前截断的IP个数,s表示原字符串
void dfs(int u, int k, string &s)
{
if (u == s.size())
{
if (k == 4)
{
string ip = to_string(path[0]);
for (int i = 1; i < 4; i ++ )
ip += '.' + to_string(path[i]);
ans.push_back(ip);
}
return;
}
if (k > 4) return;
unsigned t = 0;
for (int i = u; i < s.size(); i ++ )
{
t = t * 10 + s[i] - '0';
if (t >= 0 && t < 256)
{
path.push_back(t);
dfs(i + 1, k + 1, s);
path.pop_back();
}
if (!t) break;
}
}
};
2.1 给出一列互不相同的整数,返回其全排列。
(回溯) O(n×n!)
我们从前往后,一位一位枚举,每次选择一个没有被使用过的数。
选好之后,将该数的状态改成“已被使用”,同时将该数记录在相应位置上,然后递归。
递归返回时,不要忘记将该数的状态改成“未被使用”,并将该数从相应位置上删除。
时间复杂度分析:
搜索树中最后一层共 n! 个叶节点,在叶节点处记录方案的计算量是 O(n),所以叶节点处的计算量是 O(n×n!)
搜索树一共有 n!+n!2!+n!3!+…=n!(1+12!+13!+…)≤n!(1+12+14+18+…)=2n! 个内部节点,在每个内部节点内均会for循环 n次,因此内部节点的计算量也是 O(n×n!)。 所以总时间复杂度是 O(n×n!)
class Solution {
public:
vector<vector<int>> res;
vector<bool> st;
vector<int> path;
vector<vector<int> > permute(vector<int> &num) {
for(int i=0;i<num.size();i++)
st.push_back(false);
dfs(num,0);
return res;
}
void dfs(vector<int> &num,int u){
if(u==num.size()){
res.push_back(path);
return ;
}
for(int i=0;i<num.size();i++){
if(!st[i]){
st[i]=true;
path.push_back(num[i]);
dfs(num,u+1);
st[i]=false;
path.pop_back();
}
}
}
};
2.2 给出一组可能包含重复项的数字,返回该组数字的所有排列
(回溯) O(n!)
由于有重复元素的存在,这道题的枚举顺序和 Permutations 不同。
不要忘记递归前和回溯时,对状态进行更新。
时间复杂度分析: 搜索树中最后一层共 n!个节点,前面所有层加一块的节点数量相比于最后一层节点数是无穷小量,可以忽略。且最后一层节点记录方案的计算量是 O(n),所以总时间复杂度是 O(n×n!)。
class Solution {
public:
vector<bool> st;
vector<int> path;
vector<vector<int>> ans;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
st = vector<bool>(nums.size(), false);
path = vector<int>(nums.size());
dfs(nums, 0, 0);
return ans;
}
void dfs(vector<int>& nums, int u, int start)
{
if (u == nums.size())
{
ans.push_back(path);
return;
}
for (int i = start; i < nums.size(); i ++ )
if (!st[i])
{
st[i] = true;
path[i] = nums[u];
if (u + 1 < nums.size() && nums[u + 1] != nums[u])//排序后相同的数在一起
dfs(nums, u + 1, 0);
else
dfs(nums, u + 1, i + 1);
st[i] = false;
}
}
};
如果需要按照字典序排列,则采用方法二
/*枚举每个位置上放哪个数
以[1,1,2]为例:
[ , , ]
/ |
/ |
[1, , ] [2 ,, ]
/ \ |
/ \ |
[1,1, ] [1,2, ] [2,1, ]
| | |
[1,1,2] [1,2,1] [2,1,1]
*/
class Solution {
public:
vector<bool> st;
vector<vector<int>> res;
vector<int> path;
vector<vector<int> > permuteUnique(vector<int> &num) {
sort(num.begin(),num.end());
int n=num.size();
st=vector<bool> (n,false);
path=vector<int> (n);
if(n==0)return res;
dfs(num,0);
return res;
}
void dfs(vector<int> &num,int u){
if(u==num.size()){
res.push_back(path);
return ;
}
for(int i=0;i<num.size();i++){
if(i>0&&st[i-1]&&num[i-1]==num[i])
continue;
if(!st[i]){
st[i]=true;
path[u]=num[i];
dfs(num,u+1);
st[i]=false;
}
}
}
};
2.3 排列序列
(计数) O(n2)
做法:
从高位到低位依次考虑每一位;
对于每一位,从小到大依次枚举未使用过的数,确定当前位是几;
为了便于理解,我们这里给出一个例子的具体操作:n=4,k=14。
首先我们将所有排列按首位分组:
1 + (2, 3, 4的全排列)
2 + (1, 3, 4的全排列)
3 + (1, 2, 4的全排列)
4 + (2, 3, 4的全排列)
接下来我们确定第 k=14个排列在哪一组中。每组的排列个数是 3!=6个,所以第14个排列在第3组中,所以首位已经可以确定,是3。
然后我们再将第3组的排列继续分组:
31 + (2, 4的全排列)
32 + (1, 4的全排列)
34 + (1, 2的全排列)
接下来我们判断第 k=14 个排列在哪个小组中。我们先求第 14个排列在第三组中排第几,由于前两组每组有6个排列,所以第14个排列在第3组排第 14−6∗2=2。
在第三组中每个小组的排列个数是 2!=2个,所以第 k个排列在第1个小组,所以可以确定它的第二位数字是1。
依次类推,可以推出第14个排列是 3142。
时间复杂度分析:两重循环,所以时间复杂度是 O(n2)。
class Solution {
public:
string getPermutation(int n, int k) {
string res;
vector<bool> st(n, false);
for (int i = 0; i < n; i ++ ) //从高位到低位依次枚举每一位
{
int f = 1;
for (int j = 1; j <= n - i - 1; j ++ ) f *= j; //计算 (n-i-1)!
int next = 0;
if (k > f) //确定当前位是第几个未使用过的数
{
int t = k / f;
k %= f;
if (k == 0) k = f, t -- ;
while (t)
{
if (!st[next]) t -- ;//没用过,则算上一次排列,t-1
next ++ ;
}
}
while (st[next]) next ++ ;
res += to_string(next + 1);
st[next] = true;
}
return res;
}
};
2.4 给定一个字符串S,我们可以将其中的大写字母换成小写字母,或将小写字母换成大写字母,从而得到一个新的字符串。
(DFS) O(n×2^n)
深度优先搜索。从左到右一位一位枚举:
小技巧:可以用位运算改变当前字母的大小写,从而简化代码:将一个字母异或32,即可改变这个字母的大小写。比如:
‘a’ ^ 32 = ‘A’;
‘B’ ^ 32 = ‘b’;
时间复杂度分析:最坏情况下,所有字符都是字母,则每个字符都有两种选择,一共会得到 2^n 个字符串,最后将每个字符串记录在答案中还需要 O(n)O(n) 的计算量,所以总时间复杂度是 O(n×2n)。
class Solution {
public:
vector<string> ans;
vector<string> letterCasePermutation(string S) {
dfs(S, 0);
return ans;
}
void dfs(string &S, int u)
{
if (u == S.size())
{
ans.push_back(S);
return;
}
dfs(S, u + 1);//不进行大小写变换
if (S[u] >= 'A')//当前位不是数字时候进行大小写变换
{
S[u] ^= 32;
dfs(S, u + 1);
}
}
};
3.1 给出两个整数n和k,返回从1到n中取k个数字的所有可能的组合
(DFS) O(Ckn)
深度优先搜索,每层枚举第 u个数选哪个,一共枚举 k 层。由于这道题要求组合数,不考虑数的顺序,所以我们需要再记录一个值 start,表示当前数需要从几开始选,来保证所选的数递增。
时间复杂度分析:一共有 Ckn 个方案,所以时间复杂度是 O(Ckn)。
class Solution {
public:
vector<int> path;
vector<vector<int>> res;
vector<vector<int> > combine(int n, int k) {
dfs(0,1,n,k);
return res;
}
void dfs(int u,int start,int n,int k){
if(u==k){
res.push_back(path);
return;
}
for(int i=start;i<=n;i++){
path.push_back(i);
dfs(u+1,i+1,n,k);
path.pop_back();
}
}
};
3.2 给定一个无重复元素的数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
(递归枚举)
class Solution {
public:
vector<vector<int> > combinationSum(vector<int> &candidates, int target) {
vector<int> path;
vector<vector<int>> res;
sort(candidates.begin(),candidates.end());
dfs(candidates,target,0,path,res);
return res;
}
void dfs(vector<int> &num, int target,int start,vector<int> &path,vector<vector<int>> &res){
if(target<0)return ;
if(target==0){
res.push_back(path);
return;
}
for(int i=start;i<num.size();i++){//每个坑位都是num里的数据
if (target < num[i]) {//再加上当前数超过目标值则退出
return ;
}
path.push_back(num[i]);
dfs(num,target-num[i],i,path,res);//下一个坑位从当前的第i个数字开始遍历
path.pop_back();
}
}
};
3.3 给定一个数组 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
class Solution {
public:
vector<vector<int> > combinationSum2(vector<int> &num, int target) {
vector<int> path;
set<vector<int>> res;
sort(num.begin(),num.end());
dfs(num,target,0,path,res);
return vector<vector<int>> (res.begin(),res.end());
}
void dfs(vector<int> &num, int target,int start,vector<int> &path,set<vector<int>>&res){
if(target<0)return ;
if(target==0){
res.insert(path);
return;
}
for(int i=start;i<num.size();i++){
if (target < num[i]) {
break;
}
if (i > start && num[i] == num[i - 1]) {
continue;
}
path.push_back(num[i]);
dfs(num,target-num[i],i+1,path,res);
path.pop_back();
}
}
};
3.4 给定数字1到9,从中选 k 个数,不考虑顺序,使得它们的和等于 n,返回所有方案。要求方案中不包含相同数字,且答案中不包含相同的方案。
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> combinationSum3(int k, int n) {
dfs(k, n, 1);
return ans;
}
void dfs(int k, int n, int start)
{
if (!k)
{
if (!n) ans.push_back(path);
return;
}
for (int i = start; i < 10; i ++ )
if (n >= i)
{
path.push_back(i);
dfs(k - 1, n - i, i + 1);//剩k-1个数,余数n-i,下一位第i+1位
path.pop_back();
}
}
};
4.1 给定一个集合,包含互不相同的数,返回它的所有子集(幂集)。
注意;结果不能包含相同子集。
(集合的二进制表示) O(2^n*n)
假设集合大小是 n,我们枚举每个数选与不选,一共 2^n个数。
另外,如果 n≥30,则 2^n≥ 10^9,肯定会超时,所以我们可以断定 n≤30,可以用int型变量来枚举。
时间复杂度分析:一共枚举 2^n 个数,每个数枚举 n 位,所以总时间复杂度是 O(2^n*n)。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
int n = nums.size();
for (int i = 0; i < (1 << n); i ++ )
{
vector<int> temp;
for (int j = 0; j < n; j ++ )
if (i >> j & 1)
temp.push_back(nums[j]);
res.push_back(temp);
}
return res;
}
};
4.2 给定一个整数数组,可能包含重复元素。请返回它的所有子集(幂集)。
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int> > subsetsWithDup(vector<int> &S) {
if(S.empty())return res;
sort(S.begin(),S.end());
dfs(S,0);
return res;
}
void dfs(vector<int> &S,int start){
res.push_back(path);//每次递归都算一种情况
for(int i=start;i<S.size();i++){
if(i>start&&S[i]==S[i-1])continue;
path.push_back(S[i]);
dfs(S,i+1);
path.pop_back();
}
}
};
5.1 给定括号对数 n,生成出所有合法的括号序列。
(直接生成合法的括号序列) O(Cn2n)
使用递归。
每次可以放置左括号的条件是当前左括号的数目不超过 nn。
每次可以放置右括号的条件是当前右括号的数目不超过左括号的数目。
时间复杂度
时间复杂度就是答案的个数,乘上保存答案的 O(n)计算量,该问题是经典的卡特兰数。
总时间复杂度为 O(n/n+1Cn2n)=O(Cn2n)
class Solution {
public:
vector<string> res;
void solve(int l, int r, int n, string cur) {
if (l == n && r == n) {
res.push_back(cur);
return;
}
if (l < n)
solve(l + 1, r, n, cur + "(");
if (r < l)
solve(l, r + 1, n, cur + ")");
}
vector<string> generateParenthesis(int n) {
if (n == 0)
return res;
solve(0, 0, n, "");
return res;
}
};
5.2 给定一个嵌套的括号序列,含有 ( ) [ ] { } 三种括号,判断序列是否合法。
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for (int i = 0; i < s.length(); i++) {
if (s[i] == '(' || s[i] == '[' || s[i] == '{')
stk.push(s[i]);
else if (s[i] == ')') {
if (stk.empty() || stk.top() != '(')
return false;
stk.pop();
}
else if (s[i] == ']') {
if (stk.empty() || stk.top() != '[')
return false;
stk.pop();
}
else {
if (stk.empty() || stk.top() != '{')
return false;
stk.pop();
}
}
return stk.empty();
}
};
5.3 给你一个由 ‘(’、’)’ 和小写字母组成的字符串 s。
你需要从字符串中删除最少数目的 ‘(’ 或者 ‘)’ (可以删除任意位置的括号),使得剩下的括号字符串有效。请返回任意一个合法字符串。
方法一:(栈) O(n)
时间复杂度
每个位置遍历常数次,故时间复杂度为 O(n)。
空间复杂度
需要 O(n)的空间存放栈,标记数组和答案字符串。
class Solution {
public:
string minRemoveToMakeValid(string s) {
int n = s.length();
stack<int> st;
vector<bool> v(n, true);
for (int i = 0; i < n; i++) {
if (s[i] == ')') {
if (st.empty()) v[i] = false;
else st.pop();
} else if (s[i] == '(') {
st.push(i);
}
}
while (!st.empty()) {
v[st.top()] = false;
st.pop();
}
string ans;
for (int i = 0; i < n; i++)
if (v[i])
ans += s[i];
return ans;
}
};
方法二 (两次线性扫描) O(n)
时间复杂度
两次线性扫描,故时间复杂度为 O(n)。
空间复杂度
需要 O(n)的空间记录中间和答案字符串。
class Solution {
public:
string minRemoveToMakeValid(string s) {
int n = s.length();
string t;
int cnt = 0;
for (int i = 0; i < n; i++) {
if (s[i] == '(') {
cnt++;
t += s[i];
} else if (s[i] == ')') {
if (cnt > 0) {
cnt--;
t += s[i];
}
} else {
t += s[i];
}
}
cnt = 0;
string ans;
for (int i = t.length() - 1; i >= 0; i--)
if (t[i] == ')') {
cnt++;
ans += t[i];
} else if (t[i] == '(') {
if (cnt > 0) {
cnt--;
ans += t[i];
}
} else {
ans += t[i];
}
reverse(ans.begin(), ans.end());
return ans;
}
};
5.4 删除最小数量的无效括号,使得输入的字符串有效,返回所有可能的结果。
说明: 输入可能包含了除 ( 和 ) 以外的字符。
DFS+剪枝
首先我们要知道删除多少左括号和右括号,我们从左到右遍历一遍,如果遇到左括号,那么l ++代表当前未匹配的左括号,如果遇到右括号并且当前未匹配的左括号个数大于0,那么l --,说明当前右括号前面有与之匹配的左括号,如果未匹配的左括号个数等于0,说明当前这个括号是不合法的,r ++,最后l代表还没有匹配的左括号个数,也就是我们需要删除的个数。
接下来就是搜索+剪枝了。dfs函数参数分别为当前字符串、当前字符串已经遍历过的字符,当前字符串还需要删除多少个左括号和右括号。当不需要再删除括号的时候,判断当前字符串是否合法,如果合法就加入答案。
在扫描字符串时,如果遇到非括号字符直接跳过,遇到左右括号的时候如果还可以删除当前括号,那么就删除并递归求解。
剪枝策略1:多个连续相同的括号,我们只删除最左边的那个进行搜索,因为删除后序的括号得到的字符串也是一样的。
剪枝策略2:如果剩余字符数字已经小于还需要删除的字符个数,剪枝。(因为有非括号字符,所以还可以标记剩余的左括号和右括号个数是否小于当前要删除的个数,得到更早的剪枝)
class Solution {
public:
vector<string> res;
vector<string> removeInvalidParentheses(string s) {
int l = 0,r = 0,n = s.length();
for(int i = 0 ; i < n ; i ++)
{
if(s[i] == '(') l ++;
if(l == 0 && s[i] == ')') r ++;
else if(s[i] == ')') l --;
}
dfs(s,0,l,r);
return res;
}
bool check(string s)
{
int cnt = 0,n = s.length();
for(int i = 0 ; i < n ; i ++)
{
if(s[i] =='(') cnt ++;
else if(s[i] == ')') cnt --;
if(cnt <0) return false;
}
return cnt == 0;
}
void dfs(string s,int u,int l,int r)
{
if(l == 0 && r == 0)
{
if(check(s)) res.push_back(s);
return;
}
int n = s.length();
for(int i = u ; i < n ; i ++)
{
if(s[i] != '(' && s[i] != ')') continue;
if(i == u || s[i] != s[i - 1])//重复连续的(或)则只删第一个,因为删哪个都一样
{
string cur = s;
cur.erase(i,1);
if(s[i] == '(' && l > 0) dfs(cur,i,l - 1,r);
else if(s[i] == ')' && r > 0) dfs(cur,i,l,r - 1);
}
}
}
};
5.5 给定一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。
class Solution {
public:
int longestValidParentheses(string s) {
stack<int> stk;
stk.push(-1);//处理边界问题,
int res=0;
for(int i=0;i<s.size();i++){
if(s[i]=='(')
stk.push(i);
else{
stk.pop();//如果s[i] == ‘)’,那么弹出栈顶元素 (代表栈顶的左括号匹配到了右括号)
if(stk.empty())stk.push(i);//比如第一个字母为‘)’时,栈需要弹栈-1后为0,
res=max(res,i-stk.top());
}
}
return res;
}
};
方法二:
双向扫描。不需要使用额外空间
int longestValidParentheses(string s) {
int res = 0,n = s.length(), left = 0,right = 0;
for(int i = 0 ; i < n ; i ++)
{
if(s[i] == '(') left ++;
else right ++;
if(left == right) res = max(res,2 * right);
if(left < right) {left = 0;right = 0;}
}
left = 0,right = 0;
for(int i = n - 1;i >= 0; i --)
{
if(s[i] == ')') right ++;
else left ++;
if(left == right) res = max(res,2 * right);
if(right < left) {left = 0;right = 0;}
}
return res;
}
5.6 两个有效括号字符串的最大嵌套深度
栈) O(n)
括号序列一般会联想到用栈来解决(左括号进栈,匹配右括号后出栈)
class Solution {
public:
vector<int> maxDepthAfterSplit(string seq) {
int d = 0;
vector<int> res;
for (char c: seq){
if (c == '('){
d++;
res.push_back(d % 2);
}
else {
res.push_back(d % 2);
d--;
}
}
return res;
}
};
5.7 请实现一个简易计算器,计算一个算数表达式的值。
表达式中仅包含左括号(、右括号)、加号+、减号-、非负整数 和空格
(栈,表达式求值) O(n)
开两个栈,一个记录数字,一个记录操作符。
然后从前往后扫描整个表达式:
如果遇到 (、+、-,直接入栈;
如果遇到数字,则判断操作符栈的栈顶元素,如果不是(,则弹出操作符的栈顶元素,并用相应操作更新数字栈的栈顶元素。从而保证操作符栈的栈顶最多有一个连续的+或-;
如果遇到 ),此时操作符栈顶一定是 (,将其弹出。然后根据新栈顶的操作符,对数字栈顶的两个元素进行相应操作;
时间复杂度分析:每个数字和操作进栈出栈一次,所以总时间复杂度是 O(n)O(n)。
class Solution {
public:
void calc(stack<char> &op, stack<int> &num) {
int x=num.top();
num.pop();
int y=num.top();
num.pop();
if(op.top()=='+')num.push(x+y);
else num.push(y-x);
op.pop();
}
int calculate(string s) {
stack<int> num;
stack<char> op;
for(int i=0;i<s.size();i++){
char c=s[i];
if(c==' ')continue;
if(c=='+'||c=='-'||c=='(')
op.push(c);
else if(c==')'){
op.pop();//此时栈顶元素一定是(
if(op.size()&&op.top()!='(')
calc(op,num);
}
else {
int j = i;
while (j < s.size() && isdigit(s[j])) j ++ ;
num.push(atoi(s.substr(i, j - i).c_str()));
i = j - 1;
if (op.size() && op.top() != '(') {
calc(op, num);
}
}
}
return num.top();
}
};
5.8 实现一个基本的计算器来计算一个简单的字符串表达式的值。
字符串表达式仅包含非负整数,+, - ,*,/ 四种运算符和空格 。 整数除法仅保留整数部分。
(栈模拟) O(n)
class Solution {
public:
int calculate(string s) {
s += "+";
stack<int> nums;
stack<char> op;
for (int i = 0; i < s.size(); i ++ ) {
if (s[i] == ' ') continue;
if (s[i] == '*' || s[i] == '/') op.push(s[i]);
else if (s[i] == '+' || s[i] == '-') {
if (op.size()) {
calc(nums, op);
}
op.push(s[i]);
} else {
int j = i;
int n = 0;
while (j < s.size() && isdigit(s[j])) {
n = n * 10 + (s[j] - '0');
j ++ ;
}
i = j - 1;
nums.push(n);
if (op.size() && (op.top() == '*' || op.top() == '/')) {
calc(nums, op);
}
}
}
return nums.top();
}
void calc(stack<int> &nums, stack<char> &op) {
int n2 = nums.top(); nums.pop();
int n1 = nums.top(); nums.pop();
char c = op.top();
op.pop();
if (c == '+') nums.push(n1 + n2);
else if (c == '-') nums.push(n1 - n2);
else if (c == '*') nums.push(n1 * n2);
else nums.push(n1 / n2);
}
};
5.9 计算逆波兰表达式的值。
表达式中的运算符仅包含 +,-,*,/。
注意:
/表示整除运算;
给定的逆波兰表达式一定合法。即不会导致除零运算。
(栈操作) O(n)
遍历所有元素。如果当前元素是整数,则压入栈;如果是运算符,则将栈顶两个元素弹出做相应运算,再将结果入栈。
最终表达式扫描完后,栈里的数就是结果。
时间复杂度分析:每个元素仅被遍历一次,且每次遍历时仅涉及常数次操作,所以时间复杂度是 O(n)
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> sta;
for (auto &t : tokens)
if (t == "+" || t == "-" || t == "*" || t == "/")
{
int a = sta.top();
sta.pop();
int b = sta.top();
sta.pop();
if (t == "+") sta.push(a + b);
else if (t == "-") sta.push(b - a);
else if (t == "*") sta.push(a * b);
else sta.push(b / a);
}
else sta.push(atoi(t.c_str()));
return sta.top();
}
};
6.1 判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
(位运算判重)
分别使用一个整型数组记录每行、每列和每个九宫格内数字的存在情况。
位运算可以极大的简化判断,提高效率,具体看代码。
class Solution {
public:
bool isValidSudoku(vector<vector<char>>& board) {
vector<int> row(9), col(9), squ(9); // 使用三个整型数组判重。
for (int i = 0; i < 9; i++)
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.')
continue;
if (board[i][j] < '1' || board[i][j] > '9') return false;
int num = board[i][j] - '0';
// 以row[i] & (1 << num)为例,这是判断第i行中,num数字是否出现过。
// 即row[i]值的二进制表示中,第num位是否是1。
// 以下col和squ同理。
if ((row[i] & (1 << num)) ||
(col[j] & (1 << num)) ||
(squ[(i / 3) * 3 + (j / 3)] & (1 << num)))
return false;
row[i] |= (1 << num);
col[j] |= (1 << num);
squ[(i / 3) * 3 + (j / 3)] |= (1 << num);
}
return true;
}
};
6.2 编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
(递归回溯)
class Solution {
public:
bool solve(int x, int y, vector<vector<char>>& board,
vector<int>& row, vector<int>& col, vector<int>& squ) {
if (y == 9) {
x++;
y = 0;
}
if (x == 9)
return true;
if (board[x][y] == '.') {
for (int i = 1; i <= 9; i++)
if (! (
(row[x] & (1 << i)) ||
(col[y] & (1 << i)) ||
(squ[(x / 3) * 3 + (y / 3)] & (1 << i))
)) {
row[x] |= (1 << i);
col[y] |= (1 << i);
squ[(x / 3) * 3 + (y / 3)] |= (1 << i);
board[x][y] = i + '0';
if (solve(x, y + 1, board, row, col, squ))
return true;
board[x][y] = '.';
row[x] -= (1 << i);
col[y] -= (1 << i);
squ[(x / 3) * 3 + (y / 3)] -= (1 << i);
}
} else {
if (solve(x, y + 1, board, row, col, squ))
return true;
}
return false;
}
void solveSudoku(vector<vector<char>>& board) {
vector<int> row(9), col(9), squ(9);
for (int i = 0; i < 9; i++)
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.')
continue;
int num = board[i][j] - '0';
row[i] |= (1 << num);
col[j] |= (1 << num);
squ[(i / 3) * 3 + (j / 3)] |= (1 << num);
}
solve(0, 0, board, row, col, squ);
}
};
7.1 n-皇后问题是将 n 个皇后放在 n∗n 的棋盘上,使得皇后不能相互攻击到
为了优化时间效率,定义 vectorrow, col, diag, anti_diag;,用来记录每一行、每一列、每条对角线上是否有皇后存在。
搜索时需要记录4个状态:x,y,s,nx,y,s,n,分别表示横纵坐标、已摆放的皇后个数、棋盘大小。
对于每步搜索,有两种选择:
时间复杂度分析:
由于 nn 个皇后不能在同行同列,所以每行恰有一个皇后,我们计算一下在不考虑对角线的情况下,方案数的上限:第一行有 n个位置可选,第二行有 n−1个位置可选,依次类推,可得方案数最多是 n!。所以时间复杂度是 O(n!)
class Solution {
public:
vector<vector<string>> ans;
vector<string> path;
vector<bool> row, col, diag, anti_diag;
vector<vector<string>> solveNQueens(int n) {
row = col = vector<bool>(n, false);
diag = anti_diag = vector<bool>(2 * n, false);
path = vector<string>(n, string(n, '.'));
dfs(0, 0, 0, n);
return ans;
}
void dfs(int x, int y, int s, int n)
{
if (y == n) x ++ , y = 0;
if (x == n)
{
if (s == n) ans.push_back(path);
return ;
}
dfs(x, y + 1, s, n);
if (!row[x] && !col[y] && !diag[x + y]
&& !anti_diag[n - 1 - x + y])
{
row[x] = col[y] = diag[x + y]
= anti_diag[n - 1 - x + y] = true;
path[x][y] = 'Q';
dfs(x, y + 1, s + 1, n);
path[x][y] = '.';
row[x] = col[y] = diag[x + y]
= anti_diag[n - 1 - x + y] = false;
}
}
};
8.1 给定一个 n∗m的矩阵,对于其中是0的元素,把它所在的整行整列都置成0。
请使用 原地算法,即只能使用额外 O(1)的空间。
(原地算法) O(nm)
我们只需统计出矩阵中每一行或者每一列是否有0,然后把含有0的行或者列都置成0即可。
用两个变量记录第一行和第一列是否有0。
遍历整个矩阵,用矩阵的第一行和第一列记录对应的行和列是否有0。
把含有0的行和列都置成0。
时间复杂度分析: 矩阵中每个元素只遍历常数次数,所以时间复杂度是 (nm)(nm)。
空间复杂度分析: 只用了两个额外的变量记录第一行和第一列是否含有0,所以额外的空间复杂度是 (1),满足原地算法的要求。
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
if (matrix.empty()) return;
int n = matrix.size(), m = matrix[0].size();
int col0 = 1, row0 = 1;
for (int i = 0; i < n; i ++ )
if (!matrix[i][0]) col0 = 0;
for (int i = 0; i < m; i ++ )
if (!matrix[0][i]) row0 = 0;
for (int i = 1; i < n; i ++ )
for (int j = 1; j < m; j ++ )
if (!matrix[i][j])
{
matrix[i][0] = 0;
matrix[0][j] = 0;
}
for (int i = 1; i < n; i ++ )
if (!matrix[i][0])
for (int j = 1; j < m; j ++ )
matrix[i][j] = 0;
for (int i = 1; i < m; i ++ )
if (!matrix[0][i])
for (int j = 1; j < n; j ++ )
matrix[j][i] = 0;
if (!col0)
for (int i = 0; i < n; i ++ )
matrix[i][0] = 0;
if (!row0)
for (int i = 0; i < m; i ++ )
matrix[0][i] = 0;
}
};
8.2 给定一些整数,代表火柴棍的长度。求这些火柴棍是否可以组成一个正方形。火柴棍不可以拆分,但是可以拼接。
DFS搜索+剪枝。
DFS函数参数:k已经拼好了几根火柴,cur当前在拼的这根火柴已经拼了多长了,state,因为最多只有15根火柴,所以将其状态用二进制来表示,火柴数组以及数组长度。
搜索策略:如果k = 4,返回true;如果cur=target说明找到了一根新的火柴;否则:遍历所有可拼接的火柴(未被使用过并且拼上去之后不会大于目标长度的火柴)。
四种剪枝方法:
class Solution {
public:
bool makesquare(vector<int>& nums) {
int n = nums.size(),sum = 0;
for(int i = 0 ; i < n ; i ++)
sum += nums[i];
if(n < 4 || sum % 4 != 0) return false;
sort(nums.begin(),nums.end(),greater<int>());
return dfs(0,0,0,sum / 4,nums,n);
}
bool dfs(int k,int cur,int state,int target,vector<int>& nums,int n)
{
if(k == 4) return true;
if(cur == target)
return dfs(k + 1,0,state,target,nums,n);
for(int i = 0 ; i < n ; i ++)
{
if(cur + nums[i] > target)
continue;
if(((state >> i) & 1) == 0)
{
state = state | (1 << i);
if(dfs(k,cur + nums[i],state,target,nums,n))
return true;
state = state & ~(1 << i);
//如果当前木棍填充失败,那么跳过接下来所有相同长度的木棍
while(i+1<n&&nums[i+1]==nums[i])i++;
//如果在cur=0的时候就失败了,直接返回false,说明还没有使用的最长的火柴找不到可匹配的组合。
if(cur == 0) return false;
//如果当前木棍填充失败,并且是当前边的最后一个,则直接剪掉当前分支
//这是因为我们已经将火柴降序排列,那么后序搜索的火柴和当前cur的和要小于target
if(cur + nums[i] == target) return false;
}
}
return false;
}
};
8.3 (Number of Islands) 给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
(深度优先遍历) O(nm)
class Solution {
public:
int dy[4]={-1,0,1,0},dx[4]={0,1,0,-1};
void dfs(vector<vector<char>>& grid,int x,int y){
int n=grid.size(),m=grid[0].size();
grid[x][y]='0';
for(int i=0;i<4;i++){
int a=x+dx[i],b=y+dy[i];
if(a<0||a>=n||b<0||b>=m||grid[a][b]=='0')
continue;
dfs(grid,a,b);
}
}
int numIslands(vector<vector<char>>& grid) {
int n=grid.size();
if(!n)return 0;
int m=grid[0].size();
int res=0;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++){
if(grid[i][j]=='1'){
res++;
dfs(grid,i,j);
}
}
return res;
}
};
8.4(Surrounded Regions) 给定一个二维地图,仅包含’X’和’O’(字母O),请攻占所有被’X’包围的’O’。一片区域被攻占,则将这片区域的’O’变成’X’。
深度优先遍历O(n^2)
逆向考虑问题,我们先统计出哪些区域不会被攻占,然后将其它区域都变成’X’即可。
具体做法如下:
时间复杂度分析:每个位置仅被遍历一次,所以时间复杂度是 O(n2),其中 n 是地图的边长。
class Solution {
public:
vector<vector<bool>> st;
int n, m;
void solve(vector<vector<char>>& board) {
if (board.empty()) return;
n = board.size(), m = board[0].size();
st=vector<vector<bool>> (n,vector<bool>(m,false));
for (int i = 0; i < n; i ++ )
{
if (board[i][0] == 'O') dfs(i, 0, board);
if (board[i][m - 1] == 'O') dfs(i, m - 1, board);
}
for (int i = 0; i < m; i ++ )
{
if (board[0][i] == 'O') dfs(0, i, board);
if (board[n - 1][i] == 'O') dfs(n - 1, i, board);
}
for (int i = 0; i < n; i ++ )
for (int j = 0; j < m; j ++ )
if (!st[i][j])
board[i][j] = 'X';
}
void dfs(int x, int y, vector<vector<char>>&board)
{
st[x][y] = true;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
for (int i = 0; i < 4; i ++ )
{
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < n && b >= 0 && b < m && !st[a][b] && board[a][b] == 'O')
dfs(a, b, board);
}
}
};