「左程云-算法与数据结构笔记」| P11 补充视频(下)

470 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第十二天,点击查看活动详情。

最近在看左神的数据结构与算法,考虑到视频讲的内容和给出的资料的PDF有些出入,不方便去复习,打算出一个左神的数据结构与算法的笔记系列供大家复习,同时也可以加深自己对于这些知识的掌握,该系列以视频的集数为分隔,今天是第九篇:P11|补充视频


五、纸牌问题

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸 牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A 和玩家B都绝顶聪明。请返回最后获胜者的分数。

【举例】 arr=[1,2,100,4]。

开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来 玩家 B可以拿走2或4,然后继续轮到玩家A...

如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继 续轮到玩家A...

玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1, 让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家 A拿走。玩家A会获胜, 分数为101。所以返回101。

arr=[1,100,2]。

开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜, 分数为100。所以返回100


实现思路

  • 此处可以理解为先手和后手:即此处的firstsecond函数
  • 其中first为先手,那么就可以选择拿左边和右边的纸牌。拿完之后,就作为后手,调用second函数
  • 如果现在有A、B两个玩家,其中A先手
    • 那么就是A调用first(arr, 0, arr.length - 1)
    • B调用second(arr, 0, arr.length - 1)
    • 返回二者的最大值
  • first
    • 如果A玩家拿最左边的元素,那么就是作为后手second(arr, 左索引+1, 右索引),本次获得的总点数就是:arr[左索引] + second(arr, 左索引+1, 右索引)
    • 如果A玩家拿最右边的元素,那么就是作为后手second(arr, 左索引, 右索引-1),本次获得的总点数就是:arr[右索引] + second(arr, 左索引, 右索引-1)
    • 取二者的最大值即可
  • second
    • A玩家拿完之后,就该B玩家,此时A玩家作为后手实际上等效于B玩家的先手
    • 因此只需要拿到B玩家更小的那个先手值即可
  • 然后两个函数相互递归调用,得到最终的数据值

实现代码

	public static int win1(int[] arr) {  
	   if (arr == null || arr.length == 0) {  
	      return 0;  
	   }  
	   return Math.max(first(arr, 0, arr.length - 1), second(arr, 0, arr.length - 1));  
	}  
	  
	public static int first(int[] arr, int i, int j) {  
	   if (i == j) {  
	      return arr[i];
	   }  
	   return Math.max(arr[i] + second(arr, i + 1, j), arr[j] + second(arr, i, j - 1));  
	}  
	  
	public static int second(int[] arr, int i, int j) {  
	   if (i == j) {  
	      return 0;  
	   }  
	   return Math.min(first(arr, i + 1, j), first(arr, i, j - 1));  
	}

六、逆序栈


题目:给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。 如何实现?


实现思路

  • 看到题目不能申请额外的数据结构,我是没有什么思路的,因此就直接看视频了
  • 这里左神使用了两个递归函数:getAndRemoveLastElementreverse
  • 这里分别画图解释两个递归函数的作用:
  1. getAndRemoveLastElement 函数:

image.png 2. reverse 函数

image.png

实现代码

	public static void reverse(Stack<Integer> stack) {  
	   if (stack.isEmpty()) {  
	      return;  
	   }  
	   int i = getAndRemoveLastElement(stack);  
	   reverse(stack);  
	   stack.push(i);  
	}  
	  
	public static int getAndRemoveLastElement(Stack<Integer> stack) {  
	   int result = stack.pop();  
	   if (stack.isEmpty()) {  
	      return result;  
	   } else {  
	      int last = getAndRemoveLastElement(stack);  
	      stack.push(result);  
	      return last;  
	   }  
	}

七、数字转字符串


题目:规定1和A对应、2和B对应、3和C对应...

那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和"AK"。 即可以理解为"11"和"1",以及"1"和"11"。

给定一个只有数字字符组成的字符串str,返回有多少种转化结果。

