一、贪心算法
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规则则会保存以前的运行结果,并根据以前的结果对当前进行选择,有回退功能
- 贪心:当下做局部最优判断
- 回溯:能够回退
- 动态规划:最优判断+回退
贪心算法可以解决一些最优化问题,如:求图中的最小生成树、求哈弗曼编码等。然而对于工程和生活中的问题,贪心法一般不能得到我们所要求的答案
一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题
适用贪心算法的场景:问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。这种子问题最优解称为最优子结构
二、分治算法
1、如何理解分治算法
分治算法就是将原问题划分成n个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解
分治算法的递归实现中,每一层递归都会涉及这样三个操作:
- 分解:将原问题分解成一系列子问题
- 解决:递归地求解各个子问题,若子问题足够小,则直接求解
- 合并:将子问题的结果合并成原问题
分治算法能解决的问题,一般需要满足下面这几个条件:
- 原问题与分解成的小问题具有相同的模式
- 原问题分解成的子问题可以独立求解,子问题之间没有相关性
- 具有分解终止条件,也就是说,当问题足够小,可以直接求解
- 可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减小算法总体复杂度的效果了
2、分治算法相关题目
1)、LeetCode50:Pow(x, n)
实现pow(x, n),即计算x的n次幂函数
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
示例 3:
输入: 2.00000, -2
输出: 0.25000
解释: 2-2 = 1/22 = 1/4 = 0.25
说明:
-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1]
题解:
public double myPow(double x, int n) {
if (n < 0) {
x = 1 / x;
n = -n;
}
return helper(x, n);
}
private double helper(double x, int n) {
if (n == 0) return 1.0;
double half = helper(x, n / 2);
if (n % 2 == 0) return half * half;
else return half * half * x;
}
2)、LeetCode169:求众数
给定一个大小为n的数组,找到其中的众数。众数是指在数组中出现次数大于 n / 2 n/2 n/2的元素
你可以假设数组是非空的,并且给定的数组总是存在众数
示例 1:
输入: [3,2,3]
输出: 3
示例 2:
输入: [2,2,1,1,1,2,2]
输出: 2
题解:
public int majorityElement(int[] nums) {
return helper(0, nums.length - 1, nums);
}
private int helper(int low, int high, int[] nums) {
if (low == high) return nums[low];
int mid = low + ((high - low) >> 1);
int left = helper(low, mid, nums);
int right = helper(mid + 1, high, nums);
if (left == right) return left;
int leftCount = countInRange(left, low, high, nums);
int rightCount = countInRange(right, low, high, nums);
return leftCount > rightCount ? left : right;
}
private int countInRange(int num, int low, int high, int[] nums) {
int count = 0;
for (int i = low; i <= high; ++i) {
if (nums[i] == num) count++;
}
return count;
}
三、回溯算法
1、如何理解回溯算法
回溯的处理思想有点类似枚举搜索。通过枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走
2、回溯算法相关题目
1)、LeetCode22:括号生成
给出n代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合
例如,给出n=3,生成结果为:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
题解:
public List<String> generateParenthesis(int n) {
List<String> list = new ArrayList<>();
helper(0, 0, n, "", list);
return list;
}
private void helper(int left, int right, int max, String currentStr, List<String> list) {
if (left == max && right == max) {
list.add(currentStr);
return;
}
if (left < max) {
helper(left + 1, right, max, currentStr + "(", list);
}
if (right < left) {
helper(left, right + 1, max, currentStr + ")", list);
}
}
2)、LeetCode78:子集
给定一组不含重复元素的整数数组nums,返回该数组所有可能的子集(幂集)
说明:解集不能包含重复的子集
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
题解:
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
if (nums.length == 0) return result;
helper(0, nums, new ArrayList<>());
return result;
}
private void helper(int level, int[] nums, List<Integer> list) {
if (level == nums.length) {
result.add(new ArrayList<Integer>(list));
return;
}
helper(level + 1, nums, list);
list.add(nums[level]);
helper(level + 1, nums, list);
list.remove(list.size() - 1);
}
3)、LeetCode17:电话号码的字母组合
题解:
Map<Character, String> phone = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
List<String> result = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if (digits.length() == 0) return result;
helper(0, digits, "");
return result;
}
private void helper(int level, String digits, String currentStr) {
if (level == digits.length()) {
result.add(currentStr);
return;
}
String latters = phone.get(digits.charAt(level));
for (int i = 0; i < latters.length(); ++i) {
helper(level + 1, digits, currentStr + latters.charAt(i));
}
}
4)、LeetCode46:全排列
给定一个没有重复数字的序列,返回其所有可能的全排列
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
题解:
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
int[] visited = new int[nums.length];
helper(0, nums, visited, new Stack<>());
return result;
}
private void helper(int depth, int[] nums, int[] visited, Stack<Integer> stack) {
if (depth == nums.length) {
result.add(new ArrayList<Integer>(stack));
return;
}
for (int i = 0; i < nums.length; ++i) {
if (visited[i] == 1) continue;
visited[i] = 1;
stack.push(nums[i]);
helper(depth + 1, nums, visited, stack);
visited[i] = 0;
stack.pop();
}
}
5)、LeetCode47:全排列 II
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
题解:
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
int[] visited = new int[nums.length];
Arrays.sort(nums);
helper(0, nums, visited, new Stack<>());
return result;
}
private void helper(int depth, int[] nums, int[] visited, Stack<Integer> stack) {
if (depth == nums.length) {
result.add(new ArrayList<Integer>(stack));
return;
}
for (int i = 0; i < nums.length; ++i) {
if (visited[i] == 1) continue;
if (i > 0 && nums[i] == nums[i - 1] && visited[i - 1] != 1) continue;
visited[i] = 1;
stack.push(nums[i]);
helper(depth + 1, nums, visited, stack);
visited[i] = 0;
stack.pop();
}
}
6)、LeetCode77:组合
给定两个整数n和k,返回1…n中所有可能的k个数的组合
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
题解:
List<List<Integer>> result = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
helper(1, n, k, new LinkedList<Integer>());
return result;
}
private void helper(int first, int n, int k, LinkedList<Integer> list) {
if (list.size() == k) {
result.add(new LinkedList<Integer>(list));
return;
}
for (int i = first; i <= n; ++i) {
list.add(i);
helper(i + 1, n, k, list);
list.removeLast();
}
}
常用数据结构的时间、空间复杂度:
https://www.bigocheatsheet.com/