一次遍历获取数组最大值和第二大值(最小值和第二小值)
前言
最近做了两道题,有了一点想法,记录一下
问题
问题一:递增的三元子序列
给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。
如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。
示例 1:
输入:nums = [1,2,3,4,5]
输出:true
解释:任何 i < j < k 的三元组都满足题意
示例 2:输入:nums = [5,4,3,2,1]
输出:false
解释:不存在满足题意的三元组
示例 3:输入:nums = [2,1,5,0,4,6]
输出:true
解释:三元组 (3, 4, 5) 满足题意,因为 nums[3] == 0 < nums[4] == 4 < nums[5] == 6
方法一:双向遍历
创建两个长度为 n 的数组 leftMin 和 rightMax,对于 0≤i<n,leftMin[i] 表示 nums[0] 到 nums[i] 中的最小值,rightMax[i] 表示 nums[i] 到 nums[n−1] 中的最大值。
得到数组 leftMin 和 rightMax 之后,遍历 1≤i<n−1 的每个下标 i,如果存在一个下标 i 满足 leftMin[i−1]<nums[i]<rightMax[i+1],则返回true,否则返回false。
此方法需要三次遍历,并且需要O(n)的时间复杂度
1 class Solution { 2 public boolean increasingTriplet(int[] nums) { 3 int n = nums.length; 4 if (n < 3) { 5 return false; 6 } 7 int[] leftMin = new int[n]; 8 leftMin[0] = nums[0]; 9 for (int i = 1; i < n; i++) { 10 leftMin[i] = Math.min(leftMin[i - 1], nums[i]); 11 } 12 int[] rightMax = new int[n]; 13 rightMax[n - 1] = nums[n - 1]; 14 for (int i = n - 2; i >= 0; i--) { 15 rightMax[i] = Math.max(rightMax[i + 1], nums[i]); 16 } 17 for (int i = 1; i < n - 1; i++) { 18 if (nums[i] > leftMin[i - 1] && nums[i] < rightMax[i + 1]) { 19 return true; 20 } 21 } 22 return false; 23 } 24 }
方法二:贪心
a 始终记录最小元素,b 为某个子序列里第二大的数。
接下来不断更新 a,同时保持 b 尽可能的小。
如果下一个元素比 b 大,说明找到了三元组。
1 class Solution: 2 def increasingTriplet(self, nums): 3 a = float("inf") 4 b = float("inf") 5 6 for i in nums: 7 if i <= a: 8 a = i 9 elif i <= b: 10 b = i 11 else: 12 return True 13 14 return False
问题二:至少是其他数字两倍的最大数
给你一个整数数组 nums ,其中总是存在 唯一的 一个最大整数 。
请你找出数组中的最大元素并检查它是否 至少是数组中每个其他数字的两倍 。如果是,则返回 最大元素的下标 ,否则返回 -1 。
示例 1:
输入:nums = [3,6,1,0]
输出:1
解释:6 是最大的整数,对于数组中的其他整数,6 大于数组中其他元素的两倍。6 的下标是 1 ,所以返回 1 。
示例 2:输入:nums = [1,2,3,4]
输出:-1
解释:4 没有超过 3 的两倍大,所以返回 -1 。
示例 3:输入:nums = [1]
输出:0
解释:因为不存在其他数字,所以认为现有数字 1 至少是其他数字的两倍。
除了遍历以外,很直观的一个想法,为什么我们要将最大值与所有元素进行比较呢?如果我们找到第二大的元素,将它的两倍的值与最大值进行比较,不就能证明最大值是否大于所有元素两倍的值吗?
正好上面那道题目的贪心就是记录数组中的最小值和次小值,我们换一下判断符号就可以了,于是,我写下如下代码:
1 class Solution: 2 def dominantIndex(self, nums: List[int]) -> int: 3 if len(nums) == 1: 4 return -1 5 6 a = float("-inf") 7 a_ind = -1 8 b = float("-inf") 9 10 11 for i, val in enumerate(nums): 12 if val > a: 13 a = val 14 a_ind = i 15 elif val >b: 16 b = val 17 18 19 if a>=2*b: 20 return a_ind 21 return -1
其中,a_ind是为了记录最大值的下标,但是,这个方法在遇到某些测试用例的时候是通不过的。
由于求最大值和次大值 与 求最小值和次小值 的逻辑是一样的,我们用求最小值和次小值来测试一下
验证求求最小值和次小值
借助题目一的贪心策略代码,如下:
class Solution: def getNum(self, nums): a = float("inf") b = float("inf") for i, val in enumerate(nums): if val <= a: a = val elif val <= b: b = val return a, b if __name__ == '__main__': s = Solution() nums = [1, 2, 7, 6] print(s.getNum(nums)) nums = [5, 6, 6, 7] print(s.getNum(nums)) nums = [5, 6, 1, 7] print(s.getNum(nums))
结果为:
>>>
(1, 2)
(5, 6)
(1, 6)
可以看出,在第三个测试用例中,最小值是1,但是次小值是6,结果是错的,原因在于,更新最小值的时候,没有把最小值更新给次小值,因此将核心代码加上那个一行赋值即可
1 class Solution: 2 def getNum(self, nums): 3 a = float("inf") 4 b = float("inf") 5 6 for i, val in enumerate(nums): 7 if val <= a: 8 b = a 9 a = val 10 elif val <= b: 11 b = val 12 13 return a, b
上述题目二的正确解法也是如此,加上一行赋值即可,代码如下:
1 class Solution: 2 def dominantIndex(self, nums: List[int]) -> int: 3 a = float("-inf") 4 a_ind = -1 5 b = float("-inf") 6 7 for i, val in enumerate(nums): 8 if val > a: 9 b = a 10 a = val 11 a_ind = i 12 elif val > b: 13 b = val 14 15 if a >= 2 * b: 16 return a_ind 17 return -1
这个问题解决了,另一个问题来了,为什么对于题目一,贪心解法没有在更新最小值时,将最小值赋值给次小值,为怎么结果是对的呢?
原因很简单,因为题目找的是有没有符合要求的序列是否存在,而不是具体的值。
尽管下标不满足要求,但不会对结果的true/false判断造成影响,因为if的顺序,次小值b其实记录了一个额外的信息:自己前面有一个比自己小的元素。
例如,对于序列 [5, 6, 7, 1] , 一开始a=5,b=6,b可以等于6就已经代表了其前面出现过比它小的数组,当a更新为1时,不影响,这时候如果找到了比b大的数字,就代表找到了递增子序列。