leetcode思路简述(131-170)
131. 分割回文串
回溯。从前往后遍历所有位置 i,假如前面的 s[0: i+1] 子串是回文串,则后面的成为子问题,在字符串 s[i+1: ] 中分割回文串。
加入记忆优化,保存位置 loc 开始的所有回文串,减少重复。
还可以动态规划加快检测回文,一开始就初始化 check 数组,check[i][j] 表示 s 从位置 i 到 j(闭区间)的子串是否为回文,可以快速判断回文。初始化 check,两层循环 for j in range(n),for i in range(j+1),遍历所有子串,如果左右边界字符相等,且去掉边界是回文 if (s[i] == s[j]) and (j - i <= 2 or checki+1][j-1]),则 check[i][j] = 1。
132. 分割回文串 II
动态规划。dp[i] 表示子串 s[0: i+1] 回文串最小分割次数。如果 s[0: i+1] 是回文,则 dp[i] = 0;否则遍历 i 之前所有分割点,当分割点 j 到位置 i 的子串 s[j+1: i+1] 为回文时,分割次数为 s[j] + 1,也就是 j 之前的回文串数加上 j+1 到 i 的一个回文串,将这些分割方法中最小分值赋给 dp[i]。
判断是否回文串用第 131 题的动态规划初始化 check 数组。
dp = [i for i in range(n)]
for i in range(1, n):
if check[0][i] == 1:
dp[i] = 0
continue
for j in range(i):
if check[j+1][i] == 1:
dp[i] = min(dp[i], dp[j] + 1)
133. 克隆图
用各种图的遍历都行,记录访问过的点,如果未访问过结点的某个邻居,dfs 进入该点,新建结点,即深拷贝。如果要访问的邻居点以前访问过,这时不需要新建,只需要把复制的邻居点放到它的 neighbors,也就是浅拷贝。
字典visited 的 key 为原图结点,value 为复制的结点。
如果该点访问过,就直接返回 visited[node]。
否则新建 cur = Node(node.val) 并放到字典 visited[node] = cur。遍历原结点邻居,并把返回的复制邻居放到复制结点邻居列表 for i in node.neighbors: cur.neighbors.append(dfs(i))。返回复制结点 return cur。
134. 加油站
一次遍历。如果必然存在起点,把不能成为起点的排除,最后留下来的就是起点了。
如果 sum(gas) - sum(cost) >= 0 则必然存在起点。那么如何排除起点,如果 A 站作为起点不能到 B 站,则 A,B 之间到任何一个站都不能作为起点到达 B 站。因为 A 站可以是起点时,到下一站的油量肯定大于等于 0,还到不了说明中间作为起点更不行。所以此时起点只能是 B 及 B 以后的点。
令全局剩余油量为 v_total += gas[i] - cost[i],到终点就是跑完全部后剩余的油量,如果它小于 0 说明不存在起点。
令当前剩余油量为 v_curr,是以某加油站为起点时,当前的剩余油量。
遍历所有加油站 i,对每个 i 更新 v_total 和 v_curr。如果遇到 v_curr < 0,则把 i + 1 当做新起点 start,v_curr 重置 0。
最后根据 v_total 判断是否存在起点进行返回,如果 v_total >= 0,返回起点 start;如果小于 0,返回 -1。
135. 分发糖果
每个孩子的糖果数 = max(左边单增多少个人,右边单减多少个人)。局部极小值直接是 0。
两个数组。定义数组 left_height,right_height,表示每个孩子左右边有多少人分数连续减少。从左往右遍历完成 left_height数组,如果 ratings[i] > ratings[i-1],则当前比前个位置多一个糖果 left_height[i] = left_height[i-1] + 1,否则left_height[i] = 0。right_height同理。然后第三遍遍历对每个点 candy += max(left[i], right[i])。
可以用一个数组,相当于把两个数组合起来,右往左遍历时如果比左往右更大直接覆盖即可,还可以同时计算 candy 数。
136. 只出现一次的数字
最容易想到的是哈希和排序,但空间或时间不合要求。使用位运算,异或同一个数两次原数不变。遍历数组,对每个数 ans = ans ^ nums[i],最后 return ans。
137. 只出现一次的数字 II
如果每个数字出现次数是 3,那么二进制的每一位都会是三的倍数,这里每一位用两个比特位计算,初始为 00,遇到第一个 1 变为 01,遇到第二个 1 变为 10,遇到第三个 1 变回 00。
令两个位掩码为 once 和 twice。对每个数字 num 同时对 32 位计数 :once = ~twice & (once ^ num),twice = ~once & (twice ^ num)。最后返回 once。
138. 复制带随机指针的链表
如果没访问过就创建,如果访问过就传指针。字典 d 记录创建的结点,key 为原链表结点,value 为复制结点。
① 直接遍历。对每个原链表结点 p,如果 p 的复制结点存在,即 p in d,则 cur = dic[p];否则新建个并放到字典中 cur = Node(p.val),dic[p] = cur。
对于 p.random,如果为 None 则不进行操作,否则和 p 同样的,在字典就直接连上 cur.random = dic[p.random],不在就新建放到字典 cur.random = Node(p.random.val),dic[p.random] = cur.random。
然后把 cur 连到前一个复制结点的后面 pre.next = cur,两个链表指针向后移动 pre = cur,p = p.next。
② 回溯。copyList(node) 参数为原结点。同样用字典避免重复创建,字典得到或创建结点 cur,然后构建两个指针 cur.next = copyList(node.next),cur.random = copyList(node.random)。最后返回 cur。
139. 单词拆分
① 回溯。每次截出一个在列表的单词,然后调用回溯函数检查去掉这个词的子串 s[i+1: ],返回是否可拆分。加上记忆优化,把检查失败的子串 s[i+1: ] 放到集合。
② BFS。如果当前子串是列表的单词,就把结束的下标放进队列;每次弹出一个下标,找下个单词。
③ 动态规划。dp[i]表示前 i 位是否可以用 wordDict 中的单词表示。初始化 dp[0] = True(第 0 位为空字符,是第 i 个不是下标),其他为 False。
两层循环遍历所有子串,外层 i 为子串起点下标,内层结束下标 j。若 dp[i] 为真(前面的子串可以拆分)且 s[i: j] in wordDict(当前子串可),则dp[j]=True。
140. 单词拆分 II
① 回溯。和第 139 题的回溯相似,检查字符串每个位置 i,如果 s[: i+1] 在wordDict,回溯剩余字符串,回溯函数返回结果集合,s[: i+1] 与返回的结果做笛卡尔积。记忆优化,每层返回结果前,把结果存在字典中 d[s] = res,每次回溯函数最开始先查这次的 s 在不在字典中。
② 动态规划。dp[i] 保存到第 i 个字符的所有拆分的单词组合。
141. 环形链表
① 哈希。把遇到的结点放到 set 里。
② 快慢指针。慢指针移动一步,快指针移动两步,如果两个相遇则存在环返回 True,有一个到了末尾(为 None)则返回 False。
142. 环形链表 II
① 哈希。同第 141 题。
② Floyd。
阶段 1:快慢指针同 141 题,判断是否有环,并找到快慢指针相遇结点。
阶段 2:令指针 p1 指向快慢针相遇时的结点,指针 p2 指向 head。p1,p2 同时移动,直到相遇,相遇点为环的入口,返回相遇点。
设链表节点数为 a + b,分别是非环结点数和环内结点数;设快慢指针分别走了 f,s 步,f = 2 s。因为最终两指针重合,所以最后两指针的步数刚好差了 n 个环长:f = s + nb。
两式相减得 s = n b。而走到环入口的步数为 a + n b,所以从两指针相遇的位置开始还需要走 a 步。相遇点指针 s 走 a 步后,s 总步数为 a + n b,指向头结点的指针同时走 a 步,刚好和 s 相差 n 个环的步数,会相遇,且在环入口 a 处。
143. 重排链表
快慢指针确定中点,翻转中点以后的部分,把前半部分与翻转的后半部分交替连接。
def reorderList(self, head: ListNode) -> None:
if not head or not head.next: return head
fast, slow = head, head
#找到中点并断开
while fast.next and fast.next.next:
fast = fast.next.next
slow = slow.next
#反转后半链表
p, right = slow.next, None
slow.next = None
while p:
right, right.next, p = p, right, p.next
#重排链表
left = head
while left and right:
left.next,right.next,left,right = right,left.next,left.next,right.next
144. 二叉树的前序遍历
处理完当前结点将右结点入栈,下到左结点,当左结点空时,出栈一个,循环。
简单点的写法可以每次出栈一个访问,然后入栈右结点,入栈左键点。但是每个结点会多压一次栈。
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = []
stack = []
node = root
while stack or node:
while node:
res.append(node.val)
stack.append(node.right)
node = node.left
node = stack.pop()
return res
145. 二叉树的后序遍历
把第 144 题先序遍历中,左子树改右子树,右子树改左子树,此时遍历顺序为根右左,结果倒序就是了。
146. LRU缓存机制
get() 通过 key 对应的 value 使用字典实现即可,而 put() 主要判断哪个是最久未使用的,用队列实现。队首是最久未使用的,每次 get() 时把请求的那一项移到队尾。
关键是需要在常数时间内将队列中某一项移到队尾。需要用双链表实现。每个链表结点有两个指针 prev 和 next,分别指向它的前后项。head 和 tail 指向双链表的两端。
哈希表里面 key 对应的是链表结点,所以从 key 不仅可以得到密钥的值,还可以得到它在队列中的前后项。假如 get 到它,就把它的前后项连起来,把它放到队尾。
147. 对链表进行插入排序
定义一个 dummy,每次从原链表中取一个结点放到 dummy 中合适的位置。找位置时把前一个位置用 pre 记录,方便把结点接进去。
148. 排序链表
归并。
① 递归。 递归传入要排序的链表头结点。每次先快慢指针找到中点 mid,并断开得到左右两个子链表。对 head 和 mid(原链表左右边)分别递归,得到排好序的左右半边,再将有序的两个链表一次遍历边合起来,返回排好序的链表。
② 迭代。设置变量 step = 1,表示将链表分割成长度为 1 的单元,将这些单元按顺序两两合并。每轮合并后 step 翻倍,再两两合并。当 step 等于链表长度时结束。
149. 直线上最多的点数
枚举吧。遍历所有的直线,看有多少点。如果线上不止两点,把线的方程(k、b)和点数保存到字典,下次再遇到就跳过不用找点了。或者用一个点与斜率确定一条直线,每次用一个点对其他点求斜率,保存到字典计数。注意一样位置的点和斜率分子为0的点。
考虑斜率是小数不精确,将分子分母约分到最简(辗转相除法,除数和余数分别作为下一轮的被除数和除数直到除数为0,求最大公约数 a,再一起除以 a),约分后的分子分母作为判断依据。
150. 逆波兰表达式求值
如果是数字就入栈,是操作数出栈两个数,把两数按操作数的到的结果入栈。
151. 翻转字符串里的单词
遇到空格表示单词结束,把单词保存到列表里,再反向遍历列表用空格连接。
也可调包大法:" ".join(reversed(s.split()))
152. 乘积最大子数组
连续最大乘积与每个数的正负号有关,dp_max 和 dp_min 分别保存连续到当前位置的最大值与最小值。初始化都为nums[0]。
更新最大值时有三种可能,1. 与目前最乘积相乘;2. 与目前最小乘积相乘;3. 当前值(之前乘积为0)。即 dp_max = max(nums[i]*dp_max, nums[i]*dp_min, nums[i])
同理,dp_min = min(nums[i]*dp_min, nums[i]*dp_max, nums[i])。
注意这里 dp_min 用到了 dp_max,而之前更新 dp_max 时覆盖了原来的 dp_max,所以更新 dp_max 时用 temp 保存一下,或者用逗号分隔两个 dp 更新写在一行一起赋值。
每次个数更新完两个 dp 后记录下当前连续最大乘积 res = max(res, dp_max)。
153. 寻找旋转排序数组中的最小值
二分法。直接用标准二分法改。
返回条件 if nums[mid] < nums[mid-1]: return nums[mid]。
左右边界更新 if nums[mid] > nums[right]: left = mid + 1 else: right = mid - 1
也就是判断最小值在哪个半边,在里面搜索就好了。
154. 寻找旋转排序数组中的最小值 II
和 153 题差不多。
返回条件多了一个,因为最小值不一定小于前面的数。if nums[mid] < nums[mid-1] or right == left: return nums[mid]
左右边界更新时,如果中间与 nums[right] 相等,无法判断最小值在哪边,此时 right - 1。
if nums[mid] > nums[right]:
left = mid + 1
elif nums[mid] < nums[right]:
right = mid - 1
else:
right -= 1
155. 最小栈
辅助栈 helper,如果新 push 的值小于栈顶则入栈;pop 时,如果pop的值等于 helper 栈顶,则 helper 也 pop。
160. 相交链表
双指针法。p1 遍历 A,p2 遍历 B,当指针走到链表末尾时,从另一个链表头从新开始遍历。第二次遍历中,如果两指针同时指向一个结点,就是相交起始结点。第二次遍历结束没有相交就是没有。
因为第二次遍历,两指针到达相交结点时,它们走过的结点数是相同的,所以会指向同一个结点。
162. 寻找峰值
二分法。常规二分法上修改。
返回条件 if left == right: return left
边界更新 if nums[mid+1] < nums[mid]: right = mid else: left = mid + 1
因为如果当前值与右侧值处于一个下降坡度,那峰值肯定在左边(当前值也有可能),否则在右边(当前值不可能)。
164. 最大间距
① 基数排序。以第一位为准进行计数排序,然后以第二位进行计数排序这样直到每一位排完。
计数排序:待排序为A,排好放在B,辅助数组C
# C放下标的数出现次数
for num in A:
C[num] += 1
# C放下标的数排在整体第几位
for i in range(1, len(C)):
c[i] += c[i-1]
# 把每个数放在自己的位置上(前面求的每个数是第n位-1就是下标)
for i in range(len(A)-1, -1, -1):
B[C[A[i]]-1] = A[i]
C[A[i]] -= 1
② 桶
不需要真正将所有元素严格排序,只需要求出最大的间隔即可。同一个桶的数一定不会有最大间距。
桶大小 size = (max-min) / (n-1) 向上取整 (n 个元素有 n-1 个间距,假设这些间距平均分布在区间 max-min 中,如果两个数间距小于这个值,那一定不是最大间距)
桶的数量 k = (max-min) / size
每个数num放的桶 (num-min) / bucket
遍历一遍放在对应的桶里,比较 k-1 个相邻桶找到最大间距。
165. 比较版本号
将两个字符串以 “.” 分割放到两个列表里,循环比较列表中的数字,循环次数为较长的列表长度。如果较短列表当前位置没有数字,令它的数字为0,比如对于列表nums1: x1 = int(nums1[i]) if i < n1 else 0
166. 分数到小数
记录符号,取绝对值。整除得到整数部分,取模得到小数 rest。
判断循环小数,先把 rest 保存到字典。rest *= 10,然后 rest // denominator 放在小数集合中,取余数作为下次被除数 rest = rest % denominator。
循环中,如果 rest 为 0 或 rest 在字典中,就可以结束了。
167. 两数之和 II - 输入有序数组
双指针 p1, p2 指向头尾。while(p1 < p2),如果两数和大于 target 则 p2 减一,如果小于则 p1 加 1,相等就是找到。
168. Excel表列名称 *
有点像10进制转26进制,但是区别在于26进制应该是满26进1然后低位补0,但这里是满26还是26,满27进位低位补1。
这里让n每次减1,A从0开始。
while(n > 0):
n -= 1
ans += chr(ord('A') + n%26)
n = n // 26
返回要反转 return ans[::-1]
169. 多数元素 *
① 哈希。
② 排序。排好序后返回 nums[len(nums)//2]。因为数量占一半以上,中位数一定是这个数。
③ 随机。随机挑选一个数,验证它是否数量大于一半。因为众数占一半以上,所以很大概率挑中。
④ 投票。与候选数相同获得票数+1,否则-1,票数为0重置候选人。初始化 count = 0。直到循环结束 count 都不会小于 0。
for num in nums:
if count == 0:
candidate = num
count += (1 if num == candidate else -1)