文章目录
LeetCode 1. 两数之和
题干
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
提示:
2 <= nums.length <= 10^4
-10^9 <= nums[i] <= 10^9
-10^9 <= target <= 10^9
只会存在一个有效答案
思路
如果直接使用暴力求解,遍历两次数组判断和为target时间复杂度为O(n^2) ;而如果使用哈希表判断当前数的另一个数是否已经出现过,只需要遍历一遍即可,时间复杂度为O(n)
解题过程
- 定义一个哈希表,哈希表中记录了数组中每个数对应的下标。如果用map各个操作的时间复杂度为O(logn),而使用unordered_map所有操作时间复杂度为O(1),由于不需要保证有序和一一对应,因此直接使用unordered_map
- 遍历数组中每一个数,同时检查哈希表中是否存在与之相加为target的另一个数(即哈希表中target - nums[i]是否存在),如果不存在,就将当前数作为key,当前数的下标作为value记录在哈希表中;如果存在,就返回当前数的下标i以及相加为target的另一个数的下标(即哈希表中key为target - nums[i]的value值)
复杂度
时间复杂度: O(n)
空间复杂度: O(n)
Code
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp; // unordered_map所有操作复杂度为O(1) 哈希表存的是已经出现过的数的下标
for (int i = 0; i < nums.size(); i++)
{
int r = target - nums[i]; // 确定另一个数
if(mp.count(r)) return {i, mp[r]}; //如果哈希表中另一个数已经出现过 就返回这俩数的下标
mp[nums[i]] = i; // 如果哈希表中不存在另一个数的下标,就把当前位置数的下标存在哈希表中
}
return {}; // 默认一定有答案 也必须保证每次答案都有返回值
}
};
LeetCode 2. 两数相加
题干
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
提示:
每个链表中的节点数在范围 [1, 100] 内
0 <= Node.val <= 9
题目数据保证列表表示的数字不含前导零
思路
模拟高精度加法,从低位向高位计算,超过10则向高位进一
解题过程
- 用虚拟头节点dummy统一了非空链表和空链表的插入,dummy->next才是真正的第一个节点
- 用变量t记录当前位的和,并用来维护是否进位,和链表每一位节点的值为t % 10,遍历下一位时更新t为t / 10,t = 1表示当前位要向前一位进一,带着t = 1直接参与下一位的求和运算
- 当l1或l2没有遍历完时或者最后一位有进位没有处理完时继续执行循环
复杂度
时间复杂度: O(n)
空间复杂度: O(n)
Code
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
// 小技巧: 定义一个虚拟头结点 就不需要判断要插入的点是否为第一个点了 只要是需要特判第一个点的时候都可以用虚拟头结点 → 统一空链表和非空链表的插入操作
auto dummy = new ListNode(-1), cur = dummy; // dummy为和链表的虚拟头结点 cur为尾结点
// 沿着l1和l2遍历每一位 相当于做低位到高位的高精度加法
int t = 0; // t表示是否进位 t = 0表示不进位 t = 1表示进位
while (l1 || l2 || t) // 当l1或l2没有遍历完 或者 仍有进位操作没有处理完的时候一直循环
{
if (l1) t += l1 -> val, l1 = l1 -> next; // t为当前位的和 如果l1没遍历完,更新l1往后
if (l2) t += l2 -> val, l2 = l2 -> next; // t为当前位的和 如果l2没遍历完,更新l2往后
// 和链表当前位的值应该为t % 10,t变为t / 10 即下一位的进位,继续参与下一位的运算
cur -> next = new ListNode(t % 10); // t % 10作为和链表当前位节点的val
cur = cur -> next; //更新尾节点并准备做下一次的插入
t /= 10; //更新t
}
return dummy -> next; // dummy -> next才是第一个真正节点
}
};
LeetCode 3. 无重复字符的最长子串
题干
给定一个字符串 s ,请你找出其中不含有重复字符的最长子串的长度。
子串:子字符串 是字符串中连续的 非空 字符序列
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3
提示:
0 <= s.length <= 5 * 10^4
s 由英文字母、数字、符号和空格组成
(可能有空串"")
思路
如果暴力求解,i从0到n-1遍历,j从i+1往后遍历,并用哈希表维护各个字符出现的次数,当出现相同的元素时记录当前长度并往后移动i指针,(记录从i指针开始的最长无重复字母的子串)这样j指针每次都要从i+1往后遍历,每次遍历时间复杂度为O(n),外层i指针遍历时间复杂度也是O(n),总的时间复杂度为O(n^2)
可以看到优化过程主要体现在j指针的遍历上,即能否在i指针从0到n-1遍历的过程中,让j指针不需要每一趟都从头遍历,而是一共只需要走一遍,是优化时间时间复杂度为O(n)的关键
我们的目的是:从所有子串中 的 所有字符不同的子串中 找到最大长度的子串,可以将问题的所有状态空间划分为n类,即以i = 0为结尾的 字符不同的子串 一直到 以i = n-1为结尾的 字符不同的子串,将这n类情况的每个子串的长度取最大值,即所有字符不同的子串的最大长度。(逻辑是,先划分所有子串,即以i=0为结尾的子串 一直到 以i=n-1为结尾的子串,这是所有的子串,一共n类,再分别对每一类,固定他的结尾i,往前找其最长的字符不同的子串,将以i结尾的不包含重复字符的子串的最左指针位置记为j,j到i,也就是以i为结尾的最长字符不同的子串,再对所有的n类子串的j到i的长度取max,也就是所有子串中,所有字符不同的子串中的最大子串的长度了)
此类双指针问题可以用单调性来优化,不妨让i表示当前子串的右指针的位置,j表示当前子串左指针的位置(j表示使j到i区间没有相同的字符的最靠左的位置),有如下命题:当i向右移动到i’时,j要么仍在原位置,要么也会往后移动到j’位置(j’和i’往后移动的距离不一定相当,但是i’往后移动时,j’一定要么原地不动要么也往后移动,不可能往前移动
该命题可以用反证法证明:假设i对应最靠左的不包含重复字符的子串的左指针位置为j,i往后移动到i’位置而j是往前移动到j’位置(j’ —— j —— i —— i’),由于j’到i’之间不存在重复的字符,那么j’到i之间也不存在重复的字符,i对应最靠左的左指针位置可以取到j’,而我们原假设j才是i对应最靠左的左指针位置,故得出矛盾,因此得证当i’往后移动时,j’一定要么原地不动,要么也会往后移
这样的好处是,当枚举完i继续枚举i’时,对应的j’只需要从上一个j往后移动即可,而不需要从头枚举,这样i和j每一个指针最多走n次,加在一起俩指针最多走2n次,时间复杂度为O(n)
解题过程
用哈希表维护j到i之间每个字符出现的次数,i往后移动到i’(i + 1)时,将下一个字母s[i+1]加入到哈希表中,看此时哈希表中是否有重复元素,如果有重复元素,则一定为s[i + 1],此时一直往后移动j直到移动到出现s[j] = s[i + 1]为止,此时j与i’(也就是i + 1)对应的元素相同,j++,这样就可以保证区间内只有一个s[i+1]对应的字符,具体见代码实现
复杂度
时间复杂度: O(n)
空间复杂度: O(n)
Code
暴力求解代码:O(n^2)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int max_length = 0; // 防止空串情况
int len = 0;
for(int i = 0; i < s.size(); i++)
{
unordered_map<char, bool> mp;
mp[s[i]] = true;
len = 1; // 空串不会进入这个循环 len仍是0
for(int j = i + 1; j < s.size(); j++)
{
if(mp[s[j]]) break;
else
{
mp[s[j]] = true;
len++;
}
}
max_length = max(max_length, len);
}
return max_length;
}
};
双指针优化代码:O(n)
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> mp;
int res = 0;
for (int i = 0, j = 0; i < s.size(); i++) // i表示右指针 j表示左指针
{
mp[s[i]]++; // 由于每个状态表示的时以i结尾的子串,以i结尾,所以s[i]必然已经加入进来了,哈希表中也要加入s[i]
while (mp[s[i]] > 1) mp[s[j++]]--; // 由于j到i一定没有重复的元素,因此如果添加进来的s[i]在哈希表中出现重复,一定是s[i]对应的字符已经出现过,因此让j往后移动,并把中间经过的字符的数量减掉,直到s[i]在哈希表中只出现一次为止
res = max(res, i - j + 1); //执行完while后此时j到i的子串就是以i结尾的最长的不包含重复字符的子串,长度为i - j + 1,用这一段的长度来更新答案,一共更新n次(i从0到n-1)
}
return res;
}
};