场景问题 | 针对性算法 | 复杂度 | 说明 |
---|---|---|---|
查找有序数组的某个数 | 二分查找 | O(log n) | |
位运算 | |||
全排列 | 回溯 | ||
穷举最优值 | 动态规划 | ||
局部最优解 | 贪心算法 | 总体不一定最优 | |
链表、子串、数组 遍历 | 双指针 | 快慢指针、左右指针、滑动窗口(用到出队入队 | |
查找一个数) | 哈希表 | O(1) | |
图 | |||
树 | |||
先进先出 | 队列 | ||
先进后出 | 栈 | ||
最大值 | 堆 |
1,数据结构
1)线性表:顺序表、单链表
2)队列和栈
3)散列(哈希表)
- 不能从哈希值反向推导出原始数据;
场景:安全加密(md5、sha算法)。 - 对输入数据非常敏感,一个bit不同就会导致哈希值非常不一样;
场景:唯一标识、数据校验。md5校验、CRC校验 - 散列冲突的概率要很小;
负载均衡、数据分片、分布式存储等,通过取模运算计算目标地址; - 哈希算法的计算过程要足够简单高效,即使原始数据很长,也能很快得到哈希值;
- 查询复杂度o(1)
查找算法。
如:查找一组数据中和为target的数据。
1>哈希函数
- 除余
- 随机
- 平方后取中间某几位
- 折叠
- H(key)= a*key + b
- 数字分析:若10位key的特定某几位中,数字大小分布均衡,就取那几位的
2>处理冲突
- 开放定址
- 公共溢出
- 多个哈希表
- 链表
3>性能分析
- 哈希函数
- 处理冲突的方法
- 哈希表的装填因子
装填因子 a = 哈希表中元素的个数 / 哈希表的长度
a 可描述哈希表的装满程度。
a 越小,发生冲突的可能性越小;
a 越大 ,发生冲突的可能性越大。
4)小(大)根堆、最大堆
完全二叉树中任意结点小于(大于)其孩子。
n个元素的序列{k1,k2,…,kn}满足以下关系时,称为堆。
1>堆排序
是对树型选择排序占用空间多的改进。
见排序算法-堆排序
2>合并m个长度为n的已排序数组
合并m个长度为n的已排序数组的时间复杂度为O(nmlogm)。
思路是:首先将m个已排序数组的第一个数,建立大小为m的小根堆,时间复杂度O(m)。
每次输出堆顶的数,再将其所属已排序数组的后一个数放入堆顶,调节小根堆。因为我们有m*n个数,小根堆调整时间为O(logm),所以时间复杂度O(nmlogm)。
3>Topk
5)跳表(跳跃表,SkipList)
数组可以用二分查找,链表的二分查找就是跳表查找,用空间换时间提高查找效率。
- 在链表的基础上实现一级索引、二级索引、n级索引。每个高级索引是低级索引的子集。
- 表尾全部由
NULL
组成,表示跳跃表的末尾。索引节点包含两个指针,一个向下,一个向右。 - 为了提高查询效率,程序从高层次开始查找,随着范围缩小慢慢降低层次。
比如下图中查找17:
1)没有缓存的时候,单链表从投开始查询6次:1,3,6,7,11,15,17;
2)一级缓存情况下,检索4次:1 6 15 (null)17。
3)二级缓存情况下,检索3次:6 15(null)17
- 跳表的插入:链表每插入一个数字,按序在任意一层(随机)插入即可。
比如插入node=8,可以在一级缓存的6~15
之间插入8,也可以在二级缓存的6~15
插入8,也可以不插入。如果按规律每2个结点就取一级缓存,那就是平衡二叉树了。 - 跳表的删除:链表+缓存的node删除。
1>跳表和红黑树的对比
2>使用
- redis的zSet
- ConcurrentSkipListMap
concurrentHashMap用的红黑树。
6)树
2,算法
1)概念
算法是指令的集合,是为了解决特定问题而规定的一些列操作。
①主要功能
对输入特定的运算产生期望的输出。
②算法结果
输入数据、处理数据、输出结果。
③影响因素
硬件层面:计算机执行每条指令的速度
软件层面:编译产生的代码质量
算法策略:算法的好坏
问题规模
2)算法与程序区别
①算法
性质:
(1)输入:由外部提供的量作为算法的输入。
(2)输出:算法产生至少一个量作为输出。
(3)确定性:组成算法的每条指令是清晰,无歧义的。
(4)有限性:算法中每条指令的执行次数是有限的,执行每条指令的时间也是有限的。
②程序
程序是算法用某种程序设计语言的具体实现。
程序可以不满足算法的性质(4)。
例如操作系统,是一个在无限循环中执行的程序,因而不是一个算法。
3)复杂度
算法复杂度,即算法在编写成可执行程序后,运行时所需要的资源,资源包括时间资源和内存资源。
①时间复杂度
指算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示。
一般取主要工作语句的耗费时间作为算法的时间复杂性。
②空间复杂度
包括程序本身的存储和它所使用的工作单元存储。
③常用公式
a.等比数列求和
b.常见复杂度阶排序
多项式:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3)
指数阶:O(2^n) < O(n!) < O(n^n)
c.其他
3=2^(log3)
3,递归与分治策略
1)概念
①递归
直接或间接地调用自身的算法称为递归算法。
递归条件:
a.要解决的问题可以转换为一个新问题,且与原问题的解决方案相同。
b.这个转换可以使问题得得解决
c.有一个明确的结束递归条件。
优缺点:
可读性强,容易用数学归纳法证明算法的正确性;
运行效率低(时间和空间都低)。
递归算法都可以通过采用一个用户定义的栈来模拟系统的递归调用工作栈,从而改为非递归算法。
②分治
基本思想:将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同。
递归是分治法的一种方法。
2)场景
①二叉树
②阶乘n!
int factorial(int n){
if( 0==n) return 1;
return n*factorial(n-1);
}
复杂度:T(n) = T(n-1) +1 = O(n)
③Fibonacci数列
int fibonacci(){
if(n<=1) return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
复杂度:T(n) = T(n-1) + T(n-2) + 1 = 2T(n-1) + 1 (n趋于无穷大)= 1+2+2^2 + …+2^n = O(2^n) (等比数列)
④排列问题
void Perm(Type list[], int k, int m){
if( k==m ){//只剩下一个元素
for(int i = 0; i<=m; i++) cout<<list[i];
cout<<endl;
}else{
for{
Swap(list[k],list[i]);
Perm(list,k+1,m);
Swap(list[k],list[i]);//恢复交换前的状态
}
}
}
复杂度:T(n) = nT(n-1) + 1 = n! + n = O(n!)
⑤整数划分
⑥Hanoi塔问题
分析:
假设移动n个圆盘需要f(n)次移动。
一个圆盘,只需一步就可以了 f(1)=1……①
n个圆盘,假设开始圆盘在A柱,可以先把A柱的上面n-1个圆盘移到B,再将A剩下的一个移到C,最后将B的n-1个移到C。总共需要f(n)=2f(n-1)+1……②
根据①②两式,可求出f(n)=2^n-1 所以O(n)=2^n
/**
* 汉诺塔算法(递归)
* @author luo
*1.有三根杆子A,B,C。A杆上有若干碟子 (最大的一个在底下,其余一个比一个小,依次叠上去)
*2.每次移动一块碟子,小的只能叠在大的上面
*3.把所有碟子从A杆全部移到C杆上
*
*/
public class Hanoi{
public static void main(String[] args) throws NumberFormatException, IOException{
int n;
BufferedReader buf = new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入碟子盘数:");
n = Integer.parseInt(buf.readLine());
Hanoi hanoi = new Hanoi();
hanoi.move(n, 'A', 'B', 'C');
}
public void move(int n, char a, char b, char c) {
if (n == 1){
System.out.println("盘 " + n + " 由 " + a + " 移至 " + c);
}else {
move(n - 1, a, c, b);
System.out.println("盘 " + n + " 由 " + a + " 移至 " + c);
move(n - 1, b, a, c);
}
}
}
复杂度:T(n) = 2T(n-1) + 1 = O(2^n)
时间复杂度:O(2^n) 。
⑦二分搜索
⑧合并排序
⑨快速排序
⑩大整数乘法
问题:
X和Y都是n位二进制整数,计算它们的乘积XY。
解决办法:
a.小学方法,效率低。
b.改进:
⑪Strassen矩阵乘法
a.8次乘法4次加法
T(n)=O(n^3)
b.改进:7次乘法18次加法
T(n)=O(n^log7)
⑫棋盘覆盖
⑬线性时间选择
a.问题描述:给定n个元素(无序),找出第K小的元素。
当k=1,则要找的数为最小元素。k=n为最大元素。k=(n+1)/2,表示中位数。
b.基本思想:类似快速排序。
⑭快速傅里叶变换
基于大整数乘法。
a.算法
b.复杂度:O(nlogn)
4,动态规划
1)概念
①基本思想
将问题分解成若干子问题求解,再从子问题得到原问题的解。
-
找出最优解的性质和结构特征,构建状态转移方程
f1 = …
fn= … -
初始化
dp[0][0][...] = base
- 自底向上计算最优值
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
- 构造最优解
②与分治法区别
动态规划的子问题不是相互独立的,再计算过程中保存已解决子问题的答案,在后续计算中使用,避免重复计算。
③适用场景
通过暴力枚举求最值,空间换时间。
2)demo
①独立任务最优调度问题(双机调度问题)
②矩阵连乘问题
【编程素质】算法-矩阵连乘问题(枚举法、备忘录法、动态规划)
③ 寻找一条从左上角(arr[0][0])到右下角(arr[m-1][n-1])的路线, 使得沿途经过的数组中的整数和最小。
package luo.main;
import luo.minPath.minPathArr;
public class Main {
public static void main(String[] args){
int[][] arr = {{1,4,3},{8,7,5},{2,1,5}};
System.out.println("路径:");
System.out.println("最小值为:" + minPathArr.getMinPathArr(arr));
}
}
package luo.minPath;
/**
* 寻找一条从左上角(arr[0][0])到右下角(arr[m-1][n-1])的路线,
* 使得沿途经过的数组中的整数和最小。
* @author luo
*
*分析:
*从右下角开始倒着分析:
*最后一步到达arr[m-1][n-1]只有两条路,
*即通过arr[m-2][n-1]或arr[m-1][n-2]到达,
*假设从arr[0][0]到arr[m-2][n-1]沿途数组最小值为minPath=f(m-2,n-1)
*因此最后一步选择的路线为min{f(m-2,n-1),f(m-1,n-2)}
*同理可推其他点的路径
*由此可推广到一般情况:
*假设arr[i-1][j]与arr[i][j-1]的minPath的和为f(i-1,j)和f(i,j-1);
*那么到达arr[i][j]的路径上所有数字和的min为
*f(i,j)=min{f(i-1,j),f(i,j-1)}+arr[i][j]。
*
*方法总结:
*1,递归法:逆向求解。效率低。改进:把每次计算到的f(i-1,j-1)缓存起来避免多余的计算,即使用动态规划算法
*2,动态规划算法:正向求解。空间换时间的算法,通过缓存计算的中间值,从而减少重复计算的次数,提高算法效率。
*/
public class minPathArr{
public static int getMinPathArr(int[][] arr){
if(null == arr || 0 == arr.length){
return 0;
}
int row = arr.length;
int col = arr[0].length;
//用来保存计算的中间值
int[][] cache = new int[row][col];
cache[0][0] = arr[0][0];
for(int i=1; i<col; i++){
cache[0][i] = cache[0][i-1] + arr[0][i];
}
for(int j=1; j<row; j++){
cache[j][0] = cache[j-1][0] + arr[j][0];
}
//在遍历二维数组的过程中不断把计算结果保存到cache中
for(int i=1; i<row; i++){
for(int j=1; j<col; j++){
//可以确定选择的路线为arr[i][j-1]
if (cache[i-1][j] > cache[i][j-1]) {
cache[i][j] = cache[i][j-1] + arr[i][j];
System.out.println("[" + i + "," + (j-1) + "] ");
}else{
//可以确定选择的路线为arr[i-1][j]
cache[i][j] = cache[i-1][j] + arr[i][j];
System.out.println("[" + (i-1) + "," + j + "] ");
}
}
}
System.out.println("[" + (row-1) + "," + (col-1) + "] ");
return cache[row-1][col-1];
}
}
5,贪心算法
1)概念
①基本思想
通过一系列的选择来得到问题的解,它所做的每一个选择都是当前状态下局部最好选择,即贪心选择。
例子:
有3种硬币:1.1角、0.5角、0.1角。给顾客找钱1.5角,最少拿几个硬币。
贪心算法:
选出一个不超过1.5的最大面值:1.1
选出一个不超过0.4的最大面值:0.14
其实最优解为30.5
所以:贪心算法不是从整体最优上考虑,而是做出某种意义上的局部最优选择,在范围广的许多问题中能获得整体最优解。
②与动态规划的区别
在贪心算法中,仅在当前状态下做出最好选择(贪心选择),即局部最优选择,然后再去解做出这个选择后产生的子问题。动态规划算法是自底向上的方式解问题,贪心算法是自顶向下的解问题,以迭代的方式做出相继的贪心选择。
2)使用场景
a.背包问题
①0-1背包问题
给定n种物品和一个背包。
物品i的重量是W[i],价值为v[i],背包容量为c。
问应如何选择装入背包的物品,使得装入背包的物品总价值最大?
要求:对每种物品i只有2种选择:装入或不装入。不能装入多次,也不能部分装入。
不可以用贪心算法求解。因为贪心算法无法保证最终能将背包装满。
②背包问题
与0-1背包类似,不同的是在选择物品i装入背包时,可以选择装入部分物品。
可以用贪心算法求解:
①计算物品单位重量价值v[i]/w[i]
②贪心选择策略:将尽可能多的单位重量价值最高的物品装入。若装入后未满,继续依次装入背包。
b.轮船的最优装载问题
贪心选择策略:优先装重量轻的物品。
c.哈夫曼编码
d.单源最短路径(Dijkstra算法)
e.最小生成树(Prim算法、Kruskal算法)
f.活动安排问题(会场安排问题、图着色问题)
g.NP完全问题(多机调度问题)
贪心选择策略:优先采用最长处理时间作业。
例子:
设7个独立作业{1,2,3,4,5,6,7}由3台机器M[1],M[2],M[3]来加工处理,各作业所需处理时间为{2,14,4,16,6,5,3}。
求解:对每个作业根据处理时间从大到小排序:
6,回溯法
以深度优先方式系统搜索问题解的算法称为回溯法,适用于解组合数较大的问题。
“通用的解题法”;可以系统的搜索一个问题的所有解或任一解。
算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。能避免不必要搜索的穷举式搜索法。
1)基本思想
①针对所给问题,定义问题的解空间;
②确定易于搜索的解空间结构;
③以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
确定了解空间的组织节后后,回溯法从开始结点(根节点)出发,以深度优先方式搜索整个解空间。
这个开始结点称为活结点,也是当前的扩展结点。
在当前扩展结点处,搜索深一层的新结点,这个新结点称为活结点,并成为当前扩展结点。
此时,往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点。
回溯法以这种工作方式递归地在解空间中搜索,直到找到所要求的解或解空间中已无活结点为止。
2)场景
需要求满足某些约束条件的最佳解。
3)基本概念
①问题的解向量
回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。
②显约束
对分量xi的取值限定。
③隐约束
为满足问题的解而对不同分量之间施加的约束。
④解空间
对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
问题的解空间至少应包含问题的一个最优解。
⑤扩展结点
一个正在产生儿子的结点称为扩展结点
⑥活结点
一个自身已生成但其儿子还没有全部生成的节点称做活结点
⑦死结点
一个所有儿子已经产生的结点称做死结点
⑧深度优先的问题状态生成法
如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在)
⑨宽度优先的问题状态生成法
在一个扩展结点变成死结点之前, 它一直是扩展结点
⑩限界函数
法为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。
具有限界函数的深度优先生成法称为回溯法。
⑪常用剪枝函数
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。
⑫解空间树:子集树和排列树
子集树:所给的问题是从n个元素的集合S中找出满足某种性质的子集。
排列树:所给的问题是从n个元素的集合S中找出满足某种性质的排列。
4)复杂度分析
①空间复杂度
如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2h(n))或O(h(n)!)内存空间。
②时间复杂度
遍历子集树需要O(2^n)计算时间。
遍历排列树需要O(n!)计算时间。
5)举例
①0-1背包问题
w=[16,15,15],p=[45,25,25],c=30
1,根结点是唯一活结点,也是当前扩展结点。A可以沿纵深方向到B或C。假设先到B。
2,A、B为活结点,B是当前扩展结点。选取了w1,故在B处剩余背包容量r=14,获取价值p=45。
3,B可以到D或E,选择D,需要r>=15,不可行,故选择E(不需要装入背包)。
4,E成为新的扩展结点,A、B、E是活结点。E可以到J或K。J不可行,K可行。K为新的扩展结点。得到一个可行解x=(1,0,0),此时K不能再纵深扩展,成为死结点。故回溯至E。
5,E也没有可扩展的结点,故E为死结点。回溯至B。
6,B也为死结点。回溯至A。此时r=30,p=0.
7,C成为扩展结点,r=30,p=0.
8,选择F为扩展结点,此时A、C、F为活结点。r=15,p=25。
9,选择L为扩展结点,r=0,p=50。找到一个解(0,1,1)
10…
7,分支限界法
1)基本思想
以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
每一个活结点只有一次成为扩展结点。活结点一旦成为扩展结点,就一次性产生所有儿子的结点。废弃导致不可行解或导致非最优解的儿子结点,其余儿子结点加入活结点表中。一直重复此过程直到找到所需的解或活结点表为空。
2)与回溯法区别
分支限界法类似回溯法,也是在问题的解空间上搜索问题解的算法。但是求解目标不同。
回溯法的求解目标是找出解空间中满足约束条件的所有解,而分支限界法的求解目标是找出满足约束条件的一个解或最优解。
回溯法以深度优先的方式搜索解空间,分支限界法以广度优先或最小耗费优先的方式搜索解空间。
他们对当前扩展结点所采取的扩展方式不同。