《剑指offer》算法题第十一天
今日题目:
- 滑动窗口的最大值
- 扑克牌中的顺子
- 圆圈中最后剩下的数字
- 求1+2+3+...+n
- 不用加减乘除做加法
- 构建乘积数组
今天的题目比较有意思,可以学到很多知识,包括第1题中的数据结构——双向队列,第3题约瑟夫环问题等。
1.滑动窗口的最大值
题目描述: 给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
思路:
比较直接的方法是维护一个窗口,然后对于每个窗口找出当前窗口的最大值。这种方法的时间复杂度为O(kn)其中k为窗口大小。
另外一种方法是,利用队列,在队列中放置有可能为最大值的对象,将当前的最大值放在队首,这样就可以在O(1)的时间内得到最大值,这需要用到双向队列。
代码如下:
1 /** 2 用一个双端队列,队列第一个位置保存当前窗口的最大值,当窗口滑动一次 3 1.判断当前最大值是否过期 4 2.新增加的值从队尾开始比较,把所有比他小的值丢掉 5 */ 6 import java.util.ArrayDeque; 7 import java.util.ArrayList; 8 public class Solution { 9 public ArrayList<Integer> maxInWindows(int [] num, int size) 10 { 11 ArrayList<Integer> res = new ArrayList(); 12 if(size == 0) return res; 13 14 ArrayDeque<Integer> queue = new ArrayDeque(); 15 int begin = 0; 16 for(int i = 0; i < num.length; i++){ 17 begin = i - size + 1; 18 if(queue.isEmpty()) 19 queue.add(i); 20 else if(begin > queue.peekFirst()) 21 queue.pollFirst(); 22 23 while(!queue.isEmpty() && num[queue.peekLast()] <= num[i]) 24 queue.pollLast(); 25 queue.add(i); 26 27 if(begin >= 0) 28 res.add(num[queue.peekFirst()]); 29 } 30 return res; 31 32 } 33 }
2. 扑克牌中的顺子
题目描述: 从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为0,可以看成任意数字。
思路:
比较直接,由于0可以代替任何数字,我们先把数组排序一下,然后计算0的个数,最后判断0的个数是否大于不连续的数字间隔数。
代码如下:
1 import java.util.*; 2 public class Solution { 3 public boolean isContinuous(int [] nums) { 4 if(nums.length == 0) 5 return false; 6 Arrays.sort(nums); 7 int count_zero = 0; 8 int count_no_con = 0; 9 for(int i = 0; i < nums.length-1; i++){ 10 if(nums[i] == 0) 11 count_zero++; 12 else if(nums[i+1]==nums[i]) 13 return false; 14 else{ 15 if(nums[i+1]-nums[i] > 1){ 16 count_no_con += (nums[i+1]-nums[i]-1); 17 } 18 } 19 } 20 return count_zero >= count_no_con; 21 } 22 }
3.圆圈中最后剩下的数字
题目描述: 0,1,2,...,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
思路:
这是有名的约瑟夫环问题,《剑指》中给出了两种解题方法:一种是用环形链表模拟圆圈;另一种是分析每次被删除的数字的规律并直接计算出圆圈中最后剩下的数字。
下面只贴出第二种算法,它是基于递归实现的,推导出递归方程的过程比较复杂,这边就不作阐述,感兴趣的朋友可参考:https://www.nowcoder.net/questionTerminal/f78a359491e64a50bce2d89cff857eb6
代码如下:
1 public class Solution { 2 public int LastRemaining_Solution(int n, int m) { 3 if(n == 0) return -1; 4 if(n == 1) return 0; 5 return (LastRemaining_Solution(n-1,m)+m)%n; 6 } 7 }
4. 求1+2+3+...+n
题目描述: 求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
思路:
1. 运用数学公式 (1+n)*n/2
2. 递归,递归出口利用且(&&)运算的短路特性。
代码如下:
1 //利用且(&&)运算的短路特性 2 public class Solution { 3 public int Sum_Solution(int n) { 4 int sum = n; 5 boolean ans = (n > 0) && (sum += Sum_Solution(n-1))>0; 6 return sum; 7 } 8 }
5. 不用加减乘除做加法
题目描述: 写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
思路:
利用位运算来模拟加法运算,先做不进位的加法(异或),再算出进位(与,左移一位),然后再做和与进位的加法。其实这是一个递归的思想。
代码如下:
1 public class Solution { 2 public int Add(int num1,int num2) { 3 int sum = 0; 4 int carry = 0; 5 do{ 6 sum = (num1 ^ num2); 7 carry = (num1 & num2) << 1; 8 num1 = sum; 9 num2 = carry; 10 }while(carry != 0); 11 return num1; 12 } 13 }
6. 构建乘积数组
题目描述: 给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。
思路:
一种比较直接的思路是用double loop来构建乘积数组,简单,但是时间复杂度为O(n2)。
另一种解决方法是将A[0]*A[1]*...*A[i-1]以及A[i+1]*...*A[n-1]分别存起来,最后再进行整合。
代码如下:
1 import java.util.ArrayList; 2 public class Solution { 3 public int[] multiply(int[] A) { 4 int n = A.length; 5 int[] left = new int[n]; 6 int[] right = new int[n]; 7 left[0] = 1; 8 for(int i = 1; i < n; i++){ 9 left[i] = left[i-1]*A[i-1]; 10 } 11 right[n-1] = 1; 12 for(int i = n-2; i >= 0; i--){ 13 right[i] = right[i+1]*A[i+1]; 14 } 15 int[] B = new int[n]; 16 for(int i = 0; i < n; i++){ 17 B[i] = 1; 18 B[i] = left[i]*right[i]; 19 } 20 return B; 21 } 22 }