数据结构-算法思维
作者:互联网
一、贪心算法
1.1 定义
贪婪算法(Greedy)的定义:是一种在每一步选中都采取在当前状态下最好或最优的选择,从而希望 导致结果是全局最好或最优的算法。贪婪算法:当下做局部最优判断,不能回退
1.2 经典问题:部分背包
背包问题是算法的经典问题,分为部分背包和0-1背包,主要区别如下: 部分背包:某件物品是一堆,可以带走其一部分
0-1背包:对于某件物品,要么被带走(选择了它),要么不被带走(没有选择它),不存在只带走一 部分的情况。
部分背包问题可以用贪心算法求解,且能够得到最优解。
假设一共有N件物品,第 i 件物品的价值为 Vi ,重量为Wi,一个小偷有一个最多只能装下重量为W的背 包,他希望带走的物品越有价值越好,可以带走某件物品的一部分,请问:他应该选择哪些物品?
假设背包可容纳50Kg的重量,物品信息如下表:
物品 重量(kg) 价值(元) 单位重量的价值(元/kg)
A 10 60 6
B 20 100 5
C 30 120 4
1.3 实现
贪心算法的关键是贪心策略的选择,
将物品按单位重量 所具有的价值排序。总是优先选择单位重量下价值最大的物品
按照我们的贪心策略,单位重量的价值排序: 物品A > 物品B > 物品C
我们尽可能地多拿物品A,直到将物品1拿完之后,才去拿物品B,然后是物品C 可以只拿一部 分
/**
* 物品信息
*/
class Goods {
String name;
double weight;
double price;
double val;
public Goods(String name, double weight, double price) {
this.name = name;
this.weight = weight;
this.price = price;
val = price / weight;
}
}
class BagDemo1 {
double bag; // 背包的大小
public void take(List<Goods> goodsList) {
sortValue(goodsList);
double sum_w = 0;
for (Goods goods : goodsList) {
sum_w += goods.weight;
System.out.println(goods.name + "取" + goods.weight + "kg");
if (sum_w > bag) {
return;
}
}
}
/**
* 按照价值排序
*
* @param goodsList
* @return
*/
private List<Goods> sortValue(List<Goods> goodsList) {
return goodsList;
}
public static void main(String[] args) {
BagDemo1 bd = new BagDemo1();
Goods goods1 = new Goods("A", 10, 60);
Goods goods2 = new Goods("B", 20, 100);
Goods goods3 = new Goods("C", 30, 120);
List<Goods> goodsList = new ArrayList<>();
goodsList.add(goods1);
goodsList.add(goods2);
goodsList.add(goods3);
bd.bag = 50;
bd.take(goodsList);
}
}
1.4 使用场景
时间复杂度
- 在不考虑排序的前提下,贪心算法只需要一次循环,所以时间复杂度是O(n)
优缺点
- 优点:性能高,能用贪心算法解决的往往是最优解
- 缺点:在实际情况下能用的不多,用贪心算法解的往往不是最好的
二、分治算法
分治算法(divide and conquer)的核心思想其实就是四个字,分而治之 ,也就是将原问题划分成 n 个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原 问题的解。
**关于分治和递归的区别 **
- 分治算法是一种处理问题的思想,递归是一种编程技巧
- 分治算法的递归实现中,每一层递归都会涉及这样三个操作:
- 分解:将原问题分解成一系列子问题
- 解决:递归地求解各个子问题,若子问题足够小,则直接求解
- 合并:将子问题的结果合并成原问题
求x的n次幂问题
一般的解法
是循环10次,该方法的时间复杂度是:O(n)
采用分治法
我们看到每次拆成n/2次幂,时间复杂度是O(logn)
public static int commpow(int x,int n) {
if(n==1) return x;
int half = commpow(x, n / 2);
// 偶次幂
if (n % 2 == 0) {
return half * half;
}
// 奇次幂
return half * half * x;
}
使用场景
- 优缺点
- 优势:将复杂的问题拆分成简单的子问题,解决更容易,另外根据拆分规则,性能有可能提高。
- 劣势:子问题必须要一样,用相同的方式解决
- 适用场景
- 原问题与分解成的小问题具有相同的模式;
- 原问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别
三、回溯算法
回溯的处理思想,有点类似枚举(列出所有的情况)搜索。我们枚举所有的解,找到满足期望的解。为
了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我
们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就
回退到上一个岔路口,另选一种走法继续走。
N皇后问题
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
package com.wuzx.algorithmicThinking.back;
/**
* 回溯算法:N皇后问题
*/
public class NQueens {
int QUEEN
S;
int[] result; // 下标表示行,值表示queen存储在哪一列
/**
* 构建n皇后哦
*
* @param n
*/
public NQueens(int n) {
this.QUEENS = n;
this.result = new int[n];
}
/**
* 在对应的行放置 皇后
*
* @param row
*/
public void setQueens(int row) {
if (row == QUEENS) {
printQueens();
return;
}
// 在每行一次放置列
for (int col = 0; col < QUEENS; col++) {
if (!isOk(row, col)) {
continue;
}
//设置列
result[row] = col;
// 开始下一行
setQueens(row + 1);
}
}
public boolean isOk(int row, int col) {
int leftup = col - 1;
int rightup = col + 1;
// 逐行往上考察每一行
for (int i = row - 1; i >= 0; i--) { //列上存在queen
if (result[i] == col) return false; //左上对角线存在queen
if (leftup >= 0) {
if (result[i] == leftup) return false;
}
//右下对角线存在queen
if (rightup < QUEENS) {
if (result[i] == rightup) return false;
}
leftup--;
rightup++; }
return true;
}
/**
* 打印输出 */
private void printQueens() {
for (int i = 0; i < QUEENS; i++) {
for (int j = 0; j < QUEENS; j++) {
if (result[i] == j) {
System.out.print("Q| ");
}
else {
System.out.print("*| ");
}
}
System.out.println();
}
System.out.println("-----------------------");
}
public static void main(String[] args) {
NQueens queens=new NQueens(8);
queens.setQueens(0);
}
}
使用场景
- 时间复杂度
- N皇后问题的时间复杂度为: 实际为n! 实际是n!/2
- 优缺点
- 优点:回溯算法的思想非常简单,大部分情况下,都是用来解决广义的搜索问题,也就是,从一组可能的解
中,选择出一个满足要求的解。回溯算法非常适合用递归来实现,在实现的过程中,剪枝操作是提高回
溯效率的一种技巧。利用剪枝,我们并不需要穷举搜索所有的情况,从而提高搜索效率 - 效率相对于低(动态规划)
- 优点:回溯算法的思想非常简单,大部分情况下,都是用来解决广义的搜索问题,也就是,从一组可能的解
四、动态规划
4.1 概念
动态规划(Dynamic Programming),是一种分阶段求解的方法。动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。
动态规划中有三个重要概念:
- 最优子结构
- 边界
- 状态转移公式(递推方程)dp方程
4.2 经典问题
通过上边的递归树可以看出在树的每层和上层都有大量的重复计算,可以把计算结果存起来,下次再用
的时候就不用再计算了,这种方式叫记忆搜索,也叫做备忘录模式
/**
* 斐波那契数列: 递归分治+记忆搜索(备忘录) */
public class Fib2 { //用于存储每次的计算结果
static int[] sub=new int[10]; public static int fib(int n){
if(n<=1) return n; //没有计算过则计算 if(sub[n]==0){
sub[n]=fib(n-1)+fib(n-2);
}
//计算过直接返回
return sub[n];
}
public static void main(String[] args) {
System.out.println(fib(9));
} }
dp方程:
最优子结构: fib[9]=finb[8]+fib[7]
边界:a[0]=0; a[1]=1;
dp方程:fib[n]=fib[n-1]+fib[n-2]
/**
* 斐波那契数列 自底向上 递推 */
public class Fib3 {
public static int fib(int n){
int a[]=new int[n+1];
a[0]=0;
a[1]=1;
int i;
// a[n]= a[n-1]+a[n-2]
for(i=2;i<=n;i++){
a[i]=a[i-1]+a[i-2];
}
// i已经加1了,所以要减掉1 return a[i-1];
}
public static void main(String[] args) {
System.out.println(fib(9));
} }
使用动态规划四个步骤
- 把当前的复杂问题转化成一个个简单的子问题(分治)
- 寻找子问题的最优解法(最优子结构)
- 把子问题的解合并,存储中间状态
- 递归+记忆搜索或自底而上的形成递推方程(dp方程)
时间复杂度
- 新的斐波那契数列实现时间复杂度为O(n)
优缺点
- 优点:时间复杂度和空间复杂度都相当较低
- 缺点:难,有些场景不适用
适用场景
尽管动态规划比回溯算法高效,但是,并不是所有问题,都可以用动态规划来解决。能用动态规划解决
的问题,需要满足三个特征,最优子结构、无后效性和重复子问题。在重复子问题这一点上,动态规划
和分治算法的区分非常明显。分治算法要求分割成的子问题,不能有重复子问题,而动态规划正好相
反,动态规划之所以高效,就是因为回溯算法实现中存在大量的重复子问题
标签:思维,goodsList,return,int,问题,算法,数据结构,public 来源: https://www.cnblogs.com/wuzhixuan/p/16184194.html