最近在刷算法题,在做题的时候也遇到了很多问题,发现好多不会的题其实用的都是一种算法思想,于是打算先整理下常用的算法思想,让自己在做题时思路更加清晰。这段时间也看了很多大佬的博客,最后简单整理了这篇文章,希望对和我一样有疑惑的朋友们有帮助。
狭义的来讲,算法可看作是数据传递和处理的顺序、方法和组成方式,就像是各种排序算法等。而广义的来讲,算法更像是一种事物运行的逻辑和规则。
常见算法思想有:枚举、递推、递归、分治、动态规划、贪心、回溯
一、枚举
也被称为穷尽列举,一般先确定好「可能解」,再进行条件范围筛选,最后验证。
二、递推
递推思想的核心就是从已知条件出发,逐步推算出问题的解。 在代码中通常通过迭代来实现。
案例说明
斐波那契数列(求第n个斐波那契数列的值)
private static long fab_iteration(int n) {
if (n ==1 ||n == 2) {
return 1;
}
else {
long f1 = 1l;
long f2 = 1l;
long f3 = 0;
for ( int i = 0; i < n-2; i++) {
f3 = f1 + f2;//利用变量的原值推算出变量的一个新值
f1 = f2;
f2 = f3;
}
return f3;
}
}
三、递归
递归中一定有迭代,但是迭代中不一定有递归,大部分可以相互转换。
在代码中一般能用迭代的不用递归,递归调用函数,浪费空间,并且递归太深容易造成堆栈的溢出。
案例说明
斐波那契数列(求第n个斐波那契数列的值)
private static int fab(int n) {
if (n == 1 || n == 2) {
return 1;
}
else {
return fab(n - 1) + fab(n - 2);//递归求值
}
}
四、分治
分治算法的核心步骤就是两步,一是分,二是治。因此分治的算法思想主要包括两个维度的处理:一是自顶向下,将主要问题划分逐层级划分为子问题;二是自底向上,将子问题的解逐层递增融入主问题的求解中。
分治思想最重要的一点是分解出的子问题是相互独立且结构特征相同的。
案例说明
归并排序就是通过分治的算法思想实现的。
public static void main(String[] args) {
int arr[] = {6,1,7,2,4,3,8,5};
//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
int []temp = new int[arr.length];
sort(arr,0,arr.length-1,temp);
System.out.println(Arrays.toString(arr));
}
private static void sort(int[] arr,int left,int right,int []temp){
if(left<right){
int mid = left +(right-left)/2;
sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
}
}
private static void merge(int[] arr,int left,int mid,int right,int[] temp){
int i = left;//左序列指针
int j = mid+1;//右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while(i<=mid){//将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while(j<=right){//将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
//将temp中的元素全部拷贝到原数组中
while(left <= right){
arr[left++] = temp[t++];
}
}
五、动态规划
动态规划和分治有一点相似,都需要拆分子问题,但与分治的思想不同的是,动态规划常常适用于有重叠子问题和最优子结构性质的问题。
动态规划通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。核心思想:拆分子问题和记住过往,减少重复计算。
动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。
1. 案例说明
leetcode的经典题:爬楼梯
一次可以爬1级台阶,也可以爬2级台阶。求爬上 n级的台阶总共有多少种方法。
对于这一题,我们可以分析:
- 最优子结构:f(n)
- 状态转移方程:f(n)= f(n-1)+f(n-2)
- 边界:f(1) = 1, f(2) = 2
- 重叠子问题:f(4)= f(2)+f(3),f(3) = f(1) + f(2),其中 f(2)就是重叠子问题。
public int climbStairs(int n) {
if(n<3){
return n;
}
int[] dp = new int[n];
dp[0]=1; dp[1]=2;
for(int i=2; i<n; i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n-1];
}
2. 动态规划的解题套路
如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等。
解题思路:穷举分析\ 确定边界\ 找出规律,确定最优子结构\ 写出状态转移方程。
六、贪心算法
贪心算法在执行的过程中,每一次都会选择最大的收益,但是总收益却不一定最大。
因此贪心算法的一般解题步骤为:
- 将问题分解为若干个子问题
- 找出适合的贪心策略
- 求解每一个子问题的最优解
- 将局部最优解堆叠成全局最优解
案例说明
leetcode问题:分发饼干
给你的孩子们分发饼干,每个孩子有满足胃口的饼干的最小尺寸,要尽可能满足最多数量的孩子。
public int findContentChildren(int[] g, int[] s) {
int count = 0;
Arrays.sort(g);
Arrays.sort(s);
int j = 0;
for(int i=0;i<s.length;i++){
if(j >= g.length){
break;
}
if(s[i]>=g[j]){
count++;
j++;
}
}
return count;
}
七、回溯
在做出下一步选择之前,先对每一种可能进行试探;只有当可能性存在时才会向前迈进,倘若所有选择都不可能,那么则向后退回原来的位置,重新选择。通常通过递归来实现。
案例说明
leetcode经典问题:组合总和 II
找出所有可以使数字和为目标值的组合。
class Solution {
List<List<Integer>> List = new ArrayList<>();
List<Integer> list = new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
A(candidates,target,0);
return List;
}
public void A(int[] candidates, int target, int k){
if(target == 0){
List.add(new ArrayList<Integer>(list));
return;
}
if( k == candidates.length || target < candidates[k]){
return;
}
int count = 1;
while(k+count<candidates.length && candidates[k] == candidates[k+count]){
count++;
}
A(candidates,target,k+count);
for(int i=1; i<=count; i++){
list.add(candidates[k]);
A(candidates,target-i*candidates[k],k+count);
}
// 每次回溯结束把加进去的数据清除
for(int i=1; i<=count; i++){
list.remove(list.size()-1);
}
}
}