leetcode思路总结
1. Two Sum
题目描述:
给定一个整数数组nums以及一个数target,求和为target的两个数在数组中的下标。约定对于给定的target有且仅有一组解。
解法一:哈希表
采用哈希表(unordered_map)。依次扫描数组,并判断target-nums[i] 是否存在哈希表中,若存在,则找到了这两个数;否则,将(nums[i],i) 存入哈希表中。
时间复杂度:O(n)
空间复杂度:O(n)
解法二:双指针
采用双指针。首先将数组从小到大排序,然后用双指针从两端开始扫描数组,若两指针指向的数之和小于target,则左指针向右移动;若两指针指向的数之和大于target,则右指针相左移动。
时间复杂度:排序的时间复杂度
空间复杂度:排序的空间复杂度
2. Add Two Numbers
题目描述:
将两个整数用非空链表逆序存储:如 1->2->3 表示整数321,求这两个数之和并用逆序链表表示。
注意:两个链表非空;可能长短不一,如1->2 和3->4->5;注意考虑最高位发生进位的情况,如 5 + 5 = 0 -> 1
链表相关的:注意需要通过链表指针取值时,如 l1->val ,需要注意判断链表指针是否为空;另外可以空出一个结点作为链表的头结点。
解法
链表的对应位相加,并用 carry 记录进位。需注意存在一个链表为空的情况;两个链表长度不一致的情况;最高位发生进位的情况。
c++ code
struct ListNode{
int val;
ListNode* next;
ListNode(int x):val(x),next(NULL){}
};
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2){
ListNode* result = new ListNode(0); // 多出来一个结点作为头结点
ListNode* pNode = result;
int sum = 0;
int carry = 0;
while(l1!=NULL || l2!=NULL){
int x = l1?l1->val:0;
int y = l2?l2->val:0;
sum = x + y + carry;
pNode->next = new ListNode(sum%10);
pNode = pNode->next;
carry = sum/10;
if(l1) l1 = l1->next; // 注意 l1 可能为空
if(l2) l2 = l2->next;
}
if(carry) pNode->next = new ListNode(carry);
return result->next;
}
3. Longest Substring Without Repeating Characters
题目描述:
给定一个字符串,求无重复字符的最长字串长度。如s=“bbb”, result=1;
s=“abcabc”,result=3。
解法一:暴力法,set,(超时)
我们可以枚举出所有的子串,然后判断各个子串是否无重复字符,并计算子串长度。枚举子串方法,用子串的起始下标 i 和 终点下标 j , 0 ≤ i < j ≤ n 0 \leq i < j \leq n 0≤i<j≤n,则子串[i,j) 表示下标 i 到 j-1的子串。
时间复杂度:O(n^3) 检验是否为无重复字符的子串需要遍历子串 j-i,枚举子串为一个二层循环
∑
i
=
0
n
−
1
∑
j
=
i
+
1
n
(
j
−
i
)
\sum_{i=0}^{n-1} \sum_{j=i+1}^{n}(j-i)
∑i=0n−1∑j=i+1n(j−i)
空间复杂度:O(min(n,m)) 需要用一个set 来检验是否存在重复字符,m 为 字母表字符个数。
int lengthOfLongestSubstring(string s){
int n = s.size();
int ans = 0;
for(int i=0;i<n;++i)
for(int j=i+1;j<=n;++j)
if(allUnique(s,i,j)) ans = max(ans,j-i); // 计算子串长度
return ans;
}
bool allUnique(string s, int start, int end){
set<char> mySet;
for(int i=start;i<end;++i){
char ch = s[i];
if(mySet.find(ch)!=mySet.end()) return false;
else mySet.insert(ch);
}
return true;
}
优化一:用哈希表
暴力解法对每个子串都需要重头判断是否存在重复字符。事实上,假设[i,j) 不存在重复字符,只需要判断 j 是否在 子串 [i, j) 内,就能判断 [i,j+1) 是否存在重复字符。我们可以用哈希表以O(1)的时间查找元素。
时间复杂度:O(n) 每个元素至多被遍历2遍。
空间复杂度:O(min(m,n)) 哈希表存储元素。
int lengthOfLongestSubstring(string s){
int n = s.size();
unordered_map<char,int> myHash;
int ans = 0, i = 0, j = 0;
while(i<n && j<n){
char ch = s[j];
if(myHash.find(ch)==myHash.end()){
// [i,j) 无重复,若[i,j+1)无重复,则找到一个新的无重复子串,计算子串长度,并继续判断[i,j+2)
myHash[ch] = 1;
++j;
ans = max(ans,j-i);
}
else{
// [i,j) 为无重复字符,若[i,j+1)重复,则[i+1,j)无重复,继续判断[i+1,j+1)是否重读
myHash.erase(s[i]);
++i;
}
}
return ans;
}
优化二:用vector记录字符所在index, vector vec(256,-1)
假设s[j] 在 [i,j) 区间内有重复,位置为
j
′
j'
j′。则可以过滤掉[i,j’] 区域。直接从 [j’+1,j+1) 开始判断。可以用哈希表记录字符所在下标,用 [i,j) 记录当前子串范围,
需注意,在哈希表查找s[j+1]时,若找到,需判断下标是否>=1(即是否在当前范围内,若在当前范围内,则字符重复,i+1),并且需要更新哈希表的存储的下标值。
时间复杂度:O(n)
空间复杂度:O(min(m,n)) 用哈希表 unordered_map<char,int> myHash;
空间复杂度:O(m) 用table 如vector vec(256,-1);
int lengthOfLongestSubstring(string s){
int n = s.size();
unordered_map<char,int> myHash;
int ans = 0;
for(int i=0,j=0;j<n;++j){
// 当前子串为[i,j),判断s[j] 是否在该子串中出现过
char ch = s[j];
if(myHash.find(ch)==myHash.end()){
// 没有出现过
myHash[ch] = j;
ans = max(ans,j-i+1);
}
else{
// 表示在哈希表中,但仍需判断重复字符是否在子串[i,j)范围内
if(myHash[ch]>=i){
// 若在子串范围内,即s[j] 与 [i,j) 内的字符重复,需更新 i
i = myHash[ch] + 1;
}
// 更新s[j] 在哈希表中的映射值
myHash[ch] = j;
ans = max(ans,j-i+1); // 计算新子串的长度
}
}
return ans;
}
int lengthOfLongestSubstring(string s){
int n = s.size();
vector<int> vec(256,-1);
int ans = 0;
for(int i=0,j=0;j<n;++j){
char ch = s[j];
if(vec[ch]>=i) i = vec[ch]+1;
vec[ch] = j;
ans = max(ans,j-i+1);
}
return ans;
}
4. Median of Two Sorted Arrays
题目描述:
求两个排序数组的中位数。假设至少一个数组不为空。
解法:
中位数将一个集合均分成元素个数相等的两部分,使得左边的数永远小于右边。因此求两个排序数组的中位数,就是将两个数组划分为左右相等的两部分,中位数为左边最大值与右边最小值的平均值(若总个数为奇数,则令左边的个数总比右边的个数多1,中位数为左边最大值)。
只需要求出其中一个数组一分为二的切分点(由于最终划分后左右两边的个数确定,只要确定了一个数组的切分点,就确定了另一个数组的切分点;至于为啥不能是跳着划分左右两部分,而一定是从某处一分为二,因为是排序数组,假设升序,则左边的永远小于右边);由于是已排序的数组,我们选择对元素个数较少的数组进行划分,查找划分点采用二分法,查找分三种结果:划分点太小,查找区间向右缩小;划分点太大,查找区间向左缩小;划分点正好。
对于已排序数组 nums1 和 nums2,元素个数分别为m, n。假设从i 处将 nums1 分为[0, i) 和 [i, m) 两部分,从 j 处将 nums2 分为 [0, j) 和 [j, n) 两部分。然后将 nums1 左边部分和 nums2 左边部分合并为左部分。同理合并为右部分。只要划分再合并后左右两部分个数相等,且左部分的最大值小于右部分的最小值,则划分正确,中位数为左边最大值和右边最小值的均值(若总个数为奇数,则中位数位左边的最大值)。
我们假设 m<n,划分后左边一共为
h
a
l
f
L
e
n
=
(
m
+
n
+
1
)
/
2
halfLen=(m+n+1)/2
halfLen=(m+n+1)/2个元素。现在对nums1进行划分。i 的初始取值范围为[0,m],当i=0时,nums1左边为0个元素;当i=m时,nums1左边为m个元素;当为i时表示nums1左边为i个元素,左边最小值为nums1[i-1]。则nums2的划分点
j
=
h
a
l
f
L
e
n
−
i
j=halfLen-i
j=halfLen−i ,表示nums2左边为
j
j
j个元素,左边最大值为nums2[j-1]。
若
n
u
m
s
2
[
j
−
1
]
>
n
u
m
s
1
[
i
]
nums2[j-1]>nums1[i]
nums2[j−1]>nums1[i],则表示划分点
i
i
i太小,i 的取值范围更新为 [i+1,m];
若
n
u
m
s
1
[
i
−
1
]
>
n
u
m
s
2
[
j
]
nums1[i-1]>nums2[j]
nums1[i−1]>nums2[j],则表示划分点
i
i
i太大,i 的取值范围更新为 [0, i-1);
若为其他,则表示找到正确的划分点,接下来求中位数。
参考这儿。
时间复杂度:O(log(min(m,n)))
空间复杂度:O(1)
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m = nums1.size();
int n = nums2.size();
if(m>n){
// 我们选取较短的数组进行切割,这样用时更短 O(log(min(m,n)))
vector<int> temp = nums1;
nums1 = nums2;
nums2 = temp;
int tmp = m; m = n; n = tmp;
}
// 一共m+n 个数,中位数将其分成个数相等的左右两个部分,左边为 halfLen 个数,
// 当 m+n 为奇数时,左边总比右边多一个数,中位数是左边最大的数;
// 当 m+n 为偶数时,左边与右边的个数相等,中位数为左边最大数与右边最小数的平均值。
// 因此只需要找到nums1的切割点,以及对应的nums2的切割点,将二者左部分结合,右部分结合,将所有数均分成两个部分,
// 且使得左边的最大值小于右边的最小值
int iMin = 0, iMax = m, halfLen = (m+n+1)/2;
while(iMin <= iMax){
// nums1 的划分点为i,左边为[0,i) 共i个数,右边 m-i个数; 右边最小值为nums1[i]
int i = (iMin + iMax) / 2;
// nums2的划分点为j,左边为[0,j)共j个数,使得num1和num2划分后左边共halfLen个数;
// 左边最大值为nums2[j-1]
int j = halfLen - i;
if(i<iMax && nums2[j-1]>nums1[i]){
// 注意边界条件 i<iMax
// 因为nums2[j-1]总比nums2右半部分小,若nums2左边最大值大于nums1右边最小值,说明 nums2左边划分的个数太多了,即i太小了,划分点 i 应处于 [i+1,iMax] 之间
iMin = i+1;
}
else if(i>iMin && nums1[i-1]>nums2[j]){
// 因为nums1总比nums1右半部分小,若nums1左边的最小值大于nums右边的最大值,说明 nums1 左边划分的个数太多了,即 i太大了,划分点 i 应处于 [iMin,i-1]之间
iMax = i-1;
}
else{
int maxLeft = 0;
if(i==0) {maxLeft = nums2[j-1];} // nums1左边为0个数
else if(j==0){maxLeft = nums1[i-1]; } // nums2左边为m个数
else{maxLeft = max(nums1[i-1],nums2[j-1]);} // 取左边的最大值
if((m+n)%2==1) {return maxLeft;} // 当总个数为奇数时,中位数为左边的最大值
int minRight = 0;
if(i==m){minRight = nums2[j];}
else if(j==n){minRight = nums1[i];}
else{minRight = min(nums1[i],nums2[j]);}
return (maxLeft + minRight) / 2.0;// 注意类型
}
}
return 0.0; // 最后需要返回值
}
5. Longest Palindromic Substring
题目描述
给定一个字符串,求其最长的回文子串。见这里 。回文是一个正读反读都相同的字符串。
解答:中心扩展法
一个回文字符串必定根据中心对称。即要么从一个字符开始向两边扩展如 bab; 要么从两个字符开始向两边扩展 cbaabc。
假设当前最长回文子串范围为[start, end] 。初始时令start=end=0;
我们从 i=0 开始直到 i<s.size() 扩展,尝试找到最长回文子串。每次要么从单个字符s[i]向两边扩展,要么从两个字符s[i], s[i+1] 向两边扩展。
时间复杂度:O(n^2)
空间复杂度:O(1)
string longestPalindrome(string s) {
if(s=="" || s.length()<1)return ""; // string 是类,不能用NULL来判断是否为空。可以用s.empty(),s.size(),s==""
int start = 0, end = 0; // 记录当前最长回文子串的范围[start,end]
for(int i=0;i<s.length();++i){
int len1 = expandAroundCenter(s,i,i); // 由一个元素开始向两边扩张
int len2 = expandAroundCenter(s,i,i+1); // 由2个元素开始向两边扩张
int len = max(len1,len2);
if(len>end-start+1){ // 回文中心为i,长度为len,计算出回文子串的范围
start = i-(len-1)/2;
end = i+len/2;
}
}
return s.substr(start,end-start+1);
}
int expandAroundCenter(string s,int left, int right){
// 判断[left,right]是否为回文,若为回文,则继续向两边扩张,并判断是否仍为回文
int L = left, R = right;
while(L>=0 && R<s.size() && s[L]==s[R]){
L--;
R++;
}
// 此时真正的回文为[L+1,R-1]
return R-L-1; // 回文长度为 (R-1)-(L+1)+1=R-1-L-1+1=R-L-1
}
6. ZigZag Conversion
题目描述:
给定一个字符串,例如“HELLOWORLD”,按 Z 字形排列:
c0 c1 c2
r0 H O L
r1 E L W R D
r2 L O
然后按行读取成“HOLELWRDLO”,因此字符串“HELLOWORLD” 转换成“HOLELWRDLO”。请实现该转换 string convert(string s, int numRows)。
解法:找规律
以上述为例。我们先看
c
0
c_0
c0列 ‘HEL’,假设一共有 numRows 行,则
c
0
c_0
c0列
r
i
r_i
ri行对应的字符为 s[i] (0<=i<numRows)。
r
0
r_0
r0行
c
0
c_0
c0列字符为 H,对应于s[0],
c
1
c_1
c1列字符为 O,对应于s[4],
c
2
c_2
c2列字符对应于s[8],设 pro = numRows
∗
2
−
2
*2-2
∗2−2,则
c
j
c_j
cj列字符对应于 s[
0
+
j
∗
0+j*
0+j∗pro]。同样对于最后一行,
c
j
c_j
cj列字符对应于 s[numRows
−
1
+
j
∗
-1 +j*
−1+j∗pro]。
现在我们来看中间的行,我们可以发现在
c
1
c_1
c1,
c
2
c_2
c2列前要多一个字符,可以计算出
r
i
r_i
ri行
c
j
c_j
cj列前多出的字符对应的是s[0+j*pro-i],(
0
<
i
<
n
−
1
0 < i < n-1
0<i<n−1,
j
>
0
j>0
j>0)。
然后我们处理边界情况,当 numRows=2时,无中间行,于是不需要加上中间字符;当numRows==1或者字符串s为空时,只需返回原字符串。
string convert(string s, int numRows) {
if(s=="" || s.size()<1 || numRows==1) return s;
int n=s.size();
vector<string> result(numRows,"");
for(int i=0;i<numRows;++i){
// r0列
if(i<n){
result[i]+=s[i];
}
}
for(int i=0;i<numRows;++i){
int echo=1,pro=numRows*2-2;
int index = i+echo*pro;
if(numRows>2&&i!=0&&i!=numRows-1){
// 中间行
int index_1 = 0+echo*pro-i; // 这是中间多出的那个字符的下标
while(index_1<n){
result[i]+=s[index_1];
if(index<n){
result[i]+=s[index];
++echo;
index = i+echo*pro;
index_1 = 0+echo*pro-i;
}
else break;
}
}
else{
// 首末行
while(index<n){
result[i]+=s[index];
++echo;
index = i+echo*pro;
}
}
}
string res = "";
for(int i=0;i<numRows;++i)
res+=result[i];
return res;
}
解法二:按字符串遍历
我们可以按顺序遍历字符串,用curRow记录当前行,用goingDown记录当前方向(向上还是向下),模拟将字符串写成z字形的过程。
string convert(string s, int numRows){
if(s=="" || s.size()<1 || numRows<=1) return s;
int n = s.size();
vector<string> vec(min(numRows,n),"");
int curRow = 0;
bool goingDown = false; // 当当前行为首行或者末行时,下一步要改变方向
for(int i=0;i<n;++i){
vec[curRow]+=s[i]; // 当前行
if(curRow==0 || curRow==numRows-1) goingDown = !goingDown; // 下一步方向
curRow += goingDown? 1:-1; // 更新当前行为下一步的行
}
string ret = "";
for(int i=0;i<vec.size();++i) ret+=vec[i];
return ret;
}
解法三:按行读取
这种解法跟第一种解法思路是一样的,我们遍历行,找到每一行每一个字符对应的是什么。
注意:字符窜大小为n,行数为numRows,按行遍历时,
0
≤
i
<
n
u
m
R
o
w
s
0 \leq i < numRows
0≤i<numRows,这里的numRows 不要写成 n 啊啊啊啊啊,写错好几次了,还检查不出来错误!!!!
时间复杂度:O(n)
空间复杂度:O(n)
string convert(string s, int numRows){
if(s=="" || numRows==1) return s;
int n = s.size();
string ret="";
int cycleLen = 2*numRows-2;
for(int i=0;i<numRows;++i){
// 按行遍历
for(int j=0;j+i<n;j+=cycleLen){
// j 为 距离 i 的间隔,j+i 为该行下一个字符的下标;
// 注意第i行的第1个字符下标为i,因此j的初始值为0。
ret += s[j+i];
if(i>0 && i<numRows-1 && j+cycleLen-i<n){
// 当为中间行的时候,会多出一个字符
ret += s[j+cycleLen-i];
}
}
}
return ret;
}
7. Reverse Integer
题目描述
给定一个32位有符号整数,将整数反转,如123变成321。注意反转后若超过整数范围 [ − 2 31 , 2 31 − 1 ] [-2^{31},2^{31}-1] [−231,231−1],则返回0。
解法
这里我们需要再补充一下负数取余的知识。
余数的概念:存在唯一整数商
q
q
q和唯一实数
r
r
r使得:
m
=
q
∗
d
+
r
m = q*d+r
m=q∗d+r,且
0
≤
∣
r
∣
<
∣
d
∣
0\leq |r| < |d|
0≤∣r∣<∣d∣。
对于正整数,7%5=2,默认商最小的原则,即商为1(而不是取商为2,余数为-3,实际上这样也是满足余数定义的。。。。)。
对于负整数,-7%5=-2,c++默认商最大的原则,即商为-1(而不是取商为-2,余数为3,事实上有其他的语言采用这种。)
总结一下,同号相除取余数大家都默认商最小的原则,即商要尽量向0靠;异号相除取余数看情况,有的取商最小的原则,有的取商最大的原则。
因此,对于c++而言,123%10=3,-123%10=-3。
反转:
pop = x % 10; x /= 10;
rev = rev*10+pop; // 首先要判断 rev*10 是否溢出,再要判断 rev*10+pop 是否溢出。
另外,需要考虑反转后溢出的情况,分为两种情况:
若
r
e
v
10
>
2
31
−
1
10
\frac{rev}{10} > \frac{2^{31}-1}{10}
10rev>10231−1,则rev 溢出
若
r
e
v
10
=
2
31
−
1
10
\frac{rev}{10}=\frac{2^{31}-1}{10}
10rev=10231−1且rev%10>7,则rev 溢出。
同理对于负数溢出。
注意
2
31
−
1
2^{31}-1
231−1 末位为7,
−
2
31
-2^{31}
−231的末位为8。
时间复杂度:
O
(
log
10
(
x
)
)
O(\log_{10}(x))
O(log10(x))
空间复杂度:O(1)
int reverse(int x) {
int rev = 0;
while(x){
int pop = x % 10;
x /= 10;
if(rev>INT_MAX/10 || (rev==INT_MAX/10 && pop>7)) return 0;
if(rev<INT_MIN/10 || (rev==INT_MIN/10 && pop<-8)) return 0;
rev = rev * 10 + pop;
}
return rev;
}
8. String to Integer (atoi)
题目描述
将字符串转换成整数。
首先抛弃前置空格,然后是符号,再然后是数字,直到第一个非数字字符为止。
如“ -567and cd” 输出-567。
如果不存在除前置空格以外的前置数字,则输出0。
注意限制为32位整数
[
−
2
−
31
,
2
31
−
1
]
[-2^{-31},2^{31}-1]
[−2−31,231−1],若溢出,则输出INT_MAX 或者 INT_MIN。
解答
首先处理前置空格。
然后处理符号。
最后处理数字。(注意判断是否溢出)
int myAtoi(string str){
if(str=="") return 0;
int n = str.size();
int ret = 0;
int sign = 1;
int i = 0;
while(i<n&&str[i]==' ')++i; // 处理前置空格
if(i<n && str[i]=='-'){ // 注意符号后面必须跟一个数字才算有效
sign = -1;
++i;
}
else if(i<n && str[i]=='+'){
sign = 1;
++i;
}
for(;i<n;++i){
if(str[i]>='0' && str[i]<='9'){
int num = str[i] - '0';
num = sign*num;
if((ret>INT_MAX/10)||(ret==INT_MAX/10 && num>7)) return INT_MAX;
if((ret<INT_MIN/10)||(ret==INT_MIN/10 && num<-8)) return INT_MIN;
ret = ret*10+num;
}
else break;
}
return ret;
}
9. Palindrome Number
题目描述
判断一个数字是否为回文。如1221 是回文。-5不是回文。
思路一:将数字转换成字符串,然后判断字符串是不是回文。但是需要额外的内存空间。
思路二:将数字反转,然后判断反转后的数是否和原来的数相等。但是要注意反转后的数是否超过int的范围。
解法:将数字反转一半,判断这一半和剩下的一半是否相等。
如1221,反转一半 为12=12。如何判断是否反转了一半:当剩下的x<=rev时。
若数字为偶数位如1221,只要
x
=
=
r
e
v
x==rev
x==rev;
若数字为奇数位如12321,只要
x
=
=
r
e
v
/
10
x==rev/10
x==rev/10;
特殊情况:负数都不是回文。末位为0的也不是回文。如10反转为01。
(注意末位为0的情况要单独拎出来,因为反转后为0==1/10,会被误判为true)
bool isPalindrome(int x){
if(x<0 || (x%10==0 && x!=0)) return false;
int rev = 0;
while(x>rev){
rev = rev*10 + x%10;
x = x/10;
}
return x==rev || x==rev/10;
}
10. Regular Expression Matching
题目描述:正则表达式匹配
‘.’:匹配任意一个字符。
‘*’:匹配0个或多个前面的字符。
“.*”:表示匹配0个或多个任意个字符。
解法一:递归
假设不含’’,只要依次匹配就行。难点是如何匹配 ‘*’。
假设’‘前面的字符和s对应的字符不匹配,则略过’ *’,继续看后面的字符是否匹配;
假设’*'前面的字符和s对应的字符匹配,此时有可能匹配0个或多个,采用递归,继续判断。
bool isMatch(string s, string p){
if(p=="") return s==""; // 递归出口
// 匹配首字符
bool first_match = (s!="" && (s[0]==p[0] || p[0]=='.'));
if(p.size()>=2 && p[1]=='*'){
// 若第二个字符为*,则可能匹配0个字符或多个字符
return isMatch(s,p.substr(2)) || (first_match && isMatch(s.substr(1),p));
}
else{
// 否则,只需要看首字符是否匹配,再递归
return first_match && isMatch(s.substr(1),p.substr(1));
}
}