leetcode思路简述(1-30)
1. 两数之和
把遍历过的数放在字典,数字做key,序号做value。遍历时先查字典,target减它是否为字典中key。时间复杂度O(n)。
2. 两数相加
可把比较短的链表差的位数置零补充。
3. 无重复字符的最长子串
可以字典存储滑动窗口,[i, j) 遇到重复i+1否则j+1。
4. 寻找两个有序数组的中位数
关键是只要确定一个数组切点的位置,另一个也就确定了。通过二分判断切点左移还是右移。
中位数的左右两边数量相同,可知中位数分隔线左边数字的数量k=(len1+len2+1) // 2。
在nums1 中取m1个数,nums2 中取m2个数,一起作为中位分隔线的左边数字,m1 + m2 = k。
找到合适的m1与m2使用二分法。使用较短的数组进行二分,设置初始left和right为较短的数组的左右边界。每次使m1 = left +(right - left)//2,m2 = k - m1。
在二分过程中,如果 nums1[m1] < nums2[m2-1],则left = m1 + 1;否则 right = m1。直到left < right。这个过程与常规的二分法完全一致,只是左右区间进行选择时的条件不同。
二分循环结束,得到m1与m2。判断总长度是奇数还是偶数确定返回值。
5. 最长回文子串
① 中心扩散
② Manacher
6. Z字形变换
不要弄太复杂,遍历字符竖着返回到numRows个字符串中,触底反弹。最后把三行直接连一起。
7. 整数反转
取最后一位用%,去掉最后一位用//。
8. 字符串转换整数 (atoi)
可正则可if各种方法注意细节。
9. 回文数
直接翻转对比是否与原数相等。
考虑到溢出,可以只翻转一半。如何判断翻转到一半:当原始数字小于等于反转后的数字时,表示已经处理了一半的数字,对比两半是否相等(总长为偶数),或反转的一半//10后与另一半是否相等(总长为奇数)。
10. 正则表达式匹配 *
① 回溯:pattern第二位是*,则 (1) isMatch(text, pattern[2:]) (*前面字符匹配数为0)
或(2)first_match and self.isMatch(text[1:], pattern)) (第一位匹配,text看下一位,pattern不变)
第二位非*,则 first_match and self.isMatch(text[1:], pattern[1:]) (第一位匹配,两个都看下一位)
② 动态规划
将中间结果保存,dp[i][j] 表示 text 的前 i 个是否能被 pattern 的前 j 个匹配
11. 盛最多水的容器
双指针。由于水容量取决于较短的边,所以即使将长的边向内移动也不会增加水量,因此移动短边。
12. 整数转罗马数字
把特殊的9啊4啊什么的都加到罗马字符中,然后每次取可取的最大数。
13. 罗马数字转整数
除了每次看i和i+1的方法之外,也可以:若上一个字符比当前小,则减去二倍较小字符。以上两题可用字典。
14. 最长公共前缀
在最简单的做法基础上,还有二分法,字典树等进阶方法。
15. 三数之和
先排序。令i为当位置,找到nums[i]与另外两数之和为0(如果i与i-1的值相同则跳过以去掉重复组合),设置左右指针L=i+1,R=n-1。
若nums[i]+L+R等于0则保存这组并LR同时向中间移动(跳过相同值)。若和大于0:R左移;若和小于0:L右移。
16. 最接近的三数之和
与上题相似,就是大于小于0变为target。
17. 电话号码的字母组合
开始写的每轮循环ans要append数字对应的每一字母,pop它前面短的单词。回溯传值为整个ans,然后下一个再加一个字母这样。
其实可以对数字对应的每一字母回溯,只传单个的单词,到了数字长度为0时最后才append。
18. 四数之和
在三数之和上改改,左右边界为i,j 。i从0开始往后,内层循环j从len-1往前,且 i - j >= 2。中间双指针。这里去重不能遇到重复则多移动一位,这样仍然会重复(不过可以减少循环次数),可以在append前用not in判断下,因为有序的如果重复的话数字顺序一定相同。
另外有一些优化:(i, L, R, j)
(1) 如果 nums[i] + 3 * nums[i + 1] > target 则break;
(2) 如果 nums[i] + 3 * nums[-1] < target,那么当前的 nums[i] 加其余三个数一定小于 target,i直接continue看下一个;
(3) 同理,j可同样优化。
最后也看到有做法把前两层循环ij的顺序设置为(i, j, L, R),可以直接通过遇重多移一次避免重复的问题,其他应该差不多一个意思。
19. 删除链表的倒数第N个节点
先一个指针p遍历,经过第n个节点时,另一指针pre从head开始一起遍历,p.next为空时,pre.next=pre.next.next。
注意细节,若count<n,就是删除第一个节点,直接返回head.next即可。
20. 有效的括号
栈。用字典左括号右括号匹配可以少写点if。最后记得判断栈是否为空。
21. 合并两个有序链表
。。。
22. 括号生成
左括号数left,右括号数right,回溯若left+right == n则append,若left > right: 加一个右括号;若left < N: 加个左括号
23. 合并K个排序链表
分治(Nlogk),N为所有链表节点数之和。取两个pop(0)链表,合并,append,直到len(lists) == 1
24. 两两交换链表中的节点
递归;迭代。
代码美观真的很重要呀。
25. K 个一组翻转链表 *
① 用栈。每次k个进栈,然后出栈 p.next = stack.pop()。剩下不到k个则直接连。
② 尾插法。依次把cur移到tail后面
③ 递归+尾插。把cur移动到第k个,然后 cur = self.reverseKGroup(cur, k),再翻转前k个
26. 删除排序数组中的重复项
① 双指针。i遍历所有,若i和j对应的值不同,nums[j]=nums[i],j+1。就是把不相同的值移到前面,最后只返回前j个。
② 把上个值保存到pre里,i遍历,若相同则pop(i),不同则pre = nums[i]。
27. 移除元素
和26差不多。快慢指针可。还可以将当前与末尾元素交换,减少复制次数,相同则nums[i] = nums[n],n--,不同则i++。
28. 实现 strStr()
① 暴力。避免不必要的遍历,
② KMP。
1. 根据子串字符串构造一个next部分匹配表。
2. 遍历主串字符串,当匹配失效时,查询next部分匹配表定位子串接着与主串比较的位置。
使用双指针遍历,i指针遍历子串,如果没有相等元素,j指针保留在头部,如果遇到相同元素,j指针后移,当元素再次不相同时,j指针回到头部。可以看到,其实i指针后缀,j指针前缀,实现前后缀相同元素的计数。
构造next,从字符串的第一位(不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。
构造好子串的next表后,i指针遍历主串,当遇到子串首元素时,i,j同时前进,当匹配失效时,查找next表中当前元素的值,将j指针移动到该处,回溯就是 i = next[i]。
29. 两数相除 *
每次除数加倍,商加倍。当被除数小于除数的时候,商与之前的累加,除数和商还原为初始值,然后继续倍增。
30. 串联所有单词的子串
首先肯定是要用一个len(words) * words[0]大小的窗口遍历。然后是对截取的每个窗口进行判断:
① 先把所有单词存到 dict1 = Counter(words),扫描窗口,如果当前单词在dict1 中,就把该单词存到新的 dict2中。若dict2中value大于dict1的,break,看下一个窗口;若小于等于,看窗口的下个单词。子串所有单词判断完表示符合,append。
② 改进版。使窗口每次移动三位(前三位都是窗口起点,分别遍历)。有三个改进点(可用双指针实现,left指向子串开头,right指向子串当前检查的单词右边):
(1)出现不匹配单词,下一个子串从不匹配的单词之后开始。
(2)出现匹配的单词,但是次数超了,比如foo多了一次,则从foo在子串第一次出现的位置之后开始下一个子串,把前面的单词在字典中去掉。实现起来的话,while(dict2[w] > dict1[w]),移动left即可,left移动时顺便修改字典。
(3)当上一个子串匹配成功时,下一个dict2不需清空重来,只需要把之前第一个单词去掉,再检查新的最后一个单词。
* 用字典可以避免纠结子串的单词顺序;对于算法优化,去分析哪些判断是不必要的,改进。