ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

leetcode思路简述(131-170)

2020-05-07 13:58:20  阅读:238  来源: 互联网

标签:结点 right nums 链表 131 遍历 leetcode dp 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)

 

标签:结点,right,nums,链表,131,遍历,leetcode,dp,170
来源: https://www.cnblogs.com/sumuyi/p/12613686.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有