为了捡回丢掉的数据结构和算法知识,也为了加强自己python编程的能力,LeetCode,Go!
应该想到的:
0、注意审题,题目给出限定条件了就不要去浪费时间了,题目给出了提示要记得挖掘
1、快慢指针破循环
2、顺序数组,二分查找
3、二叉搜索树,查找和更新都是logN,其中序遍历是顺序数组
4、注意看数字范围,巧用数组的下标做文章,还有装桶的操作
5、多个组合或者情形不能慌,可以准备列好,其实没啥变换,如题13
6、巧用栈、队列、堆、哈希表完成一些操作
征途地图:
How to effectively use LeetCode to prepare for interviews!!
Easy
13. Roman to Integer
虽然有很多组合的可能,但是注意审题发现,组合最多两个字母,那干脆把所有可能的组合都放到字典里,然后用get
其中唯一的trick就是c.get(a,b)的意思是,如果c中有a就返回a,否则返回b
1 | d = {'I':1, 'IV':3, 'V':5, 'IX':8, 'X':10, 'XL':30, 'L':50, 'XC':80, 'C':100, 'CD':300, 'D':500, 'CM':800, 'M':1000} |
20.Valid Parentheses
看到题目我还洋洋自得,这个我记得,用栈,然后就开始准备实现栈结构。。结果看了眼题解,发现太强了,虽然是用栈。。。但是用一个字典建立hash表,用左括号当key,对应的右括号为value,结合列表,就完成了匹配。。
基本思想就是,如果是左括号,那就一律入栈,如果是右括号,直接看当前栈顶的括号是否是对应的,如果不是就无法出栈,那就报错,否则就直接退栈,下一个
还有,可以用not stack判断列表是否为空。。。
1 | def isValid(s: str) -> bool: |
21. Merge Two Sorted List
1 | # Definition for singly-linked list. |
141. Linked List Cycle
1 | class ListNode: |
哈希表解法:
1 | def hasCycle(head): |
快慢指针:
1 | class Solution: |
155. Min Stack
最小栈,一是实现栈,二是找出最小值。最简单就是开一个列表,一个数字记录栈顶,然后找最小就遍历,但是明显这个方法费时间,不中;
辅助栈
那就可以用另一个列表同步记下相应元素时的最小值,也就是同步辅助栈
当然辅助栈也可以不同步,即只在最小值发生变化的时候更新,然后出栈元素是最小值的时候就一起出栈,注意最小值栈在出现与最小值相等元素的时候也要入栈,因为出栈是一起的,入栈如果不入,就弹出去一个了
当然,不出意料嘛,看了题解,又整活了。。。
辅助栈的方法其实是用空间换时间,还是有方法只用一个栈就完成了的,其实记录最小值没问题,关键在于当最小值更新时,如何保存更新前的最小值,否则最小值出栈后就无法找到了,下面两个解法,其实就是想方设法记录下最小值的信息
older-最小值入栈
当最小值更新时,将之前的最小值入栈,保存现场!
入栈值减去最小值
把最小值的信息刻在每一个进栈元素的身体里!
当然还有一个,leetcode上觉得很厉害,但是其实一开始就很直接想到的,用链表呗,节点除了data和next,多定义一个min,头插法实现栈,再用min记录下最小值,其实这个类似于辅助栈啦,只不过双栈合一进化成了小链表而已~
详细的题解:
详细通俗的思路分析,多解法
169. Majority Element
寻找数组中过半的众数,一看到题目感觉就是用hash,就能保证O(n),但是看了题解发现不仅一种思路。。
1.hash
首先是hash,虽然猜到了,但我想的是用字典,但是忘了可以用python的collection,不过不知道实际面试或者考试的时候是否让使用
另外,得到哈希表之后,如何找出具有最大values的键呢,可以通过max函数,max(counts.keys(), key=counts.get)即通过counts.keys()返回所有键,然后对这些键进行排序,排序的原则即由key=counts.get这句定义,即通过counts.get(key)得到的value进行排序,由此得出最大value的键
1 | class Solution: |
2.排序
排序后在n/2地方的元素,道理显而易见,排序完,众数在一起,而个数有超过了n/2,所以n/2点的值必为众数
1 | class Solution: |
3.随机化抽取候选
随机抽取数值然后遍历看是否满足要求,需要分析具体的复杂度,一开始我想的为什么不按顺序选,这样避免了随机抽取有可能脸黑一直抽不到众数,但是一想,因为众数的个数较多,那么抽取的时候按照概率来说是更容易被抽到的,所以随机的方式更好(但是,真的认真想想,还是按顺序抽选好啊,因为数组的初始顺序也就是随机的啊,那按顺序也是更有可能提前找到的吧
1 | import random |
4.分治
哈,没想到吧。。是的,分治又出现了,其实回头总结反思一下,好多获取数组或者链表信息的题目都可以用到分治,比如找极值,就很典型,这个也是,其实如果能够找出子问题在全局和部分都适用,且能够进行方便的局部比较,分治就应该挺有效的,给自己提个醒。接下来继续讲解法,其实解法一目了然了,不断二分直到一个元素,合并的时候看左右众数是否相等,如果相等就保留,不同就选出现次数多的那个,然后一步步再回来
1 | class Solution: |
5.Boyer-Moore 投票算法
嗯,这个当时考研的时候记得有,但很惭愧我果然忘了
简单理解,因为我们的众数的大于数组元素的1/2的,那么如果哪怕其他所有元素联合起来,和我众数硬碰硬,极限一换一,那么我还是留下至少一个的,基于这个朴素思想,操作开始了:
我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0;
我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x:
如果 x 与 candidate 相等,那么计数器 count 的值增加 1;
如果 x 与 candidate 不等,那么计数器 count 的值减少 1。
在遍历完成后,candidate 即为整个数组的众数。
我们举一个具体的例子,例如下面的这个数组:
1 | [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7] |
在遍历到数组中的第一个元素以及每个在 | 之后的元素时,candidate 都会因为 count 的值变为 0 而发生改变。最后一次 candidate 的值从 5 变为 7,也就是这个数组中的众数。
当然,这个算法还有更详细的推导:
Boyer-Moore 算法的正确性较难证明,这里给出一种较为详细的用例子辅助证明的思路,供读者参考:
首先我们根据算法步骤中对 count 的定义,可以发现:在对整个数组进行遍历的过程中,count 的值一定非负。这是因为如果 count 的值为 0,那么在这一轮遍历的开始时刻,我们会将 x 的值赋予 candidate 并在接下来的一步中将 count 的值增加 1。因此 count 的值在遍历的过程中一直保持非负。
那么 count 本身除了计数器之外,还有什么更深层次的意义呢?我们还是以数组
1 | [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7] |
作为例子,首先写下它在每一步遍历时 candidate 和 count 的值:
1 | nums: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7] |
我们再定义一个变量 value,它和真正的众数 maj 绑定。在每一步遍历时,如果当前的数 x 和 maj 相等,那么 value 的值加 1,否则减 1。value 的实际意义即为:到当前的这一步遍历为止,众数出现的次数比非众数多出了多少次。我们将 value 的值也写在下方:
1 | nums: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7] |
有没有发现什么?我们将 count 和 value 放在一起:
1 | nums: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7] |
发现在每一步遍历中,count 和 value 要么相等,要么互为相反数!并且在候选众数 candidate 就是 maj 时,它们相等,candidate 是其它的数时,它们互为相反数!
为什么会有这么奇妙的性质呢?这并不难证明:我们将候选众数 candidate 保持不变的连续的遍历称为「一段」。在同一段中,count 的值是根据 candidate == x 的判断进行加减的。那么如果 candidate 恰好为 maj,那么在这一段中,count 和 value 的变化是同步的;如果 candidate 不为 maj,那么在这一段中 count 和 value 的变化是相反的。因此就有了这样一个奇妙的性质。
这样以来,由于:
我们证明了 count 的值一直为非负,在最后一步遍历结束后也是如此;
由于 value 的值与真正的众数 maj 绑定,并且它表示「众数出现的次数比非众数多出了多少次」,那么在最后一步遍历结束后,value 的值为正数;
在最后一步遍历结束后,count 非负,value 为正数,所以它们不可能互为相反数,只可能相等,即 count == value。因此在最后「一段」中,count 的 value 的变化是同步的,也就是说,candidate 中存储的候选众数就是真正的众数 maj。
1 | class Solution: |
复杂度分析
时间复杂度:O(n)O(n)。Boyer-Moore 算法只对数组进行了一次遍历。
空间复杂度:O(1)O(1)。Boyer-Moore 算法只需要常数级别的额外空间。
题解:
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/majority-element/solution/duo-shu-yuan-su-by-leetcode-solution/
229. Majority Element II
因为是n/3,且个数是0-2个,不确定,所以很多性质丢失了,上面的方法不一定全能用,比如排序、分治,主要还是推荐BM投票
202. Happy Number
快乐数,我真的是,快乐不起来了,一个是在做这个题的时候突然想起来了一个知识点,百度到了一个博客进去因为博客很漂亮又顺便看了看博客内容结果了发了好多的感慨妈的
还有一个就是,一开始我题目没看懂,然后看了题解,我人又傻了,日常跪真的是,这个竟然是用快慢指针做的。。。
快慢指针破循环,可太强了
而还有个大佬,用你妈的什么魔数,我真是人都傻了,那起止是魔数,就尼玛魔术。。。
正常人的-朴素的-集合
集合,得到一个往里放,如果集合中出现重复的,那必定循环。
1 | class Solution: |
虽然方法简单,但是这个题解,语法糖又太酷了。。。
快慢指针
魔数
https://leetcode-cn.com/problems/happy-number/solution/ni-ting-shuo-guo-mo-shu-ma-by-jemmyyu/.
还有离谱的递归解法
https://leetcode-cn.com/problems/happy-number/solution/python-1xing-by-knifezhu-9/
为什么说离谱呢,因为除了上面的魔数,就是和我一样不快乐的数最后都会进入4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4 的循环中,而作者还分析出:已知规律:[1-4]中只有1是快乐数,[5~∞]的数字要么回归到1要么回归到4或3,因此可以递归使用判断,直到n小于4且不等于1,则不快乐,如果是1,那指定快乐啊
其中涉及到了尾递归,尾递归为啥能优化?
204. Count Primes
筛出范围内质数的个数,最朴素的想法,肯定是挨个找然后再挨个判断,那复杂度就是O(n^2),再多想一点就是大一学C的时候就记过其实只要筛到sqrt(n)就够了,因为如果存在能整除的数,肯定一个小于等于sqrt(n),一个大于等于sqrt(n),那如果sqrt(n)内没找到,那这个数就是素数了。
接下来,真正的解法:埃拉托色尼筛选法(the Sieve of Eratosthenes)简称埃氏筛法,简单来讲就是逆思维,排除法,如果一个不是素数,存在能够整除的数,那么也就能通过整除的数经过相乘得到,那我们可以找到素数后,乘以1/2/3/4/5..直到<=n,那这些都不是素数。
那么,从实现上,就开一个等长数组,先全部置为素数,然后找到一个素数种子就开始乘1/2/3…,得到的结果置为非素数,最后再遍历数组据标记位统计素数个数。
接着,还是要优化哦,首先是筛选素数种子的时候,范围可以缩小为2-sqrt(n)+1,然后是得到素数种子开始种树的时候,这个比较明显,3x2的结果在之前2x3的时候就判断过啦,所以种子的播种范围不该是2,可以从i*i开始,然后接着ix(i++),而体现出来的也就是等间隔往上加,那么如果就python就应该反映过来可以用列表的切片来当语法糖,so:
1 | def countPrimes(n: int): |
这个算法的复杂度:
该算法的时间复杂度比较难算,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是:
n/2 + n/3 + n/5 + n/7 + … = n × (1/2 + 1/3 + 1/5 + 1/7…)
括号中是素数的倒数。其最终结果是 O(N * loglogN),有兴趣的读者可以查一下该算法的时间复杂度证明。
上述题解参考:
https://leetcode-cn.com/problems/count-primes/solution/ru-he-gao-xiao-pan-ding-shai-xuan-su-shu-by-labula/
和
https://leetcode-cn.com/problems/count-primes/solution/pythonzui-you-jie-fa-mei-you-zhi-yi-liao-ba-by-bru/
事实上,这类题目是一个小领域,叫做素数筛法,有更快的筛法,看有大神提到线性的欧拉筛和亚线性的Atkin筛、Wheel筛,希望以后有机会学习吧。
242. Valid Anagram
有效的字母异位词,看到就想到了,哈希,如果限定了是字母,那就直接开26个空间,对比一下;有一个trick是,不需要开两个26空间然后比对,可以开一个,然后字符串1出现的字母++,字符串2出现的字母—,最后看是否归零;
哈希表之单数组
1 | def isAnagram(s,t): |
哈希表之集合
1 | def isAnagram(s,t): |
哈希表之collection
1 | from collections import Counter |
另外,看了题解也可以先对字符串字母排序,排序完比对,感觉也挺快的;
排序后比对
1 | def isAnagram(s,t): |
371. Sum of Two Integers
题目很简单,简单到我傻眼,当然,我的傻眼是指他妈这题咋做啊,求和,然后不能用加减法
好吧,改为二进制通过位运算算出结果
1 | def getSum(a, b): |
上述是单纯版,其他语言类似逻辑就搞定了,但是在python里不行,因为:
Python太厉害了,嗯,他的int不是32位的,他的位数是动态增长的(可以下看面代码)
1 | sys.getsizeof(0) |
也就是说咱们左移的话不是溢出的,而是增加位数的,这就需要我们手动对 Python 中的整数进行处理,手动模拟 32 位 INT 整型。
具体做法是将整数对 0x100000000
取模,保证该数从 32 位开始到最高位都是 0。
所以需要深思熟虑版:
1 | def getSum(a, b): |
原码、反码、补码知识详细讲解(此作者是我找到的讲的最细最明白的一个)
好沉重,好难,所以,
还有滑稽版
1 | def getSum(self, a, b): |
。。。。。。
88. Merge Sorted Array
合并两个排序的字序列
合并后排序
可以直接合到一个序列里再排序,时间复杂度O( (m+n) lg(m+n) )
双指针合并——边插入边排序
就和归并排序里的合并两个序列一样,只不过这里因为是直接修改nums1,所以需要额外开个空间,
所以基本思路:双指针合并,需要O(n)个空间,时间复杂度就是m+n
但是看了题解有一个不需要额外空间的高级版:
双指针倒序合并,不需要额外空间
就是倒着插入,因为第一个子序列的末尾都是0,所以先插入这些,就不会占用空间了
1 | def merge(nums1, m, nums2, n): |
108. 将有序数组转换为二叉搜索树
中序遍历二叉搜索树出来的就是排序好的数组,所以用中序遍历的方式对数组进行建树就好
1 | # Definition for a binary tree node. |
189. Rotate Array
字符串平移,可以切片,可以通过反转
切片
1 | def rotate(nums,k) -> None: |
字符串反转
1 | def rotate(nums,k) -> None: |
辅助变量
要么把一部分变量取出,之后放回,要么直接找个新的数组存值
下面是明明输出对了,但是一直提示num没修改,我他妈百思不得其解为啥。。。
1 | from copy import deepcopy |
205. Isomorphic Strings
首先想到的就是隐射,利用字典的一一对应和集合的排除重复项:
1 | def isIsomorphic(s,t) -> bool: |
226. 翻转二叉树
左右交换,递归调用就好
1 | # Definition for a binary tree node. |
448. 找到所有数组中消失的数字
简单的就类似桶排序,有的就进去消掉,但是空间复杂度是O(n)
厉害的就是不使用额外的空间复杂度,有一个题解说的很好,首先要从题目中挖掘条件,比如说数组是小于n的,那么就联想到用下标,所以就是在第一轮遍历的时候对出现过的数字所对应的下标做一些小标记,同时又不影响遍历,然后第二次遍历就可以没出现过的挑出来了
能用方法2解答必定是有限制条件,比如前面说了1到n,然后在限制条件上做文章 套路是 在首次遍历的时候做点小动作,但是不影响首次遍历. 在第二次遍历的时候根据第一次遍历的小动作就能区别出某些元素是否符合题意. 你可以看下这个,我加了点注释,这个和官方用 * -1 是一样的
1 | public List<Integer> findDisappearedNumbers(int[] nums) { |
572. 另一个树的子树
首先定义一个函数,判断两棵树是否相同,其条件就是根相同,左右俩儿子相同,即取 and
再定义一个函数,寻找是否存在这样的子树与待匹配树相同,那就通过遍历,然后取 or
1 | # Definition for a binary tree node. |
当然还有好玩的办法啦,一个就是还想到过的,一个遍历把树和待匹配子树都变成字符串,然后比对待匹配树的字符串是否为当前树的子串,这里需要注意两个点,第一个trick是,一个树的先序遍历即DFS的输出中子树就是连续的,第二个trick是,还需要额外的判断,因为存在空节点,如假设 s由两个点组成,1是根,22 是1的左孩子;t也由两个点组成,1是根,2是1的右孩子。这样一来s和t的 DFS 序相同,可是t并不是s的某一棵子树,所以可以通过lnull和rnull来标记左右两空孩子,再来遍历
转换完后就是字符串匹配了,那可以暴力或者仍有印象的kmp了
还有一种是树哈希
557.Reverse Words in a String III
句子中的单词分别倒序,一想到就可以通过python切片的[::-1]的特性,但从数据结构的角度,这种逆序输出,应该马上想到栈,是的可以把单词压入栈然后出栈,实现单词的逆序,但是要注意出栈的条件即单词结束的标志
1 | def reverseWords(s) -> str: |
589. N叉树的前序遍历
递归遍历比较简单嘛,就是自己,然后儿砸,要注意的是,不是一个个吐,要开一个list存
迭代那也类似,
1 | """ |
605. Can Place Flowers
1 | def canPlaceFlowers(flowerbed,n) -> bool: |
直接判断自身和上一个、下一个的数值
也可以你通过前后补零的方式,使得头尾统一
617. 合并二叉树
递归或者是迭代的
1 | # Definition for a binary tree node. |
665. 非递减数列
是很简单,但是要注意特殊情况! 首先比较的时候都是比较num[i]与num[i-1],那么可以把自己变大和前面一个一样大,也可以把前面的变小和自己一样大,所以是有两种可能,都要去尝试,而如果这两种不行,哪怕只有一个数字出现了这个问题,但是怎么试都没用的,所以注意count计数底下的外加判断
1 | class Solution: |
669. 修剪二叉搜索树
给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。
资料:二叉查找树与平衡二叉树
关键点:二叉搜索树的删除节点:
- 如果删除的是叶节点,可以直接删除;
- 如果被删除的元素有一个子节点,可以将子节点直接移到被删除元素的位置;
- 如果有两个子节点,这时候就采用中序遍历,找到待删除的节点的后继节点,将其与待删除的节点互换,此时待删除节点的位置已经是叶子节点,可以直接删除
题解:题解其实提示了,可能需要修改根节点,那先判断根节点的值,如果根节点大于R,那根节点和其右边的子树都没了,那就先把左子树作为根节点,如果根节点<L,那就根节点和左边的子树的都没了,先把右子树作为根节点,如果L<=root<=R,那就先保留根节点,然后继续遍历左右子树。大框架上呢,其实还是按照往常关于树的问题,每一颗子树也是一棵树,那么就可以递归地去做,也就是递归的地判断当前的根节点是否合适
1 | # Definition for a binary tree node. |
674. 最长连续递增子序列
挨个向前比较,如果大那就继续+1,并且存下当前最长的,如果不递增了,置1
1 | def findLengthOfLCIS(nums) -> int: |
记录最长子序列的下标anchor,计算差值 i- anchor +1
1 | def findLengthOfLCIS(nums) -> int: |
703. 数据流中的第K大元素
排序,或者利用python的小顶堆
1 | import heapq |
test:
1 | nums = [4,5,8,2] |
705. 设计哈希集合
1、单独链接法
按照hash的求模,分桶,然后桶里面可以用列表,但是列表的插入删除是O(N),所以建议采用链表,这样插入删除的复杂度就是O(1)了
先在hashset里定义合适的桶,合适的定义是较大且最好为质数,如题解建议的769,然后定义题目要求的函数操作
在底下定义好桶的数据结构,定义其基本操作,然后再定义单个节点的链表结构,即地址与值(其实应该是反过来)
可以改进的:虽然插入删除的复杂度是O(1),但是查找的时间复杂度是O(N),而查找速度快的结构可能更新速度慢,如排序列表,所以查找的复杂度和更新的复杂度都需要考虑,可以采用查找,删除,插入操作时间复杂度都为O(logN) 的二叉搜索树(二叉查找树)
题解参考:设计哈希集合
852. 山脉数组的峰顶索引
注意审题,确定是山脉的数组,所以最简单就是顺序查找最大的
改进方案是二分查找,应该要想到在已经排序的数组里,最快的肯定是二分
1 | class Solution: |
1 | class Solution: |
1160. 拼写单词
python自带的collections模块的Counter函数,或者通过26字母打表,注意ord为取char字符ascii码的函数,
1 | import collections |
二叉树模板
1 | # Definition for a binary tree node. |