待归类
- 10.正则表达式匹配
实际上的目的不是为了让我用正则。。。偷鸡的时候发现个问题,不能直接if r.group()。因为如果匹配不到的话,re.search()返回的会是NoneType,直接.group会报错。import re class Solution: def isMatch(self, s: str, p: str) -> bool: r = re.search(p,s) if r and r.group() == s: return True else: return False
- 剑指 Offer 29. 顺时针打印矩阵
应该算是模拟吧,放个自己的呆逼写法。。。。想法是差不多的,就是左右,上下,右左,下上,一圈一圈打印。但我这个呆逼,边界条件找不好,会有几个字数字多打印。调了一下感觉不会调,于是直接加了个visted来保存一下。。。。
这种思路官方给的比较不错的写法class Solution: def spiralOrder(self, matrix: List[List[int]]) -> List[int]: out = [] visted = [[False]*len(matrix[0]) for i in range(len(matrix))] if matrix == []: return [] for i in range(len(matrix)//2+1): for j in range(i,len(matrix[0])-i): if not visted[i][j]: out.append(matrix[i][j]) visted[i][j] = True for k in range(i+1,len(matrix)-i-1): if not visted[k][j]: out.append(matrix[k][j]) visted[k][j] = True for h in range(len(matrix[0])-i-1,i-1,-1): if not visted[len(matrix)-1-i][h]: out.append(matrix[len(matrix)-1-i][h]) visted[len(matrix)-1-i][h] = True for z in range(len(matrix)-i-2,i,-1): if not visted[z][len(matrix[0])-1-j]: out.append(matrix[z][len(matrix[0])-1-j]) visted[z][len(matrix[0])-1-j] = True return out
看到的最离谱的写法。。。。学习了一下,zip()相当于压缩,可以把两个列表压缩成一个元组列表。zip(*matrix)是解压缩,相当于矩阵转置。之所以外层嵌套list是因为zip返回的是对象,需要转成list才可以用[::-1]。矩阵转置再把行颠倒顺序,其实是相当于逆时针旋转了90度。。。。这里要注意,zip函数使用后的结果是[(),(),()]的形式,及内部是元组。那么为什么元组可以直接加在res上呢?看了解释说是在Python的列表里面,对操作符=和+=做了重载;+表示连接,+=表示追加并拓展列表;列表+=操作符右边的操作数必须是一个iterable对象。而元组确实是一个可迭代对象,因此这样写不会报错。class Solution: def spiralOrder(self, matrix: List[List[int]]) -> List[int]: if not matrix or not matrix[0]: return list() rows, columns = len(matrix), len(matrix[0]) order = list() left, right, top, bottom = 0, columns - 1, 0, rows - 1 while left <= right and top <= bottom: for column in range(left, right + 1): order.append(matrix[top][column]) for row in range(top + 1, bottom + 1): order.append(matrix[row][right]) if left < right and top < bottom: for column in range(right - 1, left, -1): order.append(matrix[bottom][column]) for row in range(bottom, top, -1): order.append(matrix[row][left]) left, right, top, bottom = left + 1, right - 1, top + 1, bottom - 1 return order
class Solution: def spiralOrder(self, matrix: List[List[int]]) -> List[int]: res = [] while matrix: res += matrix.pop(0) matrix = list(zip(*matrix))[::-1] return res
- 剑指 Offer 30. 包含min函数的栈
这题要求O(1)的复杂度来返回最小值。原理就是用一个stack来保存所有的数,另一个辅助stack来保存最小值。在push的时候,如果新push的数比stackB的最后一个数要小,在push A的同时也要push B,这样保证B是降序排列的,且B的栈顶一直是最小的数。class MinStack: def __init__(self): """ initialize your data structure here. """ self.stackA,self.stackB = [], [] def push(self, x: int) -> None: self.stackA.append(x) if not self.stackB or x <= self.stackB[-1]: self.stackB.append(x) def pop(self) -> None: if self.stackA.pop() == self.stackB[-1]: self.stackB.pop() def top(self) -> int: return self.stackA[-1] def min(self) -> int: return self.stackB[-1]
- 剑指 Offer 56 - I. 数组中数字出现的次数
应该算是一道位运算的题目。这个题目需要用到的知识是,xor,异或运算,相同为0,不同为1。那么成对出现的数字异或之后一定为0,一个数与0异或还是他本身,又由于异或无序,所以如果一个数组里只有一个数没配对,整个数组做异或之后的结果就是那个没有配对的数字。这个题里有两个没配对的数字,就先对数组做异或,结果就是这两个数字的异或。再用一个变量m,找到这两个数字不同的第一位。然后根据这一位,将数组中的数字分为两组,分别做异或就可以找出这两个数字了。
掌握几个知识点,&同或操作,^异或操作,<<左移xxx位。class Solution: def singleNumbers(self, nums: List[int]) -> List[int]: z ,m, x, y= 0, 1, 0, 0 for num in nums: z ^= num while z & m == 0: m <<= 1 for num in nums: if num & m: x ^= num else: y ^= num return x,y
二叉树
- 104. 二叉树的最大深度
自己写的比较捞的dfs
看到的比较简洁的dfsclass Solution: maxdepth = 0 def maxDepth(self, root: TreeNode) -> int: def dfs(node,depth): if node == None: return if node.left == None and node.right == None: if depth > self.maxdepth: self.maxdepth = depth return depth if node.left != None: dfs(node.left,depth+1) if node.right != None: dfs(node.right,depth+1) dfs(root,1) return self.maxdepth
class Solution: def maxDepth(self, root: TreeNode) -> int: if root == None: return 0 else: max_left = self.maxDepth(root.left) max_right = self.maxDepth(root.right) return max(max_left,max_right)+1
链表
- 206.反转链表
两个指针pre和cur,把后一个指针指向的节点指向前一个节点,然后顺序移动就可以。
比如1->2->3->4 一次之后变成1<-2->3->4,再重复执行
python多元赋值骚操作class Solution: def reverseList(self, head: ListNode) -> ListNode: pre = None cur = head while cur != None: nextNode = cur.next cur.next = pre pre = cur cur = nextNode return pre
class Solution: def reverseList(self, head: ListNode) -> ListNode: pre = None cur = head while cur != None: pre,cur.next,cur = cur,pre,cur.next return pre
- 21.合并两个有序链表
新建一个哑节点,从这个节点向后重建一个新的链表。剩下的就是比较l1和l2两个链表当前的数字大小了。class Solution: def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: new = ListNode(-1) head = new while l1 != None and l2 !=None: if l1.val <= l2.val: new.next = l1 l1 = l1.next else: new.next = l2 l2 = l2.next new = new.next if l1 == None: new.next = l2 else: new.next = l1 return head.next
- 24. 两两交换链表中的节点
迭代法,对于每个节点,将它指向下一个节点的下一个节点,再将它的下一个节点指向它就可以了。class Solution: def swapPairs(self, head: ListNode) -> ListNode: start = ListNode(-1) start.next = head temp = start while temp.next != None and temp.next.next != None: node1 = temp.next node2 = temp.next.next temp.next = node2 node1.next = node2.next node2.next = node1 temp = node1 return start.next
- 160.相交链表
这题关键是怎么确定两个链表相交。假设两条链表相交,a+c+b = b+c+a。那么意味着两个链表同时从头开始走,走到尾部之后再从另一个链表的头部开始走。两个指针相遇的位置就是链表的交点。如果没有交点,那么指针指向的会刚好为None。class Solution: def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: pa = headA pb = headB while pa != pb: pa = pa.next if pa != None else headB pb = pb.next if pb != None else headA return pa
- 234. 回文链表
遍历一遍链表,把节点的值保存下来,而后对列表中保存的值做判断即可。class Solution: def isPalindrome(self, head: ListNode) -> bool: l = [] while head: l.append(head.val) head = head.next left = 0 right = len(l)-1 while left <= right: if l[left] != l[right]: return False left += 1 right -= 1 return True
- 83.删除排序链表中的重复元素
很简单,看下后一个和前一个一不一样,不一样就删除。class Solution: def deleteDuplicates(self, head: ListNode) -> ListNode: if head == None: return head cur = head while cur.next: if cur.val == cur.next.val: cur.next = cur.next.next else: cur = cur.next return head
- 19.删除链表的倒数第 N 个结点
两个指针,第一个先走n,然后两个一起走。当第一个指针走到尾部时第二个指针也就走到了倒数第n个节点的位置。class Solution: def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode: dummynode = ListNode(0) dummynode.next = head second = dummynode first = dummynode while n>0: first = first.next n -= 1 while first.next: first = first.next second = second.next second.next = second.next.next return dummynode.next
排序
- 快速排序
class Solution: def sortColors(self, nums: List[int]) -> None: """ Do not return anything, modify nums in-place instead. """ def quicksort(nums,l,r): if l + 1 >= r: return first,last = l,r-1 key = nums[first] while first < last: while first < last and key <= nums[last]:#判断条件有first<last是为了防止待排序列是有序的,导致越界。而后半部分有等号是因为,如果没有等号,有可能会出现死循环。比如当首和尾的数字相等时,假设等于1,key = num[first] = 1,num[last]=1,内部的第一个循环会跳过,然后交换,可以看到三个变量全都没有改变,而后进入第二个循环,依旧会跳过,然后交换,依旧没有改变。然后执行最外层的循环,依旧没有任何改变。因此会陷入死循环。 last -= 1 nums[first] = nums[last] while first < last and nums[first] <= key: first += 1 nums[last] = nums[first] nums[first] = key quicksort(nums,l,first)#左边排序 #这里要理解,实际上是递归的过程。会把区间不断二分,这两次递归会分别处理左区间和又区间 quicksort(nums,first+1,r)#右边排序 quicksort(nums, 0,len(nums)) return nums
贪心算法
- 135分发糖果
要点在于,要注意前向更新和后向更新是不一样的。第一轮,从左向右,右边一个孩子比左边一个分高,就比左边多一颗糖。第二轮不能再从左向右,用左边比右边了。而应该从右向左,左边一个孩子比右边一个孩子如果分高并且糖小于或等于,左边的孩子就在右边的基础上多一个。class Solution: def candy(self, ratings: List[int]) -> int: l = len(ratings) num = [1 for i in range(l)] for i in range(l-1): if ratings[i] < ratings[i+1]: num[i+1] = num[i] + 1 for i in range(l-1,0,-1): if ratings[i-1] > ratings[i] and num[i-1] <= num[i]: num[i-1] = num[i] +1 return sum(num)
这是因为,贪心算法更新要有基准。第一轮是以左边为基准,不断累积。第二轮需要与右边为基准,不断积累。这样才能保证不会顾此失彼,出现更新后某一个位置更大的情况。例如:1 3 2 1,正常来说糖数应该是 1 3 2 1,经过第一轮更新,糖数会变为1 2 1 1,第二轮以右边为基准更新,才能保证3和2两个孩子累计上糖数。否则如果还从左向右走,就会导致结果为1 2 2 1 - 605种花问题
做防御式编程,在左右两端加上0,这样就可以不需要在边界条件处特殊考虑。class Solution: def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool: count = 0 flowerbed = [0] + flowerbed + [0] for i in range(len(flowerbed)-2): if flowerbed[i:i+3] == [0,0,0]: count += 1 flowerbed[i+1] = 1 if count < n: return False else: return True
- 452用最少数量的箭引爆气球
class Solution: def findMinArrowShots(self, points: List[List[int]]) -> int: points = sorted(points, key = lambda x:x[1]) end = points[0][1] count = 0 for i in range(1, len(points)): if end < points[i][0]: end = points[i][1] else: count += 1 return len(points) - count
采用右端升序和左端升序其实理论上都可以,但是右端升序更简单。如果左端升序,不加判断的情况下,有可能将原本不能引爆的气球一起引爆。而右端升序则能保证一定能留下没交集的气球。例子就是[0, 9], [0, 6], [7, 8],详细解释:https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/solution/tu-jie-tan-tao-wei-shi-yao-yao-an-qu-jian-de-you-d/class Solution: def findMinArrowShots(self, points: List[List[int]]) -> int: points.sort(key = lambda x:x[1]) cur,count = points[0][1], 1 for i in points: if cur < i[0]: count += 1 cur = i[1] return count
- 435. 无重叠区间
将列表按照区间右边界升序排列好。每次保留右边界最小的不重叠区间,这样能给剩下的区间留下更多的空间。因为已经排序好了,所以不需要判断哪个区间右边界最小了。只需要顺着去找,如果发现没有重叠,当前的区间尾就更新一下。如果发现重叠就扔掉那个区间。
这题和引爆气球是同一个问题,只要改个边界条件,输出结果改一下就可以。但是还是没有太理解为什么可以互相转化。看到一种解释是,引爆气球需要的最少针数量其实就是最大不相交区间的数量(应该是吧,确实没太想明白),那对于除去最少的区间使得区间不重叠,其实就是所有的区间,减去最多不相交的区间。class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: intervals = sorted(intervals, key = lambda x:x[1]) count = 0 end = intervals[0][1] for i in range(1,len(intervals)): if intervals[i][0] >= end: end = intervals[i][1] else: count += 1 return count
- 华为4.14机考第三题
贪心算法,从后往前,每一步去找距离最远的基站。equ = [4,1,2,3,1,1,1,2,3,1,1] l = len(equ)-1 count = 0 while l: for i in range(l): if l - i <= equ[i]: min = i break count += 1 l = min print(count)
双指针
-
88合并两个有序数组
class Solution: def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: """ Do not return anything, modify nums1 in-place instead. """ pos = m+n-1 p1 = m-1 p2 = n-1 while pos >= 0: if p1 < 0: nums1[pos] = nums2[p2] p2 -= 1 elif p2 < 0: nums1[pos] = nums1[p1] p1 -= 1 else: if nums1[p1] >= nums2[p2]: nums1[pos] = nums1[p1] p1 -= 1 else: nums1[pos] = nums2[p2] p2 -= 1 pos -= 1
在第一个数组上从后往前,两个指针指在两个数组有数字的末尾。之后从后往前,两个比较大小,大的那一个放到数组1的末尾。要注意,这样并不会对第一个数组造成覆盖。因为事实上,最坏的情况把第二个数组中的内容都复制进去,也不会影响到第一个数组中的内容。
以上自己实现的代码有个问题就是,多考虑了情况。实际上只要特殊考虑对第二个数组进行复制的情况。因为一旦第二个数组的指针小于0,说明复制结束,第一个数组剩下的数字就应该在原地。
更更改后的代码:class Solution: def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: """ Do not return anything, modify nums1 in-place instead. """ pos = m+n-1 p1 = m-1 p2 = n-1 while p1 >= 0 and p2 >= 0:#这里两个指针小于0的情况都要捕获,假如认为p2在后面会判断,这里只要判断p1就可以,那会造成的结果是如果nums2的数字先排完,p2会越界到-1,会让p1指向的数字与nums2最大的数进行比较,导致结果出错。 if nums1[p1] >= nums2[p2]: nums1[pos] = nums1[p1] p1 -= 1 pos -= 1 else: nums1[pos] = nums2[p2] p2 -= 1 pos -= 1 while p2 >=0: nums1[pos] = nums2[p2] p2 -= 1 pos -= 1
-
142环形链表 II
暴力解法:class Solution: def detectCycle(self, head: ListNode) -> ListNode: found = [] while head: found.append(head) if head.next in found: return head.next head = head.next return None
思路比较好理解,就是把遍历过的节点保存下来,看看后面的节点有没有再指向前面节点的。代价是有额外的空间开销。
leetcode的链表定义要注意几点问题:- print出来的链表是这样的ListNode{val: 2, next: ListNode{val: 0, next: ListNode{val: -4, next: None}}} 虽然没太看懂这个套娃是怎么弄出来的,但是可以知道的是它的链表自定义出来的每个链表节点本身并不是一个数值。(幸亏python的列表保存啥类型都不影响,人生苦短,我学python)
- val返回的是当前链表节点的值,next返回的是链表的下一个节点。
双指针法(Floyd判圈法):
fast指针一次走2步,slow指针一次走1步。如果快指针走过了最后一个节点,说明没有环。如果有环,则快指针和慢指针最终会相遇。那么相遇的关系是什么呢?到相遇的时候,由于fast的速度是slow的两倍,因此有f = 2s。再可以由套圈的关系知道(f一定比s多走了n圈),f = s + nb。那么就可以知道s = nb。这个时候是两个指针第一次相遇的时候。考虑到如果一个指针恰好走到入环节点的位置,那么他走的距离一定是k = a + nb。由此可以知道,s再走a就可以到达入环的位置。那么a的值是什么呢?要计算出a的值,只需要把f指针移到初始位置,两个指针一起开始移动,每次移动的距离均为1,这样当两个指针再次相遇的时候,相遇位置就是a。
双指针代码实现:
关于判断条件看到一个比较有趣的写法:class Solution: def detectCycle(self, head: ListNode) -> ListNode: fast = head slow = head while True: if (fast == None) or (fast.next == None):#这里应该注意要先判断fast和fast.next。本来以为fast到None之后会一直指向None,但是实际发现,到了None之后,.next就会报错。实际应该是直接指出去了。因此这里要先做两重判断。 return None fast = fast.next.next slow = slow.next if fast == slow: fast = head break while True: if fast == slow:#这里先判断是为了防止a=0的情况 return fast fast = fast.next slow = slow.next
if not (fast and fast.next): return None
-
76最小覆盖子串
-
633平方数之和
双指针的思想,和两数之和类似。不过要注意,如果两个数的平方和是第三个数,那么说明两个数中最大的一个数一定不会大于第三个数的平方根。即,若 a 2 + b 2 = c a^2 + b^2= c a2+b2=c,且b>a,那么 b ≤ c b\leq \sqrt{c} b≤c。用这个条件可以极大减少算法复杂度。class Solution: def judgeSquareSum(self, c: int) -> bool: left = 0 right = int(c**0.5) + 1 while left <= right: if left**2 + right**2 == c: return True elif left**2 + right**2 < c: left += 1 else: right -= 1 return False
-
680验证回文字符串 II
一开始有个思路就是,双指针,一个从左向右一个从右向左。如果两个字符不相等,那么分情况进行处理,具体逻辑在leetcode上提交了,但是这样的话虽然可能开销比较少,但是逻辑实在是太麻烦了。
于是看到用python直接翻转字符串来确认是不是回文的思路。自己写了一下还是有用例不通过。以下是错误的代码。class Solution: def validPalindrome(self, s: str) -> bool: if s == s[::-1]: return True else: left = 0 right = len(s) - 1 while left < right: if s[left] != s[right]: if (s[left+1:right+1] == s[right:left:-1]) or (s[left:right] == s[right-1:left-1:-1]): return True else: return False else: left += 1 right -= 1 return True
原因如下:在left为0时,left-1会导致翻转出现问题。
看到比较好的解决方法是用lambda函数:class Solution: def validPalindrome(self, s: str) -> bool: isTarget = lambda x : x == x[::-1] #这里注意lambda函数的用法,赋给isTarget后,可以通过isTarget()调用。实际上相当于 (lambda x : x == x[::-1])(s). left = 0 right = len(s) - 1 if isTarget(s): return True else: while left < right:#字符串为奇数和偶数的情况都包括了,不需要判断什么left == right之类的。 if s[left] != s[right]: return isTarget(s[left+1:right+1]) or isTarget(s[left:right]) else: left += 1 right -= 1
-
524通过删除字母匹配到字典里最长单词
思路比较简单,逐个遍历dictionary内的字符串,与s做比较。比较的方法是用两个指针,一个指向s一个指向dictionary的某个元素,逐个比较。如果发现dictionary元素的指针能越过最后一个字符,那么说明这个元素可以由s删除某些字符得到。
注意: 字典序不是按照给的dictionary排序,具体定义自行百度。class Solution: def findLongestWord(self, s: str, dictionary: List[str]) -> str: maxs = "" dictionary = sorted(dictionary)#字典序 for i in dictionary: left = 0 right = 0 while left < len(s): if s[left] == i[right]: right += 1 if right == len(i): break left += 1 if right == len(i): if len(i) > len(maxs): maxs = i return maxs
-
69. x 的平方根
二分法: 要注意的问题是选择开闭区间。这里mid + 1 和mid - 1实际上就是mid既然已经知道不是了,就可以把mid跳过了。class Solution: def mySqrt(self, x: int) -> int: l, r, ans = 0, x, -1 while l <= r: mid = (l + r) // 2 if mid **2 <= x: ans = mid l = mid + 1 else: r = mid - 1 return ans
不理解的点在于: 对于下面这种写法,也可以得到正确答案没问题。但是为什么要在最后
return r
?尝试理解应该是因为,最后要么m直接输出了,如果m没输出,得到的答案应该是在l=r的时候。跳出循环之后l会+1,所以答案应该是r。个人感觉还是上头的写法更简洁。class Solution: def mySqrt(self, x: int) -> int: if x == 0: return 0 l = 1 r = x while l <= r: m = l + (r-l)//2 #这里这么写没有问题,和(l+r)/2是一样的 sqrt = x//m if m == sqrt: return m elif m > sqrt: r = m - 1 else: l = m + 1 return r
牛顿迭代法:
牛顿迭代法其实原理是泰勒展开。具体就是用直线去逼近函数。假设有一个函数为 f ( x ) f(x) f(x),要求 ( x , f ( x ) ) (x,f(x)) (x,f(x))使得 f ( x ) = 0 f(x) = 0 f(x)=0。用牛顿迭代法就是 x n = x n − 1 − f ( x n ) f ’ ( x ) x_n = x_{n-1} - \frac{f(x_n)}{f’(x)} xn=xn−1−f’(x)f(xn)。原理也很简单,由直线斜率求直线方程及零点,求出零点的横坐标,再对这个横坐标处的函数重复上述操作,直到误差达到可以接受的范围。
但是要注意,牛顿迭代法并不一定能收敛。同时,对于多个零点的情况也有可能出现问题。解决办法是加入一些先验知识,以及选取合适的初始值。例如:对于这道题目,选择初始值为x,就可以保证迭代是单调的,同时可以保证xi*xi一直可以大于x,判断表达式不会为负。class Solution: def mySqrt(self, x: int) -> int: if x == 0: return 0 else: xi = x while xi * xi - x > 0.1: xi = 0.5*(xi+x/xi) return int(xi)
class Solution: def mySqrt(self, x: int) -> int: if x == 0: return 0 else: xi = x while xi * xi - x > 0.1: xi = xi - (xi*xi-x)/2*xi return int(xi)
报出来的问题:
ValueError: cannot convert float NaN to integer return int(xi) Line 9 in mySqrt (Solution.py) ret = Solution().mySqrt(param_1) Line 32 in _driver (Solution.py) _driver() Line 43 in <module> (Solution.py)
如果xi*xi用xi**2,还会报越界问题。至今没有想明白这俩问题哪里来的。 -
34. 在排序数组中查找元素的第一个和最后一个位置
这个题目和前面的解决方法类似。这里要注意注释里的两个点。class Solution: def searchRange(self, nums: List[int], target: int) -> List[int]: pos = [-1,-1] nums = [0.1] + nums + [0.1]#防御编程,防止[1]的情况导致搜索越界 left = 1 right = len(nums)-2 while left <= right: m = (left+right)//2 if nums[m] == target: j = m while nums[j] == target: j -= 1 pos[0] = j+1-1#这里专门写开了,其实就是因为做了防御编程,所以整体往后了一个,因此输出答案需要减一 j = m while nums[j] == target: j += 1 pos[1] = j-1-1 break elif nums[m] > target: right = m - 1 else: left = m + 1 return pos
-
153. 寻找旋转排序数组中的最小值
这题吧,本来以为挺难的,仔细看了看题解,其实这个二分法的思路倒是简单。就是判断一下mid和right指向元素的大小关系。如果mid大于right,说明最小值在mid右边。如果mid小于right,说明最小值在mid左边(特殊情况就是完全有序)。那么其实写起来就非常容易了。但是主要的问题其实是在边界条件上,因为给的数组里数字不会重复,所以等于的情况可以就不用考虑了,扔在哪里都可以。主要是在最后返回什么以及中间迭代的过程。right不能写成mid-1,不然会错过最小值。这个没太懂到底是为什么。下面贴个别人的解释。个人觉得面试实在不行,直接return min(nums[left],nums[mid],nums[right])哈哈哈哈。class Solution: def findMin(self, nums: List[int]) -> int: left,right = 0,len(nums)-1 while left < right: mid = left + (right-left)//2 #这里注意,这样写的好处是不会越界(low+high)//2容易越界 if nums[mid] > nums[right]: left = mid+1 else: right = mid return nums[left]
贴个大佬的解释:
public static int findMin(int[] nums) { int len = nums.length; int low = 0; int high = len-1; // 二分查找 while(low < high){ // 取中间值 int mid = (high+low)/2; // 如果中间值小于最大值,则最大值减小 // 疑问:为什么 high = mid;而不是 high = mid-1; // 解答:{4,5,1,2,3},如果high=mid-1,则丢失了最小值1 if (nums[mid] < nums[high]) { high = mid; } else { // 如果中间值大于最大值,则最小值变大 // 疑问:为什么 low = mid+1;而不是 low = mid; // 解答:{4,5,6,1,2,3},nums[mid]=6,low=mid+1,刚好nums[low]=1 // 继续疑问:上边的解释太牵强了,难道没有可能low=mid+1,正好错过了最小值 // 继续解答:不会错过!!! 如果nums[mid]是最小值的话,则其一定小于nums[high],走if,就不会走else了 low = mid+1; } } // 疑问:为什么while的条件是low<high,而不是low<=high呢 // 解答:low<high,假如最后循环到{*,10,1,*}的这种情况时,nums[low]=10,nums[high]=1,nums[mid]=10,low=mid+1, // 直接可以跳出循环了,所以low<high,此时low指向的就是最小值的下标; // 如果low<=high的话,low=high,还会再不必要的循环一次,此时最后一次循环的时候会发生low==high==mid, // 则nums[mid]==nums[high],则会走一次else语句,则low=mid+1,此时low指向的是最小值的下一个下标, // 则需要return[low-1] return nums[low]; }
-
154. 寻找旋转排序数组中的最小值 II
和上面一个题类似,只不过这个题目有重复的数字,所以对于mid等于high的情况要特别讨论。当mid等于high时,我们不知道最小值到底在mid右边还是在mid左边,因此如果还是直接令high = mid有可能会错过最小值。处理方法也很简单,就是当high = mid时,这里再理解一个问题就是,为什么左边界忽略mid时是low = mid + 1而右边界忽略mid时是right = mid?原因是我们的判断条件是mid与right指向元素的大小关系,如果mid > high,那么mid一定不是最小值,所以mid可以跳过。但如果mid < high,我们并不知道mid到底是不是最小值,如果mid是最小值,那么high = mid - 1就会错过最小值。之所以返回numbers[low]是因为:考虑整个迭代过程,其实是不断缩小区间范围的过程。最后的临界状态一定会是low = high-1,由此我们可以知道,无论是最后结束是low = mid+1还是high = mid 还是high -= 1,指向最小值的一定是low。(这个可以结合判断条件来理解)class Solution: def minArray(self, numbers: List[int]) -> int: low, high = 0, len(numbers)-1 while low < high: mid = low + (high-low)//2 if numbers[mid] > numbers[high]: low = mid + 1 elif numbers[mid] == numbers[high]: high -= 1 else: high = mid return numbers[low]
动态规划
- 最大回文子序列
这个方法的重点是状态转移方程,如果两个字母相等,则他的状态和内部的状态是相同的。这里要注意这个方法遍历顺序,右边界不断向右推进,应该明白,此时前面的所有位置的状态都已经判断,不会出现内部状态没有更新的情况。class Solution: def longestPalindrome(self, s: str) -> str: dp = [[0]*len(s) for i in range(len(s))] m = 0 for right in range(len(s)): for left in range(right+1): #只有一个字母的时候也是回文 if s[left] == s[right]: if right - left <= 2: dp[left][right] = 1 else: dp[left][right] = dp[left+1][right-1] if dp[left][right]: if right-left+1 > m: m = right-left+1 out = s[left:right+1] return out
- 最长公共子串
效率不是很高,但是想法比较容易。就是只需要找到相等的字符,所在位置的长度在前面的基础上加1。注意处理i或j为0时的边界条件,防止越界。while True: try: s1, s2 = input(), input() if len(s2) < len(s1): tmp = s2 s2 = s1 s1 = tmp n1 = len(s1) n2 = len(s2) dp = [[0]*n2 for i in range(n1)] m = 0 for i in range(n1): for j in range(n2): if s1[i] == s2[j]: if i == 0 or j == 0: dp[i][j] = 1 else: dp[i][j] = dp[i-1][j-1]+1 if dp[i][j] > m: index = i m = dp[i][j] print(s1[index-m+1:index+1]) except: break
- 279.完全平方数
某一个数一定是由前一个数+1,+4,+9,+16,+…得来的。那么由此可以知道构成某个数的完全平方数的最小个数,等于它-1,-4,-9,-…的数中最小的个数+1。class Solution: def numSquares(self, n: int) -> int: dp = [0] for i in range(1,n+1): j = 1 dp.append(10000) while j*j <=i : dp[i] = min(dp[i],dp[i-j*j]+1) j += 1 return dp[-1]
- 139. 单词拆分
这题比较妙。。。整个序列是否可以拆分成几个单词,意味着整个序列尾部减少一个单词后,序列依旧可以拆分成几个单词。那么也就是说,每一步都可以将整个序列分为前后两半,后一半就是一个单词,前一半是前面完整的序列。如果前面可以拆分,后面的单词又在列表里,那么就说明当前这段序列可以拆分。
算法过程用i来遍历每个位置,也就是遍历每个拆分点。初始状态前一半为空,后一半的单词只要在列表里就为真。所以设置初始位置为0。这里因为dp数组每个位置代表前面的序列是可以拆分的,因此dp数组比字符串s要多一个。i以前为前面一段,j为后面一段的结束位置。这样就可以将当前遍历到的位置分为[0,i-1]和[i,j]。如果i位置为True,说明i以前的字符串可以拆分,那么如果s[i:j]表示的单词在列表中,[0,j]代表的序列就可以拆分。依次迭代下去,直到整个字符串遍历完成。class Solution: def wordBreak(self, s: str, wordDict: List[str]) -> bool: n = len(s) dp = [True] + [False]*n for i in range(n): for j in range(i+1,n+1): if dp[i] == True and s[i:j] in wordDict: dp[j] = True return dp[-1]
- 300. 最长递增子序列
正常动态规划思路,dp[i] = max(dp[i],dp[j]+1)。状态转移方程的意思是,对于某个位置来说,只要它大于前边的某个位置,以它为终点的最长递增子序列就是前边某位置的最长递增子序列长度+1。由于我们要的是最长递增子序列,因此对某个位置i,要找的是(0,i-1)中保证nums[i]大于nums[j]时最长的那一个。因此需要将当前的dp[i]和dp[j]+1做比较。迭代后保证是最长的。
自己写了一个奇奇怪怪的方法,也能ac。用i来控制位置,j表示该位置之后的所有位置。如果发现j指向的位置数字大于i,那么如果dp[i]+1比dp[j]要大,就更新。感觉有点像是前一种方法的逆向过程。前面一种方法是更新某个点,这种方法是更新某个点之后的所有点。实际上时间复杂度应该是差不多的,都是O(n^2)。class Solution: def lengthOfLIS(self, nums: List[int]) -> int: l = len(nums) dp = [1]*l for i in range(l): for j in range(i): if nums[i] > nums[j]: dp[i] = max(dp[i],dp[j] + 1) return max(dp)
class Solution: def lengthOfLIS(self, nums: List[int]) -> int: l = len(nums) dp = [1]*l for i in range(l): for j in range(i,l): if nums[j] > nums[i]: dp[j] = max(dp[j],dp[i] + 1) return max(dp)
- 1143. 最长公共子序列
感觉好像挺难的,其实找到状态转移方程之后实现就很简单了。这题是个二维动态规划问题。因为是不连续的,所以对于text1的每个位置,都要遍历text2,看一下公共的最长子序列是什么。首先要明确dp[i][j]表示的意思是,text1在i以前text2在j以前,这两个序列的最大公共子序列。那么就比较容易得到,当text1[i-1] =text2[j-1]时,dp[i][j]会比两个序列都减1时最大子序列的长度要增加1。那么当text1[i-1] 不等于text2[j-1]时,在当前位置最大的长度应该是两个子序列分别减1,长度最大的那一个。class Solution: def longestCommonSubsequence(self, text1: str, text2: str) -> int: l1, l2 = len(text1), len(text2) dp = [[0]*(l2+1) for i in range(l1+1)] for i in range(1,l1+1): for j in range(1,l2+1): if text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1]+1 else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) return dp[l1][l2]
- 爬楼梯
经典斐波那契数列,爬到每个台阶有两种可能,一种是跨过两个台阶到达,另一种是跨过一个台阶到达。所以状态转移方程为dp[i] = dp[i-1]+dp[i-2]class Solution: def climbStairs(self, n: int) -> int: if n<=2: return n dp = [0 for i in range(n)] dp[0] = 1 dp[1] = 2 for i in range(2,n): dp[i] = dp[i-1]+dp[i-2] return dp[n-1]
- 198打家劫舍
某一间房子是否抢劫,取决于相邻的房子。对于每一间房子,两种情况,一种是抢劫当前房子,一种是不抢。状态转移方程为dp[i] = max(dp[i-1],dp[i-2]+nums[i]),意思是,看当前这个房子抢劫拿到的多还是抢上一间房子拿到的多。个人感觉有点贪心算法的意思,每一步都是当前的最优,逐步推到全局最优。class Solution: def rob(self, nums: List[int]) -> int: if not nums: return 0 size = len(nums) if size == 1: return nums[0] dp = [0] * size dp[0] = nums[0] dp[1] = max(nums[0], nums[1]) for i in range(2, size): dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]) return dp[-1]
- 413.等差数列划分
这个题比较倾向于找规律了。重点有两个,一个是等差数列的判断条件,不用去想什么判断这个差值判断那个差值。。。就只需要三个数,两两之间差值相等就好了。另一个重点是等差子数列的数量,这个的判断其实是,数列数的个数每增加一个,子数列的个数会是原来的两倍再加1,比原来多出来的子数列个数是原来的子数列的个数加1。因此dp数组的每一位保存的实际是比原来增加的子数列的个数。这个的原理就是,比如原来的数列是1 2 3,当前的等差数列数量是1,对于1 2 3 4,可以想象成它是 1 2 3 4,是1 2 3子数列个数的基础上增加了2 3 4,加了1,然后再加上整体的一个1 2 3 4。再举个例子, 1 2 3 4 子数列的数量是3 那么 1 2 3 4 5就可以看成是 1 2 3 4 5,2 3 4 5的子数列个数和1 2 3 4一样,再加上一个整体,就是总的个数。class Solution: def numberOfArithmeticSlices(self, nums: List[int]) -> int: dp = [0 for i in range(len(nums))] for i in range(2,len(nums)): if nums[i]-nums[i-1] == nums[i-1] - nums[i-2]: dp[i] = dp[i-1] + 1 return sum(dp)
- 最小路径和
先贴一个深度优先搜索的代码吧,这个方法其实可以做出来,但是会超时。权当是熟悉一下dfs怎么写了。(这里有个问题,如果用count而不是path来记录最短路径,会直接报错,不知道为什么。)
动态规划:class Solution: def minPathSum(self, grid: List[List[int]]) -> int: path = [] out = [] def dfs(grid,i,j,path): if i == len(grid)-1 and j == len(grid[0])-1: path.append(grid[i][j]) out.append(path[:]) return path.append(grid[i][j]) if i != len(grid)-1: dfs(grid,i+1,j,path) path.pop() if j != len(grid[0])-1: dfs(grid,i,j+1,path) path.pop() dfs(grid,0,0,path) for i in range(len(out)): out[i] = sum(out[i]) return min(out)
这个方法的话也比较好理解,对于每个点,它要么是从左边一个点走来的,要么是从上一个点走过来的。那么就看看它是从左边走过来小还是右边走过来小就好了。状态转移方程为:dp[i][j] = min(left,up) + grid[i][j]class Solution: def minPathSum(self, grid: List[List[int]]) -> int: dp = [[0]*(len(grid[0])) for i in range(len(grid))] for i in range(len(grid)): for j in range(len(grid[0])): if i == 0 and j == 0: dp[i][j] = grid[i][j] else: left = float('inf') up = float('inf') if i>0: left = dp[i-1][j] if j>0: up = dp[i][j-1] dp[i][j] = min(left,up) + grid[i][j] return dp[-1][-1]
- 542. 01 矩阵
累了,,自己写的bfs超时。。。。。回头再看吧。 - 最大回文子序列
这个方法的重点是状态转移方程,如果两个字母相等,则他的状态和内部的状态是相同的。这里要注意这个方法遍历顺序,右边界不断向右推进,应该明白,此时前面的所有位置的状态都已经判断,不会出现内部状态没有更新的情况。class Solution: def longestPalindrome(self, s: str) -> str: dp = [[0]*len(s) for i in range(len(s))] m = 0 for right in range(len(s)): for left in range(right+1): #只有一个字母的时候也是回文 if s[left] == s[right]: if right - left <= 2: dp[left][right] = 1 else: dp[left][right] = dp[left+1][right-1] if dp[left][right]: if right-left+1 > m: m = right-left+1 out = s[left:right+1] return out
- 最长公共子串
效率不是很高,但是想法比较容易。就是只需要找到相等的字符,所在位置的长度在前面的基础上加1。注意处理i或j为0时的边界条件,防止越界。while True: try: s1, s2 = input(), input() if len(s2) < len(s1): tmp = s2 s2 = s1 s1 = tmp n1 = len(s1) n2 = len(s2) dp = [[0]*n2 for i in range(n1)] m = 0 for i in range(n1): for j in range(n2): if s1[i] == s2[j]: if i == 0 or j == 0: dp[i][j] = 1 else: dp[i][j] = dp[i-1][j-1]+1 if dp[i][j] > m: index = i m = dp[i][j] print(s1[index-m+1:index+1]) except: break
- 剑指 Offer 42. 连续子数组的最大和
动态规划,dp数组到每个位置表示0到当前位置形成的新数组的子数组中的最大和。有点绕。其实就是每个位置都表示当前位置之前所有连续数字和的最大值。那么考虑下一个位置,对于下一个位置而言,如果dp[i-1]小于0,那么下一个位置以前的最大和就是那个位置,就是nums[i];如果dp[i-1]大于0,那么下一个位置以前的最大和就是nums[i]+dp[i-1]。根据这个可以优化一下,其实可以直接把nums当成dp数组来压缩空间,就不写了,比较简单。class Solution: def maxSubArray(self, nums: List[int]) -> int: dp = [-200 for i in nums] for i in range(len(nums)): dp[i] = max(nums[i],nums[i]+dp[i-1]) return max(dp)
背包问题
关于初始化:
- 416. 分割等和子集
转化为0-1背包问题。首先排除个特殊情况,如果元素的和为奇数,那么一定不可能分割,直接返回False。当元素的和为奇数时,问题就可以转化为01背包问题。背包容量的上限就是目标数字。dp[i][j]表示[0,i]个元素是否能够选择其中的部分元素使其和为容量j。接下来考虑状态转移方程,当nums[i]等于j时,一定可以达到容量,此时dp[i][j]为True,这是特殊的边界条件。之后考虑,遇到每个元素,都有两种情况,这个元素放入或者不放入。当这个元素大于j时,一定不放入,那么dp[i]][j] = dp[i-1][j]。当这个元素小于j时,有放入和不放入两种情况,如果放入,它的状态取决于dp[i-1][j-nums[i]],如果不放入,它的状态取决于dp[i-1][j]。主要是在理解dp[i-1][j-nums[i]],这个的意思是如果不放这个元素的情况下,前面的元素可以构成j-该元素,那么放入之后,它就可以构成j。
好像上头写的有点问题,i如果从0开始会越界。。。这题估计是因为只有True和False,碰巧了。。。class Solution: def canPartition(self, nums: List[int]) -> bool: l = len(nums) if sum(nums)%2: return False target = sum(nums)//2 dp = [[False]*(target+1) for i in range(l)] for i in range(l): for j in range(target+1): if nums[i] == j: dp[i][j] = True elif nums[i] < j: dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]] else: dp[i][j] = dp[i-1][j] return dp[-1][-1]
改成下头这样就没问题了。但是关于为什么class Solution: def canPartition(self, nums: List[int]) -> bool: l = len(nums) if sum(nums)%2: return False target = sum(nums)//2 dp = [[False]*(target+1) for i in range(l+1)] for i in range(1,l+1): for j in range(target+1): #dp[i][j] = dp[i - 1][j] if nums[i-1] == j: dp[i][j] = True else: if nums[i-1] < j: dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i-1]] else: dp[i][j] = dp[i-1][j] return dp[-1][-1]
DFS和BFS
- 695. 岛屿的最大面积
思路是搜索连通区域,遍历每个点,从每个点开始,如果发现这个点是0,就继续搜索下一个点。当发现有一个点是1,开始搜索他的连通区域。这里有个要点就是要沉没岛屿,就是搜索过的岛屿要置为0。原因是搜索过之后,这个岛屿的面积就被计算了,下一次搜索或其他节点的搜索就不可以搜索这个岛屿。
这里的dfs体现在递归,每次都向每个点的四连通域搜索,找到的点会继续向下搜四连通域,直到找不到岛屿为止。class Solution: def maxAreaOfIsland(self, grid: List[List[int]]) -> int: ans = 0 def dfs(x,y): if 0<= x <len(grid) and 0<= y <len(grid[0]) and grid[x][y] == 1: grid[x][y] = 0 return 1+dfs(x-1,y)+dfs(x+1,y)+dfs(x,y-1)+dfs(x,y+1) else: return 0 for i in range(len(grid)): for j in range(len(grid[0])): ans = max(dfs(i,j),ans) return ans
- 46. 全排列
我只能理解最呆逼的解决办法。。。深度优先搜索+回溯。交换位置的写法实在是看不懂为啥。
深度优先+回溯就是,首先把第一个位置所有可能的数字列出来。然后对第一个位置的每个数字,依次向下搜索第二个位置可能的数字、第三个位置可能的数字…
深度优先就是先顺着一支向下搜索,当搜索到底部时开始返回,返回到上一个节点,继续向下搜,搜索完成,再返回。具体如图(leetcode扒的某大神的)
具体实现如下:class Solution: def permute(self, nums: List[int]) -> List[List[int]]: out = [] path = [] used = [False for i in range(len(nums))] def dfs(nums, depth, path, out, used): if depth == len(nums): out.append(path[:]) return for i in range(len(nums)):#迭代每个元素 if not used[i]:#判断是否是可能的元素 used[i] = True path.append(nums[i]) dfs(nums,depth+1,path,out,used)#递归搜索 path.pop()#回溯 used[i] = False dfs(nums,0,path,out,used) return out
- 77.组合
这个题和排列类似。但是要注意,不要用排列的写法去写。。。折腾了一个多小时,用排列的思路去更改判断和回溯条件根本搞不出来。其实组合更加容易写,只要控制递归条件,搜过的数字不能再搜索就可以了。比如,第一次用到了1,当有1的组合全都完成后,1就不能再出现了。但是用到2的组合还可以继续出现,只不过组合中不能包括1而已。(优化的话可以进行剪枝,没仔细看)class Solution: def combine(self, n: int, k: int) -> List[List[int]]: path = [] out = [] def dfs(n,start,k,path,out): if len(path) == k: out.append(path[:]) return for i in range(start,n+1): path.append(i) dfs(n,i+1,k,path,out) path.pop() dfs(n,1,k,path,out) return out
- 79.单词搜索
这个的话,思路想到了,就是实现写的不太好,照着答案顺了一下,最后还被一个小问题干住了俩小时。。。。。
思路的要点就是,深度优先搜索,以board的每个位置作为起点,开始上下左右做深度优先搜索。一个位置一个位置的和word做比较,只要发现有一个位置与word不一样,就不用继续再搜了,继续从下一个点开始搜就可以。直到搜索到最后,搜索的深度和word的长度一样。(这里要注意好这个判断条件,和全排列不同。这个题目是,会先判断一下某个位置是否和word的某位相等,然后再判断是不是长度够了。因此长度够了就是说明搜索到了。而全排列是,排完之后还会往下搜索一层,所以会比长度要多1。)
中间的话,需要记录好搜索的路径,避免重复搜索,还要复原,避免影响下一次搜索。
注意: 踩了个大坑,关于变量的问题。- 递归的return,要注意的是,最后返回出来的是最外层的return。比如说,调用dfs(0),如果迭代到dfs(10)停止,那么最后返回出来的是dfs(0)的return,而不是dfs(10)。这就是为什么,在if下面的return返回值没卵用,除非没有递归就返回了。
- leetcode的全局变量不能乱用,会报错。意思是说,不要在leetcode里出现global var。否则基本上都会报错,除非是给定的传入参数。我的理解是:leetcode内部应该是会把我们写的代码嵌入一个工程里,那么我们如果在内部写global var,实际上调用的并不是我们类里面的var,而是外部(我们看不到)的var。那么就会有两种情况:要么外部没有这个变量var,那么会报错;要么外部有var,这种情况下也有两种情况:如果var是入参,那么没问题,程序不会出错,甚至不需要自己写global,python在内部定义的函数找不到var的时候会自动调用全局变量。但还有一种情况是,有可能外部碰巧有同名的变量,,,那盲猜答案会出问题(我没试过)。总结建议就是,如果刷leetcode的时候自己定义的函数需要引用函数外的变量,就用类的self.代替吧。
借鉴一下大神的思路。看到别人的代码只能感叹自己实在是太呆逼了。。。。。。第二遍做,还在那迭代,迭代,迭代。。。。。搞了半天发现要回溯,写出来又一堆bug。class Solution: found = False def exist(self, board: List[List[str]], word: str) -> bool: visted = [[False for j in range(len(board[0]))] for i in range(len(board))] def dfs(board,word,i,j,visted,depth): global found if not (0<=i<len(board) and 0<=j<len(board[0])): return if board[i][j] != word[depth] or visted[i][j]: return if depth == len(word)-1: self.found = True return visted[i][j] = True dfs(board,word,i+1,j,visted,depth+1) dfs(board,word,i-1,j,visted,depth+1) dfs(board,word,i,j+1,visted,depth+1) dfs(board,word,i,j-1,visted,depth+1) visted[i][j] = False for i in range(len(board)): for j in range(len(board[0])): dfs(board,word,i,j,visted,0) if self.found: return self.found return self.found
这个比较好的思路有两点:一点是通过转换和恢复board来替代掉visted,节约空间;另一点是通过res和or来保存下来出现过的True的结果,节约空间。
有个疑问是,其实思路上和我上面的写法是差不多的,但是上头的写法就是会超时,很奇怪。。明明时间复杂度是一样的。。。class Solution: def exist(self, board: List[List[str]], word: str) -> bool: def dfs(i,j,depth): if not (0<=i<len(board) and 0 <=j <len(board[0])): return False if board[i][j] != word[depth]: return False if depth == len(word)-1: return True board[i][j] = "" res = dfs(i,j-1,depth+1) or dfs(i,j+1,depth+1) or dfs(i-1,j,depth+1) or dfs(i+1,j,depth+1) board[i][j] = word[depth] return res for i in range(len(board)): for j in range(len(board[0])): if dfs(i,j,0): return True return False
- 岛屿的数量
一道和岛屿最大面积差不多的题目,既可以用dfs,也可以用bfs。岛屿的题目里已经写过了递归实现的dfs,这里写一下栈实现的dfs和队列实现的bfs。
栈和队列实现dfs还有bfs也很好理解。其实就是,当发现一个点是岛屿的时候,就从它开始搜索。如果是dfs,那么就是顺着一个方向一直搜到底。理解来说就是,每一层都会放入缓存中,dfs就要求,如果有新加入的点,就先搜索新加入的点,也就是栈先进后出。如果是bfs,那么就是一圈一圈向外扩散,也就是如果有新加入的点,要先把上一层的点搜索完再搜索下一层,也就是队列先进先出。python列表就是栈!!!
dfs:
bfs:import collections class Solution: def numIslands(self, grid: List[List[str]]) -> int: stack = collections.deque([]) count = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == "0": continue else: stack.append((i,j)) count += 1 while len(queue) != 0: cur_i,cur_j = stack.pop() if 0<=cur_i<len(grid) and 0<=cur_j<len(grid[0]) and grid[cur_i][cur_j] == "1": grid[cur_i][cur_j] = "0" queue.append((cur_i-1,cur_j)) queue.append((cur_i+1,cur_j)) queue.append((cur_i,cur_j-1)) queue.append((cur_i,cur_j+1)) else: continue return count
import collections class Solution: def numIslands(self, grid: List[List[str]]) -> int: queue = collections.deque([]) count = 0 for i in range(len(grid)): for j in range(len(grid[0])): if grid[i][j] == "0": continue else: queue.append((i,j)) count += 1 while len(queue) != 0: cur_i,cur_j = queue.popleft()#改一行就可以 if 0<=cur_i<len(grid) and 0<=cur_j<len(grid[0]) and grid[cur_i][cur_j] == "1": grid[cur_i][cur_j] = "0" queue.append((cur_i-1,cur_j)) queue.append((cur_i+1,cur_j)) queue.append((cur_i,cur_j-1)) queue.append((cur_i,cur_j+1)) else: continue return count
- 剑指 Offer 13. 机器人的运动范围
最基础的bfs。比较简单,注意几个问题。一个是deque初始化的时候,deque([0,0])表示deque里有两个元素,0和0,而不是一个元素[0,0],这个一定要注意。还有就是split方法必须有分隔符,没办法把"123"拆开。所以这里我用了list拆。其实也可以用除法之类的方法来拆分每一位。这个题的话,可以不用四个角都搜,只搜右和下就可以。import collections class Solution: def movingCount(self, m: int, n: int, k: int) -> int: queue = collections.deque([[0,0]]) count = 0 visted = [] while queue: curx,cury = queue.popleft() num = str(curx) + str(cury) num = sum([int(i) for i in list(num)]) if not (num<=k and [curx,cury] not in visted): continue else: count += 1 visted.append([curx,cury]) temp = [[curx,cury+1],[curx+1,cury]] for i in temp: if 0<=i[0]<m and 0<=i[1]<n: queue.append(i) return count
数据结构
- 448. 找到所有数组中消失的数字
应该算是哈希表方法吧,只不过根据数字的规律可以用列表来代替哈希表。然后用列表解析来加快速度。看官方有更鸡贼的写法,直接用nums当哈希表,时间复杂度还能再低一点。这里就不贴了,下面是我写的。class Solution: def findDisappearedNumbers(self, nums: List[int]) -> List[int]: bucket = [0 for i in range(len(nums))] for i in nums: bucket[i-1]= 1 return [i+1 for i in range(len(bucket)) if bucket[i] == 0]
- 48. 旋转图像
总结一下, 做旋转其实可以由矩阵转置和镜像操作组合得到。顺时针旋转90度就是先转置,然后把列做一个倒序(也就是镜像)。代码实现如下:
几个点注意一下:一是关于多元赋值的理解,多元赋值实际上先把等式右边的值算出来,然后再依次赋给左边,所以不会出现错误。二是其实上面的写法其实,当i等于len(matrix)-1时,j没有执行任何操作,此时都已经替换完了,只不过为了方便把列做倒序,还是跑了一下最后一行。class Solution: def rotate(self, matrix: List[List[int]]) -> None: """ Do not return anything, modify matrix in-place instead. """ for i in range(len(matrix)): for j in range(i+1,len(matrix[0])): matrix[i][j],matrix[j][i] = matrix[j][i], matrix[i][j] matrix[i] = matrix[i][::-1]
- 769. 最多能完成排序的块
这题确实没啥通用办法了。。。就是看一下当前最大值和数组下标的关系。排序之后,对于每个位置来说当前位置的最大值一定等于数组下标。那么也就是说,如果当前位置的最大值不等于数组下标,说明从上一个等于的位置到当前位置是一个待排序的块。最多的情况就是整个数组是有序的,每个位置最大值都等于数组下标,此时是最多的。最少的就是完全倒序,只有最后一个位置的最大值才等于数组下标,是1。class Solution: def maxChunksToSorted(self, arr: List[int]) -> int: count,curm = 0,0 for i in range(len(arr)): curm = arr[i] if curm < arr[i] else curm if curm == i : count += 1 return count
- 20. 有效的括号
是个用栈的题目,之前2021.4.14华为实习机考第一题也差不多。思路就是用栈,遇到右括号就看看栈顶是不是匹配。再注意一下边界条件,一个是输入只有左括号的时候,一个是右括号比左括号多的时候。先放自己的呆逼代码。。
自己看了下解题思路,改进了个好看点的版本。class Solution: def isValid(self, s: str) -> bool: stack = [] for i in s: if i in ['(','{','[']: stack.append(i) else: if stack: cur = stack.pop() else: return False if i == ")" and cur != "(": return False if i == "}" and cur != "{": return False if i == "]" and cur != "[": return False if len(stack): return False else: return True
class Solution: def isValid(self, s: str) -> bool: stack = [] dic = {"(":")","{":"}","[":"]"} for i in s: if i in dic: stack.append(i) else: if not stack or dic[stack.pop()] != i:#因为截断效应,如果栈为空就会直接返回False了,不会再去看or后面的条件。 return False return not stack
- 739.每日温度
单调栈。自己写了个暴力直接超时。这里要注意理解一下整个过程。整个过程是,如果栈中是空的或者栈顶元素大于等于当前元素,那么就压栈,这样就维护了一个递减的栈。而当碰到一个元素大于栈顶,说明此时栈顶元素碰到了它后面最近一个比它大的元素,此时出栈,并把栈顶元素对应的位置设成位置的差值,再继续比较下一个栈顶和元素的关系,循环此过程,直到栈顶元素大于当前元素或栈空。这样遍历完就可以保证每个位置更新都是正确的。这里注意,栈中保存数字下标,这样方便索引元素的位置以及计算差值。初始化初始一个都为0的列表,这样后面如果不更新,就是0就可以。class Solution: def dailyTemperatures(self, temperatures: List[int]) -> List[int]: stack = [] out = [0 for i in range(len(temperatures))] for i in range(len(temperatures)): while stack and temperatures[i] > temperatures[stack[-1]]: pre = stack.pop() out[pre] = i-pre stack.append(i) return out