数组问题向来是笔试与面试中最长出现的题目。其题型多变,涉及知识面广,从基础到高级数据结构均可涉及,这里总结下刷题常见的以及易错的题型。
常见基础题型
二分
对于数组最常见和基础的算法就是二分了,参考Rotated Sorted Array问题,以旋转数组为例,通过对这类题型的了解,能够很好掌握二分算法的套路与思想。特别注意边界条件以及退出条件的判断,常规二分算法的解法与套路可参考你真的会写二分查找吗?
另外,对于一些变形题也要特别注意甄别。
[leetcode]275.H-Index II
这里实际要比较的是citations[mid]与n - mid的差别,并进一步二分。具体如下:
class Solution: def hIndex(self, citations: List[int]) -> int: if not citations or len(citations) == 0: return 0 n = len(citations) lo ,hi = 0,n-1 while lo <= hi: mid = (lo + hi) // 2 if citations[mid] >= n - mid: hi = mid - 1 else: lo = mid + 1 return n - lo
[leetcode]378.Kth Smallest Element in a Sorted Matrix
将一维数组转换到了二维,仍然可以利用二分解决,注意的是这里要比较比target小的个数与k的大小。
class Solution: def kthSmallest(self, matrix: List[List[int]], k: int) -> int: lo, hi = matrix[0][0], matrix[-1][-1] while lo <= hi: mid = (lo + hi) >> 1 loc = self.countLower(matrix, mid) if loc < k: lo = mid + 1 else: hi = mid - 1 return lo def countLower(self, matrix, num): i, j = 0,len(matrix[0]) - 1 cnt = 0 while i < len(matrix) and j >= 0: if matrix[i][j] <= num: cnt += j + 1 i += 1 else: j -= 1 return cnt
计数排序
利用数组的index来作为数字本身的索引,把正数按照递增顺序依次放到数组中。即让A[0]=1, A[1]=2, A[2]=3, ..., 以寻找缺失值为例,最后如果哪个数组元素违反了A[i]=i+1即说明i+1就是我们要求的第一个缺失的正数。对于那些不在范围内的数字,我们可以直接跳过,比如说负数,0,或者超过数组长度的正数,这些都不会是我们的答案。这借鉴了计数排序的思想。
[leetcode]41.First Missing Positive
class Solution: def firstMissingPositive(self, A): n = len(A) for i in range(n): element = A[i] while 1<=element<=n and element != A[element-1] : A[element - 1], element = element,A[element-1] for i in range(n): if A[i] != i + 1: return i + 1 return n + 1
[leetcode]442.Find All Duplicates in an Array
这里与上面的差别只是在结果加入的不是i+1而是nums[i]而已。
class Solution: def findDuplicates(self, nums: List[int]) -> List[int]: res = [] for i in range(len(nums)): while nums[i]!=nums[nums[i]-1]: nums[nums[i]-1],nums[i] = nums[i],nums[nums[i]-1] for i in range(len(nums)): if nums[i]!=i+1: res.append(nums[i]) return res
桶排序
[leetcode]164.Maximum Gap
使用bucket存储数组中的每一个元素。每个bucket中的最大值-最小值就是我们最小的gap, 所以我们不用计算相邻的两个数,我们只要比较后一个bucket中的最小值和前一个bucket中的最大值相差多少,取最大相差的值就是最终的结果。注意考虑min和max两个边界值也要加进去。
class Solution: def maximumGap(self, nums: List[int]) -> int: if len(nums) <= 1 : return 0 minValue = 2**31-1 maxValue = -2**31 for num in nums: minValue = min(minValue, num) maxValue = max(maxValue, num) bucket_range = (maxValue - minValue) // len(nums) + 1 bucket_num = ((maxValue - minValue) // bucket_range) + 1 hashmapMax = {} hashmapMin = {} for i in range(len(nums)): bucket_id = (nums[i]-minValue) // bucket_range if not bucket_id in hashmapMax: hashmapMax[bucket_id] = nums[i] hashmapMin[bucket_id] = nums[i] else: hashmapMax[bucket_id] = max(hashmapMax[bucket_id],nums[i]) hashmapMin[bucket_id] = min(hashmapMin[bucket_id],nums[i]) prev = 0 res = 0 for i in range(1,bucket_num): if not i in hashmapMax: continue if not prev in hashmapMax: continue res = max(res, hashmapMin[i] - hashmapMax[prev]) prev = i return res
[leetcode]347.Top K Frequent Elements
统计出数组中元素的频次。接着,将数组中的元素按照出现频次进行分组,即出现频次为 i 的元素存放在第 i个桶。最后,从桶中逆序取出前 \(k\) 个元素。时间复杂度\(O(n)\),其中\(n\)表示数组的长度。
class Solution: def topKFrequent(self, nums: List[int], k: int) -> List[int]: res = [] data = collections.Counter(nums) bucket = [[] for i in range(len(nums)+1)] for key in data: bucket[data[key]].append(key) for i in reversed(range(len(bucket))): res += bucket[i] if len(res) >= k: break return res[:k]
双指针
[leetcode]11.Container With Most Water
使用两个指针,一个在开头,一个在数组末尾,构成行长度。在每步中,我们找到它们之间形成的区域,更新max并将指针指向另一端的指针。因为木桶原理,容积取决于行长度和最短高度的积,所以,两个端点高度较低的需要移动,因为高度较高的移动不可能大于原来的两端点积。这样,每次都是高度低的移动,直到两指针相邻。
class Solution: def maxArea(self, height: 'List[int]') -> 'int': if not height:return 0 start,end = 0,len(height)-1 res = 0 while start < end: res = max(res,min(height[start],height[end])*(end-start)) if height[start] < height[end]: start += 1 else: end -= 1 return res
[leetcode]15.3Sum
双指针,首先需要对数组排序。使用一个循环,对于每一个元素 S[i],令start = i+1, end = len-1 。若 s[i] + S[start] + S[end] == 0, 则为原问题的一个解。否则更新end或者start。另外, 需要注意判重的问题。
class Solution: def threeSum(self, nums: 'List[int]') -> 'List[List[int]]': res = [] nums.sort() for i in range(len(nums)-2): if i > 0 and nums[i] == nums[i-1]:continue start,end = i+1,len(nums)-1 while start<end: val = nums[i]+nums[start]+nums[end] if val > 0: end -= 1 elif val < 0: start += 1 else: res.append([nums[i],nums[start],nums[end]]) while start < end and nums[start] == nums[start+1]: start += 1 while start < end and nums[end] == nums[end-1]: end -= 1 start += 1 end -=1 return res
对于判断数组内三个数,四个数求和或者大小的关系,可以使用哈希或者双指针来优化时间复杂度。相似的题目还有:[leetcode]16.3Sum Closest;[leetcode]18.4Sum;[leetcode]611.Valid Triangle Number
[leetcode]26.Remove Duplicates from Sorted Array
使用一个指针\(i\)指向原数组需要判断的元素,一个指针\(j\)指向新数组新加入的元素。对于这类题,需要特别注意指针指向的位置。
class Solution(object): def removeDuplicates(self, nums): """ :type nums: List[int] :rtype: int """ if not nums:return newTail = 0 for i in range(len(nums)): if nums[i]!=nums[newTail]: newTail += 1 nums[newTail] = nums[i] return newTail+1
相似的题目还有:[leetcode]27.Remove Element;[leetcode]283.Move Zeroes
[leetcode]209.Minimum Size Subarray Sum
定义两个指针 \(start\) 和 \(i\),分别记录子数组的左右的边界位置,然后让 \(i\) 向右移,直到子数组和大于等于给定值或者 \(i\) 达到数组末尾,此时更新最短距离,并且将 \(start\) 像右移一位,然后再 sum 中减去移去的值,然后重复上面的步骤,直到 \(i\) 到达末尾,且 \(start\) 到达临界位置,即要么到达边界,要么再往右移动,和就会小于给定值。
class Solution: def minSubArrayLen(self, s: int, nums: List[int]) -> int: if not nums:return 0 start = cur_sum = 0 res = float('inf') for i in range(len(nums)): cur_sum += nums[i] while cur_sum >= s: res = min(res,i-start+1) cur_sum -= nums[start] start += 1 return res if res != float('inf') else 0
[leetcode]287.Find the Duplicate Number
给一个数组,数组中数字大小为1-n,求数组中的一个重复元素。这个解决的方法有二分法,循环链表法和交换元素法。这里重点讲述循环链表法,数组中也可以采用快慢指针的方法来寻找环,这便是一个典型例子。
class Solution: def findDuplicate(self, nums: List[int]) -> int: slow = 0 fast = 0 while True: slow = nums[slow] fast = nums[nums[fast]] if slow == fast: break fast = 0 while slow!=fast: slow = nums[slow] fast = nums[fast] return slow
剑指offer也有同样的题,但是题目有些许出入:
在一个长度为\(n\)的数组里的所有数字都在\(0\)到\(n-1\)的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
注意:数字的范围变成了\(0\)~\(n-1\),且数组可修改,二分法可以解决,循环链表法不行。因为循环链表从第\(0\)个元素开始且数组中的值从\(1\)开始,可以保证从第一个元素开始不会再回到第一个元素,不像这道题(比如\([1,0]\),该数组会形成:第一个元素->第二个元素->第一个元素......的死循环,但是其实并没有重复元素)
[leetcode]54.Spiral Matrix
对于一维数组有双指针,对于二维数组当然也有四指针。这是一道经典题,可以使用四指针的思想来解决。
class Solution: def spiralOrder(self, matrix: List[List[int]]) -> List[int]: if not matrix: return [] top,bottom,left,right = 0,len(matrix)-1,0,len(matrix[0])-1 res = [] while top <= bottom and left <= right: for j in range(left,right+1): res.append(matrix[top][j]) for i in range(top+1,bottom): res.append(matrix[i][right]) for j in reversed(range(left,right+1)): if top<bottom: res.append(matrix[bottom][j]) for i in reversed(range(top+1,bottom)): if left<right: res.append(matrix[i][left]) top,bottom,left,right = top+1,bottom-1,left+1,right-1 return res
哈希
为了降低时间复杂度,使用哈希来记录之前遍历的信息是常见的做法,数组问题中也大量出现此类方法。
[leetcode]1.Two Sum
leetcode第一题便是使用哈希来解决数组问题的方案。这里可以使用字典记录遇见的索引和值。
class Solution: def twoSum(self, nums, target): """ :type nums: List[int] :type target: int :rtype: List[int] """ dic = {} for i,n in enumerate(nums): if n in dic: return(dic[n],i) else: dic[target-n] = i
[leetcode]128.Longest Consecutive Sequence
使用一个哈希表存储连续数值的端点和对应的长度,这样如果新遍历的数左边或右边可以和已有的区间连上的话就可以对原有的区间进行扩张。这里使用的trick是,每次只要更新断点即可,不用更新所有的连续数值。
class Solution: def longestConsecutive(self, nums: List[int]) -> int: nums_set = set(nums) dic = {} res = 0 for num in nums_set: left = dic.get(num-1,0) right = dic.get(num+1,0) cur = left+right+1 dic[num-left] = cur dic[num+right] = cur res = max(res,cur) return res
[leetcode]149.Max Points on a Line
用到哈希表来记录斜率和共线点个数之间的映射,需要注意的是横轴相同以及纵轴相同的情况。
# Definition for a point. class Point(object): def __init__(self, a=0, b=0): self.x = a self.y = b class Solution(object): def maxPoints(self, points): """ :type points: List[Point] :rtype: int """ length = len(points) if length < 3: return length res = 0 for i in range(1,length): dic = {'inf': 0} samePointsNum = 1 for j in range(i): if points[i].x == points[j].x and points[i].y != points[j].y: dic['inf'] += 1 elif points[i].x != points[j].x: dx =points[i].x - points[j].x dy = points[i].y - points[j].y d = self.gcd(dx, dy) k = (dx/d,dy/d) if k not in dic: dic[k] = 1 else: dic[k] += 1 else: samePointsNum += 1 res = max(res, max(dic.values()) + samePointsNum) return res def gcd(self,p,q): if q==0:return p r = p%q return self.gcd(q,r)
[leetcode]220.Contains Duplicate III
维护一个大小为\(k\)的字典,其中key为⌊num/t⌋⌊num/t⌋,value为numnum,如果存在一个数\(x\)满足条件,那么这个数的key必然是{⌊num/t⌋−1,⌊num/t⌋,⌊num/t⌋+1⌊num/t⌋−1,⌊num/t⌋,⌊num/t⌋+1}三数之一;也就是说我们只需要验证key等于这三数对应的的value,与num的差的绝对值是否小于\(t\)。
class Solution: def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool: n=len(nums) if t==0 and n==len(set(nums)): return False dic = {} for i, v in enumerate(nums): # t == 0 is a special case where we only have to check the bucket # that v is in. key, offset = (v // t, 1) if t else (v, 0) for idx in range(key - offset, key + offset + 1): if idx in dic and abs(dic[idx] - nums[i]) <= t: return True dic[key] = v if i >= k: # Remove the bucket which is too far away.Beware of zero t. del dic[nums[i - k] // t if t else nums[i - k]] return False
[leetcode]523.Continuous Subarray Sum
求出从序列nums从0开始到end的子序列之和,记录其除以K的余数q,若有两个子序列余数相同,并且相应的end差值大于1,则说明所求子序列存在,否则便不存在。 所以,这里使用哈希要记录每一步的余数以及索引。另外要注意为0的情况。
class Solution: # O(N) 注意为0的情况 def checkSubarraySum(self, nums: List[int], k: int) -> bool: if len(nums)<2: return False dic = {0:-1} cur = 0 for i in range(len(nums)): cur += nums[i] r = cur if k: r = r%k if r in dic and i-dic[r]>=2: return True if r not in dic: dic[r] = i return False
[leetcode]525.Contiguous Array
使用的方法是求和+hashmap的方法,首先从头开始遍历,如果当前值是0就cur-1,否则就sum+1.这样如果得到了一个cur就知道在此之前出现了1的个数和0的个数的差值。因此,当后面该cur再次出现的时候,我们就知道了这个差值再次出现,也就是说,从第一次这个差值出现和第二次这个差值出现之间0和1的个数是一样多的。
#cur表示以每个数字结尾时1比0多的个数 class Solution: def findMaxLength(self, nums: List[int]) -> int: if not nums: return 0 dic = {0:-1} cur = res = 0 for i,n in enumerate(nums): if n == 1: cur += 1 else: cur -= 1 if cur in dic: res = max(res,i-dic[cur]) else: dic[cur] = i return res
与上一题类似,针对这类判断是否出现,或者最长满足条件的子序列,可以采用以某个数字结尾/开头怎么怎么样来遍历的判断。类似的题目还有[leetcode]560.Subarray Sum Equals K。
栈
在很多场景中,我们都需要用单调栈来解决数组中一个数组中右边第一个比他大/小的问题。
[leetcode]239.Sliding Window Maximum
这里使用的是双端队列,队列元素降序排序,队首元素为所求最大值。滑动窗口右移,若出现的元素比队首(最大元素)大,此时清空队列,并将最大值插入队列。若比当前值小,则插入尾部。每次窗口右移的时候需要判断当前的最大值是否在有效范围,若不在,则需要将其从队列中删除。
class Solution: def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: if not nums or k <= 0: return [] res = [] stack = [] for i in range(len(nums)): if stack and i>=stack[0]+k: stack.pop(0) while stack and nums[i]>=nums[stack[-1]]: stack.pop() stack.append(i) if i+1>=k: res.append(nums[stack[0]]) return res
[leetcode]300.Longest Increasing Subsequence
先建立一个空的dp数组,然后开始遍历原数组,对于每一个遍历到的数字,我们用二分查找法在dp数组找第一个不小于它的数字,如果这个数字不存在,那么直接在dp数组后面加上遍历到的数字,如果存在,则将这个数字更新为当前遍历到的数字,最后返回dp数字的长度即可,特别注意的是dp数组的值可能不是一个真实的LIS。时间复杂度可以达到\(O(NlogN)\)。
class Solution: def lengthOfLIS(self, nums: List[int]) -> int: stack = [] for num in nums: if not stack or num > stack[-1]: stack.append(num) else: lo = self.getlo(stack,num) stack[lo] = num return len(stack) #第一个不小于target的数 def getlo(self,nums,target): lo,hi = 0,len(nums)-1 while lo<=hi: mid = lo+(hi-lo)//2 if nums[mid] == target: return mid elif nums[mid] > target: hi = mid - 1 else: lo = mid + 1 return lo
[leetcode]456.132 Pattern
建一个栈来维持一个单调子序列,倒序扫描。
class Solution: def find132pattern(self, nums: List[int]) -> bool: if not nums: return False stack = [] er = -float('inf') for num in nums[::-1]: if num < er: return True #递减栈 while stack and num>stack[-1]: er = stack.pop() stack.append(num) return False
132不好找,231比较容易,这也是比较难想的一点。
分治递归
[leetcode]4.Median of Two Sorted Arrays
超级经典的分治算法,分奇数和偶数,得到数据规模变小的同类问题,递归解决
class Solution: def findMedianSortedArrays(self, nums1, nums2): """ :type nums1: List[int] :type nums2: List[int] :rtype: float """ L = len(nums1) + len(nums2) if L%2 ==1: return self.findkth(nums1,nums2,L//2) else: return((self.findkth(nums1,nums2,L//2)+self.findkth(nums1,nums2,L//2-1))/2) def findkth(self,nums1,nums2,k): if not nums1:return nums2[k] if not nums2:return nums1[k] ma = len(nums1)//2 mb = len(nums2)//2 ia = nums1[ma] ib = nums2[mb] if ma+mb < k: if ia > ib: return self.findkth(nums1,nums2[mb+1:],k-mb-1) else: return self.findkth(nums1[ma+1:],nums2,k-ma-1) else: if ia > ib: return self.findkth(nums1[:ma],nums2,k) else: return self.findkth(nums1,nums2[:mb],k)
[leetcode]315.Count of Smaller Numbers After Self
计算后面较小数字的个数。应用合并排序。如果我们对数组进行排序,那么对于某个特定的数据,其后面的逆序数等于在排序过程中需要移动到该数前面的个数。时间复杂度O(nlogn)。
class Solution: def countSmaller(self, nums): if not nums: return [] self.res = [0 for i in range(len(nums))] self.func(list(enumerate(nums)),0,len(nums)-1) return self.res def func(self,s,lo,hi): if hi <= lo:return mid = lo+(hi-lo)//2 self.func(s,lo,mid) self.func(s,mid+1,hi) self.merge(s,lo,mid,hi) def merge(self,a,lo,mid,hi): i,j = lo,mid+1 aux = a[:] for k in range(lo,hi+1): if i>mid: a[k] = aux[j] j+=1 elif j>hi: self.res[aux[i][0]] += j-mid-1 a[k] = aux[i] i+=1 elif aux[j][1] < aux[i][1]: a[k] = aux[j] j+=1 else: self.res[aux[i][0]] += j-mid-1 a[k] = aux[i] i+=1
[leetcode]327.Count of Range Sum
区间和计数。Merge Sort方法。构建一个sums数组,对它进行merge sort,在merge的过程中抽取解。
在合并sums[left:mid]和sums[mid+1:right]时,两个数组分别已经是有序的,因此可以使用两指针的方法,对于左数组中的每一个元素,在右数组中寻找rl、rr;
rl:对于左数组中的sums[i],右数组中第一个不满足sums[rl] - sums[i] < lower的位置;
rr:对于左数组中的sums[i],右数组中第一个不满足sums[rr] - sums[i] <= upper的位置;
那么rr-rl就是右数组元素减sums[i]在[lower,upper]中的个数。
由于两数组都是递增的,左数组向右移动的过程中,右数组的rl、rr指针也是向右移动的,不会回溯,因此合并部分的复杂度是O(n)。总体时间复杂度为O(nlogn)。
class Solution(object): def countRangeSum(self, nums, lower, upper): """ :type nums: List[int] :type lower: int :type upper: int :rtype: int """ if len(nums) == 0: return 0 sums = [0] self.res = 0 for i in range(len(nums)): sums.append(sums[i] + nums[i]) self.mergeSort(sums, 0, len(sums)-1, lower, upper) return self.res def mergeSort(self, sums, lo, hi, lower, upper): if hi - lo <= 0: return 0 mid = lo+(hi-lo) // 2 self.mergeSort(sums, lo, mid, lower, upper) self.mergeSort(sums, mid+1, hi, lower, upper) self.merge(sums, lo, mid , hi, lower, upper) def merge(self, sums, lo, mid , hi, lower, upper): rl = rr = mid+1 for i in range(lo, mid+1): while rl <= hi and sums[rl] - sums[i] < lower: rl += 1 while rr <= hi and sums[rr] - sums[i] <= upper: rr += 1 self.res += rr-rl aux = sums[:] i = lo j = mid + 1 for k in range(lo, hi+1): if i>mid: sums[k] = aux[j] j+=1 elif j>hi: sums[k] = aux[i] i+=1 elif aux[i] < aux[j]: sums[k] = aux[i] i+=1 else: sums[k] = aux[j] j+=1
DP、贪心
数组结合简单的dp或者贪心法也是常规操作了,常见的有最大子序列的和,最长递增子序列等。这里最长递增子序列也可以使用栈的方法。
[leetcode]55.Jump Game
配合贪心法和动态规划,使用一个步进指针,用一个上界指针。
每次遍历的时候,不停的更新上界指针的位置(也就是当前位置+当前可以跳到的位置),直到看你能遇到结尾吗?如果成功了,就范围true,没有就返回false
class Solution: def canJump(self, nums): """ :type nums: List[int] :rtype: bool """ reach = 0 i = 0 while(reach<len(nums) and i<=reach): reach = max(reach,i+nums[i]) i+=1 return reach>=len(nums)-1
[leetcode]45.Jump Game II
配合贪心法,用一个变量step来记录我们所走的步数,用一个变量reach来记录我们所能到达的索引。
class Solution: def jump(self, nums: 'List[int]') -> 'int': step = reach = max_num = 0 for i in range(len(nums)-1): max_num = max(max_num,i+nums[i]) if i==reach: step += 1 reach = max_num return step