10大算法
开始~~~
十大算法
1.二分查找算法
1.1基本介绍
1)二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找。
2)二分查找法的运行时间为对数时间O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n步,假设从[0,99]的队列(100个数,即n=100)中寻到目标数30,则需要查找步数为㏒₂100 , 即最多需要查找7次( 2^6 < 100 < 2^7)。
1.2代码
public static int binarySearch(int[] arr,int target){
int left = 0;
int right = arr.length - 1;
while(left <= right){
int mid = (left + right) / 2;
if(arr[mid] == target){
return mid;
}else if(arr[mid] > target){
right = mid - 1;
}else{
left = mid + 1;
}
}
return -1;
}
2.分治算法
2.1基本介绍
分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
分治算法可以求解的一些经典问题:
二分搜索、大整数乘法、棋盘覆盖、合并排序、快速排序、线性时间选择、最接近点对问题、循环赛日程表、汉诺塔
2.2分治算法的基本步骤
分治法在每一层递归上都有三个步骤:
1)分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。
2)解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题。
3)合并:将各个子问题的解合并为原问题的解。
2.3汉诺塔游戏的思路分析
1)如果是有一个盘, A->C
2)如果我们有 n >= 2 情况,我们总是可以看做是两个盘 ①最下边的盘 ②上面的盘
先把 最上面的盘 A->B
把最下边的盘 A->C
把B塔的所有盘 从 B->C
2.4代码
public static void hanoiTower(int num,char a,char b,char c){
//如果只一个盘
if(num == 1){
System.out.println("第1个盘从" + a + "->" + c);
}else{
//如果我们n>=2情况,我们总是可以看作是两个盘 1.最下边的一个盘 2.上面的所盘
//1.先把上面的所盘A->B,移动过程使用到C
hanoiTower(num - 1, a, c, b);
//2.把最下面的盘A->C
System.out.println("第" + num + "个盘从" + a + "->" + c);
//3.把B塔的所盘从B->C,移动过程中使用到A塔
hanoiTower(num - 1, b, a, c);
}
}
3.动态规划算法
3.1应用场景
背包问题:有一个背包,容量为4磅 , 现有如下物品
1)要求达到的目标为装入的背包的总价值最大,并且重量不超出
2)要求装入的物品不能重复
3.2动态规划算法介绍
1)动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。
2)动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
3)与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )。
4)动态规划可以通过填表的方式来逐步推进,得到最优解。
3.3背包问题的思路分析
3.4代码
public static void main(String[] args) {
int[] w = {3,4,1};//物品的重量
int[] val = {2000,3000,1500};//物品的价值
int m = 4;//背包的容量
int n = val.length;//物品的个数
int[][] path = new int[n + 1][m + 1];
int[][] v = new int[n + 1][m + 1];
for (int i = 0; i < v.length; i++) {
v[i][0] = 0;
}
for (int i = 0; i < v[0].length; i++) {
v[0][i] = 0;
}
for (int i = 1; i < v.length; i++) {
for (int j = 1; j < v[i].length; j++) {
if (w[i - 1] > j){
v[i][j] = v[i - 1][j];
}else{
if (v[i - 1][j] > val[i - 1] + v[i - 1][j - w[i - 1]]){
v[i][j] = v[i - 1][j];
}else{
v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
path[i][j] = 1;
}
}
}
}
int i = path.length - 1;
int j = path[0].length - 1;
while (i > 0 && j > 0){
if (path[i][j] == 1){
System.out.printf("第%d个商品加入背包\n",i);
j -= w[i - 1];
}
i--;
}
}
4.KMP算法
4.1应用场景
字符串匹配问题:
1)有一个字符串 str1= ““硅硅谷 尚硅谷你尚硅 尚硅谷你尚硅谷你尚硅你好””,和一个子串 str2=“尚硅谷你尚硅你”。
2)现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1。
4.2暴力匹配算法
如果用暴力匹配的思路,并假设现在str1匹配到 i 位置,子串str2匹配到 j 位置,则有:
1)如果当前字符匹配成功(即str1[i] == str2[j]),则i++,j++,继续匹配下一个字符
2)如果失配(即str1[i] ! = str2[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0
3)用暴力方法解决的话就会有大量的回溯,每次只移动一位,若是不匹配,移动到下一位接着判断,浪费了大量的时间
4)暴力匹配算法实现:
public static int violenceMatch(String str1,String str2){
char[] s1 = str1.toCharArray();
char[] s2 = str2.toCharArray();
int s1Len = s1.length;
int s2Len = s2.length;
int i = 0;//i索引指向s1
int j = 0;//i索引指向s2
while(i < s1Len && j < s2Len){//保证匹配时,不越界
if(s1[i] == s2[j]){//匹配
i++;
j++;
}else{//没匹配成功
i = i - (j - 1);
j = 0;
}
}
//判断是否匹配成功
if(j == s2Len){
return i - j;
}
return -1;
}
4.3KMP算法介绍
1)KMP是一个解决模式串在文本串是否出现过,如果出现过,最早出现的位置的经典算法
2)KMP方法算法就利用之前判断过的信息,通过一个next数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过next数组找到,前面匹配过的位置,省去了大量的计算时间。
4.4KMP算法最佳应用-字符串匹配问题
有一个字符串 str1= “BBC ABCDAB ABCDABCDABDE”,和一个子串 str2="ABCDABD"现在要判断 str1 是否含有 str2, 如果存在,就返回第一次出现的位置, 如果没有,则返回-1
要求:使用KMP算法完成判断,不能使用简单的暴力匹配算法
代码:
public static int kmpSearch(String str1,String str2,int[] next){
//遍历
for(int i = 0,j = 0;i < str1.length();i++){
while(j > 0 && str1.charAt(i) != str2.charAt(j)){
j = next[j - 1];
}
if(str1.charAt(i) == str2.charAt(j)){
j++;
}
if(j == str2.length()){
return i - j + 1;
}
}
return -1;
}
//获取到一个字符串(子串)的部分匹配值表
public static int[] kmpNext(String dest){
//创建一个next数组保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0;//如果字符串是长度为1部分匹配值就是0
for(int i = 1,j = 0;i < dest.length();i++){
//当dest.charAt(i) != dest.charAt(j),需要从next[j - 1]获取新的j
//直到发现dest.charAt(i) == dest.charAt(j)成立才退出
while(j > 0 && dest.charAt(i) != dest.charAt(j)){
j = next[j - 1];
}
//当dest.charAt(i) == dest.charAt(j)满足时,部分匹配值就是+1
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
5.贪心算法
5.1应用场景-集合覆盖问题
假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。
5.2贪心算法介绍
1)贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
2)贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
5.3贪心算法最佳应用-集合覆盖
1)假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
2)思路分析
①可以使用穷举法来实现,列出每个可能的广播台的集合。假设总的有n个广播台,则广播台的组合总共有2ⁿ -1 个,假设每秒可以计算10个子集
②使用贪婪算法,效率高
1)遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
2)将这个电台加入到一个集合中(比如ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。
3)重复第1步直到覆盖了全部的地区
4)思路图解:
3)代码
//创建广播电台,放入到map
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
HashSet<String> hashSet1 = new HashSet<>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<>();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<>();
hashSet5.add("杭州");
hashSet5.add("大连");
broadcasts.put("K1", hashSet1);
broadcasts.put("K2", hashSet2);
broadcasts.put("K3", hashSet3);
broadcasts.put("K4", hashSet4);
broadcasts.put("K5", hashSet5);
//allAreas存放所的地区
HashSet<String> allAreas = new HashSet<>();
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("广州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大连");
//创建一个ArrayList,存放择的电台集合
ArrayList<String> selects = new ArrayList<>();
//定义一个临时的集合,在遍历的过程中,存放电台覆盖的区域和当前还没覆盖的地区的交集
HashSet<String> tempSet = new HashSet<>();
//定义给maxKey,保存在一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的key
//如果maxKey不为null,则加入到selects中
String maxKey;
int maxSize;
while(allAreas.size() != 0){//如allAreas不为0,则表示还没覆盖到所的地区
maxKey = null;
//遍历broadcasts,取出key
for(String key : broadcasts.keySet()){
tempSet.clear();
//当前这个key能够覆盖的地区
HashSet<String> areas = broadcasts.get(key);
tempSet.addAll(areas);
//求出tempSet与allAreas集合的交集,交集会赋给tempSet
tempSet.retainAll(allAreas);
if(maxKey == null){
maxSize = 0;
}else{
HashSet<String> maxSet = broadcasts.get(maxKey);
maxSet.retainAll(allAreas);
maxSize = maxSet.size();
}
// if(tempSet.size() > 0 && (maxKey == null || tempSet.size() > broadcasts.get(maxKey).size())){
if(tempSet.size() > 0 && (maxKey == null || tempSet.size() > maxSize)){
maxKey = key;
}
}
if(maxKey != null){
selects.add(maxKey);
allAreas.removeAll(broadcasts.get(maxKey));
}
}
System.out.println(selects);
6.普里姆算法
6.1应用场景-修路问题
1)胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通
2)各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短
正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少
6.2最小生成树
修路问题本质就是就是最小生成树问题
最小生成树:
1)给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
2)N个顶点,一定有N-1条边
3)包含全部顶点
4)N-1条边都在图中
如图:
5)求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法
6.3普里姆算法介绍
1)普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图。
2)普利姆的算法如下:
①设G=(V,E)是连通网,T=(U,D)是最小生成树,V,U是顶点集合,E,D是边的集合
②若从顶点u开始构造最小生成树,则从集合V中取出顶点u放入集合U中,标记顶点v的visited[u]=1
③若集合U中顶点ui与集合V-U中的顶点vj之间存在边,则寻找这些边中权值最小的边,但不能构成回路,将顶点vj加入集合U中
④将边(ui,vj)加入集合D中,标记visited[vj]=1
⑤重复步骤②,直到U与V相等,即所有顶点都被标记为访问过,此时D中有n-1条边
6.4解决修路问题
1)有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通
2)各个村庄的距离用边线表示(权) ,比如 A – B 距离 5公里
3)问:如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
4)代码:
public class PrimAlgorithm {
public static void main(String[] args) {
char[] data = new char[]{'A','B','C','D','E','F','G'};
int verxs = data.length;
//邻接矩阵的关系使用二维数组表示,用10000表示两个点不联通
int[][] weight = new int[][]{
{10000,5,7,10000,10000,10000,2},
{5,10000,10000,9,10000,10000,3},
{7,10000,10000,10000,8,10000,10000},
{10000,9,10000,10000,10000,4,10000},
{10000,10000,8,10000,10000,5,4},
{10000,10000,10000,4,5,10000,6},
{2,3,10000,10000,4,6,10000}
};
//创建Graph对象
MGraph graph = new MGraph(verxs);
MinTree minTree = new MinTree();
minTree.createGraph(graph, verxs, data, weight);
//输出
minTree.showGraph(graph);
minTree.prim(graph, 0);
}
}
// 创建最小生成树->村庄的图
class MinTree {
/**
*
* @Description 创建图的邻接矩阵
* @author jiatianyu
* @date 2021年3月18日下午2:23:02
* @param graph 图对象
* @param verxs 图对应的顶点个数
* @param data 图的各个顶点的值
* @param weight 图的邻接矩阵
*/
public void createGraph(MGraph graph, int verxs, char data[], int[][] weight) {
int i,j;
for(i = 0;i < verxs;i++){//顶点
graph.data[i] = data[i];
for(j = 0;j < verxs;j++){
graph.weight[i][j] = weight[i][j];
}
}
}
//显示图的结构
public void showGraph(MGraph graph){
for(int[] link : graph.weight){
System.out.println(Arrays.toString(link));
}
}
public void prim(MGraph graph,int v){
//visited[] 标记点(顶点)是否被访问过
int[] visited = new int[graph.verxs];
//把当前这个结点标记为已访问
visited[v] = 1;
//h1和h2记录两个顶点的下标
int h1 = -1;
int h2 = -1;
int minWeight = 10000;//将minWeight初始成一个大数,后面在遍历过程中会被替换
for(int k = 1;k < graph.verxs;k++){//因为graph.verxs个顶点,总共需要graph.verxs - 1个边
//每一次生成的子图,和那个结点的距离最近
for(int i = 0;i < graph.verxs;i++){//i结点表示被访问过的结点
for(int j = 0;j < graph.verxs;j++){//j结点表示没被访问过的结点
if(visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){
//替换minWeight
minWeight = graph.weight[i][j];
h1 = i;
h2 = j;
}
}
}
//找到一条边最小
System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + ">权值:" + minWeight);
//将当前这个结点标记为已经访问
visited[h2] = 1;
//minWeight重新设置成最大值10000
minWeight = 10000;
}
}
}
class MGraph {
int verxs;// 表示图的结点个数
char[] data;// 存放结点数据
int[][] weight;// 存放边,即邻接矩阵
public MGraph(int verxs) {
this.verxs = verxs;
data = new char[verxs];
weight = new int[verxs][verxs];
}
}
7.克鲁斯卡尔算法
7.1应用场景-公交站问题
1)某城市新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通
2)各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里
问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
7.2克鲁斯卡尔算法介绍
1)克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
2)基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
3)具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止
7.3克鲁斯卡尔最佳实践-公交站问题
1)有北京有新增7个站点(A, B, C, D, E, F, G) ,现在需要修路把7个站点连通
2)各个站点的距离用边线表示(权) ,比如 A – B 距离 12公里
3)问:如何修路保证各个站点都能连通,并且总的修建公路总里程最短?
4)代码:
public class KruskalCase {
private int edgeNum;//边的个数
private char[] vertexs;//顶点数组
private int[][] matrix;//邻接矩阵
//使用INF表示两个顶点不能联通
private static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) {
char[] vertexs = {'A','B','C','D','E','F','G'};
int matrix[][] = {
{0,12,INF,INF,INF,16,14},
{12,0,10,INF,INF,7,INF},
{INF,10,0,3,5,6,INF},
{INF,INF,3,0,4,INF,INF},
{INF,INF,5,4,0,2,8},
{16,7,6,INF,2,0,9},
{14,INF,INF,INF,8,9,0}
};
//创建KruskalCase对象实例
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
EData[] edges = kruskalCase.getEdges();
kruskalCase.sortEdges(edges);
kruskalCase.kruskal();
}
//构造器
public KruskalCase(char[] vertexs,int[][] matrix){
//初始化顶点数和边的个数
int vlen = vertexs.length;
//初始化顶点,复制拷贝的方式
this.vertexs = new char[vlen];
for(int i = 0;i < vertexs.length;i++){
this.vertexs[i] = vertexs[i];
}
//初始化边,使用的是复制拷贝的方式
this.matrix = new int[vlen][vlen];
for(int i = 0;i < vlen;i++){
for(int j = 0;j < vlen;j++){
this.matrix[i][j] = matrix[i][j];
}
}
//统计边
for(int i = 0;i < vlen;i++){
for(int j = i + 1;j < vlen;j++){
if(this.matrix[i][j] != INF){
edgeNum++;
}
}
}
}
public void kruskal(){
int index = 0;//表示最后结果数组的索引
int[] ends = new int[edgeNum];//用于保存"已最小生成树"中的每个顶点在最小生成树中的终点
//创建结果数组,保存最后的最小生成树
EData[] rets = new EData[edgeNum];
//获取图中所的边的集合,一共有12边
EData[] edges = getEdges();
sortEdges(edges);
//遍历edges数组,将边添加到最小生成树中时,判断准备加入的边是否形成了回路,如果没,就加入rets,否则不能加入
for(int i = 0;i < edgeNum;i++){
//获取到第i条边的第一个顶点(起点)
int p1 = getPosition(edges[i].start);//p1 = 4
//获取到第i条边的第2个顶点
int p2 = getPosition(edges[i].end);//p2 = 5
//获取p1这个顶点在已最小生成树中的终点
int m = getEnd(ends,p1);//m = 4;
//获取p2这个顶点在已最小生成树中的终点
int n = getEnd(ends,p2);//n = 5;
//是否构成回路
if(m != n){//没构成回路
ends[m] = n;//设置m在"已最小生成树"中的终点
rets[index++] = edges[i];
}
}
//打印rets
System.out.println("最小生成树为:");
for(int i = 0;i < index;i++){
System.out.println(rets[i]);
}
}
//打印邻接矩阵
public void print(){
System.out.println("邻接矩阵为:\n");
for(int i = 0;i < vertexs.length;i++){
for(int j = 0;j < vertexs.length;j++){
System.out.printf("%12d\t",matrix[i][j]);
}
System.out.println();//换行
}
}
//对边进行排序处理,冒泡排序
private void sortEdges(EData[] edges){
EData temp = null;
for(int i = 0;i < edges.length - 1;i++){
for(int j = 0;j < edges.length - 1 -i;j++){
if(edges[j].getWeight() > edges[j + 1].getWeight()){
temp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = temp;
}
}
}
}
/**
*
* @Description
* @author jiatianyu
* @date 2021年3月18日下午9:44:25
* @param ch ch:顶点值,比如'A','B'
* @return 顶点ch对应的下标,如果找不到,返回-1
*/
private int getPosition(char ch){
for(int i = 0;i < vertexs.length;i++){
if(vertexs[i] == ch){
return i;
}
}
return -1;
}
private EData[] getEdges(){
int index = 0;
EData[] edges = new EData[edgeNum];
for(int i = 0;i < vertexs.length;i++){
for(int j = i + 1;j < vertexs.length;j++){
if(matrix[i][j] != INF){
edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges;
}
/**
*
* @Description 获取下标为i的顶点的终点,用于判断两个顶点的终点是否相同
* @author jiatianyu
* @date 2021年3月19日上午11:55:43
* @param ends 数组记录了各个顶点的对应的终点是哪个,ends数组是在遍历过程中,逐步形成的
* @param i 表示传入的顶点对应的下标
* @return 返回下标为i的顶点对应的终点的下标
*/
private int getEnd(int[] ends,int i){
while(ends[i] != 0){
i = ends[i];
}
return i;
}
}
//创建一个类EData,它的对象实例表示一条边
class EData{
char start;//边的一个点
char end;//边的另一个点
int weight;//边的权值
//构造器
public EData(char start, char end, int weight) {
super();
this.start = start;
this.end = end;
this.weight = weight;
}
//重写toString,便于输出边
@Override
public String toString() {
return "EData [start=" + start + ", end=" + end + ", weight=" + weight + "]";
}
public char getStart() {
return start;
}
public void setStart(char start) {
this.start = start;
}
public char getEnd() {
return end;
}
public void setEnd(char end) {
this.end = end;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
总结
To be continued~~,JavaEE部分