Leetcode 1-10
這篇文章介紹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<num, index>的字典(雜湊表),存入對應的值和索引,遍歷map, 計算target-num, 利用雜湊表常數時間的定址,只要在字典中找到的索引不與當前索引一樣,即找到結果。時間複雜度\(O(n)\) , 空間複雜度\(O(n)\) .
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<char, int> 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 :heart:
給定一個字串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<int> 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”在給定行數上以之字形書寫,如下所示:
PAHN A P L S I I G YIR
然後逐行讀取:“PAHNAPLSIIGYIR”編寫程式碼,該程式碼將接受一個字串,並在給定行數的情況下進行轉換:
string convert(string s, int numRows);
例子:
Input: s = "PAYPALISHIRING", numRows = 3 Output: "PAHNAPLSIIGYIR" Input: s = "PAYPALISHIRING", numRows = 4 Output: "PINALSIGYAHRPI" Explanation: PIN AL SI G Y AH R PI
方法:首先審好題,觀看結果解釋,是以旋轉翻轉Z的形狀進行排列的,根據這一性質,就可以找到字元按週期排列規則,生成對應行數的vector
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]