这篇文章介绍Leetcode1到10题的解决思路和相关代码。
1. Two sum
问题描述:给定一个整数数组,返回两个数字的索引,使它们加起来等于一个特定的目标。
例子:
Given nums = [2, 7, 11, 15], target = 9,
Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].
常规方法:使用双重循环,第一重从左往右固定索引,计算需要查找的结果,第二层循环从固定索引出发依次向右查找第一层计算的结果。时间复杂度\(O(n^2)\), 空间复杂度\(O(1)\).
def twoSum(nums: List[int], target: int) -> List[int]:
nums_len = len(nums)
for ind1 in range(nums_len):
value = target - nums[ind1]
for ind2 in range(ind1+1, nums_len):
if value == nums[ind2]:
return [ind1, ind2]
行程哈希表(第一次行程遍历nums生成字典,第二次遍历nums找结果): 首先,构建dict
def twoSum(nums: List[int], target: int) -> List[int]:
nums_dict = {value: index for index, value in enumerate(nums)}
for i, num in enumerate(nums):
find_value = target - num
if find_value in nums_dict and nums_dict[find_value] != i:
return [i, nums_dict[find_value]]
单行程哈希表(遍历一次nums):与上面一种的区别在于不先将所有的值和索引都放入map中,在遍历中依次放入,少了一次遍历的时间,速度更快占用内存更小。时间复杂度\(O(n)\), 空间复杂度\(O(n)\)。
def twoSum(nums: List[int], target: int) -> List[int]:
nums_dict = {}
for i, num in enumerate(nums):
find_value = target - num
if find_value in nums_dict:
return [i, nums_dict[find_value]]
nums_dict[num] = i
运行时间与内存占用比较:
方法 | 时间 | 内存 |
---|---|---|
常规 | 3244ms | 13.9Mb |
双行程 | 56ms | 15Mb |
单行程 | 40ms | 14.1Mb |
2. Add Two Numbers
问题描述:给定两个非空链表,表示两个非负整数。这些数字以相反的顺序存储,它们的每个节点都包含一个数字。将这两个数字相加并以链表的形式返回。可以假设这两个数字不包含任何前导零,除了数字0本身。
例子:
Input: (2 -> 4 -> 3) + (5 -> 6 -> 4)
Output: 7 -> 0 -> 8
Explanation: 342 + 465 = 807.
方法:建立一个新的ListNode,依次对l2和l3的每个元素进行相加,这里重要的是对进位的判断,还要考虑两个ListNode的长度不一样,以及长度不一样,最后相加的时候是否有进位,有进位则还要考虑没有结束的ListNode与进位的相加,相加完是否还有进位。
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def addTwoNumbers(self, l1, l2):
"""
:type l1: ListNode
:type l2: ListNode
:rtype: ListNode
"""
head = l3 = ListNode(0) #相当于双指针指同一个listNode,l3构建head的next, 会在后面发生变化
carry_flag = 0
while( l1 and l2 ): #循环依次按位相加,直到至少一个listnode为空
l3.next = ListNode(0)
l3 = l3.next
temp = l1.val + l2.val + carry_flag
if temp < 10:
l3.val = temp
carry_flag = 0
else:
l3.val = temp - 10
carry_flag = 1
l1 = l1.next
l2 = l2.next
if l1: #如果l1非空,将l1剩余的listnode赋给l3.next,相当于l3的高位数
l3.next = l1
carry_flag, l3 = self.control_carry(carry_flag, l3) #让l3.next与carry_flag进行运算,直到进位符为0或者l3为空退出循环, 返回l3和进位符。
if l2:
l3.next = l2
carry_flag, l3 = self.control_carry(carry_flag, l3)
if not l1 and not l2:# l1和l2同时为空,并且有进位,让l3.next为1
if carry_flag == 1:
l3.next = ListNode(1)
if carry_flag == 1: # 例如,l2剩余的部分赋给l3.next, 但carry_flag为1,并且在control_carry中一为1,比较高位都是9,最终进位为1,需要添加最高位为1.
l3.next = ListNode(1)
return head.next
def control_carry(self, carry_flag, l3):
while(carry_flag):
if l3.next:
l3 = l3.next
else:
break
temp1 = l3.val + carry_flag
if temp1 < 10:
l3.val = temp1
carry_flag = 0
else:
l3.val = temp1 - 10
carry_flag = 1
return carry_flag, l3
Times = 76ms, memory_usage = 13.2Mb
3. Longest Substring Without Repeating Characters
问题描述: 给定一个字符串,在不重复字符的情况下找出最长子字符串的长度。
例子:
Input: "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3.
Input: "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.
Input: "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.
Note that the answer must be a substring, "pwke" is a subsequence and not a substring.
我的方法:循环遍历字符串的每个字符,对每个字符相加,新加入的字符要查找前面的字符串是否存在新加入的字符,不存在继续下一个字符,存在比较当前最大长度。Times = 76ms, memory_usage = 13.2Mb
def lengthOfLongestSubstring(self, s: str) -> int:
max_length = 0
longest_sub_str = ''
for i, elem in enumerate(s):
longest_sub_str += elem
if len(longest_sub_str) > 1:
if elem in longest_sub_str[:-1]:
if max_length < len(longest_sub_str) - 1:
max_length = len(longest_sub_str) - 1
same_char_index = longest_sub_str[:-1].find(longest_sub_str[-1])
longest_sub_str = longest_sub_str[same_char_index+1:]
if len(longest_sub_str) > max_length:
return len(longest_sub_str)
else:
return max_length
更好的方法:使用哈希表
int lengthOfLongestSubstring(string s) {
//建立ascii码的字典,字典的键代表字符,字典的值代表字符所在的索引,初始为0
int m[256] = {0}, res = 0, left = 0;
for (int i = 0; i < s.size(); ++i) {
//if中第一个条件判断当前s[i]是否出现过,第二个条件是当出现重复字符时,更换left的位置为重复的字符的前一个索引
if (m[s[i]] == 0 || m[s[i]] < left) {
res = max(res, i - left + 1);
} else {
left = m[s[i]];
}
m[s[i]] = i + 1;
}
return res;
}
下面这个与上面类似,用到了unordered_map构建字典:
int lengthOfLongestSubstring(string s) {
int res = 0, left = 0, i = 0, n = s.size();
unordered_map m;
for (int i = 0; i < n; ++i) {
left = max(left, m[s[i]]);
m[s[i]] = i + 1;
res = max(res, i - left + 1);
}
return res;
}
4. Median of Two Sorted Arrays
问题描述: 有两个大小分别为m和n的排序数组nums1和nums2。求两个排序数组的中值。总的运行时复杂度应该是O(log(m+n))。您可以假设nums1和nums2不能同时为空。
例子:
nums1 = [1, 3]
nums2 = [2]
The median is 2.0
nums1 = [1, 2]
nums2 = [3, 4]
The median is (2 + 3)/2 = 2.5
方法:建立一个新的列表,长度是输入两个列表长度之和,两个列表依次倒序比较,大的放入新列表的相应位置,当其中至少一个列表为空时,退出循环,还得将那个列表为空最后pop出的数与非空列表进行比较,找到小于那个书即终止,将剩余的数赋值给新列表。Times = 64ms, memory_usage = 13.4Mb
class Solution:
def getMiddleValue(self, nums, nums_len):
if nums_len % 2 == 0:
mid_idx = nums_len // 2
return (nums[mid_idx] + nums[mid_idx - 1])/2.0
else:
return nums[(nums_len - 1) // 2]/1.0
def dealTheRest(self, res, nums, num1, num2, lens):
while num1 > num2 and nums:
res[lens - 1] = num1
num1 = nums.pop()
lens -= 1
if nums:
res[lens - 1] = num2
res[lens - 2] = num1
res[0:lens - 2] = nums
else:
if num1 > num2:
res[lens - 1] = num1
res[lens - 2] = num2
else:
res[lens - 1] = num2
res[lens - 2] = num1
return res
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
if not nums1:
return self.getMiddleValue(nums2, len(nums2))
if not nums2:
return self.getMiddleValue(nums1, len(nums1))
lens = len(nums1) + len(nums2)
res = [0]*lens
num1 = nums1.pop()
num2 = nums2.pop()
if not nums1 and not nums2:
return (num1 + num2)/2.0
while nums1 and nums2:
if num1 > num2:
res[lens - 1] = num1
num1 = nums1.pop()
else:
res[lens - 1] = num2
num2 = nums2.pop()
lens -= 1
if nums1:
res = self.dealTheRest(res, nums1, num1, num2, lens)
if nums2:
res = self.dealTheRest(res, nums2, num2, num1, lens)
return self.getMiddleValue(res, len(res))
还可以进一步优化,不需要将新的list全部填满,判断列表长度之和是否为偶数,是则新列表长度为2,否则长度为1,只存储中间的值,存储完即返回,后面的值无需比较,这种方法在大规模数据下更快占用内存更小。在提交后想到的,有兴趣的可以自己动手试试。
另外在Leetcode上发现更简单的方法,使用系统自带的sorted函数,简洁很快。Times = 56ms
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
num = sorted(nums1 + nums2)
leng = len(num)
median = 0
if ( (leng%2) == 1):
median = num[(leng-1)//2]
else:
median = (num[leng//2] + num[leng//2 - 1]) / 2
return median
5. Longest Palindromic Substring ❤️
给定一个字符串s,找出s中最长的回文子串。可以假设s的最大长度为1000。
例子:
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
Input: "cbbd"
Output: "bb"
方法1:暴力搜索,两个循环确定左右索引\(i,j\),判断中间\([i, j)\)代表的字符串是否是回文,与最大回文比较,长就替换,时间复杂度太高\(O(n^3)\). Times = 8904ms
class Solution:
def longestPalindrome(self, s: str) -> str:
longestPStr = ''
for i in range(len(s), 0, -1):
for j in range(len(s)):
if s[j:i] == s[j:i][::-1]:
if len(s[j:i]) > len(longestPStr):
longestPStr = s[j:i]
if len(longestPStr) > i:
return longestPStr
return longestPStr
方法2:以当前索引为中间,往外扩张判断字符串是否为回文数,不符合回文数退出内层循环,取最大字符串。参考自答案。这个方法占用了O(n)的空间,可以使用\([begin, end)\)降至\(O(1)\). Times = 932ms.
class Solution:
def longestPalindrome(self, s: str) -> str:
res = ''
for i in range(len(s)):
odd = self.palindromeAt(s, i, i)
even = self.palindromeAt(s, i, i+1)
res = max(res, odd, even, key=len)
return res
def palindromeAt(self, s, l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1
r += 1
return s[l+1:r]
方法3: 这种方法比较巧妙,当想s增加一个字符时,最长的回文数可能加1,加2或者不动,所以处理的方式就是建立一个最长回文数长度maxLen,跟踪这个长度,当前索引为i,判断\(s[i-maxLen-1:i+1]\)(对应'abba')和\(s[i-maxLen:i+1]\)(对应'aba')是否为回文数,对最大maxlen加上对应的长度。参考自答案. Times = 88ms
class Solution:
def longestPalindrome(self, s):
if len(s)==0:
return 0
maxLen=1
start=0
for i in xrange(len(s)):
if i-maxLen >=1 and s[i-maxLen-1:i+1]==s[i-maxLen-1:i+1][::-1]:
start=i-maxLen-1
maxLen+=2
continue
if i-maxLen >=0 and s[i-maxLen:i+1]==s[i-maxLen:i+1][::-1]:
start=i-maxLen
maxLen+=1
return s[start:start+maxLen]
方法4:非常巧妙,时间复杂度达到\(O(n)\),这个方法的名字是==Manacher‘s Algorithm==,也叫马拉车方法,方法建立了一个数组,数组第i个元素表示以 i 为中心的最长回文的半径,利用回文数的对称性,在一个大的回文数后面的索引可以利用对称性映射前面而大大简化计算。算法细节描述请参考博客,讲得非常好,墙裂推荐,以下代码来自博客。
class Solution {
public:
string longestPalindrome(string s) {
if (s.size() < 2) return s;
string t = "$#";
for (int i = 0; i < s.size(); ++i){
t += s[i];
t += '#';
}
vector p(t.size(), 0);
int mx = 0, id = 0, resLen = 0, resCenter = 0;
for (int i = 1; i < t.size(); ++i){
p[i] = mx > i ? min( p[2 * id - i], mx - i ) : 1;
while (t[ i + p[i] ] == t[ i - p[i] ]) ++p[i];
if (mx < i + p[i]){
mx = i + p[i];
id = i;
}
if (resLen < p[i]){
resLen = p[i];
resCenter = i;
}
}
return s.substr((resCenter - resLen) / 2, resLen - 1);
}
};
6. ZigZag Conversion
描述:字符串“PAYPALISHIRING”在给定行数上以之字形书写,如下所示:
P A H N
A P L S I I G
Y I R
然后逐行读取:“PAHNAPLSIIGYIR”编写代码,该代码将接受一个字符串,并在给定行数的情况下进行转换:
string convert(string s, int numRows);
例子:
Input: s = "PAYPALISHIRING", numRows = 3
Output: "PAHNAPLSIIGYIR"
Input: s = "PAYPALISHIRING", numRows = 4
Output: "PINALSIGYAHRPI"
Explanation:
P I N
A L S I G
Y A H R
P I
方法:首先审好题,观看结果解释,是以旋转翻转Z的形状进行排列的,根据这一性质,就可以找到字符按周期排列规则,生成对应行数的vector, 存放每一行的字符,最后相加起来。Times = 88ms
class Solution:
def convert(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
dict = {i:'' for i in range(numRows)}
law_len = 2 * numRows - 2
for i, e in enumerate(s):
temp = i % law_len
if temp > numRows - 1:
temp = (numRows - 1) - (temp - (numRows - 1))
dict[temp] += e
res = ''
for i in range(numRows):
res += dict[i]
return res
7. Reverse Integer
问题描述:给定一个32位带符号整数,对其进行反数运算. 超过int的最大值或者最小值,返回相应最大值或最小值
例子:
Input: 123
Output: 321
Input: -123
Output: -321
Input: 120
Output: 21
方法1:使用循环对输入整数按10取余,取余后取整除以10,然后余数循环乘10,直到取整除以为0停止循环,得到最后反转的数。Times = 72ms, memory_usage = 13.3Mb
注意:在python3中,-123%10是不等于3的,这点是个坑,所以python3实现中加入了负号的判断。
class Solution:
def reverse(self, x: int) -> int:
a = 0
max = 2**31
if x==0 or x > max-1 or x < -max:
return 0
if x < 0:
x = -x
flag_neg = True
else:
flag_neg = False
while(x != 0):
a = a*10 + x%10
x //= 10
if a < max-1 and a > -max:
return a if not flag_neg else -a
else:
return 0
方法2:更加粗暴,直接判断整数是否为负数,标识符用字符来表示,是负数则'-',不是则空字符,直接将整数转成字符串反转加上前面的标识符符号,后面判断越界即可, 更快. Times = 56ms, memory_usage = 13.3Mb
class Solution:
def reverse(self, x: int) -> int:
if x==0:
return 0
max = 2**31
if x > max-1 or x < -max:
return 0
a = 0
if x < 0:
x = -x
flag_neg = '-'
else:
flag_neg = ''
a = int(flag_neg + str(x)[::-1])
if a < max-1 and a > -max:
return a
else:
return 0
8. String to Integer (atoi)
问题描述: 实现将字符串转换为整数的atoi。
函数首先丢弃尽可能多的空白字符,直到找到第一个非空白字符。然后,从这个字符开始,取一个可选的初始正负号,后跟尽可能多的数字,并将它们解释为一个数值。
字符串可以包含构成整数的字符之后的附加字符,这些字符将被忽略,并且对函数的行为没有影响。
如果不能执行有效的转换,则返回一个零值。
Input: "42"
Output: 42
Input: " -42"
Output: -42
Explanation: The first non-whitespace character is '-', which is the minus sign.
Then take as many numerical digits as possible, which gets 42.
Input: "4193 with words"
Output: 4193
Explanation: Conversion stops at digit '3' as the next character is not a numerical digit.
Input: "words and 987"
Output: 0
Explanation: The first non-whitespace character is 'w', which is not a numerical
digit or a +/- sign. Therefore no valid conversion could be performed.
Input: "-91283472332"
Output: -2147483648
Explanation: The number "-91283472332" is out of the range of a 32-bit signed integer.
Thefore INT_MIN (−231) is returned.
方法:首先判断字符的长度,长度为0,返回0,长度为1,判断是否为数字,不为数字,返回0,还要判断整个str是否都是空格,是则返回0;然后找到第一个不是空格的索引,在这个索引下依次递加查询数字,在查询之前要判断第一个字母是否为负号或者正号,建立了一个符号标志,然后依次生成数字,终止条件是发现字符不是数字,另外,这里为了防止溢出,在c++中使用long类型进行数字的计算,在循环的过程中会判断是否超过了整形的最大值,超过返回最大值或者最小值。
class Solution:
def myAtoi(self, str: str) -> int:
if len(str) == 0 or not str.strip(): return 0;
if len(str) == 1 and not str.isdigit():
return 0
ls = list(str.strip())
if not ls[0].isdigit() and ls[0] != '-' and ls[0] != '+':
return 0
elif ls[0] == '-':
flag = '-'
begin = 1
elif ls[0] == '+':
flag = '+'
begin = 1
else:
begin = 0
flag = '+'
res = ''
for i in range(begin, len(ls)):
if ls[i].isdigit():
res += ls[i]
else:
break
if not res:
return 0
if flag == '-':
return max(-2**31, -int(res))
else:
return min(2**31 - 1, int(res))
9. Palindrome Number
问题描述:给定一个整数,判断是否是回文数
Input: 121
Output: true
Input: -121
Output: false
Explanation: From left to right, it reads -121. From right to left, it becomes 121-. Therefore it is not a palindrome.
Input: 10
Output: false
Explanation: Reads 01 from right to left. Therefore it is not a palindrome.
方法: 数值判断,两种情况,第一种数的位数是偶数,如1881,可以除10取余再乘以10的方法对低位数进行逆序,最后判断18=18,第二种数的位数是奇数,如18281,同样按照上面方法,最后判断18=182/10,循环条件是逆序的数大于剩余的数即停止。
class Solution {
public:
bool isPalindrome(int x) {
if (x < 0 || (x % 10 == 0 && x != 0))
return false;
int res = 0;
while(x > res){
res = res * 10 + x % 10;
x /= 10;
}
return x == res || x == res/10;
}
};
10. Regular Expression Matching
问题描述:给定一个输入字符串和一个模式,实现正则表达式匹配并支持'.'和'*'.
'.'匹配任何单个字符。'*'匹配前一个元素的零个或多个. 匹配应该覆盖整个输入字符串(而不是部分)。
例子:
Input:
s = "aa"
p = "a"
Output: false
Explanation: "a" does not match the entire string "aa".
Input:
s = "aa"
p = "a*"
Output: true
Explanation: '*' means zero or more of the precedeng element, 'a'. Therefore, by repeating 'a' once, it becomes "aa".
Input:
s = "ab"
p = ".*"
Output: true
Explanation: ".*" means "zero or more (*) of any character (.)".
Input:
s = "aab"
p = "c*a*b"
Output: true
Explanation: c can be repeated 0 times, a can be repeated 1 time. Therefore it matches "aab".
Input:
s = "mississippi"
p = "mis*is*p*."
Output: false
方法:基于动态规划,如果\(s[0..i)\)匹配\(P[0..j]\), 定义状态\(P[i][j]\)为真, 否则为假。状态方程为:
a. P[i][j] = P[i - 1][j - 1], if p[j - 1] != ‘*’ && (s[i - 1] == p[j - 1] || p[j - 1] == ‘.’);
b. P[i][j] = P[i][j - 2], if p[j - 1] == ‘*’ and the pattern repeats for 0 times;
c. P[i][j] = P[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == ‘.’), if p[j - 1] == ‘*’ and the pattern repeats for at least 1 times.
将三种情况合在一起,得到下面的解决方案。
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False for _ in range(n+1)] for _ in range(m+1)]
dp[0][0] = True
for i in range(0, m+1):
for j in range(1, n+1):
if p[j - 1] == '*':
dp[i][j] = dp[i][j - 2] or (i > 0 and (s[i - 1] == p[j - 2] or p[j -2] == '.') and dp[i - 1][j])
else:
dp[i][j] = i > 0 and dp[i - 1][j - 1] and (s[i - 1] == p[j - 1] or p[j - 1] == '.')
return dp[m][n]