本文是学习尚硅谷韩顺平老师的数据结构与算法中十大算法部分的笔记。
详细见:
尚硅谷韩顺平老师的数据结构与算法
文章目录
算法
概述:
1、定义
用来解决问题的思路。
是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
2、算法的特性
输入
输出
有穷性:在有限的步骤中执行出结果
确定性:一定数据在运行算法后有确定的结果
可行性:能够分析解决实际问题
3、算法的基本要求
正确性
可读性
健壮性
时间复杂度:算法占用的时间,运算速度。
空间复杂度:算法在运行时候占用的内存,占用的资源。
分治算法
概述:
一种把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并的算法。
是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)。
核心思想是将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治法所能解决的问题一般具有以下几个特征:
该问题的规模缩小到一定的程度就可以容易地解决
该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
利用该问题分解出的子问题的解可以合并为该问题的解;
该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
分治法的使用一般有以下步骤:
分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
合并:将各个子问题的解合并为原问题的解。
常用于解决以下问题:
(1)二分搜索
(2)大整数乘法
(3)Strassen矩阵乘法
(4)棋盘覆盖
(5)合并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔
分支法解决汉诺塔问题:
代码实现:
public class Hanoitower {
public static void main(String[] args) {
hanoiTower(10, 'A', 'B', 'C');
}
//汉诺塔的移动的方法
//使用分治算法
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);
}
}
}
动态规划算法
概述:
动态规划算法的核心是将大问题划分为小问题进行解决,从而逐步获取目标解的方法。
动态规划与分治算法类似,区别在动态规划解决的问题往往子问题的解是下一个问题的解之一,每个解是存在关系,逐步递进的。
动态规划可以通过类似填表的方式解决。
在使用动态规划算法时核心是要推出递归公式。
动态规划常用问题:
- 硬币找零
- 字符串相似度/编辑距离(edit distance)
- 最长公共子序列(Longest Common Subsequence,lcs)
- 最长递增子序列(Longest Increasing Subsequence,lis)
- 最大连续子序列和/积
- 矩阵链乘法
- 背包问题
- 有代价的最短路径
- 瓷砖覆盖(状态压缩DP)
- 工作量划分
- 三路取苹果
动态规划解决单一背包问题:
题目描述:
一个给定容量的背包、若干具有一定价值和重量的商品。选择放入该背包的商品价值最大。每个物品最多放一个。
代码实现:
public class KnapsackProblem {
public static void main(String[] args) {
int[] w = { 1, 4, 3 };// 物品的重量
int[] val = { 1500, 3000, 2000 }; // 物品的价值 这里val[i] 就是前面讲的v[i]
int m = 4; // 背包的容量
int n = val.length; // 物品的个数
int v[][] = new int[n + 1][m + 1];
// 开始动态规划
// 用遍历来完成表格
//核心是选与不选,构建动态规划表
for (int i = 1; i < v.length; i++) { //
for (int j = 1; j < v[0].length; j++) {
if(w[i-1] > j) {
v[i][j] = v[i-1][j]; //如果当前商品重量大于背包容量,不加商品,商品数量减一
}else {
v[i][j] = Math.max(v[i-1][j],val[i-1] + v[i-1][j-w[i-1]]); //如果小于或者等于,可以加入商品,就要比较加入与不加入的价格差别,此处比较不加入跟加入的价格过程就是用到了上一个问题的解的规律,体现了动态规划中的根据上一问题的解来推出这一问题的解的规律。
}
}
}
for (int i = 0; i <= n; i++) {
v[i][0] = 0;
}
for (int i = 0; i <= m; i++) {
v[0][i] = 0;
}
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
System.out.print(v[i][j] + " ");
}
System.out.println();
}
}
}
KMP算法
概述:
KMP算法,又称模式匹配算法,能够在线性时间内判定字符串 T 是否为S 的子串,并求出字符串 T 在 S 中各次出现的位置。
KMP算法的核心是根据已经遍历的字符串中建立起一个部分匹配数组,next数组,在next数组中已经储存了已遍历字符的遍历数据,就是最长前缀后缀相同的长度,这个可以被利用与遍历过程中根据遍历过的字符串减少重复遍历的步骤。
代码实现:
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "ABCSD ABCDF ASDCFG ABCDABCESADW";
String str2 = "ABCDABCE";
// int next[] = getNext(str2);
// System.out.println(KMPAlgorithm(str1, str2, next));
int next[] = getNext("ABCABCABCABCD");
for(int i= 0;i<next.length;i++) {
System.out.println(next[i]);
}
}
/*
* kmp匹配核心是根据next数组储存的匹配记录来移动数组,避免匹配过程的重复步骤
* str1:字符串
* str2:目标匹配字串
* next[]:next数组
*/
public static int KMPAlgorithm(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角标,移动原则也是依赖KMP的思想
j = next[j - 1]; //当目前的匹配不上后就会查看next数组中的j-1,也就是已经匹配过的数据的下标next数组位置
} //就是在比较字符串的前n位跟字符串后除了最后一个的n位已经是相同的了,再次比较最后一个跟
//第n+1个
if(str1.charAt(i) == str2.charAt(j)) {
j++; //匹配的话字串坐标+1
} //不匹配的话j不处理,i++(j放到前面循环开始的时候处理)
if(j == str2.length()) {
return i - j + 1; //找到的话返回i的开始值
}
}
return -1;
}
public static int [] getNext(String str) { //根据字符返回next数组,next数组记录最长前缀后缀相同值(不包括头、尾)
int [] next = new int[str.length()]; //定义next数组
next[0] = 0; //第一个先确定为零
for(int i = 1,j = 0;i < str.length();i++) { //循环遍历字符串
while(j > 0 && str.charAt(i) != str.charAt(j)) { //核心:用kmp的思想来将j定位,若最长的不匹配,用最长的前面一位匹配最长长度来开始匹配
//而不用从头开始,也是一种避免重复的操作
j = next[j - 1];
}
if(str.charAt(i) == str.charAt(j)) { //相同的话j++
j++;
}
next[i] = j; //把当前值设为j
}
return next;
}
}
贪心算法
概述:
贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。
必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关。)
贪心算法最终的选择不一定会是最优解。
贪心算法的基本思路是建立数学模型来描述问题,把求解的问题分成若干个子问题,对每个子问题求解,得到子问题的局部最优解,把子问题的解局部最优解合成原来问题的一个解。
贪心算法经典问题:
-
货币选择问题
-
区间调度问题
-
字典序最小问题
-
集合覆盖问题
贪心算法解决集合覆盖问题:
题目要求:
选择电台覆盖所有地区。
代码实现:
public class GreedyAlgorithm {
public static void main(String[] args) {
//先创建所有广播台集合
HashMap<String,HashSet<String>> broadcasts = new HashMap<String, HashSet<String>>();
//将各个电台放入到broadcasts
HashSet<String> hashSet1 = new HashSet<String>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<String>();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<String>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<String>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<String>();
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<String>();
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("广州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大连");
//存放已经选择的集合
ArrayList<String> selects = new ArrayList<String>();
//定义一个临时的集合, 在遍历的过程中,存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集
HashSet<String> tempSet = new HashSet<String>();
HashSet<String> comSet = new HashSet<String>();
while(allAreas.size() != 0) {
String maxIndex = null;
for(String k1 : broadcasts.keySet()) {
//每进行一次循环,tempSet要清空
tempSet.clear();
tempSet.addAll(broadcasts.get(k1)); //将当前遍历的数据地区放到tempSet中
tempSet.retainAll(allAreas); //求tempSet跟allAreas的交集
//贪心算法的核心,每次找最优解
if(tempSet.size() > 0 ) {
if(maxIndex == null ) {
maxIndex = k1;
}else{
comSet.clear();
comSet.addAll(broadcasts.get(k1));
comSet.retainAll(allAreas);
if(tempSet.size() > 0 && tempSet.size() > comSet.size()) {
maxIndex = k1;
}
}
}
}
//如果此时manIndex不为空,则开始加入到selects集合中
if(maxIndex != null) {
selects.add(maxIndex);
//再把目标集合中的数据删除
allAreas.removeAll(broadcasts.get(maxIndex));
}
}
System.out.println("得到的选择结果是" + selects);//[K1,K2,K3,K5]
}
}
普利姆(PRIM)算法
概述:
普利姆算法是一种生成图的最小生成树的问题,最小生成树是指在n个顶点的图中找出只有n-1条边的联通n个顶点的最小连通图的算法。
普利姆算法实际就是逐步遍历所有节点的所有路径,选出一条最小的路径并且路径双顶点都未访问过(不构成回路),直到遍历所有顶点,算法完成。
实现步骤:
输入:一个加权连通图,其中顶点集合为V,边集合为E;
初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
重复下列操作,直到Vnew = V:
a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;输出:使用集合Vnew和Enew来描述所得到的最小生成树。
普利姆算法实现公交站问题:
题目要求:
有n个地点,要求做出连通n个地点的最小公交线路。
public class PrimAlgorithm {
public static void main(String[] args) {
// 测试看看图是否创建ok
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 }, };
// 创建MGraph对象
MGraph graph = new MGraph(verxs);
// 创建一个MinTree对象
MinTree minTree = new MinTree();
minTree.createGraph(graph, verxs, data, weight);
minTree.prim(graph, 1);
}
}
class MinTree {
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.print(Arrays.toString(link));
System.out.println();
}
}
//编写prim算法
// 第二个参数表示开始遍历的下标点
public void prim(MGraph graph,int v) {
int visited [] = new int[graph.verxs]; //记录遍历的情况,若为0表示未遍历
System.out.println(Arrays.toString(visited));
int minWeight = 10000; //记录当前的最小权值,初始化设为最大值,作用是可以选择到可以遍历的路径
int h1 = -1; //h1,h2用来记录最小顶点相连的下标
int h2 = -1;
visited[v] = 1; //先将当前的坐标设置为遍历过
for(int k = 1 ;k<graph.verxs ; k ++) { //循环遍历节点的总数-1次
h1 = -1;
h2 = -1;
for(int i = 0 ;i < graph.verxs; i ++) { //循环遍历整个临界数组
for(int j = 0 ; j <graph.verxs ;j++) {
if(visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < 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; //重新把最小值设置为最大,可以寻找下一个可以遍历的节点
}
}
}
class MGraph {
int verxs; // 存放节点数
char[] data; // 存放节点数据
int[][] weight; // 存放边的权值,本图是用邻接矩阵在做
public MGraph(int verxs) {
super();
this.verxs = verxs;
data = new char[verxs];
weight = new int[verxs][verxs];
}
}
克鲁斯卡尔(Kruskal)算法
概述:
Kruskal算法也是一种寻找最小路径的算法,与普利姆算法的不同之处在于:
Kruskal算法每次寻找的是最小的边,在不构成回路的条件下循环寻找未利用的最小边,直到连接所有的顶点。
基本步骤:
-
记Graph中有v个顶点,e个边
-
新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边
-
将原图Graph中所有e个边按权值从小到大排序
-
循环:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中
if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中 添加这条边到图Graphnew中
克鲁斯卡尔解决公交站问题
题目要求:
有n个地点,要求做出连通n个地点的最小公交线路。
代码实现:
public class KruskalCase {
private int EdgeNum;
private char[] vertxs;
private int[][] matrix;
private static final int INF = Integer.MAX_VALUE;
private int [] ends;
public static void main(String[] args) {
char [] vertexs = {'A','B','C','D','E','F','G'};
int matrix[][] = {
/*A*//*B*//*C*//*D*//*E*//*F*//*G*/
/*A*/ { 0, 12, INF, INF, INF, 16, 14},
/*B*/ { 12, 0, 10, INF, INF, 7, INF},
/*C*/ { INF, 10, 0, 3, 5, 6, INF},
/*D*/ { INF, INF, 3, 0, 4, INF, INF},
/*E*/ { INF, INF, 5, 4, 0, 2, 8},
/*F*/ { 16, 7, 6, INF, 2, 0, 9},
/*G*/ { 14, INF, INF, INF, 8, 9, 0}};
KruskalCase case1 = new KruskalCase(vertexs, matrix);
case1.kuruskal();
}
//开始Kruskal算法
public void kuruskal() {
int h1,h2; //h1,h2 分别是边的两个顶点
int index = 0 ; //目前放入边的坐标
EData rets[] = new EData[EdgeNum];
//获得边集
EData edges1[] = getEDatas(matrix);
//排序边集
sort(edges1);
for(int i = 0;i < EdgeNum ;i++) {
h1 = getPosition(edges1[i].start);
h2 = getPosition(edges1[i].end);
//获得字符对应的下标
if(getEnd(ends, h1) != getEnd(ends, h2)) { //每次连接点时判断是否相同
ends[h1] = h2; //不同时将起始点的终点设置为终点的终点,完成结果集的更新
rets[index++] = edges1[i]; //同时加入结果集
}
}
//<E,F> <C,D> <D,E> <B,F> <E,G> <A,B>。
//统计并打印 "最小生成树", 输出 rets
System.out.println("最小生成树为");
for(int i = 0; i < index; i++) {
System.out.println(rets[i]);
}
}
/**
* 在本方法中,边的终点集合实际上实在一种动态的构建过程。
* 在构建树的过程中,在开始是每个点的终点都可以设置为自己或者null,在使用kruskal算法时。
* 就可以在每次构建过程中更新终点集,实质上是一种类似动态规划的思想,此终点的位置在其子节点的终点位置。
* 在连接的过程中慢慢形成一个完整的终点集,更新的就是联通的。未更新的就是终点是自身的。
* @param ends:边的终点集合
* @param v:要找的边
* @return
*/
public int getEnd(int ends[],int v) {
while(ends[v] != 0) {
v = ends[v];
}
return v;
}
//对边集合进行排序
public void sort(EData[] edatas) {
for(int i = 0;i < edatas.length;i++) { //使用冒泡排序法
for(int j = 0;j < edatas.length - i -1 ;j++) {
if(edatas[j].weight > edatas[j+1].weight) {//交换
EData tmp = edatas[j];
edatas[j] = edatas[j+1];
edatas[j+1] = tmp;
}
}
}
}
//根据矩阵获取边集合
public EData[] getEDatas(int [][] martrix){
int inedx = 0;
EData []Edatas = new EData[EdgeNum];
for(int i = 0 ; i < vertxs.length; i ++) {
for(int j = i+1;j < matrix[0].length;j++) { //遍历矩阵的上三角获取边数,减少重复操作
if(matrix[i][j] != INF) {
EData edata = new EData(vertxs[i], vertxs[j], martrix[i][j]);
Edatas[inedx++] = edata;
}
}
}
return Edatas;
}
//根据传入的数据获得数据下标
public int getPosition(char vertex) {
for(int i = 0; i < vertxs.length; i ++) {
if(vertxs[i] == vertex) {
return i;
}
}
return -1;
}
//构造器
public KruskalCase(char[] vertxs, int[][] matrix) {
int vlen = vertxs.length; //这里用的初始化方式是复制拷贝,不会改变传入的数组数据
this.vertxs = new char[vlen];
this.matrix = new int[vlen][vlen];
ends = new int [vlen];
for (int i = 0; i < vlen; i++) {
this.vertxs[i] = vertxs[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(matrix[i][j] < INF) {
EdgeNum++;
}
}
}
}
//打印邻接矩阵
public void print() {
System.out.println("邻接矩阵为: \n");
for(int i = 0; i < vertxs.length; i++) {
for(int j=0; j < vertxs.length; j++) {
System.out.printf("%12d", matrix[i][j]);
}
System.out.println();//换行
}
}
// 边类:用于储存一条边的数据,可以更有效实现边的排序等操作
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;
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "EData [< "+start +", " + end + ">=" + weight+ "]";
}
}
}
迪杰斯特拉算法
概述:
迪杰斯特拉算法定义:
求解单元点的最短路径问题,给定带权有向图G和源点v,求v到G中其他顶点的最短路径。
迪杰斯特拉最最朴素的思想就是按长度递增的次序产生最短路径。即每次对所有可见点的路径长度进行排序后,选择一条最短的路径,这条路径就是对应顶点到源点的最短路径。
思想是广度遍历图,用动态规划和贪心算法的思想,每次将规划完的结果保存,在遍历时将每次的最优解保存,最终形成一个对所有点的最路径解。
迪杰斯特拉算法解决邮差路径问题:
题目要求:
求出在几个顶点中邮差送信的最短路径。
代码实现:
public class DijkstraAlgorithm {
public static void main(String[] args) {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
//邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;// 表示不可以连接
matrix[0]=new int[]{N,5,7,N,N,N,2};
matrix[1]=new int[]{5,N,N,9,N,N,3};
matrix[2]=new int[]{7,N,N,N,8,N,N};
matrix[3]=new int[]{N,9,N,N,N,4,N};
matrix[4]=new int[]{N,N,8,N,N,5,4};
matrix[5]=new int[]{N,N,N,4,5,N,6};
matrix[6]=new int[]{2,3,N,N,4,6,N};
//创建 Graph对象
Graph graph = new Graph(vertex, matrix);
//测试迪杰斯特拉算法
graph.dsj(6);//C
graph.showDijkstra();
}
}
//图的核心类
class Graph {
char[] vertexs;
int[][] matrix;
public static final int IN = Integer.MAX_VALUE;
VisitedVertex vv;
// 已经访问过的数组
public Graph(char[] vertexs, int[][] matrix) {
super();
this.vertexs = vertexs;
this.matrix = matrix;
}
//显示结果
public void showDijkstra() {
vv.show();
}
//开始迪杰斯特啦算法
//思想是广度遍历数组,用动态规划和贪心算法的思想,每次将规划完的结果保存,在遍历时将每次的最优解保存
public void dsj(int index) {
vv = new VisitedVertex(vertexs.length, index);
//传入后先更新目标
update(index);
for (int i = 1; i < vertexs.length; i++) { //再循环更新所有
index = vv.updateArr();
update(index);
}
}
//核心方法,根据传入的节点来更新数组数据
public void update(int index) {
int len = 0;
for (int i = 0; i < matrix[index].length; i++) {
//将最短节点(当前节点)遍历所有长度值,并且将长度值设置为顶点到该节点再到i节点的长度
len = vv.getDis(index) + matrix[index][i];
//若通过此路径到该节点的长度小于原先的最小长度,则更新最小长度以及前驱,将前驱设置为当前节点(动态规划的思想)
if(vv.getDis(i) > len && !vv.in(i)) {
vv.updateDis(i, len);
vv.updatePre(i, index);
}
}
}
public void show() {
for(int link[] : matrix) {
System.out.println(Arrays.toString(link));
}
}
}
//开始写访问过的类,此处是关键,写了index到各个点的距离数组。各个点的前驱节点,点index到其他节点的最短距离(结果集合)
class VisitedVertex{
public static final int IN = Integer.MAX_VALUE;
// 记录各个顶点是否访问过 1表示访问过,0未访问,会动态更新
public int[] already_arr;
// 每个下标对应的值为前一个顶点下标, 会动态更新
public int[] pre_visited;
// 记录出发顶点到其他所有顶点的距离,比如G为出发顶点,就会记录G到其它顶点的距离,会动态更新,求的最短距离就会存放到dis
public int[] dis;
//构造器:根据传入的节点数量和初始节点index来开始初始化各个数组
public VisitedVertex(int length ,int index) {
already_arr = new int [length];
pre_visited = new int [length];
dis = new int [length];
Arrays.fill(dis, IN);
dis[index] = 0; //将除了自身以外的节点全部设置为IN
already_arr[index] = 1; //将自身设置为已访问
}
//开始写各种方法
//是否访问过
public boolean in(int index) {
return already_arr[index] == 1;
}
//更新距离数组
public void updateDis(int index,int length) {
dis[index] = length;
}
//更新前驱节点数组
public void updatePre(int pre,int index) {
pre_visited[pre] = index;
}
/**
* 继续选择并返回新的访问顶点, 比如这里的G 完后,就是 A点作为新的访问顶点(注意不是出发顶点)
* @return
*/
public int updateArr() {
int min = Integer.MAX_VALUE;
int index = 0;
for(int i = 0;i < already_arr.length;i++) {
if(already_arr[i] == 0 && dis[i] < min) {
min = dis[i];
index = i;
}
}
already_arr[index] = 1;
return index;
}
//返回出发节点到该节点的距离
public int getDis(int index) {
return dis[index];
}
//显示最后的结果
//即将三个数组的情况输出
public void show() {
System.out.println("==========================");
//输出already_arr
for(int i : already_arr) {
System.out.print(i + " ");
}
System.out.println();
//输出pre_visited
for(int i : pre_visited) {
System.out.print(i + " ");
}
System.out.println();
//输出dis
for(int i : dis) {
System.out.print(i + " ");
}
System.out.println();
//为了好看最后的最短距离,我们处理
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int count = 0;
for (int i : dis) {
if (i != 65535) {
System.out.print(vertex[count] + "("+i+") ");
} else {
System.out.print("N ");
}
count++;
}
System.out.println();
}
}
弗洛伊德算法:
概述:
弗洛伊德算法跟克鲁斯卡尔算法一样,都是用来解决最短路径问题。
而弗洛伊德算法可以求所有节点到所有节点的最短路径。当然需要更高的复杂度。
迪杰斯特拉算法同通过选定的被访问节点,求出访问顶点到其他顶点的最短路径。
简单来说就是用一个中间顶点来尝试连接两个节点,比较两个节点直接连接与通过中间节点连接的路径长度,取比较小的那个值为两个点间的最短路径,中间节点可能性取值为所有除两个节点外所有节点。
弗洛伊德每一个顶点都是出发访问点,需要将每一个顶点都看作出发访问点,从而求出一个顶点到其他顶点的最短路径。
弗洛伊德算法的步骤:
设置顶点vi到vk的最短路径已知为Lik,Vk到Vj得最短路径为Lkj,定点Vi到Vj的已知路径为Lij,则Vi到Vj的最短路径就是:min(Lik+Lkj,Lij),Vk为所有顶点可能性,最终遍历完就可以获得Lij的最短路径。
至于其他的Vi到Vk等最短路径也是上面一样的步骤执行。
弗洛伊德算法解决邮差送报问题
题目描述:
求出在几个顶点中邮差送信的最短路径。
附带找出最小路径要走的路径,用一个动态规划解决数组路径。
基本思路:
通过设置其前驱节点完成。
前驱节点数组在最短路径算法设置中很重要。
前驱节点数组就是用来寻找最短路径的具体路径。
代码实现:
public class FloydAlgorithm {
public static void main(String[] args) {
//先创建图来进行测试
// 测试看看图是否创建成功
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
//创建邻接矩阵
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[] { 0, 5, 7, N, N, N, 2 };
matrix[1] = new int[] { 5, 0, N, 9, N, N, 3 };
matrix[2] = new int[] { 7, N, 0, N, 8, N, N };
matrix[3] = new int[] { N, 9, N, 0, N, 4, N };
matrix[4] = new int[] { N, N, 8, N, 0, 5, 4 };
matrix[5] = new int[] { N, N, N, 4, 5, 0, 6 };
matrix[6] = new int[] { 2, 3, N, N, 4, 6, 0 };
Graph graph = new Graph(vertex.length,vertex,matrix);
graph.floyd();
graph.show();
graph.dsl(2, 5);
}
}
class Graph {
private char[] vertexs;// 保存各个顶点的数据
private int[][] dis; // 保存,从各个顶点出发到其它顶点的距离,最后的结果,也是保留在该数组
private int[][] pre;// 保存到达目标顶点的前驱顶点
public Graph(int length, char[] vertexs, int[][] matrix) {
super();
this.vertexs = vertexs;
this.dis = matrix;
pre = new int[length][length];
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
// 显示pre数组和dis数组
public void show() {
// 为了显示便于阅读,我们优化一下输出
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
for (int k = 0; k < dis.length; k++) {
// 先将pre数组输出的一行
for (int i = 0; i < dis.length; i++) {
System.out.print(vertex[pre[k][i]] + " ");
}
System.out.println();
// 输出dis数组的一行数据
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路径是" + dis[k][i] + ") ");
}
System.out.println();
System.out.println();
}
}
//找最短路劲长度
public void dsl(int i,int j) {
if(pre[i][j] == i) {
System.out.println(vertexs[i] + "->" + vertexs[j] + "权值:" + dis[i][j]);
return;
}
dsl(i,pre[i][j]);
dsl(pre[i][j],j);
}
/**
* pre数组的作用就是可以在遍历的时候不断的找回其路劲,用递归的方法,当找的的pre[i][j]!=i的时候
* 说明还有中间值,再度遍历中间值进行遍历。
* 在创建这个表的时候,用的也是动态规划的思想,在更新的时候pre[k][j]的意思便是
*/
public void floyd() {
int len = 0; //len表示储存遍历时候的数据长度
for(int k = 0;k < vertexs.length;k++) { //以k为顶点
for(int i = 0;i < vertexs.length;i++) { //以i为顶点
for(int j = 0;j < vertexs.length;j++) { //以j为顶点
len = dis[i][k] + dis[k][j]; //len设置为以k为中点走这条路的长度,这里蕴含着如果两边长度不连通。则加任意一个值都会大于不连通的值,所以不会又遍历上的遗漏
if(len < dis[i][j]) { //如果小于直接到达ij的长度的话进行操作,如果不连通也会进行操作
dis[i][j] = len; //这里体现的是动态规划的思想,用之前遍历过程中已经找到最短值来代替,
//其思想就是找从i->k的用某个中点做桥梁的时候找的最小值,同时在照这个值得过程中也是用到了找其最小值得思想。
pre[i][j] = pre[k][i]; //把这两个点得中间值设置为k,表示通过k来走这条路。
}
}
}
}
}
}
马踏棋盘算法:
概述:
将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。
马踏棋盘算法是深度优先遍历的一种应用。
遍历所有深度遍历可能的条件,若发现不可行,则开始回溯回上一步骤开始下一个步骤可能性,直到完成遍历。
优化方案很简单,运用贪心算法的思想,每次遍历时走下一步可以走的步骤最小的,也就是最有可能先完成目标的方法,可以有效减少很多遍历步骤。
代码实现:
public class HorseChessboard {
//先定义棋盘
private static int X; //棋盘的行数
private static int Y; //棋盘的列数
private static boolean visited[]; //棋盘是否访问过的标记
private static boolean finished; //遍历是否完全完成的标记
public static void main(String[] args) {
System.out.println("骑士周游算法,开始运行~~");
//测试骑士周游算法是否正确
X = 8;
Y = 8;
int row = 1; //马儿初始位置的行,从1开始编号
int column = 1; //马儿初始位置的列,从1开始编号
//创建棋盘
int[][] chessboard = new int[X][Y];
visited = new boolean[X * Y];//初始值都是false
//测试一下耗时
long start = System.currentTimeMillis();
traversalChessboard(chessboard, row - 1, column - 1, 1);
long end = System.currentTimeMillis();
System.out.println("共耗时: " + (end - start) + " 毫秒");
}
//开始算法
public static void traversalChessboard(int[][] chessboard, int row, int column, int step) {
chessboard[row][column] = step;
//row = 4 X = 8 column = 4 = 4 * 8 + 4 = 36
visited[row * X + column] = true; //标记该位置已经访问
//获取当前位置可以走的下一个位置的集合
ArrayList<Point> ps = next(new Point(column, row));
//对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
sort(ps);
//遍历 ps
while(!ps.isEmpty()) {
Point p = ps.remove(0);//取出下一个可以走的位置
//判断该点是否已经访问过
if(!visited[p.y * X + p.x]) {//说明还没有访问过
traversalChessboard(chessboard, p.y, p.x, step + 1);
}
}
//判断马儿是否完成了任务,使用 step 和应该走的步数比较 ,
//如果没有达到数量,则表示没有完成任务,将整个棋盘置0
//说明: step < X * Y 成立的情况有两种
//1. 棋盘到目前位置,仍然没有走完
//2. 棋盘处于一个回溯过程
if(step < X * Y && !finished ) {
chessboard[row][column] = 0;
visited[row * X + column] = false;
} else {
finished = true;
}
}
//跟据传入的点的位置把所有可以访问的点返回一个集合,用内置点类来方便使用
public static ArrayList<Point> next(Point point){
//创建一个List
ArrayList<Point> ps = new ArrayList<Point>();
//用一个point来方便初始化调用
Point p1 = new Point();
//若往某个日字点走不越界则可以放到集合中
if((p1.x = point.x-2) > 0 && (p1.y = point.y-1) > 0) {
ps.add(new Point(p1));
}
if((p1.x = point.x - 1) >=0 && (p1.y=point.y-2)>=0) {
ps.add(new Point(p1));
}
if ((p1.x = point.x + 1) < X && (p1.y = point.y - 2) >= 0) {
ps.add(new Point(p1));
}
if ((p1.x = point.x + 2) < X && (p1.y = point.y - 1) >= 0) {
ps.add(new Point(p1));
}
if ((p1.x = point.x + 2) < X && (p1.y = point.y + 1) < Y) {
ps.add(new Point(p1));
}
if ((p1.x = point.x + 1) < X && (p1.y = point.y + 2) < Y) {
ps.add(new Point(p1));
}
if ((p1.x = point.x - 1) >= 0 && (p1.y = point.y + 2) < Y) {
ps.add(new Point(p1));
}
if ((p1.x = point.x - 2) >= 0 && (p1.y = point.y + 1) < Y) {
ps.add(new Point(p1));
}
return ps;
}
//开始优化,优化方案,采用贪心算法,每次选取点可以走的集合中最小的那次进行,既找到最有可能先完成棋盘遍历的点
//先根据传进来的集合进行排序
public static void sort(ArrayList<Point> ps) {
ps.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
// TODO Auto-generated method stub
//获取到o1的下一步的所有位置个数
int count1 = next(o1).size();
//获取到o2的下一步的所有位置个数
int count2 = next(o2).size();
if(count1 < count2) { //若小于将count放到后面,实现递增排序
return -1;
} else if (count1 == count2) {
return 0;
} else {
return 1;
}
}
});
}
}