#002.兩數相加2019/10/15
題目考察單鏈表操作,包括獲取鏈表節點和創建新的鏈表。
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
"""兩數相加,返回由和的各位數字組成的鏈表"""
sum_head = ListNode(0)
sum_tail = sum_head
carry = 0
while l1 or l2:
num1 = l1.val if l1 else 0
num2 = l2.val if l2 else 0
res = num1 + num2 + carry
carry = 0 if res < 10 else 1
sum_tail.next = ListNode(res % 10)
sum_tail = sum_tail.next
if l1:
l1 = l1.next
if l2:
l2 = l2.next
if carry == 1:
sum_tail.next = ListNode(1)
return sum_head.next
鏈表由表頭和節點組成,節點分爲數據域和指針域,數據域中存貯數據元素,指針域存儲下個結點的地址。獲取單鏈表的節點要從頭結點開始,依次獲取下一個節點;創建鏈表可以採用尾插的方式,sum_head屬性存儲鏈表頭結點,插入新節點ListNode時,當前鏈表尾節點的next屬性改爲ListNode對象。
按照本題的代碼模板,並沒有鏈表類來實現鏈表的各種操作,最終的到的其實是一個next屬性指向鏈表頭結點的啞結點。
實現一個單鏈表:
1.創建節點類Node和鏈表類Linklist,Node類中包含val屬性存儲數據,next屬性存儲下個節點的地址;Linklist類中包含head屬性存儲頭結點,和一系列鏈表操作方法。
2.尾插節點從head開始取next屬性,直到next=None到達當前鏈表的尾節點,將其next屬性指向新節點的地址。
3.獲取節點val時,從head開始區next屬性,直到到達索引位置,返回該節點出的val。
4.刪除節點時,需要將待刪節點之前一個節點的next屬性指向待刪節點之後的一個節點,即可完成待刪節點的刪除;若刪除頭結點,只需將head屬性指向head.next(即原鏈表的第二個節點)
5.清空鏈表只需要令頭結點head爲None。
class Node:
def __init__(self, val):
self.val = val
self.next = None
class LinkList():
def __init__(self):
self.head = None
self.length = 0
def is_empty(self):
"""鏈表爲空?"""
return self.length == 0
def append(self, val_or_node):
"""尾插節點"""
new_node = None
if isinstance(val_or_node, Node):
new_node = val_or_node
else:
new_node = Node(val_or_node)
if self.is_empty():
self.head = new_node
self.length += 1
else:
node = self.head
while node.next:
node = node.next
node.next = new_node
self.length += 1
def get_val(self, index: int):
"""獲取指定索引的節點元素"""
if type(index) is int:
if index >= self.length or index < 0:
print("Index is out of index")
return
else:
node = self.head
while index:
node = node.next
return node.val
else:
print("Index is not int")
return
def del_node(self, index: int):
"""刪除指定索引位置的節點"""
if type(index) is int:
if index >= self.length or index < 0:
print("Index is out of index")
return
else:
if index == 0:
self.head = self.head.next
else:
node = self.head
while index > 1:
node = node.next
index -= 1
node.next = node.next.next
self.length -= 1
return
else:
print("Index is not int")
def clear(self):
"""清空鏈表"""
self.head = None
self.length = 0
print("Clear LinkList")
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
"""兩數相加,返回由和的各位數字組成的鏈表"""
sum_head = ListNode(0)
sum_tail = sum_head
carry = 0
while l1 or l2:
num1 = l1.val if l1 else 0
num2 = l2.val if l2 else 0
res = num1 + num2 + carry
carry = 0 if res < 10 else 1
sum_tail.next = ListNode(res % 10)
sum_tail = sum_tail.next
if l1:
l1 = l1.next
if l2:
l2 = l2.next
if carry == 1:
sum_tail.next = ListNode(1)
return sum_head.next
#005.最長迴文子串2019/10/15
leetcode longest-palindromic-substring
題目考察字符串操作,對於最長迴文字串問題可以採用中間擴展算法求解。
class Solution:
def longestPalindrome(self, s: str) -> str:
"""返回字符串s中的最長迴文子串"""
str_start, str_end = 0, 0
if (s == None) or (len(s) < 1):
return ''
for index in range(len(s)):
str_len1 = get_around_center(s, index, index)
str_len2 = get_around_center(s, index, index + 1)
str_len = max(str_len1, str_len2)
if str_len > str_end - str_start:
str_start = index - int((str_len - 1) / 2)
str_end = index + int(str_len / 2)
return s[str_start:str_end + 1]
def get_around_center(string, left_index, right_index) -> int:
"""中心擴展 返回迴文子串長度"""
left, right = left_index, right_index
while (left >= 0) and (right < len(string)) \
and (string[left] == string[right]):
left -= 1
right += 1
return right - left - 1
中心擴展:
由於迴文子串是鏡像對稱的,因此可以每次循環選則一箇中心,同時向左和向右擴展索引,判斷左右索引對應的字符是否相同。這裏特別需要注意,對於一個長度爲n的字符串,有2n-1箇中心,這是因爲有迴文串可能有奇數個字符或偶數的字符,如圖所示。
#006.字符串轉換整數2019/10/16
Leetcode string-to-integer-atoi
題目考察正則表達式在字符串處理中的的應用(第一次解題時使用了暴力解法)
class Solution:
def myAtoi(self, str: str) -> int:
curr_index = 0 # 當前字符索引
res = 0 # 結果
sign = 1 # 符號,1正號,-1負號
# 獲得第一個非空字符索引
while curr_index < len(str) and str[curr_index] == ' ':
curr_index += 1
if curr_index == len(str):
return 0
if str[curr_index] == '-':
sign = -1
curr_index += 1
elif str[curr_index] == '+':
sign = 1
curr_index += 1
while (curr_index < len(str)) and (str[curr_index] >= '0') \
and (str[curr_index] <= '9'):
res = res * 10 + int(str[curr_index])
curr_index += 1
if res >= 2147483648:
if sign == 1:
return 2147483647
elif sign == -1:
return -2147483648
return res * sign
在處理較複雜的字符串索引和匹配問題時,用暴力解法需要很多循環和if條件語句,這不是一種好的方法。計算機科學中正則表達式語言是一種專門用於字符串處理的語言,它使用一種數學算法來解決計算機程序中的文本檢索,匹配等問題(re 模塊使 Python 語言擁有全部的正則表達式功能)。
檢索:通過正則表達式,從字符串中獲取我們想要的部分
匹配:判斷給定的字符串是否符合正則表達式的過濾邏輯
可以理解爲正則表達式表述了一個字符串的書寫規則,如:判斷用戶輸入的密碼是否合法,判斷用戶輸入的郵箱格式是否合法。
正則表達式就是由普通字符以及特殊字符組成(成爲元字符)的文字模式。該模式描述在查找文字主體時待匹配的一個或多個字符串。
下面是整理的一些元字符及示例:
元字符 |
描述 |
\ |
將下一個字符標記爲一個特殊字符、或一個原義字符、或一個向後引用、或一個八進制轉義符。例如,“\\n”匹配\n。“\n”匹配換行符。序列“\\”匹配“\”而“\(”則匹配“(”。即相當於多種編程語言中都有的“轉義字符”的概念。 |
^ |
匹配輸入字符串的開始位置。如果設置了RegExp對象的Multiline屬性,^也匹配“\n”或“\r”之後的位置。 |
$ |
匹配輸入字符串的結束位置。如果設置了RegExp對象的Multiline屬性,$也匹配“\n”或“\r”之前的位置。 |
* |
匹配前面的子表達式任意次。例如,zo*能匹配“z”,“zo”以及“zoo”。*等價於{0,}。 |
+ |
匹配前面的子表達式一次或多次(大於等於1次)。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等價於{1,}。 |
? |
匹配前面的子表達式零次或一次。例如,“do(es)?”可以匹配“do”或“does”中的“do”。?等價於{0,1}。 |
? |
當該字符緊跟在任何一個其他限制符(*,+,?,{n},{n,},{n,m})後面時,匹配模式是非貪婪的。非貪婪模式儘可能少的匹配所搜索的字符串,而默認的貪婪模式則儘可能多的匹配所搜索的字符串。例如,對於字符串“oooo”,“o+?”將匹配單個“o”,而“o+”將匹配所有“o”。 |
.點 |
匹配除“\r\n”之外的任何單個字符。要匹配包括“\r\n”在內的任何字符,請使用像“[\s\S]”的模式。 |
x|y |
匹配x或y。例如,“z|food”能匹配“z”或“food”或"zood"(此處請謹慎)。“(z|f)ood”則匹配“zood”或“food”。 |
[xyz] |
字符集合。匹配所包含的任意一個字符。例如,“[abc]”可以匹配“plain”中的“a”。 |
[^xyz] |
負值字符集合。匹配未包含的任意字符。例如,“[^abc]”可以匹配“plain”中的“plin”。 |
[a-z] |
字符範圍。匹配指定範圍內的任意字符。例如,“[a-z]”可以匹配“a”到“z”範圍內的任意小寫字母字符。 注意:只有連字符在字符組內部時,並且出現在兩個字符之間時,才能表示字符的範圍; 如果出字符組的開頭,則只能表示連字符本身. |
[^a-z] |
負值字符範圍。匹配任何不在指定範圍內的任意字符。例如,“[^a-z]”可以匹配任何不在“a”到“z”範圍內的任意字符。 |
\b |
匹配一個單詞邊界,也就是指單詞和空格間的位置(即正則表達式的“匹配”有兩種概念,一種是匹配字符,一種是匹配位置,這裏的\b就是匹配位置的)。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。 |
\B |
匹配非單詞邊界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。 |
\d |
匹配一個數字字符。等價於[0-9]。 |
\D |
匹配一個非數字字符。等價於[^0-9]。 |
\f |
匹配一個換頁符。等價於\x0c和\cL。 |
\n |
匹配一個換行符。等價於\x0a和\cJ。 |
\r |
匹配一個回車符。等價於\x0d和\cM。 |
\s |
匹配任何不可見字符,包括空格、製表符、換頁符等等。等價於[ \f\n\r\t\v]。 |
\S |
匹配任何可見字符。等價於[^ \f\n\r\t\v]。 |
\t |
匹配一個製表符。等價於\x09和\cI。 |
\v |
匹配一個垂直製表符。等價於\x0b和\cK。 |
\w |
匹配包括下劃線的任何單詞字符。類似但不等價於“[A-Za-z0-9_]”,這裏的"單詞"字符使用Unicode字符集。 |
\W |
匹配任何非單詞字符。等價於“[^A-Za-z0-9_]”。 |
| |
將兩個匹配條件進行邏輯“或”(Or)運算。例如正則表達式(him|her) 匹配"it belongs to him"和"it belongs to her",但是不能匹配"it belongs to them."。注意:這個元字符不是所有的軟件都支持的。 |
+ |
匹配1或多個正好在它之前的那個字符。例如正則表達式9+匹配9、99、999等。注意:這個元字符不是所有的軟件都支持的。 |
? |
匹配0或1個正好在它之前的那個字符。注意:這個元字符不是所有的軟件都支持的。 |
題目的正則表達式解法如下:
class Solution:
def myAtoi(self, s: str) -> int:
return max(
min(int(*re.findall('^[\+\-]?\d+', s.lstrip())), 2 ** 31 - 1), -2 ** 31)
分析:
^ 匹配字符串開頭
\+\- 表示加減字符
[] 匹配方括號內的任一個字符,[\+\-]即匹配加號或減號
? 前一個字符可有可無
\d 匹配數字字符0~9
+ 匹配前面的一個或多個
整個pattern = '^[\+\-]?\d+'表示從字符串開始,匹配加號或減號(可以沒有),並匹配數字字符0~9,直到匹配結果不是數字字符。
re.findall(pattern, string) 返回string中所有與pattern(正則表達式)不重疊匹配項的列表
*將re.findall返回的列表打散爲元素(如res=[1, 3], *res輸出爲1 3),同樣*也用於將元祖、字典、字符串打散
int()將結果轉爲整型並用max(),min()防止越界,滿足題目要求(實際上python中不存在32位int類型,不會有越界的問題)
#010.正則表達式匹配2019/10/16
Leetcode regular-expression-matching
題目考察對正則表達式的理解(*和.),以及遞歸思想的理解。
class Solution:
def isMatch(self, s: str, p: str) -> bool:
# 以規則p是否爲空,可以分爲兩種情況
if len(p) == 0:
return not len(s)
# 遞歸問題考慮好當下(第一個字符能否匹配),其餘交給遞歸
first_match = len(s) != 0 and p[0] in [s[0], '.']
# 考慮到若p的第一個字符之後是*,會存在兩種情況
if len(p) >= 2 and p[1] == '*':
# p[1]是*,且s與p首字符不匹配,那麼跳過p的*,繼續用s與剩餘的p匹配
# p[1]是*,且s與p首字符匹配,那麼從s的下一個字符與p匹配
return self.isMatch(s, p[2:]) \
or first_match and self.isMatch(s[1:], p)
# 若p的第一個字符之後不是*,則只要首字符匹配,就匹配s和p的剩餘字符
else:
return first_match and self.isMatch(s[1:], p[1:])
遞歸包括遞歸條件(什麼情況函數調用自己)和基線條件(什麼情況停止調用自己)。
#007.整數反轉2019/10/17
題目考察數字界限、對算數運算符操作的理解以及彈出和推入數字的方法(最開始的想法是數字轉字符列表彈出推入)
class Solution:
def reverse(self, x: int) -> int:
val, res = abs(x), 0
# x正負
sign = 1 if x >= 0 else 0
# 彈出和推入數字
while val > 0:
res = res * 10 + val % 10
if (res > 2147483647 and sign == 1) \
or (res > 2147483648 and sign == 0):
return 0
val //= 10
return res if sign == 1 else -res
彈出x最後一位,推入res的後面就可以實現x和res相反,在不使用輔助堆棧和數組的情況下,可以使用數學方法,利用取模和取整運算實現這一過程。
需要注意的是Python中,取整運算支持浮點數,是向下取整(而不是C中的向0取整),並且//纔是取整運算,而/在Python2.x與3.x中的結果不同。
向下取整:-5//2=-3,5/2=2,C向0取整:-5/2=-2,5/2=2
Python中 2.x中x/y,x、y爲整數,返回整數;x、y有浮點數,返回浮點數;3.x中不論x、y是整數或浮點數都會返回浮點數。而Python2.x和3.x中x//y的結果一致,x、y爲整數返回整數,x、y有浮點數返回浮點數。
5 / -2
#2.X 商:-3
#3.X 商:-2.5
5.0 / -2
#2.X 商:-2.5
#3.X 商:-2.5
-5 // 2
#2.X 商:-3
#3.X 商:-3
-5.0 // 2
#2.X 商:-3.0
#3.X 商:-3.0
#012.整數轉羅馬數字2019/10/17
題目考察貪心算法的基本思想
class Solution:
def intToRoman(self, num: int) -> str:
roma_val = (('M', 1000), ('CM', 900), ('D', 500), ('CD', 400),
('C', 100), ('XC', 90), ('L', 50), ('XL', 40),
('X', 10), ('IX',9), ('V', 5), ('IV', 4), ('I', 1))
res = ''
for roma, val in roma_val:
# 貪心選擇
while num >= val:
res += roma
num -= val
return res
貪心算法以迭代的方式作出相繼的貪心選擇,每作一次貪心選擇就將所求問題簡化爲規模更小的子問題。對於一個具體問題,要確定它是否具有貪心選擇性質,必須證明每一步所作的貪心選擇的局部最優解最終導致問題的整體最優解。
在本題中,將數字轉化爲羅馬數字的過程,就是將roma_val中的數字作爲分解因子,用盡可能少的分解因子去分解一個整數的過程。每次循環中,都在剩餘數字中選擇儘可能大的數字去作爲分解因子(貪心法則)。
#001.兩數之和2019/10/18
本題考查列表及其操作。如果使用字典模擬哈希表可以極大的提高效率。
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for index in range(len(nums)):
if target - nums[index] in nums[index + 1:]:
return [index, nums.index(target - nums[index], index + 1)]
python列表的index(obj)方法可以從列表中找出某個值第一個匹配項的索引位置。
上述方法本質是暴力解法,雖然通過切片減少了一部分循環,但效率仍比較低。通過Python中的字典實現哈希表(一個用於存儲Key-Value鍵值對的集合)結構,可以提高執行效率。
爲了找到列表元素對應的索引,可以用enumerate()函數,它用於將一個可遍歷的數據對象(如列表、元組或字符串)組合爲一個索引序列,同時列出數據和數據下標。
seq = ['one', 'two', 'three']
for index, element in enumerate(seq):
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashmap = {}
for index in range(len(nums)):
# 用字典創建哈希表
if hashmap.get(target - nums[index]) is not None:
return [hashmap.get(target - nums[index]), index]
hashmap[nums[index]] = index
#013.羅馬數字轉整數2019/10/18
題目考察字符串處理,這裏可以用查表的方法實現轉化。
class Solution:
def romanToInt(self, s: str) -> int:
hashmap = {'I': 1, 'IV': 4, 'V': 5, 'IX': 9, 'X': 10, 'XL': 40, 'L': 50,
'XC': 90, 'C': 100, 'CD': 400, 'D': 500, 'CM': 900,
'M': 1000}
res, index = 0, 0
while index < len(s):
if hashmap.get(s[index:index + 2]) is not None:
res += hashmap[s[index:index + 2]]
index += 2
else:
res += hashmap[s[index]]
index += 1
return res
爲什麼這裏用字典創建表,而在#012題中不用?因爲#012題創建表時是按值從大到小的順序排列,爲了便於接下來從頭到尾順序訪問。而這裏如果用列表或元組創建表,接下來查找羅馬數字就需要遍歷,時間複雜複雜度是O(n);而用字典創建表,通過鍵(待查找的羅馬數字)可以直接訪問其值,不需要遍歷,時間複雜度是O(1)。
#021.合併兩個有序鏈表2019/10/19
Leetcode merge-two-sorted-lists
題目考察鏈表操作和遞歸
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if l1 and l2:
if l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
else:
temp = l1
l1 = l2
l2 = temp
l1.next = self.mergeTwoLists(l1.next, l2)
return l1 or l2
再次對遞歸進行一次理解,引用一位博主的例子:
遞歸:你打開面前這扇門,看到屋裏面還有一扇門(這門可能跟前面打開的門一樣大小(靜),也可能門小了些(動)),你走過去,發現手中的鑰匙還可以打開它,你推開門,發現裏面還有一扇門,你繼續打開,。。。, 若干次之後,你打開面前一扇門,發現只有一間屋子,沒有門了。 你開始原路返回,每走回一間屋子,你數一次,走到入口的時候,你可以回答出你到底用這鑰匙開了幾扇門。 遞歸就是有去(遞去)有回(歸來)。
具體來說,爲什麼可以”有去“?
這要求遞歸的問題需要是可以用同樣的解題思路來回答除了規模大小不同其他完全一樣的問題。
爲什麼可以”有回“?
這要求這些問題不斷從大到小,從近及遠的過程中,會有一個終點,一個臨界點,一個baseline,一個你到了那個點就不用再往更小,更遠的地方走下去的點,然後從那個點開始,原路返回到原點。
本例中,將問題轉化爲“l1,l2當前頭節點元素值較小的是l1嗎”這樣一個子問題,接着從鏈表下一個節點開始繼續解決這個子問題(遞),直到到達任一鏈表的啞節點(終止條件),從結尾開始,將返回的鏈表添加到上一節點的next屬性中,直到返回頭結點(歸)。
另外,Python中變量交換值有一種特殊的方式,不需要使用中間變量:x,y=y,x即可交換x,y的值。它的機制是首先構造一個元組(y, x),然後構造另一個元組(x, y),接着用元組(y, x)賦值給(x, y),元組賦值過程從左到右,依次進行。