递归面试算法总结
通常用递归实现的代码比用循环实现的代码简洁,且也更加容易。如过面试没有特殊要求,则可以优先考虑用递归的方法编写程序。
递归虽然有简洁的优点,但也有显著的缺点。递归是函数调用自身,而函数调用自身是有时间和空间消耗的;同时递归中有很多计算是重复的,对性能有很大的影响。尤其要注意递归有可能引起调用栈溢出。
1. LeetCode 剑指 Offer 07 : 重建二叉树
题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
/*
* 1
* LeetCode 剑指 Offer 07 : 重建二叉树
* https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/
*/
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder)
{
if(preorder.empty() || inorder.empty())
{
return NULL;
}
TreeNode* root = new TreeNode(preorder[0]);
int mid = distance(begin(inorder), find(inorder.begin(), inorder.end(), preorder[0]));
vector<int> leftPre(preorder.begin()+1, preorder.begin()+mid+1);
vector<int> rightPre(preorder.begin()+mid+1, preorder.end());
vector<int> leftIn(inorder.begin(), inorder.begin()+mid);
vector<int> rightIn(inorder.begin()+mid+1, inorder.end());
root->left = buildTree(leftPre, leftIn);
root->right = buildTree(rightPre, rightIn);
return root;
}
2. LeetCode 687 : 最长同值路径
题目:给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。
注意:两个节点之间的路径长度由它们之间的边数表示。
/*
* 2
* LeetCode 687 : 最长同值路径
* https://leetcode-cn.com/problems/longest-univalue-path/
*/
int Length;
int arrowLength(TreeNode* root)
{
if(root == NULL)
return 0;
int left = arrowLength(root->left);
int right = arrowLength(root->right);
int arrowLeft = 0, arrowRight = 0;
if(root->left != NULL && root->left->val == root->val)
arrowLeft += left + 1;
if(root->right != NULL && root->right->val == root->val)
arrowRight += right + 1;
Length = max(Length, arrowLeft + arrowRight);
return max(arrowLeft, arrowRight);
}
int longestUnivaluePath(TreeNode* root)
{
Length = 0;
arrowLength(root);
return Length;
}
3. LeetCode 面试题 08.06 : 汉诺塔问题
题目:在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
你需要原地修改栈。
/*
* 3
* LeetCode 面试题 08.06 : 汉诺塔问题
* https://leetcode-cn.com/problems/hanota-lcci/
*/
void move(int n, vector<int>& A, vector<int>& B, vector<int>& C)
{
if(n == 1)
{
C.push_back(A[0]);
A.pop_back();
return;
}
move(n-1, A, C, B); // 将A上面n-1个通过C移到B
C.push_back(A[0]); // 将A最后一个移到C
A.pop_back(); // 这时,A空了
move(n-1, B, A, C); // 将B上面n-1个通过空的A移到C
}
void hanota(vector<int>& A, vector<int>& B, vector<int>& C)
{
int n = A.size();
move(n, A, B, C);
}
4. LeetCode 894 : 所有可能的满二叉树
题目:满二叉树是一类二叉树,其中每个结点恰好有 0 或 2 个子结点。
返回包含 N 个结点的所有可能满二叉树的列表。 答案的每个元素都是一个可能树的根结点。
答案中每个树的每个结点都必须有 node.val=0。
你可以按任何顺序返回树的最终列表。
/*
* 4
* LeetCode 894 : 所有可能的满二叉树
* https://leetcode-cn.com/problems/all-possible-full-binary-trees/
*/
vector<TreeNode*> allPossibleFBT(int N)
{
if(N%2 == 0)
return {};
if(N == 1)
return {new TreeNode(0)};
vector<TreeNode*> ans;
for(int lef = 1; lef + 1 < N; lef++)
{
vector<TreeNode*> left = allPossibleFBT(lef);
vector<TreeNode*> right = allPossibleFBT(N-1-lef);
for(TreeNode* l : left)
{
for(TreeNode* r : right)
{
TreeNode *root = new TreeNode(0);
root->left = l;
root->right = r;
ans.push_back(root);
}
}
}
return ans;
}
5. LeetCode 面试题 17.12 : BiNode
题目:二叉树数据结构TreeNode可用来表示单向链表(其中left置空,right为下一个链表节点)。实现一个方法,把二叉搜索树转换为单向链表,要求依然符合二叉搜索树的性质,转换操作应是原址的,也就是在原始的二叉搜索树上直接修改。
返回转换后的单向链表的头节点。
注意:本题相对原题稍作改动
/*
* 5
* LeetCode 面试题 17.12 : BiNode
* https://leetcode-cn.com/problems/binode-lcci/
*/
void inorderTraversal(TreeNode* root, TreeNode* &cur)
{
//中序遍历
if(root)
{
inorderTraversal(root->left, cur);
cur->right = root; //将此节点赋给cur的右子树
root->left = NULL; //将此节点的左子树赋值NULL
cur = root; //更新
inorderTraversal(root->right, cur);
}
}
TreeNode* convertBiNode(TreeNode* root)
{
TreeNode* head = new TreeNode(-1);
TreeNode* cur = head;
inorderTraversal(root, cur);
return head->right;
}
6. LeetCode 783 : 二叉搜索树节点最小距离
题目:给定一个二叉搜索树的根节点 root,返回树中任意两节点的差的最小值。
/*
* 6
* LeetCode 783 : 二叉搜索树节点最小距离
* https://leetcode-cn.com/problems/minimum-distance-between-bst-nodes/
*/
// 在二叉搜索树中,中序遍历会将树中节点按数值大小顺序输出。只需要遍历计算相邻数的差值,取其中最小的就可以了。
int _pre, _res;
bool _flag = true;
void inorder(TreeNode* root)
{
if(root == NULL)
return;
inorder(root->left);
if(!_flag)
{
_res = min(_res, root->val - _pre);
}
_pre = root->val;
_flag = false;
inorder(root->right);
}
int minDiffInBST(TreeNode* root)
{
_res = INT_MAX;
inorder(root);
return _res;
}
回溯面试算法总结
回溯算法实际上一个类似枚举的搜索尝试过程。回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。
1. LeetCode 79 : 单词搜索
题目:给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
/*
* 1
* LeetCode 79 : 单词搜索
* https://leetcode-cn.com/problems/word-search/
*/
vector<vector<int> > dirs = {{-1,0},{0,1},{1,0},{0,-1}};
bool dfs(vector<vector<char>>& board, string& word, int id, int dx, int dy, vector<vector<bool> >& visited)
{
if(id == word.size()-1)
{
return word[id] == board[dx][dy];
}
if(word[id] == board[dx][dy])
{
for(int i = 0; i < 4; i++)
{
visited[dx][dy] = true;
int ndx = dx + dirs[i][0];
int ndy = dy + dirs[i][1];
if(ndx >= 0 && ndx < board.size() && ndy >= 0 && ndy < board[0].size() && (!visited[ndx][ndy]) && dfs(board, word, id+1, ndx, ndy, visited))
{
return true;
}
}
visited[dx][dy] = false;
}
return false;
}
bool exist(vector<vector<char>>& board, string word)
{
if(board.size() == 0 || board[0].size() == 0)
{
return false;
}
if(word.empty())
{
return true;
}
vector<vector<bool> > visited(board.size(), vector<bool>(board[0].size(), false));
for(int i = 0; i < board.size(); i++)
{
for(int j= 0; j < board[i].size(); j++)
{
if(dfs(board, word, 0, i, j, visited))
{
return true;
}
}
}
return false;
}
2. LeetCode 131 : 分割回文串
题目:给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例:
输入: “aab”
输出:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]
/*
* 2
* LeetCode 131 : 分割回文串
* https://leetcode-cn.com/problems/palindrome-partitioning/
*/
bool isPalindrome(string s)
{
int first = 0;
int end = s.size()-1;
while(first < end)
{
if(s[first++] != s[end--])
return false;
}
return true;
}
void helper(string& s, int pos, vector<string>& path, vector<vector<string> >& result)
{
if(pos == s.size())
{
result.push_back(path);
return ;
}
for(int i = pos; i < s.size(); i++)
{
if(isPalindrome(s.substr(pos, i-pos+1)))
{
path.push_back(s.substr(pos, i-pos+1));
helper(s, i+1, path, result);
path.pop_back();
}
}
}
vector<vector<string>> partition(string s)
{
vector<string> path;
vector<vector<string> > result;
helper(s, 0, path, result);
return result;
}
3. LeetCode 面试题 08.08 : 有重复字符串的排列组合
题目:有重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合。
示例1:
输入:S = “qqe”
输出:[“eqq”,“qeq”,“qqe”]
示例2:
输入:S = “ab”
输出:[“ab”, “ba”]
提示:
字符都是英文字母。
字符串长度在[1, 9]之间。
/*
* 3
* LeetCode 面试题 08.08 : 有重复字符串的排列组合
* https://leetcode-cn.com/problems/permutation-ii-lcci/
*/
void backtrack(const int len, map<char, int>& rec, string pre, vector<string>& res)
{
if(pre.size() == len)
{
res.push_back(pre);
return;
}
auto iter = rec.begin();
for(; iter != rec.end(); iter++)
{
if(iter->second)
{
(iter->second)--;
backtrack(len, rec, pre + iter->first, res);
(iter->second)++;
}
}
return;
}
vector<string> permutation(string S)
{
vector<string> res;
map<char, int> rec;
for(int i = 0; i < S.size(); i++)
{
if(rec.count(S[i])) {
rec[S[i]]++;
} else {
rec[S[i]] = 1;
}
}
backtrack(S.size(), rec, "", res);
return res;
}
4. LeetCode 39 : 组合总和
题目:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
提示:
1 <= candidates.length <= 30
1 <= candidates[i] <= 200
candidate 中的每个元素都是独一无二的。
1 <= target <= 500
/*
* 4
* LeetCode 39 : 组合总和
* https://leetcode-cn.com/problems/combination-sum/
*/
void backtrack(vector<int>& candidates, int target, int id, vector<int>& item, vector<vector<int> >& res)
{
if(target < 0)
{
return;
}
if(target == 0)
{
res.push_back(item);
return;
}
for(int i = id; i < candidates.size(); i++)
{
item.push_back(candidates[i]);
backtrack(candidates, target-candidates[i], i, item, res);
item.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
vector<vector<int> > res;
if(candidates.empty())
{
return res;
}
vector<int> item;
backtrack(candidates, target, 0, item, res);
return res;
}
5. LeetCode 40 : 组合总和 II
题目:给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
/*
* 5
* LeetCode 40 : 组合总和 II
* https://leetcode-cn.com/problems/combination-sum-ii/
*/
void backtrack2(vector<int>& candidates, int start, int target, vector<int>& item, vector<vector<int>> & res)
{
if(target < 0)
return;
if(target == 0)
{
res.push_back(item);
return;
}
for(int i = start; i < candidates.size(); i++)
{
if(i > start && candidates[i] ==candidates[i - 1])
continue;
item.push_back(candidates[i]);
backtrack2(candidates, i+1, target - candidates[i], item, res);
item.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
{
vector<vector<int>> res;
vector<int> item;
sort(candidates.begin(),candidates.end());
backtrack2(candidates,0,target,item,res);
return res;
}
6. LeetCode 面试题 08.07 : 无重复字符串的排列组合
题目:无重复字符串的排列组合。编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。
示例1:
输入:S = “qwe”
输出:[“qwe”, “qew”, “wqe”, “weq”, “ewq”, “eqw”]
示例2:
输入:S = “ab”
输出:[“ab”, “ba”]
提示:
字符都是英文字母。
字符串长度在[1, 9]之间。
/*
* 6
* LeetCode 面试题 08.07 : 无重复字符串的排列组合
* https://leetcode-cn.com/problems/permutation-i-lcci/
*/
// method 1
vector<string> permutation(string S)
{
vector<string> res;
dfs(S, 0, res);
return res;
}
void dfs(string& S, int index, vector<string>& res)
{
if(index == S.size())
{
res.push_back(S);
return;
}
for(int i = index; i < S.size(); ++i)
{
swap(S[i], S[index]);
dfs(S, index + 1, res);
swap(S[i], S[index]);
}
}
// method 2
vector<string> permutation(string S)
{
sort(S.begin(), S.end());
vector<string> res;
res.emplace_back(S);
while(next_permutation(S.begin(), S.end()))
res.emplace_back(S);
return res;
}
// method 3
vector<string> permutation(string S)
{
vector<string> res;
if(S.size() == 0)
{
return res;
}
vector<bool> vis(S.size(), false);
backtrack(S, "", vis, res);
return res;
}
void backtrack(string& S, string item, vector<bool>& vis, vector<string>& res)
{
if(S.size() == item.size())
{
res.push_back(item);
} else {
for(int i = 0; i < vis.size(); i++)
{
if(!vis[i])
{
item += S[i];
vis[i] = true;
backtrack(S, item, vis, res);
item.pop_back();
vis[i] = false;
}
}
}
}
7. LeetCode 47 : 全排列 II
题目:给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
/*
* 7
* LeetCode 47 : 全排列 II
* https://leetcode-cn.com/problems/permutations-ii/
*/
void generate(vector<int>& nums, int index, vector<int>& item, vector<bool>& vis, vector<vector<int> >& res)
{
if(index == nums.size()){
res.push_back(item);
return;
}
for(int i = 0; i < nums.size(); i++)
{
if(vis[i])
continue;
if((i > 0) && (nums[i-1] == nums[i]) && (!vis[i-1]))
continue;
item.push_back(nums[i]);
vis[i] = true;
generate(nums, index+1, item, vis, res);
item.pop_back();
vis[i] = false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums)
{
vector<vector<int> > res;
if(nums.empty())
return res;
vector<bool> vis(nums.size(), false);
vector<int> item;
sort(nums.begin(),nums.end());
generate(nums, 0, item, vis, res);
return res;
}
8. LeetCode 1079 : 活字印刷
题目:你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。
注意:本题中,每个活字字模只能使用一次。
示例 1:
输入:“AAB”
输出:8
解释:可能的序列为 “A”, “B”, “AA”, “AB”, “BA”, “AAB”, “ABA”, “BAA”。
示例 2:
输入:“AAABBC”
输出:188
提示:
1 <= tiles.length <= 7
tiles 由大写英文字母组成
/*
* 8
* LeetCode 1079 : 活字印刷
* https://leetcode-cn.com/problems/letter-tile-possibilities/
*/
int mres;
void dfs(string& tiles, vector<bool>& vis)
{
for(int i = 0; i < tiles.size(); i++)
{
if((i > 0) && (tiles[i] == tiles[i-1] && !vis[i-1]))
continue;
if(!vis[i])
{
vis[i] = true;
mres++;
dfs(tiles, vis);
vis[i] = false;
}
}
}
int numTilePossibilities(string tiles)
{
mres = 0;
if(tiles.empty())
return mres;
sort(tiles.begin(), tiles.end());
vector<bool> vis(tiles.size(), false);
dfs(tiles, vis);
return mres;
}
总结
以上是递归和回溯相关面试算法题目汇总,个别题目也给出了解题思路和注释。
所有代码都可以去我的GitHub网站查看,后续也将继续补充其他算法方面的相关面试题目。