一、概述
1、什么是算法
算法是指解决问题的一种方法或一个过程
算法是由若干条指令组成的有穷序列,且满足以下性质:
(1)输入:有零个或多个由外部提供的量作为算法的输入。
(2)输出:算法产生至少一个量作为输出。
(3)确定性:组成算法的每条指令是清晰的,无歧义的。
(4)有限性:算法中每条指令执行次数是有限的,执行每条指令的时间是有限的。
2、算法复杂性分析
一个算法的复杂性的高低体现在运行该算法所需的计算机资源(主要是指时间和空间资源)的多少上。算法的复杂性分为时间复杂性和空间复杂性。
由于内存等资源的增多,为了提高用户体验,主要考虑时间复杂度
主要考虑如下三种情况的时间复杂度:(1)最好情况;(2)最坏情况;(3)平均情况
时间复杂度的计算方法
加、减、乘、除、比较、赋值等操作,一般被看做是基本操作,并约定所用的时间都是一个单位时间;通过计算这些操作分别执行多少次来确定程序总的执行步数。一般地,一些关键操作执行的次数决定了算法的时间复杂度。
渐进复杂性
T(n) =
则 T ~ (n) =
进一步简化 T ~ (n) =
渐进记号O
用来表示时间复杂性的记号
例子:3n + 2 = O(n);10000000n + 6 = O(n); = ;
;
3、递归方程解的渐进阶求法
套用公式法
套用公式法给出求解如下形式的递归式的方法:
此式是分治法的时间复杂性所满足的递归关系,即一个规模为n的问题被分为规模均为n/b的a个子问题,递归求解这a个子问题,然后通过对这a个子问题的解综合,得到原问题的解。
二、分治法
1、分治法思想
分治法的基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归地求解这些子问题,然后利用子问题的解合并(构造)出原问题的解
2、分治算法的设计
分治算法的设计过程分为三个阶段
①分解阶段:将整个问题划分为多个子问题
②递归求解阶段:(递归调用正在设计的算法)求解每个子问题
③合并阶段:合并子问题的解,形成原始问题的解
(1)求一个非空集合的最大值问题
// 求一个非空集合的最大值
public class s分治法例子1 {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5,6,7,8,9,10};
System.out.println(dfs(arr, 0, arr.length - 1));
}
static int dfs(int[] arr, int begin, int end) {
if (begin == end) return arr[begin]; // T(n) = O(1)
int temp = (begin + end) / 2;
int leftMax = dfs(arr, begin, temp); // T(n) = O(n/2)
int rightMax = dfs(arr, temp + 1, end); // T(n) = O(n/2)
if (leftMax > rightMax) return leftMax;
return rightMax;
}
}
// 总体复杂度为T(n) ~ O(n)
(2)最大子段和问题
给定由n个整数(可能为负整数)组成的序列a1,a2,a3,...,an。求该序列的子段和的最大值。当所有整数均为负数时其最大子段和为0。
序列:-2,11,-4,13,-5,-2
①穷举法
public class q最大子段和 {
public static void main(String[] args) {
int[] arr = new int[]{-2,11,-4,13,-5,-2};
int len = arr.length;
int sum = 0;
// i是起始下标
for (int i = 0; i < len; i++) {
// j是结束下标
for (int j = i; j < len; j++) {
int tem = 0;
for (int k = i; k < j; k++) {
tem += arr[k];
}
sum = Math.max(sum,tem);
}
}
System.out.println(sum);
}
}
// 时间复杂度 T(n) ~ O(n的三次方)
分治算法
①将数组分为两半
②求出左边最大子段和
③求出右边最大子段和
④从中间向两边开始求最大子段和
public class q最大子段和分治法 {
public static void main(String[] args) {
int[] arr = new int[]{-2,11,-4,1,-5,-2};
System.out.println(dfs(arr,0,arr.length-1));
}
static int dfs(int[] arr, int left, int right) {
int sum = 0;
if (left == right) return sum = arr[left] < 0 ? 0 : arr[left]; // O(1)
int center = (left + right) / 2;
int leftSum = dfs(arr,left,center);
int rightSum = dfs(arr,center+1,right);
int s1 = 0,lefts = 0;
for (int i = center; i >= left; i--) {
lefts += arr[i];
if (lefts > s1) s1 = lefts;
}
int s2 = 0,rights = 0;
for (int i = center + 1; i <= right; i++) {
rights += arr[i];
if (rights > s2) s2 = rights;
}
sum = s1 + s2;
if (sum < rightSum) sum = rightSum;
if (sum < leftSum) sum = leftSum;
return sum;
}
}
复杂度为nlogn
三、动态规划法
1、基本思想
动态规划也是将要求解问题一层一层地分解成一级一级、规模逐步减小的子问题,直到可以直接求解其解的子问题为止。所有子问题按层次关系构成一颗子问题树。树根是原问题。原问题的解依赖于子问题树中所有子问题的解。
与分治法不同,动态规划的子问题往往不是相互独立的。动态规划法所针对的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。因此动态规划法的相应特征是,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存下来,让以后遇到时直接引用,不必重新求解。
动态规划法通常用于求一个问题在某种意义下的最优解,适合采用动态规划方法的优化问题必须具备最优子结构性质和子问题重叠性质,当一个问题的优化解包含了子问题的优化解时,则称该问题具有优化子结构性质。
在求解一个问题的过程中,很多子问题的解被多次调用,则成该问题具有子问题的重叠性质。
(1)最长公共子序列
String s1 = "ABCDEFGHI"
String s1 = "BACDFUI";
最长公共子序列为:"BCDFI"
import java.util.Scanner;
public class q最长公共子序列 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
String str1 = in.next();
char[] str1char = str1.toCharArray();
String str2 = in.next();
char[] str2char = str2.toCharArray();
if (str1.length() == 0 || str2.length() == 0) {
System.out.println(0);
} else {
System.out.println(dfs(str1char,str2char));
}
}
}
public static int dfs(char[] str1char, char[] str2char) {
int len1 = str1char.length, len2 = str2char.length;
int[][] dp = new int[len1 + 1][len2 + 1];
for (int i = 0; i < len1 + 1; i++) {
dp[i][0] = 0;
}
for (int i = 0; i < len2+1; i++) {
dp[0][i] = 0;
}
for (int i = 1; i < len1 + 1; i++) {
for (int j = 1; j < len2 + 1; j++) {
if (str1char[i-1] == str2char[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = dp[i-1][j] > dp[i][j-1] ? dp[i-1][j] : dp[i][j-1];
}
}
}
return dp[len1][len2];
}
}
(2)青蛙跳阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。
import java.util.HashMap;
import java.util.Map;
public class q青蛙跳阶 {
// 备忘录
static Map<Integer, Integer> map = new HashMap<Integer, Integer>(){{
put(1,1);
put(2,2);
}};
public static void main(String[] args) {
int n = 10;
System.out.println(getVal(n));
}
public static int getVal(int n) {
if (!map.containsKey(n)) {
map.put(n, getVal(n - 1) + getVal(n - 2));
}
return map.get(n);
}
}
(3)最长严格递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。最长长度不是要在数组连续排列的--->求出以数组nums每个元素结尾的最长子序列集合,再取最大值
public class q最长递增子序列 {
public static void main(String[] args) {
int[] num = new int[]{10,9,2,5,3,7,101,18};
System.out.println(getVal(num));
}
public static int getVal(int[] num) {
int len = num.length;
if (len == 0 || len == 1) return len;
int[] dp = new int[len];
dp[0] = 1;
int maxLength = 1;
for (int i = 1; i < len; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (num[j] < num[i]) {
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
maxLength = Math.max(dp[i],maxLength);
}
return maxLength;
}
}
逻辑推理:
- 穷举分析
- 确定边界
- 找规律,确定最优子结构
- 状态转移方程
nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,
四、贪心法
1、基本思想
求解组合最优化问题的贪心算法包含一系列步骤。每一步都在一组选择中做成在当前看来最好的选择,希望通过做出局部优化选择达到全局优化选择。
2、背包问题和0-1背包问题
背包问题
给定种物品和一个背包,物品的重量是,其价值为,背包容量为。问应该如何选择物品放入背包,使得装入背包中的物品的总价值最大?在选择物品装入背包时,可以选择物品的一部分而不是全部。
0-1背包问题
给定种物品和一个背包,物品的重量是,其价值为,背包容量为。问应该如何选择物品放入背包,使得装入背包中的物品的总价值最大?在选择物品装入背包时,对于每种物品只有两种选择,要么装入,要么不装入,不能将同一物品装进背包多次,也不能只装入物品的一部分。
在考虑0-1背包问题的物品选择时,应比较选择该物品和不选择该物品锁导致的最终结果,然后做出最好的选择。
贪心法求解的问题必须具有最优子结构和贪心选择性
(1)活动安排问题
设有n个活动的集合{1,2,3,4,5,....,n},每个活动都要求以独占的方式使用同一资源,如演唱会等,而在同一时间内,只允许一个活动使用这个资源。每个活动i都有一个要求使用该资源的起始时间Si和一个结束时间Fi,且Si < Fi,如果选择活动i,则它在半开的时间区间[Si,Fi)内占用资源。
若区间[Si,Fi)与区间[Sj,Fj)不相交,则称活动i和活动j是相容的。也就是说Si≥Fj或Sj≥Fi时,活动i与活动j是相容的。
活动安排问题是要在所给的活动集合中选出最大的相容活动子集合。
解题思路:①将活动按结束时间进行升序排列,选择活动1;②在剩余活动中选择和活动1相容且结束时间早的活动;③在剩余活动中选择和活动2相容且结束时间早的活动;
public class Question {
public boolean[] void GreedySelector(int n, int[] s, int[] f, boolean[] flag){
flag[1] = true;
int j = 1;
for(int i = 2; i<= n; i++) {
if(s[i] > f[j]) {
flag[i] = true;
j = i;
} else {
flag[i] = false;
}
}
return flag;
}
}
(2)力扣 134.加油站
class Q134加油站 {
public static void main(String[] args) {
int[] gas = new int[]{5,1,2,3,4};
int[] cost = new int[]{4,4,1,5,1};
System.out.println(canCompleteCircuit(gas,cost));
}
public static int canCompleteCircuit(int[] gas, int[] cost) {
int length = gas.length; // 加油站的个数
boolean flag = false;
int res = -1;
// 寻找起始加油站
for(int i = 0; i < length; i++) {
if(gas[i] >= cost[i]) { // 可以作为起始点的条件
flag = isCanReturn(gas, cost, i);
if(flag == true){
res = i;
break;
}
}
}
return res;
}
public static boolean isCanReturn(int[] gas, int[] cost, int begin) {
int sum = 0, length = gas.length;
int location = 0;
while(location < length) {
int j = (begin + location) % length;
sum += gas[j] - cost[j];
location++;
if(sum < 0) {
return false;
}
}
return true;
}
}
五、回溯法
1、基本思想
回溯法在包含问题的所有解的解空间树中,按照深度优先的策略,从根出发进行搜索。搜索每达到解空间树的一个结点,总是先判断以该结点为根的子树是否肯定不包含问题的解,如果肯定不包含,则跳过对该子树的系统搜索,一层一层地向它的祖先回溯,直到遇到一个还有为被搜索过子节点的结点,才转向该结点的一个未曾搜索过的子结点,继续搜索;否则,进入该子树,继续按深度优先的策略进行搜索了;