目录
基本参考自leetcode官方写法,希望能写出简洁、高效、标准的模式
二分查找
二分查找的核心,明确 l 和 r 在什么条件下进行移动(更新)
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int mid = (high - low) / 2 + low;
int num = nums[mid];
if (num == target) {
return mid;
} else if (num > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
进阶:33. 搜索旋转排序数组 153. 寻找旋转排序数组中的最小值
双指针
回文串判断:
//while形式
public boolean isPalindrome(String s) {
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) {
return false;
}
++left;
--right;
}
return true;
}
//for形式
public boolean isPalindrome(String s) {
for(int i =0, j=s.length()-1; i < j; i++, j--){
if(s.charAt(i) != s.charAt(j)){
return false;
}
}
return true;
}
参考:125. 验证回文串
快速选择
如选择数组中第K大的数,如下方法中,最后一个参数为数组下标,所以传参时传数组长度N减去K
public int quickselect(int[] nums, int l, int r, int nMinusK){
if (l == r) {
return nums[nMinusK];
}
int x = nums[l];
int i=l-1;
int j = r+1;
int tmp=0;
while(i < j){
do {
i++;
} while(nums[i] < x);
do {
j--;
} while(nums[j] > x) ;
if(i<j){
tmp = nums[i];
nums[i]=nums[j];
nums[j]=tmp;
}
}
if(nMinusK <= j ){
return quickselect(nums, l, j, nMinusK);
} else {
return quickselect(nums, j+1, r, nMinusK);
}
}
参考:215. 数组中的第K个最大元素 :. - 力扣(LeetCode)
二叉树:深度优先搜索
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
} else {
int leftHeight = maxDepth(root.left);
int rightHeight = maxDepth(root.right);
return Math.max(leftHeight, rightHeight) + 1;
}
}
参考: 104. 二叉树的最大深度:. - 力扣(LeetCode)
二叉树:广度优先搜索
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ret = new ArrayList<List<Integer>>();
if (root == null) {
return ret;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> level = new ArrayList<Integer>();
int currentLevelSize = queue.size();
for (int i = 1; i <= currentLevelSize; ++i) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
ret.add(level);
}
return ret;
}
参考:102. 二叉树的层序遍历 :. - 力扣(LeetCode)
图:深度优先搜索
void dfs(char[][] grid, int r, int c) {
int nr = grid.length;
int nc = grid[0].length;
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
return;
}
grid[r][c] = '0';
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
dfs(grid, r, c);
}
}
}
return num_islands;
}
参考: 200. 岛屿数量 : . - 力扣(LeetCode)
图:广度优先搜索
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
grid[r][c] = '0';
Queue<Integer> neighbors = new LinkedList<>();
neighbors.add(r * nc + c);
while (!neighbors.isEmpty()) {
int id = neighbors.remove();
int row = id / nc;
int col = id % nc;
if (row - 1 >= 0 && grid[row-1][col] == '1') {
neighbors.add((row-1) * nc + col);
grid[row-1][col] = '0';
}
if (row + 1 < nr && grid[row+1][col] == '1') {
neighbors.add((row+1) * nc + col);
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1] == '1') {
neighbors.add(row * nc + col-1);
grid[row][col-1] = '0';
}
if (col + 1 < nc && grid[row][col+1] == '1') {
neighbors.add(row * nc + col+1);
grid[row][col+1] = '0';
}
}
}
}
}
return num_islands;
}
参考: 200. 岛屿数量 : . - 力扣(LeetCode)
常用数据结构
栈:可用LinkedList实现,常用方法pop、push、peek、isEmpty
Deque<T> stack = new LinkedList<T>();
参考练习:20. 有效的括号
队列:
Queue<T> queue = new LinkedList<T>();
参考练习:387. 字符串中的第一个唯一字符
技巧
x、y的最大公约数:
public int gcd(int x, int y) {
return y > 0 ? gcd(y, x % y) : x;
}
参考自 189. 轮转数组 的官方解法二: . - 力扣(LeetCode)
性能
【防止超时的方式:
1.缓存,通常使用哈希表
2.分治(动态规划)
3.当可以使用for或while循环时,排序+双指针,时间复杂度通常优于回溯,双指针可以使n层循环减少为n-1层循环。 参考练习:18. 四数之和 ,该参考练习中,也使用了比较有效的剪枝操作。
4.剪枝操作,即不满足功能要求时提前break/continue掉for/while循环,或者下标 i++不用进行逻辑处理(尤其在合适的用例下 i 能不停的向后移动一大段会加速很快)】
动态规划
算法导论这本书是这样介绍这个算法的,动态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。再来了解一下什么是分治方法,以及这两者之间的差别,分治方法将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。而动态规划与之相反,动态规划应用与子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。动态规划对于每一个子子问题只求解一次,将其解保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作()。
动态规划算法的核心就是记住已经解决过的子问题的解
记住求解的方式有两种:①自顶向下的备忘录法 ②自底向上。(递归法比较简单,不讲了,且其步骤被包含于自顶向下的备忘录法中)
动态规划的应用场景:
1.最优子结构
用动态规划求解最优化问题的第一步就是刻画最优解的结构,如果一个问题的解结构包含其子问题的最优解,就称此问题具有最优子结构性质。因此,某个问题是否适合应用动态规划算法,它是否具有最优子结构性质是一个很好的线索。使用动态规划算法时,用子问题的最优解来构造原问题的最优解。因此必须考查最优解中用到的所有子问题。
2.重叠子问题
在斐波拉契数列和钢条切割结构图中,可以看到大量的重叠子问题,比如说在求fib(6)的时候,fib(2)被调用了5次,在求cut(4)的时候cut(0)被调用了4次。如果使用递归算法的时候会反复的求解相同的子问题,不停的调用函数,而不是生成新的子问题。如果递归算法反复求解相同的子问题,就称为具有重叠子问题(overlapping subproblems)性质。在动态规划算法中使用数组来保存子问题的解,这样子问题多次求解的时候可以直接查表不用调用函数递归。(动态规划帮助我们解决一类递归问题,这些问题可以分解成一个个高度重叠的子问题。“高度重叠”指的是子问题一遍遍地重复出现。相反地,像归并排序这种递归算法,会先独立地排好列表的一半,再把结果合并。这种把问题分解成子问题,并且产生的子问题不会重叠的算法,就是分治法)
(后边这个描述我认为不够好: 动态规划方法一般用来求解最优化问题。这类问题可以有很多可行解,每个解都有一个值,我们希望找到具有最优值的解,我们称这样的解为问题的一个最优解,而不是最优解,因为可能有多个解都达到最优值。)
我们解决动态规划问题一般分为四步:
1、定义一个状态,这是一个最优解的结构特征
2、进行状态递推,得到递推公式
3、进行初始化
4、返回结果
自顶向下的备忘录法模板:
1. 定义备忘录 (视情况看是否进行备忘录初始化)
2. 调用带备忘录的dpMethod
3. dpMethod方法结构:备忘录有值直接返回备忘录的值;否则用递归终止条件和递归递推公式分情况为备忘录赋值,并返回。
public static int dpMethod(int n)
{
int []Memo=new int[n+1]; //定义备忘录
for(int i=0;i<=n;i++) //视情况看是否需要初始化备忘录, 不显式初
Memo[i]=-1; //始化则备忘录各元素为默认值,显式初始化会额外耗时
return dpMethodWithMem(n, Memo);
}
public static int dpMethodWithMem(int n,int []Memo)
{
if(Memo[n]!=-1) //如果已经求出了dpMethodWithMem(n)的值直接返回(不为初始值则为已求出)
return Memo[n];
//否则将求出的值保存在Memo备忘录中。
if(n<=2)
Memo[n]=1;//递归的终止条件,当dpMethodWithMem的入参n为1或2时,Memo[1] Memo[2]没被初始化,进入此分支将Memo[1] Memo[2]赋值为1,并在下边return
else
Memo[n]=dpMethodWithMem(n-1,Memo)+dpMethodWithMem(n-2,Memo);
return Memo[n];
}
备忘录法还是利用了递归,上面算法不管怎样,计算fib(6)的时候最后还是要计算出fib(1),fib(2),fib(3)…,那么何不先计算出fib(1),fib(2),fib(3)…,呢?这也就是动态规划的核心,先计算子问题,再由子问题计算父问题。由此得到:
自底向上法模板:
1.定义dp数组
2.初始化dp数组的前1个或2个元素的值(也即递归时递归的终止条件对应的值,有时可用定义数组时的默认值,与问题相关,具体问题具体分析)
3.采用for循环,依次从前往后根据递推公式赋值dp数组的每个元素,直到n(数组后边的元素求解依赖前边的元素值,n为要求解的规模,通常为问题入参。)
具体示例1:
public static int dpMethod(int n)
{
if(n<=0)
return n;
int []Memo=new int[n+1];
Memo[0]=0;
Memo[1]=1;
for(int i=2;i<=n;i++)
{
Memo[i]=Memo[i-1]+Memo[i-2]; //根据递推公式自底向上往后计算Memo值,不同问题,递推公式不同
}
return Memo[n];
}
观察参与循环的只有 i,i-1 , i-2三项,因此该方法的空间可以进一步的压缩如下。
public static int dpMethod(int n)
{
if(n<=1)
return n;
int Memo_i_2=0;
int Memo_i_1=1;
int Memo_i=1;
for(int i=2;i<=n;i++)
{
Memo_i=Memo_i_2+Memo_i_1;
Memo_i_2=Memo_i_1;
Memo_i_1=Memo_i;
}
return Memo_i;
}
具体示例2:
public static int buttom_up_cut(int []p)
{
int []r=new int[p.length+1];
for(int i=1;i<=p.length;i++)
{
int q=-1;
//① 根据递推公式自底向上往后计算r[i]值,此处递推公式是带for循环的范围内的求最值
for(int j=1;j<=i;j++)
q=Math.max(q, p[j-1]+r[i-j]);
r[i]=q;
}
return r[p.length];
}
以上2示例均来自:https://blog.csdn.net/u013309870/article/details/75193592
由于备忘录方式的动态规划方法使用了递归,递归的时候会产生额外的开销,使用自底向上的动态规划方法要比备忘录方法好。
动态规划的经典模型
线性模型的是动态规划中最常用的模型,上文讲到的钢条切割问题就是经典的线性模型,这里的线性指的是状态的排布是呈线性的。
区间模型
区间模型的状态表示一般为d[i][j],表示区间[i, j]上的最优解,然后通过状态转移计算出[i+1, j]或者[i, j+1]上的最优解,逐步扩大区间的范围,最终求得[1, len]的最优解。
【例题2】给定一个长度为n(n <= 1000)的字符串A,求插入最少多少个字符使得它变成一个回文串。
典型的区间模型,回文串拥有很明显的子结构特征,即当字符串X是一个回文串时,在X两边各添加一个字符’a’后,aXa仍然是一个回文串,我们用d[i][j]来表示A[i…j]这个子串变成回文串所需要添加的最少的字符数,那么对于A[i] == A[j]的情况,很明显有 d[i][j] = d[i+1][j-1] (这里需要明确一点,当i+1 > j-1时也是有意义的,它代表的是空串,空串也是一个回文串,所以这种情况下d[i+1][j-1] = 0);当A[i] != A[j]时,我们将它变成更小的子问题求解,我们有两种决策:
1、在A[j]后面添加一个字符A[i];
2、在A[i]前面添加一个字符A[j];
根据两种决策列出状态转移方程为:
d[i][j] = min{ d[i+1][j], d[i][j-1] } + 1; (每次状态转移,区间长度增加1)
背包模型
背包问题是动态规划中一个最典型的问题之一。(注:暂未看用动态规划解背包问题的解决方案代码,我收藏中的一片博文中观点,是用回溯解的背包问题。 除本参考文章https://blog.csdn.net/u013309870/article/details/75193592 描述背包问题属于背包模型,暂未看到过其他背包模型的动态规划算法题,线性模型和区间模型的动态规划算法题居多。)
分治算法
在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)
分治基本思想及策略:
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
分治法使用场景:
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
分治法的基本步骤及模板
分治法在每一层递归上都有三个步骤:
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。
它的一般的算法模板如下:
Divide-and-Conquer(P)
1. if |P|≤n0
2. then return(ADHOC(P))
3. 将P分解为较小的子问题 P1 ,P2 ,…,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi
6. T ← MERGE(y1,y2,…,yk) △ 合并子问题
7. return(T)
分治法的复杂性分析(选读)
一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
T(n)= k T(n/m)+f(n)
通过迭代法求得方程的解:
递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。通常假定T(n)是单调上升的,从而当mi≤n<mi+1时,T(mi)≤T(n)<T(mi+1)。
分治法参考:五大常用算法:分治算法_可以使用分治法求解的有-CSDN博客
贪心算法
贪心基本思想及策略:
这个思维过程就是贪心:每一阶段做决策都找出当前条件下的最优解。当然这个问题,这种解法的正确性显而易见,贪心类的问题最难的地方在于正确性无法证明。
贪心算法是指:在每一步求解的步骤中,它要求“贪婪”的选择最佳操作,并希望通过一系列的最优选择,能够产生一个问题的(全局的)最优解。
贪心算法每一步必须满足以下条件:
1、可行的:即它必须满足问题的约束。
2、局部最优:他是当前步骤中所有可行选择中最佳的局部选择。
3、不可取消:即选择一旦做出,在算法的后面步骤就不可改变了。
回溯算法
回溯法(back tracking)(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。回溯法可以理解为通过选择不同的岔路口寻找目的地,一个岔路口一个岔路口的去尝试找到目的地。如果走错了路,继续返回来找到岔路口的另一条路,直到找到目的地。
求解步骤(0-1背包问题):
求解步骤:
1)针对所给问题,定义问题的解空间;
2)确定易于搜索的解空间结构;
3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
回溯法对解空间做深度优先搜索时,有递归回溯和迭代回溯(非递归)两种方法,但一般情况下用递归方法实现回溯法。
问题的解空间及剪枝函数
问题的解空间:
用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少包含问题的一个(最优)解。对于 n=3 时的 0/1 背包问题,可用一棵完全二叉树表示解空间,如图所示:
常用的剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树;用限界函数剪去得不到最优解的子树。