思考

  • 感觉这类题目类似于前面的子序列字符串全排列的问题:
    • 子序列是判断第一个字符是否使用、第二个字符是否使用……
    • 字符串全排列是判断第一个字符是用哪一个,第二个字符是用哪一个……
  • 而此处的数字转字符串可以理解为:选两个数字还是选一个数字,其中三位数没有数字可以对应,也没有0存在,因此只有两位数和一位数可以对应字符,同时要对两位数做校验,如果不行就代表没有这种可能
  • 那么base case就是:所有数字字符串遍历结束,或者两位数超过26
  • 这是我看到这个题目的实现思路,不妨看看老师的实现思路

实现思路

  • 看了左神的视频讲解后,首先纠正一个读题的错误观念:就是数字字符串中时能出现0的
  • 再者就是对于两位数的判断,左神是把数字分为三类:
    • 1开头的两位数,这类数就可以以两位数进行下一步递归
    • 2开头的两位数,这类数要做判断:如果第二位数小于7,就继续递归,否则不递归调用
    • 3-9 开头的数字,不进行组合数字递归,只是继续看下一个数
  • 其他的点与最开始的猜想基本一致

实现代码

	public static int number(String str) {  
	   if (str == null || str.length() == 0) {  
	      return 0;  
	   }  
	   return process(str.toCharArray(), 0);  
	}  
	  
	public static int process(char[] chs, int i) {  
	   if (i == chs.length) {  
	      return 1;  
	   }  
	   if (chs[i] == '0') {  
	      return 0;  
	   }  
	   if (chs[i] == '1') {  
	      int res = process(chs, i + 1);  
	      if (i + 1 < chs.length) {  
	         res += process(chs, i + 2);  
	      }  
	      return res;  
	   }  
	   if (chs[i] == '2') {  
	      int res = process(chs, i + 1);  
	      if (i + 1 < chs.length && (chs[i + 1] >= '0' && chs[i + 1] <= '6')) {  
	         res += process(chs, i + 2);  
	      }  
	      return res;  
	   }  
	   return process(chs, i + 1);  
	}

八、最大价值物品


题目:给定两个长度都为N的数组weights和values:weights[i]和values[i]分别代表 i号物品的重量和价值。

给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?

思考

  • 第一眼看上去有点像之前贪心算法中的会议安排最大项目利益的结合
  • 如果按照性价比来进行装,即 values[i]/weights[i] 作为选择的标准的话,就可能出现和会议安排一样的情况,由于装了这个而装不了原本可以装两个并且利益更多的物品
  • 如果按照价值来装,同样会出现上述的情况
  • 就贪心算法而言,没有找到合适的贪心策略
  • 这里是出现在递归这一章中,不过也没有合适的递归想法,想了一阵后就继续看了视频

实现思路

  • 看了左神的视频,发现这题可以说是一个暴力递归,点题了hhh
  • 其实就是不断的去做尝试:有点类似于子序列,第一个商品要还是不要,第二个商品要还是不要,这样就能列出所有的情况
  • 其中如果超过了能承担的重量就退出,否则就把下面二者做比较:
    • process1(weights, values, i + 1, alreadyweight, bag)
    • values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag))
    • 前者是不要当前的物品,后者是当前物品的价值加上下一层递归调用
    • 然后取二者的最大值就是物品价值的最优解

实现代码

	public static int maxValue1(int[] weights, int[] values, int bag) {  
	   return process1(weights, values, 0, 0, bag);  
	}  
	  
	public static int process1(int[] weights, int[] values, int i, int alreadyweight, int bag) {  
	   if (alreadyweight > bag) {  
	      return 0;  
	   }  
	   if (i == weights.length) {  
	      return 0;  
	   }  
	   return Math.max(  
	         process1(weights, values, i + 1, alreadyweight, bag)  
			,
	         values[i] + process1(weights, values, i + 1, alreadyweight + weights[i], bag));  
	}
  • 这里在看弹幕的时候发现了一个左神的问题:
  • 关于超重的判断,如果超重了我们不应该返回0,而是返回超重之前的值,也就是:
  • if(alreadyweight > bag) return -value[i-1]