Open xiedaxia1hao opened 3 years ago
Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.
这种要求我们求子集的题目,又是return all possible subsets
,我第一反应就是要用backtrack来做,因为backtrack的特点就是穷尽所有可能的情况来找到满足条件的解。
这题还有一种思路,就是用BFS来做,在实现上会比backtrack稍微简单点,也更直观一点。这一题我会先用BFS来做一遍,然后再提供backtrack的解法。
那这题如何用BFS来做呢?
举个例子,对于集合[1, 5, 3]
来说,我们可以:
因为输入的集合的数字都是unique的,这种解法不会产生重复解。
我们可以用下图来可视化这个过程:
代码如下:
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> subsets = new ArrayList<>();
subsets.add(new ArrayList<>());
for(int num : nums) {
int size = subsets.size();
for(int i = 0; i < size; i++) {
List<Integer> set = new ArrayList<>(subsets.get(i));
set.add(num);
subsets.add(new ArrayList<>(set));
}
}
return new ArrayList<>(subsets);
}
}
要注意的是,这里如果我们把subsets这个变量定义成Queue的话,也就是LinkedList,在subsets.get(i)
这个操作会花费O(n)的时间,而ArrayList的话,subsets.get(i)
仅花费O(1)的时间。所以我们可以把subsets定义成ArrayList而不是LinkedList。
时间复杂度:O(N 2 ^ N), N代表set里面存的数字个数,我们一共会有2^N的集合,所以复杂度为O(N 2 ^ N)。 空间复杂度:O(N 2 ^ N),我们一共有2^N个subset,每一个subset最多可以占用O(N)的空间,所以复杂度为O(N 2 ^ N)。
上面这题的input是没有重复数字的,那如果input里含有重复数字了,并且要求outpu不能有重复的subsets,我们应该怎么做呢?
Given an integer array nums that may contain duplicates, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.
首先比较直接的能想到的是,我们需要把input set给sort一下,这样我们可以保证所有的重复数字都是sit next to each other。
那如果我们碰见了重复数字以后,该怎么办呢?
让我们用 [1, 5, 3, 3]
来举个例子:
下图是以上步骤的可视化操作:
所以我们只需要用两个新的变量,startIndex
和endIndex
,来控制我们的循环区间就可以了。代码如下:
class Solution {
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> subsets = new ArrayList<>();
if(nums == null || nums.length < 1) return subsets;
Arrays.sort(nums);
subsets.add(new ArrayList<>());
int startIndex = 0, endIndex = 0;
for(int i = 0; i < nums.length; i++) {
startIndex = 0;
// if current and the previous elements are same, create new subsets only from the subsets
// added in the previous step
if(i > 0 && nums[i] == nums[i-1]) {
startIndex = endIndex+1;
}
endIndex = subsets.size()-1;
for(int j = startIndex; j <= endIndex; j++) {
// DO NOT DO: List<Integer> set = subsets.get(j)
// Java is passed by reference and when we modify this set in the following code,
// the set in the result list will also be affected.
List<Integer> set = new ArrayList<>(subsets.get(j));
set.add(nums[i]);
subsets.add(set);
}
}
return subsets;
}
}
时间复杂度:O(N * 2 ^ N)*, N代表set里面存的数字个数,我们一共会有2^N的集合,所以复杂度为O(N 2 ^ N)。 空间复杂度:O(N * 2 ^ N)*,我们一共有2^N个subset,每一个subset最多可以占用O(N)的空间,所以复杂度为O(N 2 ^ N)。
Given an array nums of distinct integers, return all the possible permutations. You can return the answer in any order.
之前我们的求subsets的时候,每个subset的长度在[0, nums.length]
之间都是可以的,但是求permutations的时候,每个permutation一定要包含所有的数字才行,那这种情况下,我们应该怎么做呢?
我们用数组[1, 3, 5]来举个例子:
我们来具体看一下如何从第三步走向第四步的:
我们先看如何把数字5加入到[3, 1]来生成它的全排列。我们把当前数字5,分别加到[3, 1]的最前面,中间,和最后面,即:
所以我们每要加一个数字的时候,都对上一轮所有生成的全排列依次循环,在每个排列的所有位置分别加入该数字。等我们加完最后一个数字,就得到了我们要的结果了。
以下为上述过程的示意图:
代码实现如下:
class Solution {
public List<List<Integer>> permute(int[] nums) {
Queue<List<Integer>> permutations = new LinkedList<>();
permutations.offer(new ArrayList<>());
for(int num : nums) {
int size = permutations.size();
for(int i = 0; i < size; i++) {
List<Integer> oldPermutation = permutations.poll();
for(int j = 0; j <= oldPermutation.size(); j++) {
List<Integer> newPermutation = new ArrayList<>(oldPermutation);
newPermutation.add(j, num);
permutations.offer(newPermutation);
}
}
}
return new ArrayList<>(permutations);
}
}
时间复杂度:O(N * N!)*, 其中N代表input里的数字总和。我们一共会有N!个全排列,每个全排列,最多会有N个可以插入数字的节点,所以复杂度为O(N N!)。 空间复杂度:O(N * N!)**,我们一共有N!全排列,每个全排列,最多可以保存N个数字。
Given a string s, we can transform every letter individually to be lowercase or uppercase to create another string. Return a list of all possible strings we could create. You can return the output in any order.
这题我第一个想到的是用subsets的bfs的方式来做,代码会附在最后。
Grokking. 上有一个有意思的解法,我们一起来用"ab7c"
这个例子来看看:
"ab7c"
出发同样的,我们来看看如何从第三步跳到第五步的:
在第五步中,我们先把第三步的所有permutations copy下来,然后把对应的'c'转换成'C',加入到最后的结果里。
下图是上述过程的可视化:
代码如下:
class Solution {
public List<String> letterCasePermutation(String s) {
List<String> permutations = new ArrayList<>();
permutations.add(s);
for(int i = 0; i < s.length(); i++) {
if(Character.isLetter(s.charAt(i))) {
int size = permutations.size();
for(int j = 0; j < size; j++) {
char[] chrs = permutations.get(j).toCharArray();
if(Character.isUpperCase(chrs[i])) {
chrs[i] = Character.toLowerCase(chrs[i]);
} else {
chrs[i] = Character.toUpperCase(chrs[i]);
}
permutations.add(new String(chrs));
}
}
}
return permutations;
}
}
*时间复杂度:O(N2^N)*, 我们一共有2^N个permutations,对于每个permutation,我们要将其转换成char array,然后进行大小写的更换,这个过程的复杂度是O(N), 所以一共的时间复杂度是O(N2^N)。
*空间复杂度:O(N2^N)**, 我们一共有2^N个permutations,每个permutation所占的space是N。
这题也可以用上题subsets的bfs的思路来做,代码如下:
class Solution {
public List<List<Integer>> permute(int[] nums) {
Queue<List<Integer>> permutations = new LinkedList<>();
permutations.offer(new ArrayList<>());
for(int num : nums) {
int size = permutations.size();
for(int i = 0; i < size; i++) {
List<Integer> oldPermutation = permutations.poll();
for(int j = 0; j <= oldPermutation.size(); j++) {
List<Integer> newPermutation = new ArrayList<>(oldPermutation);
newPermutation.add(j, num);
permutations.offer(newPermutation);
}
}
}
return new ArrayList<>(permutations);
}
}
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
这一题可以可以很直观地用backtrack来做,但是同样也可以采用bfs的方法来做,那应该怎么做呢?
对于bfs来说,我们可以在每一步的时候,给当前的string加 (
或者 )
,但是给string加括号并不是随意加的,有一定的限制:
(
。(
的个数大于 )
的个数的时候加 )
。让我们来用n=3
来看看具体的逻辑:
""
(
和 )
,加的过程中要注意上面说的两条限制。(
。因为我们没有足够的 (
来配对 )
,所以我们不能加(
。这时候,我们的combinations就是: "("
(
和 )
。对于(
来说,因为现在 (
的总个数小于3,所以我们可以直接加上。因为这时候 (
的数量是大于 )
的数量的,所以 )
也可以加上。这一轮结束后,我们的combinations就是: "((", "()"
"(("
, 加上一个新的 (
之后,我们的(
个数还是没有超过3,所以我们可以加上一个(
,得到"((("
。同样的,我们也可以加上 )
,"(()"
。对于第二个combination "()"
来说,我们不能加 )
,因为没有对于的(
和)
匹配。所以这一轮循环过后,我们的combinations就是: "(((", "(()", "()("
"((()", "(()(", "(())", "()((", "()()"
"((())", "(()()", "(())(", "()(()", "()()("
"((()))", "(()())", "(())()", "()(())", "()()()"
下图是上述过程的可视化:
所以为了方便我们的处理,知道每个parenthesis里面有多少个 (
和 )
,我们可以建一个新的ParenthesisString对象来保存,整个过程代码如下:
class Solution {
class ParenthesisString {
String str;
int openCount;
int closeCount;
public ParenthesisString(String str, int openCount, int closeCount) {
this.str = str;
this.openCount = openCount;
this.closeCount = closeCount;
}
}
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<>();
Queue<ParenthesisString> queue = new LinkedList<>();
queue.offer(new ParenthesisString("", 0, 0));
while(!queue.isEmpty()) {
ParenthesisString ps = queue.poll();
if(ps.openCount == n && ps.closeCount == n) {
res.add(ps.str);
} else {
if(ps.openCount < n) {
queue.offer(new ParenthesisString(ps.str + "(", ps.openCount+1, ps.closeCount));
}
if(ps.openCount > ps.closeCount) {
queue.offer(new ParenthesisString(ps.str + ")", ps.openCount, ps.closeCount+1));
}
}
}
return res;
}
}
这道题的时间复杂度比较复杂,一共要生成n pair的括号,那如果不考虑括号的生成限制的话,一共有2^(2N)的可能性,对于每一个括号string的生成,我们又需要给所在的string添加 (
或者 )
,这样的话,时间复杂度是O(N*2^(2N))。
这个upper bound不是完全准确的,最准确的时间复杂度是O(4^n/\sqrt{n}),面试中应该不要求手动推,了解一下就好。
因为最多有2^(2N)的combinations,每一个combination需要N个space,所以空间复杂度是O(N*2^(2N))。
Given word ="word", return the following list (order does not matter): ["word", "1ord", "w1rd", "wo1d", "wor1", "2rd", "w2d", "wo2", "1o1d", "1or1", "w1r1", "1o2", "2r1", "3d", "w3", "4"]
Leetcode的一道会员题(lintcode里也有原题)和上面的一题类似,也是通过BFS和构造一个新的对象来帮助我们解题。
那这题我们要怎么想呢?和上面生成有效括号的题目类似,我们在通过bfs来构建我们的string的时候,每一步我们有两种选择:
根据这两种选择,我们来试试看缩写BAT
:
""
B
,我们可以1)缩写这个B
,或者2)直接加到空字符里。这个过程我们得到了两个新的words: _
, B
。A
了。
_
来说,我们根据上面两条abbreviation rules,可以得到:_ _
和 1A
。B
来说,我们可以得到: B_
和 BA
_ _
, 1A
, B_
, BA
T
:
_ _
来说,我们可以得到: _ _ _
, 2T
。1A
来说,我们可以得到: 1A_
, 1AT
。B_
来说,我们可以得到: B_ _
, B1T
。BA
来说,我们可以得到: BA_
, BAT
。_ _ _
, 2T
, 1A_
, 1AT
, B_ _
, B1T
, BA_
, BAT
。_
代表skip的占位符改成数字,我们得到了: 3
, 2T
, 1A1
, 1AT
, B2
, B1T
, BA1
, BAT
。以下是上述过程的可视化:
具体的代码实现中,因为我们要用到每一个回合要加什么元素,当前有几个字符是被skip掉的, 以及现在这个string的本身。所以我们可以通过构建一个新的AbbreviatedWord对象,来帮助我们解题(其实仔细想想这种构建新的对象,就等于在backtrack的helper函数里加新的参数,对象只是用来帮助我们保存参数的而已)。
代码如下:
public class Solution {
class AbbreviatedWord {
StringBuilder str;
int start;
int count;
public AbbreviatedWord(StringBuilder str, int start, int count) {
this.str = str;
this.start = start;
this.count = count;
}
}
public List<String> generateAbbreviations(String word) {
int wordLen = word.length();
List<String> res = new ArrayList<>();
Queue<AbbreviatedWord> queue = new LinkedList<>();
queue.add(new AbbreviatedWord(new StringBuilder(), 0, 0));
while(!queue.isEmpty()) {
AbbreviatedWord abWord = queue.poll();
if(abWord.start == wordLen) {
if(abWord.count != 0) {
abWord.str.append(abWord.count);
}
res.add(new String(abWord.str));
} else {
// continue abbreviating by incrementing the current abbreviation count
queue.offer(new AbbreviatedWord(new StringBuilder(abWord.str), abWord.start+1, abWord.count+1));
// restart abbreviating, append the count and the current character to the string
if(abWord.count != 0) {
abWord.str.append(abWord.count);
}
queue.offer(new AbbreviatedWord(new StringBuilder(abWord.str).append(word.charAt(abWord.start)), abWord.start+1, 0));
}
}
return res;
}
}
以下是从Grokking. 里找到的比较有意思的题目,可以对上面学到的pattern融会贯通。
Given a string expression of numbers and operators, return all possible results from computing all the different possible ways to group numbers and operators. You may return the answer in any order.
这一题我们同样可以根据上面的思路来求解此题,具体的思路如下:
*
, +
, 和 -
的时候,把这个expression分成两个half我们用(2 * 3 - 4 * 5)
这个例子来说,把对应的递归可视化以后,就是下图这样:
代码如下:
class Solution {
public List<Integer> diffWaysToCompute(String expression) {
List<Integer> res = new ArrayList<>();
// base case: if the input string is a number, parse and add it to output.
if(!expression.contains("*") && !expression.contains("+") && !expression.contains("-")) {
res.add(Integer.parseInt(expression));
} else {
for(int i = 0; i < expression.length(); i++) {
char chr = expression.charAt(i);
if(!Character.isDigit(chr)) {
//break the equation into two halves and make recursive calls
List<Integer> leftRes = diffWaysToCompute(expression.substring(0, i));
List<Integer> rightRes = diffWaysToCompute(expression.substring(i+1));
for(int part1 : leftRes) {
for(int part2 : rightRes) {
if(chr == '*') {
res.add(part1 * part2);
} else if(chr == '+') {
res.add(part1 + part2);
} else if(chr == '-') {
res.add(part1 - part2);
}
}
}
}
}
}
return res;
}
}
其实做到这里,递归我们通常能想到的优化方法是加memorization。对于本题来说,我们同样可以用一个hashmap保存一个expression和对应的List
优化后的代码如下:
class Solution {
Map<String, List<Integer>> map = new HashMap<>();
public List<Integer> diffWaysToCompute(String expression) {
if(map.containsKey(expression)) {
return map.get(expression);
}
List<Integer> res = new ArrayList<>();
// base case: if the input string is a number, parse and add it to output.
if(!expression.contains("*") && !expression.contains("+") && !expression.contains("-")) {
res.add(Integer.parseInt(expression));
} else {
for(int i = 0; i < expression.length(); i++) {
char chr = expression.charAt(i);
if(!Character.isDigit(chr)) {
//break the equation into two halves and make recursive calls
List<Integer> leftRes = diffWaysToCompute(expression.substring(0, i));
List<Integer> rightRes = diffWaysToCompute(expression.substring(i+1));
for(int part1 : leftRes) {
for(int part2 : rightRes) {
if(chr == '*') {
res.add(part1 * part2);
} else if(chr == '+') {
res.add(part1 + part2);
} else if(chr == '-') {
res.add(part1 - part2);
}
}
}
}
}
}
map.put(expression, res);
return res;
}
}
Given an integer n, return all the structurally unique BST's (binary search trees), which has exactly n nodes of unique values from 1 to n. Return the answer in any order.
这题和 241. Different Ways to Add Parentheses 的思路类似。我们可以从1开始遍历到n,把每个数字i
都当作root,任何比i
小的数就是该root的左子树,任何比i
大的数就是该root的右子树。我们可以不断通过递归来得到左边子树和右边子树的集合,代码如下:
class Solution {
public List<TreeNode> generateTrees(int n) {
if(n <= 0) {
return new ArrayList<>();
}
return generateTreesRecursive(1, n);
}
public List<TreeNode> generateTreesRecursive(int start, int end) {
List<TreeNode> res = new ArrayList<>();
// base condition, return 'null' for an empty sub-tree
// consider n=1, in this case we will have start=end=1, this means we should have only one tree
// we will have two recursive calls, findUniqueTreesRecursive(1, 0) & (2, 1)
// both of these should return 'null' for the left and the right child
if(start > end) {
res.add(null);
return res;
}
// making "i" the root of the tree
for(int i = start; i <= end; i++) {
List<TreeNode> leftPart = generateTreesRecursive(start, i-1);
List<TreeNode> rightPart = generateTreesRecursive(i+1, end);
for(TreeNode part1 : leftPart) {
for(TreeNode part2 : rightPart) {
TreeNode root = new TreeNode(i);
root.left = part1;
root.right = part2;
res.add(root);
}
}
}
return res;
}
}
Given an integer n, return the number of structurally unique BST's (binary search trees) which has exactly n nodes of unique values from 1 to n.
这题和上题完全一样,唯一的区别就是上题返回生成树的root组成的list,这题返回可以生成的个数就好。相同的思路,代码如下:
class Solution {
public int numTrees(int n) {
if(n <= 1) {
return 1;
}
int count = 0;
// making i the root of tree
for(int i = 1; i <= n; i++) {
int left = numTrees(i-1);
int right = numTrees(n-i);
count += left * right;
}
return count;
}
}
这题同样可以加上memorization进行优化,加了之后的代码:
class Solution {
Map<Integer, Integer> map = new HashMap<>();
public int numTrees(int n) {
if(map.containsKey(n)) {
return map.get(n);
}
if(n <= 1) {
return 1;
}
int count = 0;
// making i the root of tree
for(int i = 1; i <= n; i++) {
int left = numTrees(i-1);
int right = numTrees(n-i);
count += left * right;
}
map.put(n, count);
return count;
}
}
The time complexity of the memoized algorithm will be O(n2), since we are iterating from ‘1’ to ‘n’ and ensuring that each sub-problem is evaluated only once. The space complexity will be O(n) for the memoization map.
78. Subsets 90. Subsets II 46. Permutations 784. Letter Case Permutation 22. Generate Parentheses LintCode. Generalized Abbreviation 320. Generalized Abbreviation 241. Different Ways to Add Parentheses 95. Unique Binary Search Trees II 96. Unique Binary Search Trees
刷题的过程中,我们经常会碰见排列组合的题目。
通常情况下,我们可以用BFS或者backtrack来解决这类问题。