1.1 二分查找算法(非递归)介绍
1) 前面《数据结构7 -查找》中的二分查找算法,是使用递归的方式,下面我们讲解二分查找算法的非递归方式。
2) 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找。
3) 二分查找法的运行时间为对数时间O(㏒₂n) ,即查找到需要的目标位置最多只需要㏒₂n步。假设从[0,99]的队列(100个数,即n=100)中寻到目标数30,则需要查找步数为㏒₂100 , 即最多需要查找7次( 2^6 < 100 < 2^7)。
1.2 二分查找算法(非递归)代码实现
数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成.
public class BinarySearchNoRecur {
public static void main(String[] args) {
int[] arr = {1,3, 8, 10, 11, 67, 100};
System.out.println(binarySearch(arr,8)); //2
}
//二分查找的非递归实现
/**
* @param arr 待查找的数组,arr是升序排列
* @param target 需要查找的数
* @return 返回对应下标,-1 表示没有找到
* */
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;
}
}
递归:
/*
* 只查找到一个符合结果的数就行
*
* 假设传入的数组是从小到大排列的有序数组
* */
public int binarySearch(int[] arr, int left, int right, int findVal){
if(left > right)
return -1;
int mid = (left + right) / 2;
if(findVal > arr[mid])
return binarySearch(arr, mid + 1, right, findVal );
else if(findVal < arr[mid])
return binarySearch(arr, left, mid - 1, findVal );
else
return mid;
}
2.1 分治算法介绍
1) 分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
2) 分治算法可以求解的一些经典问题
2.2 分治算法的基本步骤
分治法在每一层递归上都有三个步骤:
1) 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
2) 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
3) 合并:将各个子问题的解合并为原问题的解。
2.3 分治(Divide-and-Conquer(P))算法设计模式
if |P|≤n0
then return(ADHOC(P))
//将P分解为较小的子问题 P1 ,P2 ,…,Pk
for i←1 to k
do yi ← Divide-and-Conquer(Pi) 递归解决Pi
T ← MERGE(y1,y2,…,yk) 合并子问题
return(T)
其中|P|表示问题P的规模;为一阈值,表示当问题P的规模不超过时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。
2.4 分治算法最佳实践 - 汉诺塔
汉诺塔的传说
汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
假如每秒钟一次,共需多长时间呢?移完这些金片需要5845.54亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。
public class Hanoitower {
public static void main(String[] args) {
Hanoitower hanoitower = new Hanoitower();
hanoitower.hanoiTower(3,'a','b','c');
}
//移动的次数
private static int time = 0;
//a--->c, 借助b
public static void hanoiTower(int num, char a, char b, char c){
if(num == 1)
move(num,a,c);
else {
hanoiTower(num - 1, a, c, b); //a--->b, 借助c 把前n-m个盘移动到b盘上,借助c
move(num,a,c);//把第nmu个从a移动到c
hanoiTower(num - 1, b, a, c); //把前n-m个盘从b移动到c盘上,借助a
}
}
public static void move(int num, char x, char y){
System.out.println("第"+ ++time + "次移动,"+ num +"号圆盘:"+ x + "-->" + y);
}
}
Output:
第1次移动,1号圆盘:a-->c
第2次移动,2号圆盘:a-->b
第3次移动,1号圆盘:c-->b
第4次移动,3号圆盘:a-->c
第5次移动,1号圆盘:b-->a
第6次移动,2号圆盘:b-->c
第7次移动,1号圆盘:a-->c
3.1 动态规划算法介绍
1) 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。
2) 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
3) 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )。
4) 动态规划可以通过填表的方式来逐步推进,得到最优解。
3.2 动态规划算法最佳实践-背包问题
背包问题:有一个背包,容量为4磅 , 现有如下物品:
物品 | 重量 | 价格 |
---|---|---|
吉他(G) | 1 | 1500 |
音响(S) | 4 | 3000 |
电脑(L) | 3 | 2000 |
1) 要求达到的目标为装入的背包的总价值最大,并且重量不超出。
2) 要求装入的物品不能重复。
3) 思路分析和图解
算法的主要思想,利用动态规划来解决。每次遍历到的第 i 个物品,根据 w[i] 和 v[i] 来确定是否需要将该物品放入背包中。即对于给定的 n 个物品,设 v[i] 、w[i] 分别为第 i 个物品的价值和重量,C 为背包的容量。再令 v[i][j] 表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值。则我们有下面的结果:
① v[i][0] = v[0][j] = 0; //表示 填入表 第一行和第一列是 0。
②当 w[i] > j 时:v[i][j] = v[i-1][j] // 当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略。
③当 j >= w[i]时: v[i][j] = max{ v[i-1][j], v[i] + v[i - 1][j - w[i]] } // 当 准备加入的新增的商品的容量小于等于当前背包的容量
// 装入的方式:
v[i - 1][j]: 就是上一个单元格的装入的最大值
v[i] : 表示当前商品的价值
v[i - 1][j - w[i]] : 装入 i - 1 商品,到剩余空间 j - w[i] 的最大值
/**
*
* 动态规划算法 - 背包问题
*/
public class KnaspackProblem {
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; //物品的个数
//创建二维数组
//v[i][j] 表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值
int[][] v = new int[n + 1][m + 1];
//为了记录放入商品的情况,我们定一个二维数组
int[][] path = new int[n + 1][m + 1];
//初始化第一行和第一列,这里在本程序中,可以不去处理,因为默认值就是0
for (int i = 0; i < v.length; i++) {
v[i][0] = 0; //将第一列设置为0
}
for (int i = 0; i < v[0].length; i++) {
v[0][i] = 0; //将第一行设置为0
}
//根据前面的公式来动态规划处理
for (int i = 1; i < v.length; i++) { //不处理第一行, i 从1开始
for (int j = 1; j < v[0].length; j++) { //不处理第一列, j 从1开始
//公式
if(w[i - 1] > j) //因为此程序时从 1 开始的,因此原来的公式中的 w[i] 需改为 w[i - 1]
v[i][j] = v[i - 1][j];
else { //因为此程序时从 1 开始的,因此原来的公式调整为如下:
//v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
//为了记录商品放到背包的情况,我们不能直接使用上面的公式
if(v[i - 1][j] < val[i - 1] + v[i - 1][j - w[i - 1]]){
v[i][j] = val[i - 1] + v[i - 1][j - w[i - 1]];
path[i][j] = 1; //把当前的情况记录到path
}else
v[i][j] = v[i - 1][j];
}
}
}
//输出一下v 看看目前的情况
for (int i = 0; i < v.length; i++) {
for (int j = 0; j < v[i].length; j++) {
System.out.print(v[i][j] + " ");
}
System.out.println();
}
System.out.println("--------------------------------------------");
int i = path.length - 1; //行的最大下标
int j = path[0].length - 1; //列的最大下标
while(i > 0 && j > 0){ //从path的最后开始找
if(path[i][j] == 1){
System.out.printf("第%d个商品放入到背包\n",i);
j = w[i - 1];
}
i --;
}
}
}
Output:
0 0 0 0 0
0 1500 1500 1500 1500
0 1500 1500 1500 3000
0 1500 1500 2000 3500
--------------------------------------------
第3个商品放入到背包
第1个商品放入到背包
KMP算法
5.1 贪心算法介绍
1) 贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
2) 贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
5.2 贪心算法最佳应用 - 集合覆盖
1)假设存在如下表的需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。
广播台 | 覆盖地区 |
---|---|
K1 | “北京”,“上海”,“天津” |
K2 | “广州”,“北京”,“深圳” |
K3 | “成都”,“上海”,“杭州” |
K4 | “上海”,“天津” |
K5 | “杭州”,“大连” |
2)思路分析:
使用贪婪算法,效率高:
目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
①遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系)
②将这个电台加入到一个集合中(比如ArrayList), 想办法把该电台覆盖的地区在下次比较时去掉。
③重复第1步直到覆盖了全部的地区
/**
*
* 贪心算法 - 集合覆盖问题
*/
public class GreedyAlgorithm {
public static void main(String[] args) {
//创建广播电台,放入到Map
HashMap> broadcasts = new HashMap>();
//将各个电台放入到broadcasts
HashSet hashSet1 = new HashSet();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet hashSet2 = new HashSet();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet hashSet3 = new HashSet();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet hashSet4 = new HashSet();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet hashSet5 = new HashSet();
hashSet5.add("杭州");
hashSet5.add("大连");
//加入到map
broadcasts.put("K1", hashSet1);
broadcasts.put("K2", hashSet2);
broadcasts.put("K3", hashSet3);
broadcasts.put("K4", hashSet4);
broadcasts.put("K5", hashSet5);
//allAreas 存放所有的地区
HashSet allAreas = new HashSet();
allAreas.addAll(hashSet1);
allAreas.addAll(hashSet2);
allAreas.addAll(hashSet3);
allAreas.addAll(hashSet4);
allAreas.addAll(hashSet5);
//创建ArrayList, 存放选择的电台集合
ArrayList selects = new ArrayList();
//定义一个临时的集合, 在遍历的过程中,存放遍历过程中的电台覆盖的地区和当前还没有覆盖的地区的交集
HashSet tempSet = new HashSet();
//定义给maxKey , 保存在一次遍历过程中,能够覆盖最大未覆盖的地区对应的电台的key。如果maxKey 不为null , 则会加入到 selects
String maxKey = null;
while(allAreas.size() != 0) { // 如果allAreas 不为0, 则表示还没有覆盖到所有的地区
maxKey = null; //每进行一次while,需要
for(String key : broadcasts.keySet()) { //遍历 broadcasts, 取出对应key
tempSet.clear(); //每进行一次for
HashSet areas = broadcasts.get(key); //当前这个key能够覆盖的地区
tempSet.addAll(areas);
tempSet.retainAll(allAreas); //求出tempSet 和 allAreas 集合的交集, 交集会赋给 tempSet
//如果当前这个集合包含的未覆盖地区的数量,比maxKey指向的集合地区还多,就需要重置maxKey
// tempSet.size() >broadcasts.get(maxKey).size()) 体现出贪心算法的特点,每次都选择最优的
if(tempSet.size() > 0 && (maxKey == null || tempSet.size() >broadcasts.get(maxKey).size())){
maxKey = key;
}
}
if(maxKey != null) { //maxKey != null, 就应该将maxKey 加入selects
selects.add(maxKey);
allAreas.removeAll(broadcasts.get(maxKey)); //将maxKey指向的广播电台覆盖的地区,从 allAreas 去掉
}
}
System.out.println("得到的选择结果是" + selects);//[K1,K2,K3,K5]
}
}
5.3 贪心算法注意事项和细节
(1)贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
(2)比如上题的算法选出的是K1, K2, K3, K5,符合覆盖了全部的地区。
(3)但是我们发现 K2, K3,K4,K5 也可以覆盖全部地区,如果K2 的使用成本低于K1,那么我们上题的 K1, K2, K3, K5 虽然是满足条件,但是并不是最优的。
https://blog.csdn.net/weixin_44210965/article/details/102871710
7.1 马踏棋盘算法介绍和游戏演示
1) 马踏棋盘算法也被称为骑士周游问题。
2) 将马随机放在国际象棋的8×8棋盘Board[0~7][0~7]的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。
3) 游戏演示: http://www.4399.com/flash/146267_2.htm
/**
*
* 马踏棋盘算法
*/
public class HorseChessboard {
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) + " 毫秒");
//输出棋盘的最后情况
for(int[] rows : chessboard) {
for(int step: rows) {
System.out.print(step + "\t");
}
System.out.println();
}
}
private static int X; // 棋盘的列数
private static int Y; // 棋盘的行数
//创建一个数组,标记棋盘的各个位置是否被访问过
private static boolean visited[];
//使用一个属性,标记是否棋盘的所有位置都被访问
private static boolean finished; // 如果为true,表示成功
/**
* 完成骑士周游问题的算法
* @param chessboard 棋盘
* @param row 马儿当前的位置的行 从0开始
* @param column 马儿当前的位置的列 从0开始
* @param step 是第几步 ,初始位置就是第1步
*/
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事业从0开始,所以37就是这里的36)
visited[row * X + column] = true; //标记该位置已经访问
//获取当前位置可以走的下一个位置的集合
ArrayList ps = next(new Point(column, row));
sort(ps); //对ps进行排序,排序的规则就是对ps的所有的Point对象的下一步的位置的数目,进行非递减排序
while(!ps.isEmpty()) { //遍历 ps
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;
}
}
/**
* 功能: 根据当前位置(Point对象),计算马儿还能走哪些位置(Point),并放入到一个集合中(ArrayList), 最多有8个位置
* @param curPoint
* @return
*/
public static ArrayList next(Point curPoint) {
//创建一个ArrayList
ArrayList ps = new ArrayList();
//创建一个Point
Point p1 = new Point();
//表示马儿可以走5这个位置
if((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y -1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿可以走6这个位置
if((p1.x = curPoint.x - 1) >=0 && (p1.y=curPoint.y-2)>=0) {
ps.add(new Point(p1));
}
//判断马儿可以走7这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y - 2) >= 0) {
ps.add(new Point(p1));
}
//判断马儿可以走0这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y - 1) >= 0) {
ps.add(new Point(p1));
}
//判断马儿可以走1这个位置
if ((p1.x = curPoint.x + 2) < X && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
//判断马儿可以走2这个位置
if ((p1.x = curPoint.x + 1) < X && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿可以走3这个位置
if ((p1.x = curPoint.x - 1) >= 0 && (p1.y = curPoint.y + 2) < Y) {
ps.add(new Point(p1));
}
//判断马儿可以走4这个位置
if ((p1.x = curPoint.x - 2) >= 0 && (p1.y = curPoint.y + 1) < Y) {
ps.add(new Point(p1));
}
return ps;
}
//根据当前这个一步的所有的下一步的选择位置,进行非递减排序, 减少回溯的次数
public static void sort(ArrayList ps) {
ps.sort(new Comparator() {
@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) {
return -1;
} else if (count1 == count2) {
return 0;
} else {
return 1;
}
}
});
}
}