算法重温(十): 回归基础数据结构之数组和哈希表

1. 写在前面

这篇文章开始, 准备回归基础数据结构的复习了,主要包括数组,哈希表,链表,字符串,栈与队列的相关题目了,这边的题目相对于前面的算法层面上的那些,会稍微好想一些, 难度会下降,并且有一些很关键的思想,一般会默写了之后就可以解题。比如双指针法, 滑动窗口, 单调栈啊等等。 这篇文章复习数组和哈希表,其实感觉哈希表就是一种辅助工具来解决数组题目,数组这边常用的方法思想:重建数组, 双指针, 滑动窗口, 常用的工具,字典,集合。

关于Array, 我们需要知道的知识点:

  1. Array常见的操作: 元素的增, 删, 改, 查和移动
  2. Array访问数组时间复杂度O(1), 插入和删除时间复杂度O(n), 内存连续
  3. 比较经典的题目一般就是考察元素的合并, 交换, 添加, 删除等, 数组的遍历
  4. 这里涉及到的一些思想:
    • 两指针一前一后往中间遍历, 这个可以进行数组逆序
    • 两指针从前面往后遍历, 可以进行符合特定条件元素的筛选
    • 重建数组思想
    • 滑动窗口
    • 空间换时间
    • 哈希存储
    • 逆序遍历思想
  5. 数组的优缺点(要掌握一种数据结构,就必须要懂得分析它的优点和缺点)
    • 数组的优点在于:构建非常简单, 能在 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 双指针

  • LeetCode11: 盛最多水的容器: 这个暴力的话就是两层for循环, 求两两柱子围成的面积,这显然效率太低。 所以这里用双指针的解法, 首先得知道两个柱子围成的面积咋算: min(柱子高) * (两根柱子之间的距离), 双指针的做法是一左一右指针,从两边向中间走,这个题目的核心就是决定面积大小的取决于两个柱子里面的最短的那个(木桶效应),当两个指针指向的柱子的面积算完了之后,比较下他们的长度,较短的那边指针移动(如果是i指的就右移, j指的就是左移)。为啥呢? 因为短的那边无法再优化了呀, 如果此时不动短的,再移动高的,那只剩下两根柱子的距离越来越小,那面积就更小了。 只有移动短的那边,虽然两者的距离短了,但可能还可以从高度上找回来。 代码如下:

    在这里插入图片描述

  • LeetCode15: 三数之和: 这个题里面的双指针用的很是巧妙。首先先把数组从小到大排序, 然后设置指针k开始从头遍历元素, 对于每一次遍历,分别设置i, j指针指向k后面剩下的元素的首尾,然后进行判断,如果三个指针指向的元素等于0, 保存结果。 否则,如果三者之和小于0, 那么i向后移动。 因为i指向的元素小了导致的总体之和小(又是构造木桶效应), 如果三者之和大于0, j往左移, 因为是j指向的元素过大导致的总体之和大。 这样当i, j相遇, 当前k的所有组合已经遍历完毕, 往后移动k, 重复上面的步骤。 但是这个过程里面要注意的就是避免重复元素。怎么避免重复呢? 因为我们是先把数组从小到大排序了,那么如果发现当前的k指向的元素和前面k-1相等,就跳过去,说明前面已经找完了这种情况。所以该题的关键点从小到大排序,定住其中一个极端, 看另外两个。代码如下:

    在这里插入图片描述

  • LeetCode18: 四数之和: 相比较于三位数之和来说,四位数之和这里会多加一层循环,让m下遍历每一个数,对于m遍历的每一数, k遍历当前m后面的所有数,然后i, j两个指针指向k后面数组的首尾, 然后按照上面的那种逻辑进行判断,如果四个数加起来大于target, i后移, 小于target, j前移, 等于target,保存结果,i后移,j前移。 但这里也必须要去重操作, 且去重操作需要每一个指针的地方都需要去重。 最后的代码如下:

    在这里插入图片描述
    三数之和和四数之和中,排序非常重要。

  • LeetCode88:合并两个有序数组: 这个题可以使用两个指针从后往前遍历数组, 这是数组和链表的不同之处, 链表不知道最后的尾部, 但是数组知道, 从后往前, 如果哪个数大, 就放入到最后面, 这时候, 如果nums2有剩余, 直接插入到nums1的最前面, 如果没有剩余, 就说明已经完全插入到nums1了。 代码如下:

在这里插入图片描述

