前言
前面的几篇文章我们向大家介绍了数据结构的部分,主要包括数据工具的推荐、十大排序算法、数据结构与算法概述、链表、栈、队列、排序以及查找算法以及树和图。从本文开始给大家介绍算法方面的知识,主要包括二分查找、分治算法、动态规划、KMP算法、贪心算法、prim算法、kruskal算法、迪杰斯特拉算法、Floyd算法以及马踏棋盘算法。首先给大家介绍较为简单的二分查找算法。
一、二分查找算法
其实我们在前面的查找中介绍过二分查找,我们当时介绍的实现方法是通过递归的方式进行的,本次讲解的是二分查找用非递归的方式进行实现。二分查找只适用于从有序的数列中进行查找,将数列排序后在进行查找。二分查找的时间复杂度为O(logn)
,即查找到需要的目标位置最多只需要log2_n
步,假设从【0,99】的队列中寻找目标是30,则需要log2_100,即最多需要查找7次。接下来我们以数组{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};
int index = binarySearch(arr,100);
System.out.println ("index="+index);
}
public static int binarySearch(int[] arr, int target){
int left = 0;
int right = arr.length - 1;
while (left <= right){
int mid = left + (right - left) / 2;
if(arr[mid] == target){
return mid;
}
else if (arr[mid] > target){
return mid - 1;
}
else {
left = mid + 1;
}
}
return -1;
}
}
我们以查找100 为例,执行结果如下:
二、分治法
分治法是一种重要的算法。其实就是分而治之
的意思,即:把一个复杂的问题分成两个或者更多的相同以及相似的子问题,再把子问题分成更小的子问题……,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。其实,这种算法是我们后面介绍很多算法的基础,因此,需要我们重点掌握。我们一般用到分治法的经典场景有:二分搜索、大整数乘法、棋盘覆盖、合并排序、快速排序、线性时间选择、最接近点对问题、循环赛日程表以及汉诺塔
等。接下来给大家介绍分治法的基本步骤。
其实分治法在每一层递归上都有三个步骤:
- 1、分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。
- 2、解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题。
- 3、合并:将各个子问题的解合并为原问题的解。
分治算法的伪代码如下所示:
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的规模;n0为一阈值,表示当问题p的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC( p)是该分治法中的基本子算法,用于直接解小规模的问题p。因此,当p的规模不超过n0时直接用算法ADHOC( P) 求解。算法MERGE(y1,y2,……,yk)是该分治法中的合并子算法,用于将p的子问题p1,p2,……,pk的相应的解y1,y2,……yk合并成p的解。
分治法的典型应用之一——汉诺塔
汉诺塔的问题来源于印度一个古老的传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序撂着64片黄金圆盘,大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
如果我们每移动一个圆盘需要一秒,那么移动所有圆盘需要多长时间呢?通过一位学者的研究发现:移动完这些圆盘需要5845.54亿年以上,太阳系的预期寿命据说也就具有几百亿年。我们利用汉诺塔的游戏来进行简单的思路分析:
如果我们有一个盘,A->C
如果我们有n>=2的情况,我们总是可以看做两个盘1最下边的盘2最上的盘
先把最上面的盘A->B
把最下边的盘A->C
把B塔的所有盘从B->C
根据上述对汉诺塔思路的分析,我们用java将其实现:
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);
}
}
}
具体执行的结果如下:
三、动态规划算法
动态规划的核心其实就是将一个大的问题划分成若干小问题,将一步步获得最优解。其实动态规划与我们前面介绍的分治算法类似,其基本思想也是将待求解的子问题分解成若干个子问题,先求解子问题,然后从这些子问题的解中得到原始问题的解。但是与分治法不同的是:适用于动态规划求解的问题,经分解得到的子问题往往不是相互独立的。另外就是动态规划可以通过填表的方式来逐步进行推进,从而得到最优解。
动态规划算法经典应用——背包问题
有一个背包,容量为4磅,现有如下物品:
有如下的要求:
- 1、达到的目标为装入的背包的总价值最大,并且重复不超过一个定数
- 2、要求装入的物品不能重复
具体的思路如下:
背包主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品价值最大,其中又分为01背包和完全背包(这里的完全背包是每种物品都有无限件使用)
当然我们这里提到的01背包问题,即每个物品最多放一件。当然无限背包问题也可以转换为01背包问题。
算法的主要思想是:利用动态规划来解决。即:每次遍历到第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包中。我们可以对给定的n个物品中,分别设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前第i个物品中能够装入容量为j的背包中的最大价值。
具体我们用图来阐述上述的思路。背包的过程如下:
1. 假如现在只有 吉他(G) , 这时不管背包容量多大,只能放一个吉他1500(G)
2. 假如有吉他和音响 , 验证公式:
v[1][1] =1500
- i = 1, j = 1
- w[i] = w[1] = 1 w [1] = 1 j = 1 v[i][j]=max{v[i-1][j], v[i]+v[i-1][jw[i]]} : v[1][1] = max {v[0][1], v[1] + v[0][1-1]} = max{0, 1500 + 0} = 1500 v[3][4]
- i = 3;j = 4 w[i] = w[3] =3 j = 4 j = 4 >= w[i] = 3 => 4 >= 3
v[3][4] = max {v[2][4], v[3] + v[2][1]} = max{3000,2000+1500} = 2000+1500
具体我们用代码实现以上的思路:
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];
int[][] path = new int[n+1][m+1];
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++) {
for(int j=1; j < v[0].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] = val[i - 1] + v[i - 1][j - w[i - 1]];
//把当前的情况记录到path
path[i][j] = 1;
} else {
v[i][j] = v[i - 1][j];
}
}
}
}
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]; //w[i-1]
}
i--;
}
}
}
执行的结果如下图所示:
总结
我们前面介绍的是数据结构,主要包括数据工具的推荐、十大排序算法、数据结构与算法概述、链表、栈、队列、排序以及查找算法以及树和图。从本文开始给大家介绍算法方面的知识,本文主要介绍了三种较为常用的算法,包括二分查找、分治法以及动态规划,针对后两种,我们给出了经典的应用帮助大家更好的理解其算法的核心思想。其实数据结构与算法是特别重要的,在编程中有至关重要的地位,因此,需要我们特别的掌握。生命不息,奋斗不止,我们每天努力,好好学习,不断提高自己的能力,相信自己一定会学有所获。加油!!!