zoukankan      html  css  js  c++  java
  • leetcode思路简述(31-60)

    31. 下一个排列

    首先找到从右边起第一对升序的数对(a[i1] < a[i]),此时a[i1]右侧的排列是一个最大排列(因为已经是降序了)。找到a[i-1]右起第一个比它大的数a[j],将它交换位置。相当于找到右边比原a[i-1]刚好大一点的数。再翻转a[i-1]右侧的数的顺序(不包括自己),也就是降序变升序。

    关键要想到降序的意义,代表了不存在更大的排列。而对于这种情况,要“进位”并将这部分变为最小(升序)。

     

    32. 最长有效括号

     ① 动态规划。dp[i]中存放以s[i]结尾的有效串长度,只需更新 ')' 对应的dp。

        (1)“......()” ,dp[i] = dp[i-2] + 2

        (2)“......))” ,如果s[i - dp[i-1] - 1] =‘(’  ,则 dp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2 。

        也就是,如果以 s[i-1] 结尾的子串 s' 的之前一位为 '(' ,即那一位可以与 s[i] 组成一对括号,那么 dp[i] 等于 s' 的长度 + s' 前的 '(' 前一位的dp + 这对括号长度2 。

    ② 栈。这个方法也很巧妙,是将下标入栈,两个下标相减可得字符串长度。先压入一个值,方便代码编写。

        如果 s[i] == '(' , i 入栈。

        如果 s[i] == ')' ,出栈。如果出栈后不为空,i 与出栈值做差得到一个有效字符串长度;如果出栈后栈空,说明不匹配,随便压个数进去还原栈内一个数的状态。

    ③ 双计数器法。 计数器 left 和 right,left记录 '(' 的个数,right记录 ')' 的个数,如果 left == right,为有效字符串。如果 left < right ,两个计数器置零。

       左往右遍历一次后,再右往左一次。因为存在一个计数器的值总比另一个大的情况,比如 "((()"。

     

    33. 搜索旋转排序数组

    二分法。关键是要想明白如何判断在哪半边搜索。可以找到完全升序的一边,从那一边的左右端点即可判断target是否在里面。

    (1) 如果 nums[mid] > nums[right],说明左边是严格升序,以左边为准进行判断。如果 left < target < mid,搜索左半边,否则搜索右边。

    (2) 如果 nums[mid] < nums[right],同理,说明右半边是严格升序,以右半边为准进行判断。如果 mid < target < right,搜索右半边,否则搜索左边。

     

    34. 在排序数组中查找元素的第一个和最后一个位置  *

    二分。对左右边界分别搜索。

    以左边界为例,最常规的二分查找如果找到 target 就会返回,而在这里如果找到 target 则 right = mid - 1,也就是右边界收缩。

    比如 (1, 1, 2, 2, 2),假设 target 是2, 第一次搜索时 mid = 2,也就是中间那个2,按照算法收缩右边:right = mid - 1 = 1。现在搜索区间是 [0,1],所有的数都比2小,每次都 left + 1,循环结束后 left 会大于 right,即 left = 2,也就是要找的左边界。

    最后左边界为 left ,返回前越界判断 (left >= nums.length || nums[left] != target) 。

    右边界同理,找到 target 则 left = mid + 1。

     

    35. 搜索插入位置

    和基础二分法一样。最后如果没找到的话,返回 left。

     

    36. 有效的数独

    初始化三个所有元素为0的二维数组(用字典的话弄三个 {} for i in range(9) 也可),分别记录行,列,小box。二维数组 9 * 9 对应 [行或列或小box编号,下标数字的是否出现过]。遍历board,如果数组中的值大于1,返回False,小于1则更新数组。

    假设遍历时行列为 (i, j),now = board[i][j] - 1,数组下标为0-8所以需要减一。每次检查和更新 row[i][now],col[j][now],box[(i//3)*3+(j//3)][now]。

     

    37. 解数独

    回溯。遍历board,没填的位置从1-9尝试填上x,若x符合,到下一个空位置再填,填完最后一个格子就结束了。如果当前格子填1-9全不符合,则退回上个格子改数字(需要修改三个判断矩阵,可以写个反操作的remove函数)。

     

    38. 外观数列

     写个next(s)函数根据一个字符串计算下个字符串 ,循环调用 n - 1次(第一次直接初始化1)。next(s)函数遍历字符串 for i in range(len(s)-1)。如果 s[i] == s[i+1],count += 1;如果不相等,res += count + s[i]。遍历完成记得加上最后一个返回 res + s[len(s) - 1] + count。

     

    39. 组合总和

     差不多是标准回溯。

    backTrack(index, path, sum) 参数分别是从位置index开始,已经加的数字path,path之和sum。中间有三种情况:

    (1) sum = target:   res.append(path) 

    (2) sum >= target: rerurn

    (3) 也就是要找下一个数字的情况,接着往后深度优先搜索,for i in range(index, len(candidates)):    backTrack(i, path+[candidates[i]], summ+candidates[i])。

         这里如果想要减枝,可以在最开始 sort 一下,然后在每次循环的 backTrack 前判断 sum + candidates[j] > target,如果大于则 return,停止检查后面更大的数。

     

    40. 组合总和 II

    在上一个的基础上修改一点。有两个区别,一个是给的candidates可能有重复,二是每个元素在组合中只能用一次

    修改的地方有两个,一,在每次循环开始判断下 ,if i>index and candidates[i]==candidates[i-1]:   continue。这样在同一个位置如果遇到重复数字就会跳过。

    二,每次从下一个开始找起,backTrack(i+1, path+[candidates[i]], summ+candidates[i])

     

    41. 缺失的第一个正数  *

    其实最开始想到把 {num: isExist} 存在哈希表里,r然后从1-n检查是否key存在。但空间复杂度肯定是不行的,而这题的一个思路相当于把数组下标当做key,正负号当做value。

    首先,在列表里只有大于零且小于等于n的数是有用的,其他数值对于这题没有意义,因为要找的要么在小于等于n的数里,要么就是n+1

    第一遍,一边检查当前数是不是1(用flag记录,如果到最后都没出现1,那么直接return 1),然后如果当前数小于等于0或大于len(nums)则改成1。这里就是为了把所有数改成正的,顺便方便后面根据数找对应下标不溢出。

    第二遍,for i in range(n),把 index = abs(nums[i]) 为下标的元素符号改为负号 nums[index] = - nums[index]。注意重复的数字,改之前判断是否已经为负。还有因为下标 n 溢出,可以用 nums[0] 的符号记录n的出现。

    第三遍,for i in range(1,n) ,判断nusm[i]符号,如果是正就返回i。然后判断nums[0],正返回n。都不是正就返回n+1。

    *  下标和符号等都可以用起来,不能局限于列表元素的数字本身。

     

    42. 接雨水  *

    ① 当前元素 i 上方的雨水等于 (min(左边最高值,右边最高值) - heighr[i])。

        每个元素左右最高值可以不用每次重新扫描全部。在最开始,先遍历两遍。一个数组存放对应元素的左边最大值,i从左往右 leftM[i] = max(height[i], leftM[i - 1])。rightM同理,反向扫描。

    ② 栈。如果当前的高度小于或等于栈顶的高度,入栈。如果遇到大于栈顶的,表示栈顶的条形块被当前条形块和栈中前一个条形块界定,此时弹出栈顶元素,并将水量且累加到 ans。

        栈中存放的是条形块下标。

        while(i < len(height)):

            while(当栈非空,且 height[i] > height[stack[-1]]):   

                top = stack.pop(),如果pop之后栈空就break。

               distance = i - height[stack[-1]]) - 1,curV = min(height[i], height[stack[-1]]) * distance。curV 是以当前高度为底凹陷的水量。如果中间有更低位置,那一部分凹陷的水量在之前已经累加过了。

               ans += curV。

            将 i 入栈。 i += 1。 

    ③ 双指针。

        盛水高度由左右最高值的短板决定。双指针每次只移动较矮的那边,不动的那个是当前最高的块。当这个最高块的指针动了,说明另一边出现了更高的,它变成了second(第二高的块)而新的最高块在这次不动的一边,所以second与目前最高块永远分在两边。

        每次计算second与当前矮指针的差(如果矮指针比second高就更新second,不计算水量)。

        while(left < right):

            如果 height[left] < height[right],要关注left。如果left高度比second大,说明这一块不是凹的,不计算水量,更新second;不比second大则ans += second- height[left]。

       else: right同理。

     

    43. 字符串相乘

    按平时笔算乘法的方法写。令两数最大下标为m,n,则相乘后对应最大下标为m+m+1。两层循环ij倒序遍历两个数的每一位,res[i+j+1]存储对应乘积结果。每次把进位加到前一位中。

    每次内层循环中:

        (1) tmp = int(num1[i]) * int(num2[j]) + int(res[i+j+1]),对应位相乘并加上进位

        (2) res[i+j+1] = tmp%10,取小于10的部分作为当前位

        (3) res[i+j] = res[i+j] + tmp//10,进位。

    最后要去掉前导0。

     

    44. 通配符匹配  *

    ① 递归。helper(i, j) 几种情况:

        (1) s[i: ] == p[j: ] 或者 p[j: ] 全为 *,True。 

        (2) else s[i: ] == '' or p[j: ] == '',False。 

        (3) s[i] == p[j] 或 p[j] == '?' ,helper(i+1, j+1)。

        (4) p[j] == '*',helper(i+1, j) or helper(i, j+1)。

        连续星号可以只留一个。

        递归通常可以用记忆优化,将(i, j)作为键,是否匹配作为值,递归前先查字典。每次return前把返回值先放字典。

    ② 动态规划。二维数组 dp[i][j] 大小 [len(s)+1, len(p)+1],代表 s 的前 i 个字符与 p 前 j 个字符是否匹配。初始化 dp[0][0]=True,其他为 False。最后返回 dp[len(s)][len(p)]。

        初始化 dp[0][0] 为 true,p[0] 为 * 则第一行 dp[0][j] 为 true,第一列 dp[i][0] 为 false。这里 s 与 p 的下标 i 对应第 i+1位,而 dp 的下标对应第 i 位。循环内修改对应列所有元素:

        (1) 当 s[i-1] == p[j-1],或者 p[j-1] == '?', 那么 dp[i][j] = dp[i-1][j-1]。

        (2) 当 p[j-1] == '*', 那么 dp[i][j] = dp[i][j-1] or dp[i-1][j] 。

    ③ 回溯。如果发生不匹配就回到前一个星号。while i < len(s): 

        (1) 如果 s[i] == p[j] 或 p[j] == ‘?’,则 i += 1, j += 1。

        (2) else 如果 p[j] == ‘*’,记录两个串当前位置,i_memo = i,j_memo = j。假设 * 匹配空串:j += 1。

        (3) else 如果 j_memo == -1 (不匹配且前面没 * ),返回False。 

        (4) else(也就是不匹配且前面有 * )回到上个 * ,使 * 多匹配一个字符:i = i_memo + 1,j = j_memo + 1,i_memo = i。

        循环出来如果模式串剩下的都是 * 就返回True,否则False。

        只用回溯上一层 * ,假设回溯到上两层的 * 它终归要匹配到上一层的 * ,然后再继续匹配的结果是会出现在只回溯上一层的结果里面的。

     

    45. 跳跃游戏 II

    以 [2,3,1,1,4] 为例,第一次跳跃可以选择 nums[1] = 3 或 nums[2] = 1,注意到后者可达位置前者都能到达,前者包括了后者的可能性,因此轮跳跃只需要选择能到最远距离的那个就可以了,也就是 nums[i] + i 最大的那个。找出每一次跳跃所有情况下的能够到达的最远距离 step_end,这就是新一次跳跃的位置。

    用 i 遍历,检查更新可达最远位置 max_loc = max(max_loc , nums[i]+i)。如果 max_loc ≥ len(nums)-1,返回step。检查是否到达本轮跳跃边界 i == step_end:如果是则 step += 1 ,且把下个边界位置设置为这轮可达最远位置 step_end = max_loc 。

     

    46. 全排列

    递归。把数字选项 rest 和本次排列 path 传入helper(),把 rest 里的每个数 i 挨个与 path 连接并从 rest 中移除,传入 helper(),再还原换下一个 i 。

     

    47. 全排列 II

    先 sort 一下,然后遍历rest时,if n>0 and rest[n]==rest[n-1]:   continue。其他地方和上一个一样。

     

    48. 旋转图像

    ① 找到对应规则,每次换四个元素位置。

    temp = matrix[i][j]
    matrix[i][j] = matrix[n-j][i]
    matrix[n-j][i] = matrix[n-i][n-j]
    matrix[n-i][n-j] = matrix[j][n-i]
    matrix[j][n-i] = temp

    ② 可以先对角线翻转一次,再水平翻转一次。

     

    49. 字母异位词分组

    ① 排序分类。

    用字典 dict ,key 为字母排序后的词,value 为该组异位词在结果列表 res 中的位置。

    for word in strs,字符串或元组 s 为字母排序后的 word ,判断 if s in dict,如果在则 res[dict[s]].append(word); else:  res.append([word]),d[s] = len(res) - 1。

    也可以直接 value 为对应的 word 列表, dict[s].append(word),最后返回 list(dict.values())。反正差不多一个意思。

    ② 计数分类。

    每个字符串 word 转换为字符数列表 count,长度26对应 word 中每个字母出现的次数。同样用字典,key为 tuple(count),value 对应的那些单词。

     

    50. Pow(x, n)

    ① 和第29题两数相除思路相似,每次步长 i 翻倍,current 等于自己的平方。如果 i 超过了 n 就返回上一步,ans * = current,n -= i,然后 current 和 i 重置。

    快速幂。假设已经知道了 xn/2,要知道 xn,如果 n 是偶数则 xn = xn/2 * xn/2 ,如果是奇数则 x= xn/2 * xn/2 * x。

    ② 递归 fastPow(x, n)。n == 0 时 return 1。没返回的话接着执行, xn/2 的值 half = fastPow(x, n // 2),如果 n 是偶数返回 half * half,奇数返回 half * half * x。

     

    51. N皇后

     回溯 + 剪枝。比较常规,每层回溯代表 row,函数里对该层中的每个 col 放棋子。

    其中对斜线攻击路径的标记,可以注意到经过点 (row, col) 的主斜线的 col - row = 常数,副斜线的 col + row = 常数,标记该位置常数对应的所有斜线坐标即可。这里主(副)斜线指经过该点从左上到右下(左下到右上)的斜线。

    把禁止位置放在三个set里:forbid_col,forbid_z,forbid_f。forbid_col 存放横向攻击产生的禁止位的列下标;forbid_z 和 forbid_f 存放斜线攻击产生的禁止位常数,即每个禁止位横纵坐标的差以及和的常数,方便检查和更新。检测是否禁止位: if (j not in col) and (i - j not in  forbid_z ) and (i + j not in forbid_f );回溯时更新禁止位置:backtrack(i+1, col |{j}, forbid_z |{i-j} , forbid_f |{i+j})。

    复习下集合,集合创建用set()与字典区分,元素不重复,内容格式 {'x', 'y'},运算:a | b(即a∪b),a - b(即a∩b),a - b,a ^ b。

     

    52. N皇后 II

    用51题的方法不用构造棋盘,只需要计数。

    有种使用位运算的方法可以进行优化,暂时不研究。

     

    53. 最大子序和

    ① 贪心。判断如果之前子序和加上nums[i]是否nums[i]自己小,如果小就直接从nums[i]开始。sums= max(sum+nums[i], nums[i]),然后更新最大值 max_sum = max(sums, ans)。

    ② 动态规划。把每个位置为止的最大sum放在数组里,if (nums[i-1] > 0) nums[i] += nums[i-1],否则不改 nums[i]。每一步更新 max_sum 。

    ③ 分治。以数组中点为界,目标值只能在三处:要么在中点左边(不含中点),要么必须包含中点,要么右边(不含中点)。

        helper(left, right) 求左半边最大和,右半边最大和,中间包括中点最大和,返回最大的。左右最大和递归 helper() 求。中间最大和从中间往左求最大,中间往右求最大,加起来。

    *  在整个数组或在固定大小的滑动窗口中找到总和或最大值或最小值的问题,可以通过动态规划(DP)在线性时间内解决。有两种标准 DP 方法适用于数组:

    1. 常数空间,沿数组移动并在原数组修改。
    2. 线性空间,首先沿 left->right 方向移动,然后再沿 right->left 方向移动。 合并结果。

     

    54. 螺旋矩阵

    ① 标记走过的位置,如果下一步候选移动位置在矩阵范围内并且没有被访问过,那么它将会变成下一步移动的位置;否则,将前进方向顺时针旋转之后再计算下一步的移动位置。

        可以初始化两个辅助数组 dr = [0, 1, 0, -1],dc = [1, 0, -1, 0],方便根据方向求下个横纵坐标 [r, c]:r = r + dr[direct],c = c + dc[direct]。更新方向:direct= (direct+ 1) % 4;

    ② 也可以每次访问最外层,顺时针顺序输出,然后次外层等等。主循环中四个方向小循环,每次小循环到边界,把边界内缩,然后到下一个方向的循环。

     

    55. 跳跃游戏

    初始化最大可达下标 furthest = nums[0],对每个 i 检查是否小于max_loc(也就是可以到达),不能就返回 False。可达就 max_loc = max(max_loc, nums[i]+i)。循环正常结束返回 True。

    还可以用其他思想:(1)回溯。dfs,每个元素一直往前远跳,到不了就返回上一个元素换个落点。优化方面,优先选最大的步数,可以右到左的检查。时间复杂度 O(2n)。还可以保留记忆,存在字典,key 为下标,value 为是否可达。

    (2)动态规划。访问每个元素 i 时更新它能到的所有元素的dp,如果dp[i]为0说明不可达则不做更新。还有从右到左的动态规划。

    * 回溯通常是通过反转动态规划的步骤来实现的。

     

    56. 合并区间

    把 intervals 的元素按照区间的左边界排序:intervals.sort(key=lambda x: x[0])。

    while(i < len(intervals) - 1):    如果 intervals[i][1] >= intervals[i+1][0] 说明和下个区间相交,合并后新的左边界就是 i 的左边界,合并后右边界为:max(intervals[i][1], intervals[i+1][1]),这样就合并到了区间 i 中,然后删掉区间 i+1:intervals.pop(i+1)。如果没发生合并那么 i += 1 往后移动。

     

    57. 插入区间9

    newInterval 左边界依次与 intervals中的左边界比较,找到第一个 intervals[i][0] > newInterval[0] 的位置,intervals.insert(i, newInterval)。然后检查从 intervals[i-1] 到末尾,合并可以合的区间(和56题排序后一样)。

     

    58. 最后一个单词的长度

    从右向左遍历,从第一个不是空格的字符开始计数,计数开始再遇到空格就结束。

     

    59. 螺旋矩阵 II

    模拟过程。和54题差不多。

     

    60. 第k个排列

    阶乘数系统。比如一个四位数,每个最高位后面三位的排列共有3*2*1=6个,假如要找第10个数,因为1开头有6个排列,2开头有6个排列...所以第10个数以2开头。然后要找的是以2开头的,剩下三位排列中的第10-6=4个排列,而每个二位数排序有2个组合,所以下一个数为134中第二大的数也就是3。同样剩下的两位可得,最后排列为"2341"。

    每一位的后面几位组合个数为它的当前位数阶乘。为了方便计算,把阶乘保存在列表 factorials 中:factorials.append(factorials[i-1]*i)。

    k -= 1使之与下标0到n-1对应,候选列表nums初始化为1到n,用 i 倒序遍历 factorials 使阶乘顺序与下标对应。当前位为候选列表中第x位:x = k // factorials[i]。后面位的排列数 k -= x* factorials[i]。记录当前位:res.append(nums[x]),然后把对应数字从候选列表中移除: del nums[x]。

     

  • 相关阅读:
    个人管理:提高你的搜商
    敏捷个人:提供更多文档下载,并转载一篇敏捷个人读书笔记
    个人管理: 激励你的一句话
    敏捷个人 从Scrum实践来思考如何导入价值观
    信息系统开发平台OpenExpressApp 如何解决ComboBox.TextProperty绑定带来问题的来龙去脉
    敏捷个人 敏捷个人价值观,欢迎提出你的意见和你的价值观
    使用VS2010的CodedUI来做自己的自动化测试框架
    .Net4下的MEF(Managed Extensibility Framework) 架构简介
    IronRuby - 快速在半小时学习Ruby基础知识
    敏捷个人 项目网站文档页签增加blog链接
  • 原文地址:https://www.cnblogs.com/sumuyi/p/12484132.html
Copyright © 2011-2022 走看看