• 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)

     

  • 相关阅读:
    【UVa】And Then There Was One(dp)
    【vijos】1006 晴天小猪历险记之Hill(dijkstra)
    【UVa】Palindromic Subsequence(dp+字典序)
    【UVa】Wavio Sequence(dp)
    【UVa】Salesmen(dp)
    【UVa】Partitioning by Palindromes(dp)
    小结:特殊的技巧
    java 中 进程和线程的区别
    java中的 sleep() 和 wait() 有什么区别?
    Java 静态static 关键字作用
  • 原文地址:https://www.cnblogs.com/sumuyi/p/12613686.html
Copyright © 2020-2023  润新知