1. 写在前面
这篇文章开始, 准备回归基础数据结构的复习了,主要包括数组,哈希表,链表,字符串,栈与队列的相关题目了,这边的题目相对于前面的算法层面上的那些,会稍微好想一些, 难度会下降,并且有一些很关键的思想,一般会默写了之后就可以解题。比如双指针法, 滑动窗口, 单调栈啊等等。 这篇文章复习数组和哈希表,其实感觉哈希表就是一种辅助工具来解决数组题目,数组这边常用的方法思想:重建数组, 双指针, 滑动窗口, 常用的工具,字典,集合。
关于Array, 我们需要知道的知识点:
- Array常见的操作: 元素的增, 删, 改, 查和移动
- Array访问数组时间复杂度O(1), 插入和删除时间复杂度O(n), 内存连续
- 比较经典的题目一般就是考察元素的合并, 交换, 添加, 删除等, 数组的遍历
- 这里涉及到的一些思想:
- 两指针一前一后往中间遍历, 这个可以进行数组逆序
- 两指针从前面往后遍历, 可以进行符合特定条件元素的筛选
- 重建数组思想
- 滑动窗口
- 空间换时间
- 哈希存储
- 逆序遍历思想
- 数组的优缺点(要掌握一种数据结构,就必须要懂得分析它的优点和缺点)
- 数组的优点在于:构建非常简单, 能在 O(1) 的时间里根据数组的下标(index)查询某个元素
- 数组的缺点在于: 构建时必须分配一段连续的空间、查询某个元素是否存在时需要遍历整个数组,耗费 O(n) 的时间(其中,n 是元素的个数)、删除和添加某个元素时,同样需要耗费 O(n) 的时间
所以,当你在考虑是否应当采用数组去辅助你的算法时,请务必考虑它的优缺点,看看它的缺点是否会阻碍你的算法复杂度以及空间复杂度。
2. 题目思路和代码梳理
2.1 重建数组的思路
- LeetCode283: 移动零: 重建数组的思想, 把当前的数组看成一个空数组,然后用一个指针
non_zero_index
始终指向新数组的末尾(初始为0)。 然后遍历当前数组, 如果不是0, 就加入到空数组中,同时non_zero_index
后移,始终指向新数组的末尾。这样遍历完了之后,non_zero_index
后面的都变成0即可。代码如下:
- LeetCode27: 移除元素: 依然是重建数组思想, 把当前的数组看成一个空数组,然后用一个指针
non_target_index
始终指向新数组的末尾(初始为0)。 然后遍历当前数组, 如果不是target, 就加入到空数组中,同时non_target_index
后移,始终指向新数组的末尾。这样遍历完了之后,返回non_target_index
即可。
- LeetCode26: 删除排序数组中的重复项: 重建数组的思想, 把当前的数组看成一个空数组,然后用一个指针
non_repeat_index
始终指向新数组的末尾(初始为1)。 然后遍历当前数组(从1开始), 如果不和前面一个数相等, 就加入到空数组中,同时non_repeat_index
后移,始终指向新数组的末尾。这样遍历完了之后,返回non_repeat_index
即可。
在原数组上新建数组的思想很重要, 非常适合那种顺序表中删除不符合条件的元素的题目, 比如删除重复元素并且保证位置不变, 删除负数, 删除0元素, 再拔高一层,这其实用的是逆向思维, 不是让删除0吗? 我不直接找0,然后删除它,而是找不是0的去保留, 不是让删除重复吗? 不会找的重复的然后删除,而是找不重复的保留。好好体会体会哈。
2.2 双指针
-
剑指offer21: 调整数组顺序使奇数位于偶数前面: 左右指针的经典演示, 那两个指针指向左右两边,左边的指针负责找偶数,右边的指针负责找奇数,然后两者交换,再对向遍历即可。
-
LeetCode11: 盛最多水的容器: 这个暴力的话就是两层for循环, 求两两柱子围成的面积,这显然效率太低。 所以这里用双指针的解法, 首先得知道两个柱子围成的面积咋算:
min(柱子高) * (两根柱子之间的距离)
, 双指针的做法是一左一右指针,从两边向中间走,这个题目的核心就是决定面积大小的取决于两个柱子里面的最短的那个(木桶效应),当两个指针指向的柱子的面积算完了之后,比较下他们的长度,较短的那边指针移动(如果是i指的就右移, j指的就是左移)。为啥呢? 因为短的那边无法再优化了呀, 如果此时不动短的,再移动高的,那只剩下两根柱子的距离越来越小,那面积就更小了。 只有移动短的那边,虽然两者的距离短了,但可能还可以从高度上找回来。 代码如下:
-
LeetCode15: 三数之和: 这个题里面的双指针用的很是巧妙。首先先把数组从小到大排序, 然后设置指针k开始从头遍历元素, 对于每一次遍历,分别设置i, j指针指向k后面剩下的元素的首尾,然后进行判断,如果三个指针指向的元素等于0, 保存结果。 否则,如果三者之和小于0, 那么i向后移动。 因为i指向的元素小了导致的总体之和小(又是构造木桶效应), 如果三者之和大于0, j往左移, 因为是j指向的元素过大导致的总体之和大。 这样当i, j相遇, 当前k的所有组合已经遍历完毕, 往后移动k, 重复上面的步骤。 但是这个过程里面要注意的就是避免重复元素。怎么避免重复呢? 因为我们是先把数组从小到大排序了,那么如果发现当前的k指向的元素和前面k-1相等,就跳过去,说明前面已经找完了这种情况。所以该题的关键点从小到大排序,定住其中一个极端, 看另外两个。代码如下:
-
LeetCode16: 最接近目标和的三数之和: 和上面这个就非常类似了,只需要知道最接近的然后返回就可以了。
-
LeetCode18: 四数之和: 相比较于三位数之和来说,四位数之和这里会多加一层循环,让m下遍历每一个数,对于m遍历的每一数, k遍历当前m后面的所有数,然后i, j两个指针指向k后面数组的首尾, 然后按照上面的那种逻辑进行判断,如果四个数加起来大于target, i后移, 小于target, j前移, 等于target,保存结果,i后移,j前移。 但这里也必须要去重操作, 且去重操作需要每一个指针的地方都需要去重。 最后的代码如下:
三数之和和四数之和中,排序非常重要。 -
LeetCode88:合并两个有序数组: 这个题可以使用两个指针从后往前遍历数组, 这是数组和链表的不同之处, 链表不知道最后的尾部, 但是数组知道, 从后往前, 如果哪个数大, 就放入到最后面, 这时候, 如果nums2有剩余, 直接插入到nums1的最前面, 如果没有剩余, 就说明已经完全插入到nums1了。 代码如下:
-
LeetCode4: 寻找两个正序数组的中位数: 这个题目的第一种思路就是上面这个,先把两个数组合并起来,然后再找中位数, 但显然,这个的时间复杂度 O ( m + n ) O(m+n) O(m+n),不符合题目的 O l o g ( m + n ) Olog(m+n) Olog(m+n)的要求,所以这里给出另一种思路。 这种思路不仅是求解中位数,而是解决两个正序数组中找第K大的数的方式,中位数无非是这里面的一种而已。 两个有序数组中寻找第K小元素思路:
- 要找到第k(k>1)小的元素, 那么就取
pivot1=nums1[k//2-1]
和pivote2=nums2[k//2-1]
比较- nums1中小于等于pivot1的元素
nums1[0, 1, ...k//2-2]
, 共k//2-1
个 - nums2中小于等于pivot2的元素
nums2[0, .....k//2-2]
, 共k//2-1
个
- nums1中小于等于pivot1的元素
- 取
pivot = min(pivot1, pivot2)
, 两个数组中小于等于pivot的元素不会超过(k/2-1) + (k/2-1) <= k-2
个 - 这样pivot本身最大也只能是第k-1小的元素
- 如果pivot=pivot1, 那么
nums1[0, ...k//2-1]
都不可能是第k小的元素,把这些元素删除,剩下的作为新nums1数组 - 如果pivot=pivot2, 那么
nums2[0, ...k//2-1]
都不可能是第k小的元素,删除,剩下的作为nums2数组
- 如果pivot=pivot1, 那么
- 由于删除了一些元素(这些元素都比第k小的元素小), 因此修改k的值,减去删除数的个数
这个更像是一个模板,找第k小。中位数的话无非就是两个数组长度之和的中间小的数。 不过这里要分和为奇数和偶数的情况。
- 要找到第k(k>1)小的元素, 那么就取
2.3 旋转模拟
-
LeetCode189: 旋转数组: 这个题的思路比较巧妙的一种方式就是三次逆序搞定, 首先将整体元素逆序, 然后将 1 1 1到 k k k的元素逆序,最后将 k k k~ l e n ( n u m s ) len(nums) len(nums)的元素逆序即可。但是这个题有个小陷阱就是如果 k k k大于了数组的长度,需要先取余。
-
LeetCode48: 旋转图像: 这个题两步: 矩阵转置+逆序。
-
LeetCode54: 螺旋矩阵:这个是数组的遍历模拟过程题目,不涉及什么算法,但考察代码的掌控能力。螺旋向内遍历矩阵, 和动规一样,这里也是要考虑四部曲,分别是起始位置,移动方向,边界和结束条件。拿这个题目分析下:
- 起始位置: 这个遍历起点是矩阵的左上角,也就是(0,0)位置
- 移动方向:每一圈都是先向右走到头,然后向下走到头,再向左走到头,再往上走到头。 所以对于每一圈,方向是一致的,就是右 -> 下 -> 左 -> 上。
- 边界: 这个是本题的核心,因为每一圈,这个边界是会变化的,规则是如果当前行(列)遍历结束之后,就需要把这一行(列)的边界向内移动一格。, 所以这个题在走的时候,要时刻控制好边界,这是解决关键
- 结束条件:螺旋遍历的结束条件是所有的位置都被遍历到。
看代码:
-
LeetCode59: 螺旋矩阵II: 这个题目和上面这个基本上一模一样, 只需要简单的改下代码就可以,因为上面这个题目时给定了矩阵,让螺旋遍历输出值, 而这个题目是螺旋遍历去构建矩阵, 所以在存储结果方面会有些区别。代码如下:
-
LeetCode885: 螺旋矩阵III:这个题目和上面的两个区别就挺大了,第一个就是起点不固定,第二个是边界的话得通过走的步子确定。拿上面的四部曲分析下:
-
起始点: 这个是题目里面会给定(r0,c0)
-
移动方向,依然是右 -> 下 -> 左 -> 上
-
边界条件,这个边界是动态改变的,可以按照圈进行划分下
- 第一圈, 从(r0,c0)向右走了1步, 下走了1步, 向左走了2步, 向上走了2步,到了(r1,c1)
- 第二圈, 从(r1,c1)向右走了3步,下走了3步, 向左走了4步,向上走了4步,到了(r2,c2)
- 第三圈,从(r2,c2)向右走了5步,下走了5步, 向左走了6步,向上走了6步,到了(r3,c3)
- …
由此我们可以发现规律,每一次循环中,向右和向下走的步数相同,向左和向上走的步数相同比向右多走1步。所以我们需要定义一个step去控制边界变化。
-
终止条件:这里可以用走过点的个数去控制
代码如下:
-
-
LeetCode1823: 找出游戏的获胜者: 周赛的一道题目,这是第一次参加周赛,约瑟夫环的问题, 这个我当时纯模拟做的。
-
LeetCode498: 对角线遍历: 这个题是矩阵遍历的一个模拟题目, 美团的面试还考过,矩阵的对角线遍历的逻辑还是得知道的,思路重点把握下面几条规律:
- 每一条对角线上横坐标和纵坐标的总和不变, 并且每一条对角线都是总和加1,也就是递增, 总和会从0开始增到 m + n − 2 m+n-2 m+n−2, m m m是矩阵的行数, n n n是矩阵的列数
- 当遍历对角线的时候,如果是从下往上走, 那么横坐标递减到0,而纵坐标递增到最大, 如果是从上往下走,纵坐标递减到0,横坐标递增到最大,且每一次,都是一步步的变化。
有这两条规律,其实代码就比较好写了,遍历对角线,然后从下往上遍历奇数对角线,从上往下遍历偶数对角线即可。
这个题的变式,如果只往上的话,下面那块注释掉即可,而只往下的时候,上面那块注释掉即可。 -
LeetCode1424: 对角线遍历II: 这个题我的第一个思路是按照上面的这种,先把矩阵填充起来,然后直接拿上面的代码,结果有样例超时了。
填充的这个感觉还是挺有意思的。第二个思路就是哈希表的方式,因为副对角线上 i + j i+j i+j是个常数, 主对角线上 i − j i-j i−j是个常数。 那么就可以先建立一个哈希表,按照这个常数存储对角线的值,然后再存储到结果中去。
2.4 数字计算模拟
-
LeetCode66: 加一: 先逆序,首位加1, 然后考虑往后进位的操作,再反转回来即可。
-
LeetCode43: 字符串相乘: 和上面思路差不多, 先逆序,然后相乘,然后再反转,只不过这里的相乘有个竖式乘法技巧,还是比较重要的。
-
LeetCode7: 整数反转: 这个题目操作整数, 边辗转相除边乘的操作即可,这个其实应该熟练操作的。
-
LeetCode166: 分数到小数: 这个题有类似于模拟的题目, 这种题目一定要想到负数,且要把他转成正数处理。 这个题目比较难的一点就是循环小数这里的括号的添加,这里需要记录下余数出现的位置才行,如果发现计算出来的当前余数已经在字典中了,说明开始循环了,此时要在对应位置那个地方加括号。这里又学到了一个函数叫做
divmod
函数,输入分子和分母,返回商和余数。还有这里的异或操作,也是非常的妙。
-
剑指offer43: 1~n 整数中 1 出现的次数: 这个题目还是挺复杂的, 主要是看的这个题解 ,才悟了一下,我们往往这个题想的时候,好几位连起来一块想,而忽略了一种思想叫做分而治之,不要一谈到这四个字就想分治, 二分的时候就总结过,这些思想是远高于代码逻辑之上的东西,这里分而治之的意思是说,我们完全可以把各个位分开来想,固定1位,然后看看出现1的所有可能。 比如从个位开始,固定住个位是1,然后其他位在小于n的范围任意变(玩过密码锁吗), 看看此时会有多少种可能。 然后变到十位,定住1,然后变其他几位,看看有多少种可能,这样依次往前计算,最后1的个数就是每一位固定时候可能数之和。 具体的可以看看题解,这里面我觉得最关键的是往前推的这个过程,也就是类似模拟密码锁一位一位旋转的过程。 这个是非常重要的。
这个题的这种分而治之的思想和往前推的这种写法,感觉可以学习下。而像计算1个数的那个公式,明白了思想,到时候从纸上画一画就差不多了,建议看看上面题解下面的第三个评论好像,解释的应该很清楚。 -
剑指offer44: 数字序列中某一位的数字: 又是找规律的题目, 还是挺难的, 建议看看后面题解的第一个分析
-
剑指offer39: 数组中出现次数超过一半的数字: 这个题目的思路有三个,哈希统计个数,排序或者是摩尔投票法。 摩尔投票法的思路是比较好的思路,也是一种很好的思想。核心理念是“正负抵消”。
-
LeetCode31: 下一个排列: 这个题目建议看后面的第一个题解, 这里不解释,只说下思路了,这个题其实就是从全排列里面找比当前排列大的最小数,当然不能把全排列都写出来,然后一一比对。 这里用了一个比较巧妙的方式就是①找的数比当前数大,且②增加幅度尽可能小。
那么需要先从后往前遍历, 找到第一个相邻的升序的地方, 用 i i i来记录位置,也就是第一次出现A[i+1]>A[i]
的地方。 然后再从后往前遍历, 找到第一个大于A[i]的值,也就是第一次出现A[j]>A[i]
的地方。 然后两者一交换,就满足了①。
接下来,把i+1到末尾的元素全部逆序,也就是升序排列,即满足②,也就是增加幅度最小。因为i+1的元素全是降序排列,一逆序变成升序,显然变成了最小。代码如下:
-
牛客Top200高频: 进制转换: 这个题目的思路比较直接,就是除N取余, 然后倒叙排列,高位补零。 这里之所以记录,是因为这里面有两个需要注意的点:
- M可能是负数, 这种情况必须考虑进来,否则除N那里会出现死循环, 这个情况我在阿里的笔试里面就遇见过,当时忘了整理了,这里统一整理下。
- 这里是转成任意进制,这种情况下,转成10进制之上的,要处理大于10的那些数,比如十六进制的时候, 10是用’A’表示的。
第一款代码是纯模拟计算,也就是辗转除N,然后保存成结,在单独处理第二种情况。 而这里记录一种优雅简洁的方式,那就是提前把余数存储到一个数组里面去,辗转取余的时候,从数组里拿数就好。
-
牛客Top200高频: 将字符串转化为整数: 这个题本身不是很难,但有些特殊输入不是很容易想到,比如要考虑防止整数越界,防止出现负数或者第一位是+, 防止中间出现特殊字符等。
-
剑指offer 61: 扑克牌中的顺子: 这个题考察生活中的问题抽象建模能力,整理的原因是一定要善于利用数组排序,思路还是比较独特的,说实话一开始并没有读懂题目。 这个题先排序,然后遍历数组,如果是0话,就统计0的个数,否则,就看看非0的间隔,这个是可以用0进行填补的。 当然,如果发现非0的里面有对子,那么就不可能是顺子了。所以这个题思路这些边角条件也不是很容易想。
-
用Rand7()实现Rand10(): 这个需要记住一些先验性的知识, 具体可以看后面题解里面的第三个, 这里就直接上图了:
2.5 滑动窗口
-
LeetCode209:长度最小的子数组: 这里学习了一种滑动窗口的技巧,这个东西白话的说还是双指针的操作,只不过这次双指针在维持着一个窗口在移动,所以滑动窗口听起来更好一些。 并且在解决一些数组和字符串问题上,滑动窗口也是非常好用的工具, 那么如何用这个滑动窗口呢? 首先,得考虑几个问题①当移动
right
扩大窗口时, 应该怎么更新? ② 什么条件下,窗口应该暂停扩大,开始移动left
缩小窗口? ③当移动left
缩小窗口时, 应该怎么更新? ④我们要的结果应该在扩大窗口时更新还是在缩小窗口时更新?
借着这个题目,仔细的捋捋这几个问题,我发现滑动窗口类似二分,也是一种框架,只要解决了这几个问题,代码框架的写法基本固定。- 当移动
right
扩大窗口时, 应该怎么更新? 对于这个题, 当移动right加入新元素时,win_sum
就需要进行累加新元素。 - 什么条件下,窗口应该暂停扩大,开始移动
left
缩小窗口? 在当窗口内的元素大于等于target的时候, 尝试缩小窗口。 - 当移动
left
缩小窗口时, 应该怎么更新?win_sum
要减去移出去的元素。 - 我们要的结果应该在扩大窗口时更新还是在缩小窗口时更新? 由于我们这里是想要最小的个数,所以应该在缩小窗口时更新结果,也就是在
left
往左移的过程中。
代码如下:
后面的字符串专题,还会遇到这个工具的,到那里再总结下。
上面的变式题目:剑指offer57-II: 和为s的连续正数序列: 这个题目和上面这个思路基本上一样,只不过这里是求出具体等于target的序列了,需要修改保存结果的地方。
- 当移动
2.6 哈希表
在python里面,哈希表常用的就是字典和集合set()了,这俩也是非常好用的工具,python中,collections里面还提供了defaultdict,Orderdict等。下面看几道通过建立字典映射,来达到空间换时间目的的几道数组题目。
- LeetCode242: 有效的字母异位词: 这个题可以使用字典来统计第一个字符串的各个字母的个数, 然后再遍历第二个字符串, 出现的字符进行抵消,当字典里面字符个数出现负数了,说明有字符在前面字符串里面没出现过,此时返回False即可。 代码如下:
-
LeetCode349: 两个数组的交集: 这个题可以使用set集合,因为在这里面可以去掉重复的元素, 思路就是从短的那个列表遍历,对于每个元素,如果在另一个列表出现了,就加入到结果,结果这里用个集合存储,最后转回list即可。
-
LeetCode202: 快乐数: 这个题里面的无限循环很重要, 如果出现了重复的和, 那么就会无限循环下去,所以这个题的关键就是看有没有出现重复的和,而判断重复,首先想到的应该是set集合。思路就是先计算n的各个位置数的平方和,如果等于1则返回True,否则,判断是不是之前出现过,如果出现了,那么返回False。否则,加入集合,然后判断平方和的平方和,重复下去即可。代码如下:
-
LeetCode1: 两数之和:有了map之后,这个题就可以用O(n)的时间复杂度完成了。这里首先需要建立一个
值:索引
的一个映射字典, 然后从头遍历一遍数组,对于当前的数, 查看target-该数是否在字典当中,如果在,就返回该数的索引和字典键对应的值即可。否则,就把该数存到字典中。
这个题目也可以用双指针的方式, 三数之和的简化版,这样就能更好的利用排好序的这一个特征了:
-
LeetCode454: 四数相加II: 这个和四数之和不太一样, 这个的元素是放在了每个独立的数组中, 然后也不需要考虑去不去重的问题,只要是找到四个元素相加等于0即可。 那么这个题目就能转换成两数之和了,因为
A+B+C+D = 0
, 其实可以看成(A+B) + (C+D) = 0
, 这样,遍历A,B,弄一个字典映射{A+B: count}
,cout表示A+B出现的次数, 然后再遍历C,D, 只要0-(C+D)的键在上面的字典里面, 说明找到了count组等于0的,计数器加count即可。具体代码如下:
-
LeetCode383: 赎金信: 这又是一个个数相抵消的题目, 定义一个字典统计magazine里面每个字符的个数,然后遍历ransom,如果当前字符没有在字典里面,返回False。字典中对应字符的个数抵消一个, 当是负数的时候, 返回False。 当把ransom遍历完之后,返回True。
-
LeetCode128: 最长连续序列: 这个题目的思路就是把数组新存到哈希表中, 然后遍历数组的每个数,如果nums[i-1]没有出现在哈希表中,说明nums[i]这个数没有利用到,那么就以nums[i]为起点,在这个基础上不停的加1判断是不是在哈希表中出现, 然后序列长度也对应加1,更新最大长度。
2.7 原地哈希
-
剑指offer03: 数组中重复的数字: 这个第一种思路就是可以用一个哈希表来统计每个数字的个数,一旦发现重复了,返回即可。 但是这里还有一种更加巧妙的方式,可以在常数空间复杂度下完成这个任务。 之所以整理这个题目,是这种思路非常棒,可别小看了这个简单题目, 学会了这个思路,下面的困难题也能搞定。 这个思路就是利用下标进行哈希映射。由于给定的元素是0-N-1之间的数,那么我们就可以拿着这个和位置做一个映射关系,遍历一遍数组,如果不在正确的位置上,我们就进行交换 ,让位置i存储元素i。这样,如果后面发现位置i这里已经有数了,说明找到了重复项。这个思路真的是相当巧妙:
这个题目虽然简单,但是传达了一种思想:将数组视为哈希表,这里相当于自己编写了一个哈希函数,让数值为i的元素映射到位置i上去。下面再看一个,基本上一样的题目。 -
LeetCode442: 数组中重复的数据: 这个题目和上面这个基本上一样,也是判断重不重复,只不过这个是1-N的数了,那么就需要把数值i映射到下标i-1的位置,如果发现有重复,加入到结果中。这里需要用个哈希表,否则会有重复。
这个思想也是很重要的,看下面这个hard题。 -
LeetCode41: 缺失的第一个正数: 这个题最直观的解法,就是从1到N遍历一遍,看看每个正整数是否出现在了数组中,如果没有出现,那么第一次没有出现的那个数就是缺失的正数。那么就需要把数组的元素存到哈希表里面,然后在遍历查找,空间复杂度不符合要求。那么这时候,将数组本身视为哈希表的思路就又出来了。我们可以这样,将本数组看成一个哈希表,再把元素值和所在的位置进行一种映射,把值为i的元素映射到位置i-1处, 前提是当前元素必须符合索引区间,也就是
[1,N]
。 这样假设每个数都归位好了之后(0放1,1放2,2放3…), 我们再进行遍历,如果发现当前位置i不是放的i+1
, 说明i+1
这个正整数是第一个缺失的,返回即可。这个思路是太强了。代码如下:
为了统一, 重新改写成下面这样:
-
LeetCode448: 找到所有数组中消失的数字: 这个题不解释了,有了上面的两个题目,这里就直接拿过来就干掉。
-
LeetCode287: 寻找重复数: 这个题目和上面剑指offer那个是一模一样的,只不过这里是需要把元素i映射到下标i-1上去。这里又记录一遍是发现了一个问题,就是写代码的时候无意发现的,就是python解包时候的赋值先后问题。 看下面的这段代码:
画框的这两句看似解包的时候是一样的, 因为a,b=b,a
和b,a=a,b
我们说是一样的嘛, 但是这里会发现第二句的时候会死循环。第一个就没事, 那感觉和之前的不太一样啊,我觉得是nums[nums[i]-1]
导致的问题,也就是这个nums[i]
必须要先换过来,也就是这里有个赋值先后的问题。所以这里最好还是单独把交换封装成一个函数,这样就不会出错了。 这是元组解包这里遇到的陷阱。 -
1-N排序问题: 这个是美团一面手撕的面试题, 给定1-N的N个数随机打乱, 让排序,时间复杂度是O(n), 空间复杂度是O(1)。 这个题我当时就想到了上面这种原地哈希的思路,即把每个数nums[i]映射到nums[i]-1的位置上去。也就是和上面这个一样的思路。所以代码如下:
def solve(arr): def swap(arr, index1, index2): arr[index1], arr[index2] = arr[index2], arr[index1] for i in range(len(arr)): while arr[i] - 1 != i: swap(arr, arr[i]-1, i) #arr[arr[i]-1], arr[i] = arr[i], arr[arr[i]-1] return arr if __name__ == '__main__': arr = input().strip().split(' ') arr = list(map(int, arr)) res = solve(arr) print(res)
当时也是写了这款代码,不知道为啥会有样例超时, 没过, 我自己在课下运行了几个例子挺好的呀。 唉, 当时怎么调都是超时,越调越慌,结果也就没撕出来。现在都没明白这款代码究竟应该怎么写?
-
牛客Top200: 缺失数字: 这个题也是原地哈希的操作,把a[i]映射到i的位置,这里也要注意a[i]自身的范围,有可能越界。 然后从头遍历,如果当前i位置不是a[i],说明a[i]缺失了,返回这个数。 否则,返回最后一个数。
-
LeetCode14: 最长公共前缀: 这里补一个小清晰的题目,这个题目完全是依赖Python的特性, 算是学习下Python的骚操作吧
2.8 找峰值
我发现这个还是考察的热点,就是给定一个数组,找极大值极小值点或者极值点。可以统一使用下面的思路:
- LeetCode162: 寻找峰值: 这个题目贪心那里也整理过,常规解法是下面这个解法:
这种解法可以找到所有的峰值,甚至找谷值或者极值点都可以,具体应用的话可以看贪心那篇文章, 摆动序列或者是最长连续序列都可以用这个方法去做。 而这里题目里面要求 l o g n logn logn的复杂度,并且只需要返回一个峰值即可。 那么可以采用二分的方式。 先找中间的值,如果发现这个值小于它后面的数,说明峰值在后面, 区间缩小到它后面去找,如果大于他后面的数,说明峰值在前面,往前面搜。这个题要找峰值,并且题目中明确说了最后一个可以认为是负无穷。那么其实这个题找的就是大于等于mid+1的最小值,最后负无穷在,肯定能找到这样的值。所以这个题会用到找左边界的框架。
3. 小总
数组这边的题目常用的方法是重建数组的思想,双指针法,哈希表法,滑动窗口等。重建数组的思想里面体现的是一种逆向思维, 双指针这个东西非常重要,其实双指针不仅仅是左右指针, 还有快慢指针,以及滑动窗口的这种方式,他们适用的场景还不太一样:
- 快慢指针一般适合链表类的题目,找环的这种,这个在链表的时候会遇到
- 左右指针一般是二分搜索中可以用到, 向这里的三数之和,四数之和的题目,也用了左右指针非常巧妙的解决了问题,前提是需要进行排序。这个常见的就是用在数组操作的题目中
- 滑动窗口这个也是第一次学,感觉也是非常好用的一种工具, 这个一般会解决子串的相关问题,在字符串那里会遇到
而关于哈希表法, 这个常用的场景就是统计元素个数map,判断重复set,看元素是否在某个集合里面等,字符抵消的思路,也在这里面常见。
最后就是一些小清晰的题目,这里面有纯数组遍历模拟题目,像螺旋矩阵的这种, 这种重点考察的是基本的数组操控能力,一般要确定好起始点,移动方向,边界和结束条件,这种需要先找到走的规律。 而另一些题目就是考察巧妙的思路了,这个没有固定的套路,见多识广。
在一刷动态规划的同时,抽出了一些时间来复习前面的知识,数组这块拿出了四五天的时间复习,大约10道题目, 总结如下:
重建数组的思想
双指针
- LeetCode11: 盛最多水的容器
- LeetCode15: 三数之和 ⭐️⭐️⭐️⭐️
- LeetCode18: 四数之和
- LeetCode88:合并两个有序数组 ⭐️⭐️⭐️⭐️⭐️
- LeetCode4: 寻找两个正序数组的中位数 ⭐️
旋转模拟
- LeetCode189: 旋转数组
- LeetCode48: 旋转图像 ⭐️⭐️
- LeetCode54: 螺旋矩阵 ⭐️⭐️⭐️
- LeetCode59: 螺旋矩阵II
- LeetCode885: 螺旋矩阵III
- LeetCode1823: 找出游戏的获胜者
- LeetCode498: 对角线遍历 ⭐️
- LeetCode1424: 对角线遍历II
数字计算模拟(有些是找规律,有些难度的题目)
- LeetCode66: 加一
- LeetCode7: 整数反转 ⭐️
- LeetCode166: 分数到小数 ⭐️
- 剑指offer43: 1~n 整数中 1 出现的次数
- 剑指offer44: 数字序列中某一位的数字
- 剑指offer39: 数组中出现次数超过一半的数字
- LeetCode31: 下一个排列
- 牛客Top200高频: 进制转换
- 牛客Top200高频: 将字符串转化为整数
- 剑指offer 61: 扑克牌中的顺子
滑动窗口:
哈希表:
- LeetCode242: 有效的字母异位词
- LeetCode349: 两个数组的交集
- LeetCode202: 快乐数
- LeetCode1: 两数之和 ⭐️⭐️⭐️⭐️
- LeetCode454: 四数相加II
- LeetCode383: 赎金信
原地哈希:
- 剑指offer03: 数组中重复的数字
- LeetCode442: 数组中重复的数据
- LeetCode41: 缺失的第一个正数 ⭐️⭐️
- LeetCode448: 找到所有数组中消失的数字
- LeetCode287: 寻找重复数
找峰值