算法题笔记
哈希
两数之和
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
class Solution {public int[] twoSum(int[] nums, int target) {Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();for (int i = 0; i < nums.length; ++i) {if (hashtable.containsKey(target - nums[i])) {return new int[]{hashtable.get(target - nums[i]), i};}hashtable.put(nums[i], i);}return new int[0];}
}作者:力扣官方题解
链接:https://leetcode.cn/problems/two-sum/solutions/434597/liang-shu-zhi-he-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
哈希表法有2个特点:
- 内存换时间;
- 哈希表快速查找;
如果遍历数组,把数放到另一个数组而不是哈希表,数组无法瞬间找到目标值,时间复杂度还是降不下来,内存占用也多了。它们的区别是遍历数由小到大还是由大到小。
双指针
接雨水问题
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
- 如果用一个左指针记录坑左边,用右指针往右找试图把坑封住,会遇到封不住但是里面有小坑的情况。比如左指针在最高点,右指针向右就找不到匹配的右高点,但是里面有小坑。
public int Trap(int[] height) {int p1=0,p2;//p1先找到坑左边,List<int>rains=new List<int>();int rain;while(p2<height.Length){if(nums[p1]>nums[p1+1]){q.Enqueue(p1);p1++;}// if(nums[p1]<=nums[p1+1]){//找一个下坡// p1++;// }// else{// p2=p1+1;// if(nums[p2]<nums[p1]){// rains.Add(nums[p2]);// p2++;// }// else{// lower=Math.Min(nums[p1],nums[p2]);// for(int i=0;i<rains.Count;i++){// rain+=lower-rains[i];// }// rains.Clear();// }// }}}
- 然后回忆起括号匹配的问题,可以用一个队列记录左斜坡,往右找斜坡去匹配,但是大坑套小坑时如果算所有坑的积水,肯定会重复。
两种思路都有漏洞。
滑动窗口
无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc"
,所以其长度为 3。
示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b"
,所以其长度为 1。
示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是"wke"
,所以其长度为 3。请注意,你的答案必须是 子串 的长度,"pwke"
是一个子序列,不是子串。
难点是这个子串的位置和长度不确定,如果从一个字符开始加,不知道从哪里开始,然后想从整个字符串开始剔除行不行?就从左边开始,字符在右边的子串里有则剔除,一直剔除到abcbb,左边的a无法剔除了,然后从右边剔除,剔除2个b得到结果,好像可以?然后拿例子3测试,左边无法剔除,右边剔除w后无法剔除,得到的wwke是错误结果,说明这个方法有漏洞。就是两边有独特字符,中间有重复字符。
然后又想把所有连着相同的字符之间切开,但是如果相同字符不相邻明显有不行。
然后感觉还是要从一个字符开始“生长”的方法。就想从左开始遇见已有的字符就开始一个新的子串的“生长”,得到一个字符串列表,取长度最长的。这样有什么漏洞吗?这样可能忽略了某些其他分割方式?例如向右生长时再长一个就左边就出现重复字符,但是右边可能存在更长的子串,比如cabcfke,从左往右生长会分成cab、cfke,但是有abcfke更长。所以还是有漏洞。
那如果再从右往左来一次呢?得到2个字符串列表,取其中最大的长度。对于cabcfkemne,从左长是cab、cfkemn、e,最长是6,从右长是mne、abcfke、c,最长是6,但实际最长是abcfkemn,长度8.还是有漏洞。
这个最长子串一定是向左或右加一个就会有重复的。可以把字符串分割成若干有重叠的子串,每个子串无法再延长,然后有一个暴力算法,每个字符都向两边延长,直到有重复,但是延长方式又是个问题,可以左右交替延长,可以只朝一侧,可以左2次右1次,有无数种延长方法。
那如果延长遇到重复时记录一下长度,然后把从重复字符及其另一侧的字符全部丢弃,继续延长呢?然后我们似乎能理解“滑动窗口”是什么意思了。这里每次分割,长度不得不减少时都记录一下长度,然后继续延长。好像没什么问题,然后就写伪代码:
- 有一个字符列表,从左侧字符循环;
- 用IndexOf判断列表含不含右边字符在不在列表;
- if 在 then 同时获得那个重复字符的Index,记录此时的长度,比max大则更新,用RemoveRange移除它及其左边的部分;
- 把右边字符加入列表,回到2;
- 返回max。但是为了处理从未切割(max=1,未更新过)的情况,要把max和最后字符串的长度取最大值;
- 为了把内存一次申请够,就申请和字符串一样长的列表;
public class Solution {public int LengthOfLongestSubstring(string s) {if(s==""){return 0;}List<char>list=new List<char>(s.Length){s[0]};int max=1;for(int i=1;i<s.Length;i++){int index=list.IndexOf(s[i]);if(index!=-1){if(list.Count>max){max=list.Count;}list.RemoveRange(0,index+1);}list.Add(s[i]);}return Math.Max(max,list.Count);//Max为了处理从未切割的情况}
}
官方做法
每次先移除左边一个字符,再用while循环添加右边字符,属于“减一加多”和上面的“减多加一”方法相反,在while循环多次不满足的时候也会出现连续多次减一的情况,一直减到子串里重复的字符被剔除,相当于上面方法RemoveRange的一次性剔除。
不过顺序都是先减后加,因为规则的限制。
class Solution {
public:int lengthOfLongestSubstring(string s) {// 哈希集合,记录每个字符是否出现过unordered_set<char> occ;int n = s.size();// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动int rk = -1, ans = 0;// 枚举左指针的位置,初始值隐性地表示为 -1for (int i = 0; i < n; ++i) {if (i != 0) {// 左指针向右移动一格,移除一个字符occ.erase(s[i - 1]);}while (rk + 1 < n && !occ.count(s[rk + 1])) {// 不断地移动右指针occ.insert(s[rk + 1]);++rk;}// 第 i 到 rk 个字符是一个极长的无重复字符子串ans = max(ans, rk - i + 1);}return ans;}
};作者:力扣官方题解
链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters/solutions/227999/wu-zhong-fu-zi-fu-de-zui-chang-zi-chuan-by-leetc-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
动态规划
下降路径最小和
931. 下降路径最小和 - 力扣(LeetCode)
思考过程:由“正方形”条件发现=>到达最后一行前不可能走过所有列=> 去掉最后一行一定能找到一个没走过的列,且这个列一定是最左或最右列=>dp[i-1]的解路线就是dp[i]去掉最后一行和最左或最右列的解