当前位置 博文首页 > 灰小猿的博客:天天肝大厂面试题?这几个面试必考算法你掌握了吗

    灰小猿的博客:天天肝大厂面试题?这几个面试必考算法你掌握了吗

    作者:[db:作者] 时间:2021-07-17 13:08

    目录

    一、递归法

    二、贪心法

    三、回溯法

    四、分治法

    五、动态规划法


    Hello。你好呀,我是灰小猿,一个超会写bug的程序猿!

    时隔好几天,终于更新了,最近看了很多大厂面试题和相关要求,其中关于常用算法的考察几乎是必须的,但是对于常见算法的学习,只单单的记住某几个程序肯定是不可以的,这就需要深入的对算法的定义、思想、原理及解题上下功夫。

    今天就来和大家逐个深入剖析一下常见算法的基本定义、思想、原理及解题方法,看完别忘了评论见解,一键三连!

    ?

    一、递归法

    算法定义

    递归法是指一个过程或函数在其定义或说明中直接或间接调用自身的一种方法。在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口,在递归调用的过程当中系统为每一层的返回点、局部变量等开辟了栈来存储。

    算法原理

    递归的能力在于用有限的语言来定义对象的无限集合,一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进,当边界条件满足时递归返回,

    解题步骤

    递归思想是一种典型的通过逆向思维求解问题的方法,其解题过程主要分为两个步骤:

    1、分析递归关系。得出递归式。

    2、确定终止条件,防止出现死循环,

    特征和典型的应用场合

    递归次数过多容易造成栈溢出,所以在使用递归算法时应该考虑问题的规模。它有两个常见的应用场景。

    • 场合一:数据的定义是按递归定义的(Fibonacci函数)

    • 场合二:数据的结构形式是按照递归定义的,(树的遍历、图的搜索)

    常见应用案例

    递归求一个数的阶乘,先求比它小的一个数的阶乘,再与该数相乘。

    package 典型算法题;
    
    public class 递归求阶乘 {
    
    	public static void main(String[] args) {
    		System.out.println(f(3));
    	}
    
    	static int f(int n){
    		if(n==1)   //当n=1的时候,条件终止
    		return 1;
    		else 
    		return n*f(n-1);   //求n-1的阶乘,再次缩小变为n-1*n-2,n就会越来越小
    	}   
    
    }
    

    ?

    二、贪心法

    算法定义

    贪心法是一种不追求最优解,只希望得到较为满意解的方法、贪心法常以当前情况为基础作最优选择,而不考虑各种可能的整体情况,所以贪心法不需要回溯

    算法思想

    贪心法通常以自顶向下的方式进行,分阶段工作,以迭代的方式作出相继的贪心选择,每做一次贪心选择就将所求问题简化为规模更小的子问题。在每一个阶段,总是选择认为当前最好的方案,然后从小的方案推广到大的方案的解决办法,它只需要随着过程的进行保持当前最好的方案,采用‘有好处就先占着’的贪心者的策略。

    解题步骤

    其解题过程主要分为三个步骤

    1. 从问题的某一初始解出发,循环求解;

    2. 求出可行解的一个解元素

    3. 由所有解元素组合成问题的一个可行解

    特征和典型的应用场合

    这类问题一般具有两个重要的性质,分别是贪心选择性质和最优子结构性质,贪心法的典型应用有背包问题,活动安排问题等。

    常见应用案例

    一般背包问题中,物品是可拆的,即可以分成任意部分进行装载,而最终实现的目标是,背包是满的(即剩余容量为0),且总价值尽可能高。

    package 典型算法题;
    
    import java.util.Arrays;
    
    //背包问题(贪心算法)
    public class GreedyPackage {
    
      private int MAX_WEIGHT = 150;
      private int[] weights = new int[]{35,30,60,50,40,10,25};
      private int[] values = new int[]{10,40,30,50,35,40,30};
    
      private void packageGreedy(int capacity,int[] weights,int[] values){
              int n = weights.length;
              double[] r = new double[n]; //性价比数组
              int[] index = new int[n];   //按性价比排序物品的下标
              for(int i = 0;i < n;i++){
                  r[i] = (double) values[i] / weights[i];
                  index[i] = i;//默认排序
          }
    
          double temp = 0;    //对性价比进行排序
          for(int i = 0;i < n - 1;i++){
              for(int j = i + 1;j < n;j++){
                  if(r[i] < r[j]){
                      temp = r[i];
                      r[i] = r[j];
                      r[j] = temp;
                      int x = index[i];
                      index[i] = index[j];
                      index[j] = x;
                  }
              }
          }
    
          //将排序好的重量和价值分别存到数组中
          int[] w1 = new int[n];
          int[] value1 = new int[n];
    
          for(int i = 0;i < n;i++){
              w1[i] = weights[index[i]];
              value1[i] = values[index[i]];
    
          }
          int[] x = new int[n];
          int maxValue = 0;
          for(int i = 0;i < n;i++){
              if(w1[i] <= capacity){   //表明还可以装得下
                  x[i] = 1;   //表示该物品被装了
                  capacity = capacity - w1[i];
                  maxValue += value1[i];
              }
          }
          System.out.println("总共放下的物品数量:" + Arrays.toString(x));
          System.out.println("最大价值为:" + maxValue);
      }
    
    
      public static void main(String[] args){
          GreedyPackage g = new GreedyPackage();
          g.packageGreedy(g.MAX_WEIGHT,g.weights,g.values);
      }
    }

    ?

    三、回溯法

    算法定义

    回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态点称为“回溯点”。

    算法原理

    回溯法是一种满足某些约束条件的穷举搜索法,他要求设计者找出所有可能的方法,然后选择其中的一种方法,若该方法不可行,测试探下一种可能的方法。

    解题步骤,

    其解题过程主要分为三个步骤,

    1. 针对所给问题,定义问题的解空间;

    2. 确定易于搜索的解空间结构,

    3. 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索

    特征和典型的应用场景

    回溯是递归的一个特例。但它又有别与一般的递归法,用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根节点到当前扩展节点的路径。

    典型应用有:迷宫搜索、N皇后问题、骑士巡游、正则表达式匹配等。

    常见应用案例

    如一三年蓝桥杯省赛Java组真题“剪格子”就是采用典型的回溯法思想:

    package 一三年省赛真题;
     
    import java.util.Scanner;
     
    public class Year2013_t10 {
     
    	static int[][] g;
    	static int[][] sign;
    	static int m;
    	static int n;
    	static int s=0;	//记录格子中元素的总和
    	static int answer = Integer.MAX_VALUE;	//最终格子数
    	public static void main(String[] args) {
    		Scanner scanner = new Scanner(System.in);
    		m = scanner.nextInt();		//输入格子的宽
    		n = scanner.nextInt();		//输入格子的高
    		g = new int[n][m];	
    		sign = new int[n][m];
    		for (int i = 0; i < n; i++) {
    			for (int j = 0; j < m; j++) {
    				g[i][j] = scanner.nextInt();	//为格子赋值
    				s+=g[i][j];
    			}
    		}
    		move(0, 0, 0, 0);
    		System.out.println(answer);
    	}
    	
    	/**
    	 * 记录格子的遍历过程
    	 * @param i 移动的横坐标
    	 * @param j 移动的纵坐标
    	 * @param step 步数
    	 * @param sum 格子中元素的总和
    	 * */
    	public static void move(int i,int j,int step,int sum) {
    		//如果该格子坐标不在范围内,或该格子已经走过,则返回
    		if (i==n||i<0||j<0||j==m||sign[i][j]==1) {
    			return;
    		}
    		// 如果当前数值和是总和的一半
    		if (sum*2==s) {
    			answer = Math.min(answer, step);	//对格子数(步数)与符合要求的格子数比较,取出最小值
    		}
    		sign[i][j] = 1;		//对走过的格子进行标记,表示格子已经走过
    		move(i+1, j, step+1, sum+g[i][j]);	//down
    		move(i-1, j, step+1, sum+g[i][j]);	//up
    		move(i, j-1, step+1, sum+g[i][j]);	//left
    		move(i, j+1, step+1, sum+g[i][j]);	//right
    		sign[i][j] = 0;		//将该格子重新置于未走过的状态(回溯算法)
    		
    	}
     
    }

    ?

    四、分治法

    算法定义

    分治法是把一个复杂问题分成两个或很多的相同或相似的子问题,再把子问题分成更小的子问题。直到最后子问题可以简单的直接求解,而原问题的解就是子问题的解的合并。

    算法原理

    将一个规模较大的问题分解为若干规模较小的子问题,找出各子问题的解,然后把各子问题的解组合成整个问题的解。在求解子问题时,往往继续采用同样的策略进行,即继续分解问题,逐个求解,最后合并解,这种不断用同样的策略求解规模较小的子问题,在程序设计语言实现时往往采用递归调用的方式实现,

    解题步骤

    分治法的过程主要分为三个步骤,

    1. 分解,将原问题分解为若干规模较小,相互独立,与原问题形式相同的子问题;

    2. 解决。若子问题规模较小而容易被解决则直接解,否则递归的解各个子问题;

    3. 合并。将各个子问题的解合并为原问题的解。

    特征和典型的应用场合

    问题规模缩小到一定程度就可以容易的解决,可以分解为若干个规模较小的相同问题。利用该问题分解出的子问题的解可以合并为该问题的解,该问题所分解出的各个子问题是相互独立的。

    常见应用案例

    例如快速排序,从数组a[]中找出第k小的元素。

    分治法详解案例

    package 一八年省赛真题;
     
    import java.util.Random;
     
     
    public class Year2018_Bt5 {
     
    	public static int quickSelect(int a[],int l,int r,int k) {
    		Random rand = new Random();
    		int p = rand.nextInt(r-l+1) + l;
    		int x = a[p];
    		int tmp = a[p]; a[p] = a[r]; a[r] = tmp;
    		int i = l, j = r;
    		while (i<j) {
    			while (i<j&&a[i]<x) i++;
    			if (i<j) {
    				a[j] = a[i];
    				j--;
    			}
    			while (i<j&&a[j]>x) j--;
    			if (i<j) {
    				a[i] = a[j];
    				i++;
    			}
    			
    		}
    		
    		a[i] = x;
    		p = i;
    		if(i - l + 1 == k) return a[i];
    		if(i - l + 1 < k) return quickSelect(a, i + 1, r, k - i + l - 1);	//填空
    		else return quickSelect(a, l, i - 1, k);
    		
    	}
    	
    	public static void main(String[] args) {
    		int a[] = {1,4,2,8,5,7};
    		//1,2,4,5,7,8
    		//输出5
    		System.out.println(quickSelect(a, 0, 5, 6));
    	}
     
    }

    ?

    五、动态规划法

    算法定义

    动态规划法用于求解包含重叠子问题的最优化问题的方法,

    其基本思想是:将原问题分解为相似的子问题,在求解过程中通过子问题的解求出原问题的解,动态规划实际上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态。所以它的空间复杂度要大于其他算法。

    算法原理

    动态规划法通常以自底向上的方式求解各个子问题,分多阶段进行决策,其基本思想是,按时空特点将复杂问题划分为相互联系的若干个阶段,在选定系统行进方向后,逆着这个行进方向,从终点向始点计算,逐次对每个阶段寻找某种决策,使整个过程达到最优,故又称为逆序决策过程,

    解题步骤

    其解题过程主要分为两个步骤,

    1. 划分阶段,按照问题的时间或空间特征,把问题分为若干个阶段,注意这若干阶段,一定要有顺序或者可排序的(即无后向性);

    2. 选择状态,将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来,

    特征和典型的应用场合

    不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略、将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。最后,考虑采用动态规划的关键在于解决冗余。

    常见应用案例

    动态规划法的一个典型案例是求解字符串的最大公共子串

    最大公共子串长度问题就是:求两个串的所有子串中能够匹配上的最大长度是多少.比如: "abcdkkk" 和"baabcdadabc",可以找到的最长的公共子串是"abcd",所以最大公共子串长度为4.

    “最大公共子串”问题详细分析

    public class Year2017_Bt6 {
    	
    	static int f(String s1,String s2) {
    		char[] c1 = s1.toCharArray();
    		char[] c2 = s2.toCharArray();
    		
    		int [][]a = new int[c1.length+1][c2.length+1];
    //		"abcdkkk" 和"baabcdadabc",
    		int max = 0;
    		//利用循环来进行动态规划,模拟矩阵中的匹配过程
    		for (int i = 1; i < a.length; i++) {
    			for (int j = 1; j < a[i].length; j++) {
    				//如果两个字符匹配成功
    				if (c1[i-1]==c2[j-1]) {
    					a[i][j] = a[i-1][j-1]+1;	//填空
    					//判断当前字符的长度是否大于已经匹配到的串的长度
    					if (a[i][j]>max) max=a[i][j];
    				}
    			}
    		}
    		return max;	//返回匹配到的最长的字符串
    	}
    	
    	public static void main(String[] args) {
    		int n = f("abcdkkk", "baabcdadabc");
    		System.out.println(n);
    	}
     
    }

    以上就是我们常见的五大基本算法的原理、应用和案例分析,相信你看完之后应该能对这几大算法有一个新的认识,建议收藏之后慢慢复习!

    有不懂的地方小伙伴们可以评论区留言提出!

    觉得不错记得三连一波哟!

    灰小猿带你一起写bug呀!

    cs