数据结构-算法思维
2022/4/24 1:42:46
本文主要是介绍数据结构-算法思维,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、贪心算法
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)
优缺点
- 优点:时间复杂度和空间复杂度都相当较低
- 缺点:难,有些场景不适用
适用场景
尽管动态规划比回溯算法高效,但是,并不是所有问题,都可以用动态规划来解决。能用动态规划解决
的问题,需要满足三个特征,最优子结构、无后效性和重复子问题。在重复子问题这一点上,动态规划
和分治算法的区分非常明显。分治算法要求分割成的子问题,不能有重复子问题,而动态规划正好相
反,动态规划之所以高效,就是因为回溯算法实现中存在大量的重复子问题
这篇关于数据结构-算法思维的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-26JAVA语音识别项目资料的收集与应用
- 2024-11-26Java语音识别项目资料:入门级教程与实战指南
- 2024-11-26SpringAI:Java 开发的智能新利器
- 2024-11-26Java云原生资料:新手入门教程与实战指南
- 2024-11-26JAVA云原生资料入门教程
- 2024-11-26Mybatis官方生成器资料详解与应用教程
- 2024-11-26Mybatis一级缓存资料详解与实战教程
- 2024-11-26Mybatis一级缓存资料详解:新手快速入门
- 2024-11-26SpringBoot3+JDK17搭建后端资料详尽教程
- 2024-11-26Springboot单体架构搭建资料:新手入门教程