2.3 小清新

  • 旋转数组: 这个题的思路比较巧妙的一种方式就是三次逆序搞定, 首先将整体元素逆序, 然后将 1 1 1 k k k的元素逆序,最后将 k k k~ l e n ( n u m s ) len(nums) len(nums)的元素逆序即可。但是这个题有个小陷阱就是如果 k k k大于了数组的长度,需要先取余。

    在这里插入图片描述

  • 加一: 先逆序,首位加1, 然后考虑往后进位的操作,再反转回来即可。

    在这里插入图片描述

  • LeetCode54: 螺旋矩阵:这个是数组的遍历模拟过程题目,不涉及什么算法,但考察代码的掌控能力。螺旋向内遍历矩阵, 和动规一样,这里也是要考虑四部曲,分别是起始位置,移动方向,边界和结束条件。拿这个题目分析下:

    1. 起始位置: 这个遍历起点是矩阵的左上角,也就是(0,0)位置
    2. 移动方向:每一圈都是先向右走到头,然后向下走到头,再向左走到头,再往上走到头。 所以对于每一圈,方向是一致的,就是右 -> 下 -> 左 -> 上。
    3. 边界: 这个是本题的核心,因为每一圈,这个边界是会变化的,规则是如果当前行(列)遍历结束之后,就需要把这一行(列)的边界向内移动一格。, 所以这个题在走的时候,要时刻控制好边界,这是解决关键
    4. 结束条件:螺旋遍历的结束条件是所有的位置都被遍历到。

    看代码:
    在这里插入图片描述

  • LeetCode59: 螺旋矩阵II: 这个题目和上面这个基本上一模一样, 只需要简单的改下代码就可以,因为上面这个题目时给定了矩阵,让螺旋遍历输出值, 而这个题目是螺旋遍历去构建矩阵, 所以在存储结果方面会有些区别。代码如下:
    在这里插入图片描述

  • LeetCode885: 螺旋矩阵III:这个题目和上面的两个区别就挺大了,第一个就是起点不固定,第二个是边界的话得通过走的步子确定。拿上面的四部曲分析下:

    1. 起始点: 这个是题目里面会给定(r0,c0)

    2. 移动方向,依然是右 -> 下 -> 左 -> 上

    3. 边界条件,这个边界是动态改变的,可以按照圈进行划分下

      1. 第一圈, 从(r0,c0)向右走了1步, 下走了1步, 向左走了2步, 向上走了2步,到了(r1,c1)
      2. 第二圈, 从(r1,c1)向右走了3步,下走了3步, 向左走了4步,向上走了4步,到了(r2,c2)
      3. 第三圈,从(r2,c2)向右走了5步,下走了5步, 向左走了6步,向上走了6步,到了(r3,c3)

      由此我们可以发现规律,每一次循环中,向右和向下走的步数相同,向左和向上走的步数相同比向右多走1步。所以我们需要定义一个step去控制边界变化。

    4. 终止条件:这里可以用走过点的个数去控制

    代码如下:

    在这里插入图片描述

2.4 滑动窗口

  • LeetCode209:长度最小的子数组: 这里学习了一种滑动窗口的技巧,这个东西白话的说还是双指针的操作,只不过这次双指针在维持着一个窗口在移动,所以滑动窗口听起来更好一些。 并且在解决一些数组和字符串问题上,滑动窗口也是非常好用的工具, 那么如何用这个滑动窗口呢? 首先,得考虑几个问题①当移动right扩大窗口时, 应该怎么更新? ② 什么条件下,窗口应该暂停扩大,开始移动left缩小窗口? ③当移动left缩小窗口时, 应该怎么更新? ④我们要的结果应该在扩大窗口时更新还是在缩小窗口时更新?

    借着这个题目,仔细的捋捋这几个问题,我发现滑动窗口类似二分,也是一种框架,只要解决了这几个问题,代码框架的写法基本固定

    1. 当移动right扩大窗口时, 应该怎么更新? 对于这个题, 当移动right加入新元素时, win_sum就需要进行累加新元素。
    2. 什么条件下,窗口应该暂停扩大,开始移动left缩小窗口? 在当窗口内的元素大于等于target的时候, 尝试缩小窗口。
    3. 当移动left缩小窗口时, 应该怎么更新? win_sum要减去移出去的元素。
    4. 我们要的结果应该在扩大窗口时更新还是在缩小窗口时更新? 由于我们这里是想要最小的个数,所以应该在缩小窗口时更新结果,也就是在left往左移的过程中。

    代码如下:
    在这里插入图片描述
    后面的字符串专题,还会遇到这个工具的,到那里再总结下。

2.5 哈希表

在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。

    在这里插入图片描述

3. 小总

数组这边的题目常用的方法是重建数组的思想,双指针法,哈希表法,滑动窗口等。重建数组的思想里面体现的是一种逆向思维, 双指针这个东西非常重要,其实双指针不仅仅是左右指针, 还有快慢指针,以及滑动窗口的这种方式,他们适用的场景还不太一样:

  • 快慢指针一般适合链表类的题目,找环的这种,这个在链表的时候会遇到
  • 左右指针一般是二分搜索中可以用到, 向这里的三数之和,四数之和的题目,也用了左右指针非常巧妙的解决了问题,前提是需要进行排序。这个常见的就是用在数组操作的题目中
  • 滑动窗口这个也是第一次学,感觉也是非常好用的一种工具, 这个一般会解决子串的相关问题,在字符串那里会遇到

而关于哈希表法, 这个常用的场景就是统计元素个数map,判断重复set,看元素是否在某个集合里面等,字符抵消的思路,也在这里面常见。

最后就是一些小清晰的题目,这里面有纯数组遍历模拟题目,像螺旋矩阵的这种, 这种重点考察的是基本的数组操控能力,一般要确定好起始点,移动方向,边界和结束条件,这种需要先找到走的规律。 而另一些题目就是考察巧妙的思路了,这个没有固定的套路,见多识广。

在一刷动态规划的同时,抽出了一些时间来复习前面的知识,数组这块拿出了四五天的时间复习,大约10道题目, 总结如下:

重建数组的思想

双指针

小清新:

滑动窗口

哈希表

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/114970237