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)

 

上一篇:查询局域网指定段内存活IP


下一篇:类加载器