題目說明
- 1~3題爲騰訊2019春招筆試題
- 4~7題爲頭條2019春招筆試題
- 8~70題爲《劍指offer》第二版中的習題
代碼地址:https://github.com/jh0905/data_structure_and_algorithm (裏面包含更多專題代碼,如二分專題、揹包專題、深搜專題、二叉樹專題等)
文章目錄
- 1. 硬幣問題(貪心算法)
- 2. 奇怪的數列(分情況討論)
- 3. 猜拳遊戲(排列組合)
- 4. 氣球遊戲(滑動窗口)
- 5. 變身程序員(BFS,多源最短路問題)
- 6. 特徵提取(暴力法的優化)
- 7. 機器人跳躍問題(二分法)
- 8. 找出數組中重複的數字(n個坑,n個數)
- 9. 不修改數組找出重複的數字(n個坑,n+1個數)
- 10. 重建二叉樹(DFS)
- 11. 二叉樹中的下一個節點(分情況討論)
- 12. 尋找旋轉排序數組中的最小值(二分法)
- 13. 矩陣中的路徑(回溯法 && DFS)
- 14. 機器人的運動範圍(BFS)
- 15. 剪繩子(整數劃分)
- 16. 二進制中1的個數(位運算)
- 17. 刪除鏈表中重複的節點(雙指針法)
- 18. 調整數組順序使偶數位於奇數之後(雙指針法)
- 19. 返回鏈表中倒數第k個節點(雙指針法)
- 20. 鏈表中環的入口位置(快慢指針,找規律)
- 21. 反轉鏈表(三指針法)
- 22. 合併兩個有序的鏈表(雙指針法)
- 23. 樹的子結構(雙重遞歸)
- 24. 對稱的二叉樹(二叉樹鏡像 && DFS)
- 25. 順時針打印矩陣(蛇形遍歷)
- 26. 包含min 函數的棧(輔助棧,單調遞減棧)
- 27. 棧的壓入、彈出序列(出棧順序)
- 28. 分行從上往下打印二叉樹(BFS)
- 29. 二叉搜索樹的後序遍歷(DFS)
- 30. 二叉樹中的所有路徑(DFS && 回溯法)
- 31. 二叉樹中和爲S的路徑(DFS && 回溯法)
- 32. 二叉樹中根結點到某一結點的路徑(DFS && 回溯法)
- 33. 複雜鏈表的複製(鏈表插入與刪除)
- 34. 二叉搜索樹與雙向鏈表(DFS && 分情況討論)
- 35. 數字的全排列(DFS && 二進制標記 )
- 36. 數組中出現次數超過一半的數字(消除法)
- 37. 最小的k個數(最大堆)
- 38. 連續子數組的最大和(一維動態規劃)
- 39. 從1到n的整數中1出現的次數(分情況討論)
- 40. 數字序列中某一位的數字(分情況討論)
- 41. 把數組排成最小的數(自定義排序)
- 42. 把數字翻譯成字符串(一維動態規劃)
- 43. 棋盤的最大價值(二維動態規劃)
- 44. 最長不含重複字符的子字符串(一維動態規劃)
- 45. 醜數(三路歸併)
- 46. 正整數分解成質因子表示(分解質因子)
- 47. 判斷一個數是否爲質數(質數判別)
- 48. 字符串中第一個只出現一次的字符(哈希表)
- 49. 字符流中第一個只出現一次的字符(哈希表,隊列)
- 50. 數組中的逆序對(二路歸併)
- 51. 兩個鏈表的第一個公共結點(找規律)
- 52. 數字在排序數組中出現的次數(二分法)
- 53. 有序數組中數值和下標相等的元素(二分法)
- 54. 二叉搜索樹的第k小節點(中序遍歷 && DFS)
- 55. 平衡二叉樹(DFS優化)
- 56. 數組中只出現一次的兩個數字(位運算)
- 57. 數組中唯一隻出現一次的數字(二進制統計)
- 58. 和爲S的兩個數字(哈希表)
- 58. 和爲S的連續正整數序列(高斯求和,雙指針法)
- 59. 最長遞增子序列(動態規劃或二分法)
- 60. 翻轉字符串(操作分解)
- 61. 滑動窗口的最大值(單調、雙向隊列)
- 62. n個骰子的點數(遞歸或動態規劃)
- 63. 撲克牌中的順子(抽象建模,逆向思維)
- 64. 圓圈中最後剩下的數字(抽象建模,約瑟夫環)
- 65. 股票的最大利潤(抽象建模)
- 66. 求1+2+…+n ( a and b )
- 67. 不用加減乘除做加法(位運算)
- 68. 構建乘積數組(操作分解)
- 69. 把字符串轉換成整數(字符與digit)
- 70. 二叉樹中兩個節點的最低公共祖先(DFS)
1. 硬幣問題(貪心算法)
牛家村的貨幣是一種很神奇的連續貨幣,他們貨幣的最大面額是n,並且一共有面額爲1,2,3,…,n,n種面額的硬幣。牛牛每次購買商品都會帶上所有面額的硬幣,支付時會選擇給出硬幣數量最少的方案。(每種面額的硬幣有無限多個)
輸入爲兩個整數m和n,表示貨幣的最大面額和商品的價格,輸出爲牛牛最少給出的硬幣數量。
【分析】
顯然這是一個貪心算法,即儘可能多的用最大面額的硬幣,如果剩餘的商品價格小於最大硬幣的話,就用對應金額的一枚硬幣來填充。分析完之後,這就是一個向上取整的問題,在Python3中,直接 return (m+n-1) // m
來實現。
2. 奇怪的數列(分情況討論)
有這麼一個數列,,可以發現,第奇數個元素的值爲負數,第偶數個元素的值爲正數,現在給出一個區間,表示第個元素,表示第個元素,請輸出區間所有元素的累加和。
【分析】
觀察發現,數列中每相鄰的兩個元素的和爲同一個數,要麼爲+1,要麼爲-1,於是我們可以將區間裏的元素兩兩分組,這裏分組也是有兩種情況,要麼就剩下最後一個元素,要麼所有元素都配對完成,之後就是一個簡單的求和了。【考察分情況討論的能力】
l,r = [int(x) for x in input().split()] # 獲取輸入的區間範圍
n_groups = (r-l+1)//2 # 獲取分組數
reset= 0
if l%2 == 0: # l爲偶數,相鄰元素和爲-1
res = -1*n_groups
else:
res = n_groups
if (r-l+1)%2 == 1: # 如果區間爲奇數,則還會剩下一個數,這裏的 r 記得判斷是正數還是負數!!!!!!
print(res + r*pow(-1,r))
else:
print(res)
3. 猜拳遊戲(排列組合)
兩人玩一個石頭剪刀布的遊戲,遊戲用卡片來玩,每張卡片分別是石頭、剪刀、布中的一種,每種類型的卡片數量有無數個,贏局得1分,輸局或平局得0分,小A先出牌,把張卡片擺好,那麼小B在看得到小A每張牌的擺放情況下,如果要得s分,有多少種擺牌的方法呢?
【分析】
根據題意,小B要得分,就意味着他有張卡片要勝過小A的卡片,用組合數表示爲,剩下的張卡片,則爲平局或輸掉,即有種可能,也就是說,一共有種擺法,那麼我們剩下來要做的事情,就是如何在滿足內存和時間限制的前提下,計算出這個結果的值。
- 楊輝三角公式:(用遞歸來實現)
def f(m, n):
# 一定要記得處理 n = 0 的特殊情況
if n == 0:
return 1
elif n == 1:
return m
elif m == n:
return 1
else:
return f(m - 1, n - 1) + f(m - 1, n)
- 轉換爲對數:
展開
消除相同項(大大降低了計算的複雜度)
組合數還有一個性質
於是我們在正式計算之前,判斷 ,不滿足的話,令n=m-n .最終把計算結果再取指數e,用round四捨五入得到最終值。
import math
def g(m, n):
# 一定要記得處理 n = 0 的特殊情況
if n == 0:
return 1
if n > m // 2:
n = m - n
sum_1 = sum_2 = 0
for i in range(n + 1, m + 1):
sum_1 += math.log(i)
for j in range(1, m - n + 1):
sum_2 += math.log(j)
return math.exp(sum_1 - sum_2)
經試驗證明,當n較小的時候,兩種方式時間差別不是很大,但是當n變大時,二者的時間可以相差好幾個量級!因此,推薦第二種解法。
4. 氣球遊戲(滑動窗口)
小Q在玩射擊氣球的遊戲,如果小Q在連續T槍內打爆了所有顏色的氣球,則會獲得獎勵(每種顏色至少一隻)。這個遊戲中共有m種不同顏色的氣球,編號1到m,小Q連續開了n槍,命中的話,第n槍在數組中對應的值爲氣球編號,未命中則爲0.
輸入格式
第一行輸入由空格隔開的兩個整數n, m
第二行有n個被空格隔開的整數
輸入示例:
12 5
2 5 3 1 3 2 4 1 0 5 4 3
輸出格式
輸出一個整數,表示最小連續射中所有顏色氣球的槍數
【分析】
這道題是典型的滑動窗口問題,先對滑動窗口做個簡要介紹:
它也叫雙指針算法,開始時刻,前、後指針都位於數組的第一個元素。前指針每次移動一位,後指針每次移動若干位。更進一步地分析,一開始前指針不動,後指針往後移動一位,每移動一位時,都會進行判斷兩個指針之間的元素是否滿足題目要求,當滿足要求時,後指針暫時停止移動。然後前指針開始往後移動一位,判斷兩個指針區間的元素是否仍然滿足要求,是的話,後指針不動,前指針繼續往後移一位。直到前指針判斷移動後,要求不再滿足時,則前指針不移動,隨後換後指針後移一位,然後前指針判斷是否需要後移一位。就這麼持續下去,直到後指針和前指針都停止移動爲止。這個時間複雜度是線性的。
關於本題的解析:
根據上面的分析,我們判斷的條件,就是窗口內是否包含了每一種氣球的顏色,我們可以用判斷colors == m
,然後再用一個長度爲m的數組,存儲當前窗口內每種顏色的氣球的個數。此外,我們這裏是要輸出滿足條件的最小窗口大小,所以當colors == m
成立時,更新一下min_window_size的值。(代碼更加具體,這裏要注意未命中的情況balls[i]=0,記得排查,我忘記了幾次!)
n, m = [int(x) for x in input().split()] # n爲balls數組的長度,m爲氣球的顏色數
balls = [int(x) for x in input().split()]
i = j = 0 # i表示前指針,j表示後指針
colors = 0
color_list = [0] * (m + 1) # 這裏初始化爲m+1,是爲了保證編號爲j的氣球,對應color_list[j],把color_list[0]空出來
res = n + 1 # 初始化返回值
while j < n:
# 如果擊中了氣球並且滑動窗口中沒這個值
if balls[j] != 0 and color_list[balls[j]] == 0:
colors += 1
color_list[balls[j]] += 1
if colors == m: # 判斷前指針的移動情況
# balls[i] == 0 表示第i槍未命中氣球,color_list[balls[i]] > 1表示有重複顏色氣球被打破
while balls[i] == 0 or color_list[balls[i]] > 1:
color_list[balls[i]] -= 1
i += 1
res = min(res, j - i + 1)
j += 1
print(res)
5. 變身程序員(BFS,多源最短路問題)
公司的程序員不夠用了,決定把產品經理都轉變爲程序員以解決開發時間長的問題。
在給定的矩形網格中,每個單元格可以有以下三個值之一:
- 值0表示空單元格
- 值1表示產品經理
- 值2表示程序員
每一分鐘,程序員都會把他上下左右相鄰的產品經理變成程序員(1變成2)。
返回直到單元格中沒有產品經理爲止所必須經過的最小分鐘數,如果不可能,返回-1.
以下是一個四分鐘轉換的例子:
【分析】
題目的意思很好理解,在一個由三個數字填滿的二維矩陣。每一輪,數字會把它上下左右相鄰的變成,然後進入下一輪,上一輪被轉變的會把它相鄰的數字繼續轉換爲,由此遞歸下去。這其實就是圖搜索中的寬度優先搜索過程,由於我們可能會有多個起點(元素),所以它也可以歸類爲多源最短路問題。
關於本題的解析
多源最短路問題解法分爲兩步:
(1)所有起點(源)座標插入隊列 隊列具有先進先出的性質;
(2)進行 ,每次彈出隊列中的第一個元素queue.pop(0)
,然後搜索該元素相連的點(在本題中是上下左右四個點),搜索到滿足要求的點,修改該點距離起點的距離,並把該點的座標append
到隊列中;(當隊列中的元素爲空時,搜索結束)
import sys
lines = sys.stdin.readlines()
input_mat = []
for line in lines:
input_mat.append([int(x) for x in line.strip().split()])
rows = len(input_mat)
columns = len(input_mat[0])
# 初始化distance矩陣,shape和輸入矩陣一樣,目的是存儲矩陣中每個點距離起點的距離
dist_mat = [[-1 for i in range(columns)] for j in range(rows)]
# 第一步:把第一輪遍歷的起點座標加入到隊列中
queue = []
for i in range(rows):
for j in range(columns):
if input_mat[i][j] == 2:
dist_mat[i][j] = 0
queue.append([i, j])
# 每一對[dx,dy]表示朝上下左右的某一個方向移動
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]
# 第二步:開始 breadth first search
while queue:
idx_x, idx_y = queue.pop(0) # 彈出隊列中的第一個元素
for i in range(4):
x = idx_x + dx[i]
y = idx_y + dy[i]
# 如果上下左右的點,索引沒有越界,並且它對於的值爲1,而且它還沒被訪問過dist_mat[x][y] == -1
if 0 <= x < rows and 0 <= y < columns and input_mat[x][y] == 1 and dist_mat[x][y] == -1:
dist_mat[x][y] = dist_mat[idx_x][idx_y] + 1 # 當前點距起點的距離,等於它上一點的距離值+1
queue.append([x, y]) # 把這個點添加到隊列中,之後繼續執行bfs
# 第三步:遍歷距離矩陣,找到-1則返回-1,否則返回矩陣中最大的值
res = 0
for i in range(rows):
for j in range(columns):
if dist_mat[i][j] == -1:
res = -1
else:
res = max(res, dist_mat[i][j])
print(res)
6. 特徵提取(暴力法的優化)
小明想從貓咪視頻中挖掘一些貓咪的運動信息,爲了提取運動信息,他需要從視頻的每一幀中提取特徵。
一個貓咪特徵是一個二維的 <x, y>.
當 時,我們認爲<,>和<,>爲相同特徵。
如果在連續的幾個幀裏面,都出現了相同的特徵,它將構成特徵運動。
小明期望找到最長的特徵運動長度
輸入格式
第一行爲正整數M,代表視頻的幀數
接下來的M行裏,每行代表一幀,第一個數字代表該幀的特徵個數,接下來的數字代表特徵的取值,比如樣例輸入第三行裏,2 1 1 2 2,表示2個特徵,分別爲<1, 1>、<2, 2>
輸出格式
輸出一個整數,表示最長特徵運動長度
【分析】
題目的意思是,在輸入的連續幀中,遍歷每一個特徵連續出現的最大長度。我們可以直接用暴力法來嘗試求解此題。注意哦,我們每一次搜索是從當前幀往上進行搜索!
暴力法求解思路
- 第一層for循環,是對輸入的所有幀進行遍歷;
- 第二層for循環,是對當前幀裏的每一組特徵進行遍歷;
- 第三層for循環;是從當前幀往上面的每一幀進行遍歷;
- 第四層for循環,是比較遍歷的幀中是否包含此時的特徵值。
# 接收輸入,存儲所有幀的信息
M = int(input())
frames = []
while M:
frame = [int(x) for x in input().strip().split()]
n = frame.pop(0) # 提取出特徵總數
features = [] # 用來存儲特徵對
for i in range(n):
# 注意,這裏把特徵對保存爲tuple形式,是爲了之後讓它作爲dict的鍵,因爲list不能作爲鍵
features.append(tuple(frame[2 * i: 2 * i + 2]))
frames.append(features)
M -= 1
# 當前幀是一定有該特徵的,故初始長度爲1,我們從當前層的上一幀開始查找
max_length = length = 1
for i in range(len(frames)): # 第一層:從上到下,遍歷每一幀
for j in range(len(frames[i])): # 第二層:遍歷每一幀的每一個特徵對
for k in range(i - 1, -1, -1): # 第三層:從當前幀的上一幀,往上查找
if frames[i][j] in frames[k]: # 第四層:判斷當前特徵是否在該幀出現
length += 1
else:
break # 退出第三層循環
max_length = max(max_length, length)
length = 1 # 退出第三層循環時,要把length重置爲1
print(max_length)
暴力法的優化
上面寫到的暴力法,會帶來運行超時的問題,所以我們針對上面的做法進行優化。設置last_time和count兩個字典變量,last_time[(x,y)]表示特徵對(x,y)上一次出現的幀,count(x,y)表示特徵對(x,y)的最長特徵長度。
如果last_time[(x,y)] < i-1 (i-1表示當前幀的上一幀) 的話,說明特徵不連續了,我們不需要往上進行查找,並更新last_time[(x,y)]和count(x,y)的值;
如果last_time[(x,y)] == i-1,那麼當前特徵的最長特徵長度,則爲count[(x, y)]+1,同樣更新last_time[(x,y)]的值;
max_length = 0
last_time = dict()
count = dict() # 初始化兩個字典變量
for i in range(len(frames)): # 第一層:從上到下,遍歷每一幀
for j in range(len(frames[i])): # 第二層:遍歷每一幀的每一個特徵對
feature_pair = frames[i][j]
if feature_pair not in last_time: # 如果當前特徵第一次出現
count[feature_pair] = 1
elif last_time[feature_pair] == i - 1: # 當前特徵在上一幀中出現
count[feature_pair] += 1
elif last_time[feature_pair] < i - 1: # 如果同一個幀中有兩個相同的特徵,則會出現last_time[feature_pair] == i > i-1
count[feature_pair] = 1
max_length = max(max_length, count[feature_pair])
last_time[feature_pair] = i
print(max_length)
Python裏面儘量不要使用連等於賦值變量,很容易出問題。我這邊一開始初始化last_time=count=dict()
,結果一直出錯,發現這兩個變量被綁定在一起,我對last_time賦值的時候,count也被賦值了,所以變量初始化的話,就不要用連等了,容易出錯。
7. 機器人跳躍問題(二分法)
機器人正在玩一個古老的基於DOS的遊戲,遊戲中有N+1座建築,從0到N編號,從左到右排列。
編號爲0的建築高度爲0個單位,編號爲的建築爲個單位。
起初,機器人在編號爲0的建築處,每一步,它要跳到下一個建築。
假設機器人在第k個建築,且它的能量值爲E,下一步它將跳到第k+1個建築。
如果,它將失去的能量,否則它將獲得 的能量。
遊戲目標是到底第N個建築,在這個過程中,機器人的能量不能爲負數。
現在的問題是,機器人初始時以多少能量值開始遊戲,纔可以保證成功完成這個遊戲。
輸入格式
第一行輸入正數,
第二行爲N個空格隔開的整數,
輸出格式
一個整數,表示最小的能量值
【分析】
題目的要求是,機器人的能量不能爲負數,即假設機器人到達第個建築的時候,它的能量值爲 ,那麼它跳到第k+1個建築的時候,能量值則變爲 。
於是我們可以用二分查找法,在 區間內,找到一個值,使得低於它的無法通過遊戲,高於或等於它的都能通過遊戲。
N = int(input())
h = [int(x) for x in input().strip().split()]
# 判斷能量值e能否跳完所有建築
def check(e):
for i in range(N):
e = 2 * e - h[i]
if e < 0:
return 0
return 1
# log N的時間複雜度很低,我們直接設置搜索區間爲[0,10010]
l = 0
r = 10010
while l < r:
mid = (l + r) // 2
# 如果mid成立,那麼說明答案在左區間,用模板1(見下文)
if check(mid):
r = mid
else:
l = mid + 1
print(l)
【注】上面代碼使用的前提是,在查找區間裏,一定有符合題意的搜索結果!上面代碼,可以作爲二分查找的一個模板,但是要記得使用前提。
二分查找法的時間複雜度爲,是時間複雜度最低的算法,,也就是說哪怕搜索空間擴大一個量級,搜索次數也沒擴大多少。
二分查找模板總結
假設目標值在閉區間中,每次將區間長度縮小一半,當時,我們就找到了目標值。
- 模板一:如果check條件成立,答案在左區間並且mid也可能是答案時,用此模板:
def bsearck_1(l, r):
while l < r:
mid = (l + r) // 2
if check(mid):
r = mid
else:
l = mid + 1
return l
- 模板二:如果check條件成立,答案在右區間並且mid也可能是答案時,用此模板:
def bsearck_2(l, r):
while l < r:
mid = (l + r + 1) // 2 # 避免死循環,解決l=r-1的情況
if check(mid):
l = mid
else:
r = mid - 1
return l
8. 找出數組中重複的數字(n個坑,n個數)
在一個長度爲n的數組裏的所有數字都在的範圍內。
數組中某些數字是重複的,但不知道有幾個數字是重複的,也不知道每個數字重複幾次。
請找出數組中任意一個重複的數字!
注意:如果某些數字不在範圍內,輸出 -1
【分析】
數組的特性是,所有值都在內,一共有個數,如果沒有重複數字的話,那麼每個元素應該在它對應的下標位置。於是我們從前往後遍歷,如果當前元素不在正確位置上,那就swap nums[i] 和 nums[nums[i]]
,一直進行下去,直到交換後的兩個數都在其正確位置上。當退出while循環的時候,如果當前位置的元素不在其正確位置上,而它想交換的元素已經在正確位置上,那就找到重複元素了。
class Solution:
def duplicate(self, numbers):
if numbers is None:
return -1
# 題目要求輸入的數組在[0,n-1]區間內
for i in range(len(numbers)):
if numbers[i] < 0 or numbers[i] > len(numbers) - 1:
return -1
# 時間複雜度爲O(n)
for i in range(len(numbers)):
# 當前索引與它對應的元素不等,並且以該元素作爲索引指向的值也不等於該元素時,則交換兩個元素
while i != numbers[i] and numbers[i] != numbers[numbers[i]]:
temp = numbers[i]
numbers[i] = numbers[numbers[i]]
numbers[temp] = temp # 這裏交換的時候,得小心點
# 當前索引與它對應的元素不等,並且以該元素作爲索引指向的值等於該元素時,則發現重複元素
if i != numbers[i] and numbers[numbers[i]] == numbers[i]:
return numbers[i]
return -1
9. 不修改數組找出重複的數字(n個坑,n+1個數)
給定一個長度爲n+1的數組,數組中所有的數均在1~n的範圍內,其中n 1
請找出數組中任意一個重複的數,但不能修改輸入的數組
【分析】
數組中所有的數都在內,說明我們有n個坑,數組長度爲n+1,說明我們有n+1個數。這就體現了抽屜原理,我們有3個蘋果,放在2個抽屜裏,那麼肯定有一個抽屜裏的蘋果數超過1。
我們可以用分治的思想來做,把整個區間(所有的坑)一分爲二,那麼至少有一邊,裏面數的個數,肯定大於坑的個數。我們按照這個思想,用二分法來做。
class Solution:
def find_duplicate(self, numbers):
l = 1
r = len(numbers) - 1
while l < r:
mid = (l + r) // 2
if self.check(numbers, l, mid):
r = mid
else:
l = mid + 1
return l
def check(self, numbers, l, mid):
count = 0
for i in range(len(numbers)):
if l <= numbers[i] <= mid:
count += 1
if count > mid - l + 1:
return 1
else:
return 0
10. 重建二叉樹(DFS)
根據一棵樹的前序遍歷與中序遍歷構造二叉樹。
注意:你可以假設樹中沒有重複的元素
輸入:
前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]
輸出:
3
/ \
9 20
/ \
15 7
【分析】
已知 ,,所以先序遍歷的第一個元素,即爲根結點的值,找到根結點的值之後,可以將中序遍歷數組分成兩部分,得到左子樹的元素個數和右子樹的元素個數,按照此思路遞歸下去。核心在於設定遞歸式,我們這裏用dfs(self, pl, pr, il, ir)
完成遞歸,pl
表示前序遍歷左區間的下標,pr
表示前序遍歷右區間的下標,il
表示中序遍歷左區間的下標,il
表示中序遍歷右區間的下標。
本題的難點在於區間下標的設定,不能混淆數組下標和數組分片,否則邊界肯定會出問題,我們這裏只用數組下標,並且用閉區間進行表示(不考慮數組分片)。
還有一個問題是,類變量的使用,我之前沒這麼玩過,在類的方法中,調用類變量時,記得要在前面加上self
關鍵字!
class Solution(object):
preorder = []
inorder = []
def buildTree(self, _preorder, _inorder):
"""
:type preorder: List[int]
:type inorder: List[int]
:rtype: TreeNode
"""
self.preorder = _preorder
self.inorder = _inorder
return self.dfs(0, len(self.preorder) - 1, 0, len(self.inorder) - 1)
def dfs(self, pl, pr, il, ir):
"""
數組範圍是閉區間
pl:前序遍歷左邊界
pr:前序遍歷右邊界
il:中序遍歷左邊界
ir:中序遍歷右邊界
"""
if pl > pr:
return
root = TreeNode(self.preorder[pl])
idx = self.inorder.index(self.preorder[pl])
left = self.dfs(pl + 1, pl + idx - il, il, idx - 1)
right = self.dfs(pl + idx - il + 1, pr, idx + 1, ir)
root.left = left
root.right = right
return root
11. 二叉樹中的下一個節點(分情況討論)
給定一個二叉樹和其中的一個結點,請找出中序遍歷順序的下一個結點並且返回。
注意,樹中的結點不僅包含左右子結點,同時包含指向父結點的指針
【分析】
觀察下圖,我把圖中節點用三種顏色表示:
橙色節點:存在右子樹,它的下一個節點爲右子樹中最左側的節點;
綠色節點:不存在右子樹,但是它爲父節點的左兒子,它的下一個節點爲 node.father
藍色節點:不存在右子樹,但是它爲父節點的右兒子,往上遍歷,直到它的父節點爲它父節點的左兒子(如E的父節點爲B,B的父節點爲A,它爲A的左兒子
)或者父節點爲None(如G的父節點爲C,C的父節點爲A,A的父節點爲None
)
class TreeNode(object):
def __init__(self, x):
self.val = x
self.left = None
self.right = None
self.father = None
class Solution:
def getNext(self, pNode):
if pNode.right:
p = pNode.right
while p.left:
p = p.left
return p
while pNode.next and pNode.next.right == pNode:
pNode = pNode.next
return pNode.next
12. 尋找旋轉排序數組中的最小值(二分法)
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
你可以假設數組中可能存在重複元素。
輸入: [3,4,5,1,2]
輸出: 1
【分析】
根據題意,我們繪製圖像如下:
如圖所示,原始數組是一個升序數組,可能存在重複元素,在某個點旋轉之後,得到一個旋轉數組(綠色部分與橙色部分),如果我們把綠色可能存在的與橙色數組首元素相當的項(圖中黑線表示)去除點,那麼我們得到的數組就符合二分法的要求了,即所求元素將數組分爲兩個區間,左區間內的所有元素均大於等於數組中第一個元素,右區間內的所有元素均小於數組中的第一個元素。
另外要注意考慮特殊情況,即右邊數組可能爲空,這時候直接返回第一個元素!
class Solution:
def minNumberInRotateArray(self, rotateArray):
if len(rotateArray) == 0:
return -1
n = len(rotateArray) - 1
# 1.去重
while rotateArray[n] == rotateArray[0]:
n -= 1
# 2.如果剩下數組爲遞增序列,直接返回首元素
if rotateArray[n] >= rotateArray[0]:
return rotateArray[0]
# 3.否則使用二分查找法
l = 0
r = n
while l < r:
mid = (l + r) // 2
if rotateArray[mid] < rotateArray[0]:
r = mid
else:
l = mid + 1
return rotateArray[l]
題目稍作修改,如果要返回旋轉數組中最大的元素,將二分查找做一些變化即可。(這裏只討論二分法部分的代碼)
# 使用二分查找法找到最大的元素
l = 0
r = n
while l < r:
mid = (l + r + 1) // 2 # 避免死循環
if rotateArray[mid] < rotateArray[0]:
r = mid - 1
else:
l = mid
return rotateArray[l]
13. 矩陣中的路徑(回溯法 && DFS)
請設計一個函數,用來判斷在一個矩陣中是否存在一條包含某字符串所有字符的路徑。路徑可以從矩陣中的任意一個格子開始,每一步可以在矩陣中向左,向右,向上,向下移動一個格子。
如果一條路徑經過了矩陣中的某一個格子,則之後不能再次進入這個格子。
例如
b b c e
s f c s
a d e e
這樣的 矩陣中包含一條字符串"bcced"的路徑,但是矩陣中不包含"abcb"路徑,因爲字符串的第一個字符b佔據了矩陣中的第一行第二個格子之後,路徑不能再次進入該格子。
【分析】
本題考察的是一個回溯問題。回溯法(探索與回溯法)是一種選優搜索法,又稱爲試探法,按選優條件向前搜索,以達到目標。 但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術爲回溯法,而滿足回溯條件的某個狀態的點稱爲“回溯點”。
回溯問題一般會用到暴力法,枚舉思路很重要。
我們先枚舉單詞的起點(遍歷輸入矩陣中的每一個字母),然後使用深度優先遍歷,如果矩陣中的當前元素等於單詞中的當前字母,並且當前單詞的index
不等於單詞最後一個字母的index
的話,就DFS
該單詞的下一個字母(上下左右進行搜索)。
需要注意的是:過程中需要將已經使用過的字母改成一個特殊字母,以避免重複使用字符。
class Solution(object):
def hasPath(self, matrix, path):
"""
:type matrix: List[List[str]]
:type path: str
:rtype: bool
"""
if len(matrix) == 0 or len(matrix[0]) == 0 or len(path) == 0:
return False
for row in range(len(matrix)):
for col in range(len(matrix[0])):
if self.dfs(matrix, path, 0, row, col):
return True
return False
def dfs(self, matrix, path, path_idx, x, y):
"""
:param matrix: 輸入矩陣
:param path: 輸入路徑
:param path_idx: 待查找路徑中的元素下標
:param x: 暴搜法的矩陣元素橫座標
:param y: 暴搜法的矩陣元素縱座標
:return:
"""
if matrix[x][y] != path[path_idx]:
return False
if path_idx == len(path) - 1:
return True
temp = matrix[x][y]
matrix[x][y] = "*" # 把矩陣中的元素設爲不存在的元素,避免它被重複使用
dx = [-1, 1, 0, 0]
dy = [0, 0, 1, -1]
# 尋找上下左右四個方向,是否存在一個點爲路徑中的下一個元素
for i in range(4):
a = x + dx[i]
b = y + dy[i]
if 0 <= a < len(matrix) and 0 <= b < len(matrix[0]):
if self.dfs(matrix, path, path_idx + 1, a, b):
return True
matrix[x][y] = temp # 還原矩陣原始值
return False
14. 機器人的運動範圍(BFS)
地上有一個 m 行和 n 列的方格,橫縱座標範圍分別是 0∼m−1 和 0∼n−1。
一個機器人從座標0,0的格子開始移動,每一次只能向左,右,上,下四個方向移動一格。
但是不能進入行座標和列座標的數位之和大於 k 的格子。
請問該機器人能夠達到多少個格子?
輸入:k=18, m=40, n=40
輸出:1484
解釋:當k爲18時,機器人能夠進入方格(35,37),因爲3+5+3+7 = 18。
但是,它不能進入方格(35,38),因爲3+5+3+8 = 19。
【分析】
本題考察的是一個寬搜的問題,機器人不能進入到行座標和列座標的數位之和大於k,可理解爲矩陣中,部分網格存在障礙物,機器人無法移動。
如上圖所示,機器人在一個的網格里,起點位置爲,按照題意要求,我們將矩陣下標的數位之和小於或等於的網格,用綠色標識,其他網格用障礙物標識。
很明顯可以看到,滿足條件的網格被分爲四塊區間,中間被障礙物阻隔開,如果機器人初始位置在的話,則只能在一塊綠色區域中移動,其他位置到達不了。
前面說過這是一個BFS的問題,BFS有固定的解題模板,BFS要有一個維護一個隊列Queue,每次循環pop出隊首元素,判斷當前元素是否被訪問過或者是否滿足題意要求,條件不成立的話,continue,成立的話,則往上下左右四個方向進行延伸,如果新節點在矩陣範圍內,且未被訪問,我們就將其添加到隊列末尾。
具體編寫代碼如下:
class Solution:
def get_num(self, x):
num = 0
while x:
num += x % 10
x = x // 10
return num
def check(self, threshold, x, y):
"""
判斷當前格子下標的數值和是否大於閾值
:param threshold:
:param x:
:param y:
:return: bool
"""
if self.get_num(x) + self.get_num(y) > threshold:
return True
return False
def movingCount(self, threshold, rows, cols):
res = 0
if threshold < 0 or rows <= 0 or cols <= 0:
return res
label_mat = [[0] * cols for _ in range(rows)] # 初始化label矩陣,用來標記當前元素是否已訪問
queue = [[0, 0]] # 初始化BFS搜索隊列,首先喂進去矩陣中的第一個元素
dx, dy = [1, -1, 0, 0], [0, 0, 1, -1]
while queue:
x, y = queue.pop(0) # 彈出隊列中的隊首元素的座標
# 檢查當前元素是否已訪問(因爲搜索隊列中某個元素可能被重複添加)or 矩陣下標大於閾值,爲障礙物,不能移動!
if label_mat[x][y] == 1 or self.check(threshold, x, y):
continue
res += 1
label_mat[x][y] = 1 # 將矩陣中的當前元素標記爲已訪問
for i in range(4):
a = x + dx[i]
b = y + dy[i]
if 0 <= a < rows and 0 <= b < cols and label_mat[a][b] == 0:
queue.append([a, b])
return res
15. 剪繩子(整數劃分)
給你一根長度爲 n 繩子,請把繩子剪成 m 段(m、n 都是整數,2 ≤ n ≤ 58 並且 m ≥2)。
每段的繩子的長度記爲k[0]、k[1]、……、k[m]。k[0]k[1] … k[m] 可能的最大乘積是多少?
例如當繩子的長度是8時,我們把它剪成長度分別爲2、3、3的三段,此時得到最大的乘積18。
【分析】
本題是一個經典的整數劃分問題,裏面有一些重要的結論,我們下面具體來分析!
考慮把一個整數分成段,即
如果存在 ,則 式子變形可得 ,必成立。 這說明了一個重要的結論,如果要讓劃分的段的乘積儘可能大,則每一段的長度一定要小於。那我們劃分的段的長度,只能在中取得。
長度也可以劃分爲。所以進一步縮小範圍,我們劃分的段的長度只能在2,3中取得。
然而,,我們再次得出一個結論,劃分完的段中,長度爲的段最多隻有個。
因此,結論如下:
把一個整數劃分爲段,最多有個長度爲的段,其餘全部爲;
我們令 :
- 如果 ,則把全部劃分爲長度爲3的段;
- 如果 ,則把劃分出2個長度爲2的段,其餘長度全部爲3;
- 如果 ,則把劃分出1個長度爲2的段,其餘長度全部爲3;
class Solution(object):
def maxProductAfterCutting(self, n):
if n <= 3:
return 1 * (n - 1)
a = n % 3
if a == 0: # 能被3整除,直接全部劃分爲3
return pow(3, n // 3)
elif a == 1: # 模爲1,則劃分出2個長度爲2的段,其餘全部爲3
return pow(3, (n - 4) // 3) * 4
else: # 模爲2,則劃分出1個長度爲2的段,其餘全部爲3
return pow(3, (n - 2) // 3) * 2
16. 二進制中1的個數(位運算)
輸入一個32位整數,輸出該數二進制表示中1的個數。
注意:
負數在計算機中用其絕對值的補碼來表示。
輸入:-2
輸出:31
解釋:-2在計算機裏會被表示成11111111 11111111 11111111 11111110,
一共有31個1。
【分析】
首先了解一下補碼的概念,簡單明瞭的說,在計算機中,如果兩個數互爲補碼,那就意味着它們的二進制數之和爲 ,的後面一共有個。
我們知道,的補碼是, 的二進制表示爲 , 的表示則爲,二者之和,滿足上述性質。
說回本題,思路很簡單,我們只需要把輸入整數轉爲無符號整數即可,python中用 來實現。
對於一個無符號整數,表示取二進制表示最右邊一位的值。
表示將右移一位。
具體代碼如下:
class Solution(object):
def NumberOf1(self,n):
"""
:type n: int
:rtype: int
"""
count = 0
# 直接轉換爲32位的無符號整數,排除負數的影響
n = n & 0xffffffff
while n:
count += n&1
n = n >> 1
return count
17. 刪除鏈表中重複的節點(雙指針法)
在一個排序的鏈表中,存在重複的結點,請刪除該鏈表中重複的結點,重複的結點不保留。
樣例1
輸入:1->2->3->3->4->4->5
輸出:1->2->5
樣例2
輸入:1->1->1->2->3
輸出:2->3
【分析】
首先呢,對於這種刪除鏈表中的節點類型的題,我們要考慮頭節點可能會被刪除的情況,因此,第一步是創建一個虛擬頭節點,指向真實的頭節點。dummy=ListNode(-1) dummy.next=head
。
其次呢,這裏說的刪除重複的節點,是指把所有重複的節點都刪除,而不是保留一個,注意理解題意。
然後,這題可以用雙指針法來做,這是一個排序的鏈表,所以重複節點一定相鄰。讓一個指針指向鏈表中,按從前往後遍歷的順序,未重複出現的第一個節點,所以這裏初始時指向節點,指針指向的下一個節點。
while
循環,如果 存在,且 指向的節點值與 的下一個節點指向的值相等,則 q = q.next
,當 while
不滿足時,進行if
判斷,如果p.next.next = q
,說明p.next
指向的節點爲下一個不重複的節點,則令 p = p.next
,否則說明p.next
指向的節點爲重複出現的節點,需要將這些重複節點刪除,令p.next = q
具體代碼如下:
class Solution(object):
def deleteDuplication(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
# 創建一個虛擬頭節點,避免真正的頭節點被刪掉
dummy = ListNode(-1)
dummy.next = head
# python分爲可變對象賦值和不可變對象賦值
# 此處是可變對象賦值,p和dummy指向的同一塊內存區域,p發生修改,dummy的內容也會跟着修改
p = dummy
while p.next: # 此處的while判斷是一個細節,省去了很多麻煩!!!
q = p.next
# 初始時,p.next和q指向同一個節點,所以如果q存在,循環一定會執行一次
while q and p.next.val == q.val:
q = q.next
if p.next.next == q:
p = p.next
else:
p.next = q
return dummy.next
18. 調整數組順序使偶數位於奇數之後(雙指針法)
輸入一個整數數組,實現一個函數來調整該數組中數字的順序。
使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分。
輸入:1 2 3 4 5
輸出:1 5 3 4 2
【分析】
本題是一個雙指針的問題,一個指針指向數組頭部,一個指針指向數組尾部。
我們要保證之前的每一個元素都是奇數,之後的每一個元素都是偶數。 於是兩個指針開始移動,當遇到偶數時停止,當遇到奇數時停止,然後把兩個指針的元素交換(交換前提是)。
整個過程中,始終要保證 。
class Solution:
def reorder_array(self, nums):
i = 0
j = len(nums) - 1
while i <= j:
while i <= j and nums[i] % 2 == 1:
i += 1
while i <= j and nums[j] % 2 == 0:
j -= 1
if i <= j:
nums[i], nums[j] = nums[j], nums[i]
return nums
19. 返回鏈表中倒數第k個節點(雙指針法)
輸入一個鏈表,輸出該鏈表中倒數第k個結點。
注意:,如果大於鏈表長度,那麼返回None
輸入:,
輸出:
【分析】
本題有兩種做法,第一種做法是我比較喜歡的做法,雙指針法,我們思考一下,如果求倒數第個節點,那麼我們只需要定義兩個指針,一個指針指向鏈表頭,另一個指針指向鏈表頭的下一個節點,也就是說兩個指針之間的間隔爲,然後兩個指針一起向後移動,當指針移到鏈表尾部的時候,指針也就到了倒數第個節點的位置。
第一步,我們把指針後移位,這裏存在一個可能大於表長的問題,所以在後移時進行判斷, 是否會移到None的位置;
第二步,同時將指針和向後移到,當指針移到鏈表尾的時候,指針 到達了倒數第個節點的位置。
class Solution:
def FindKthToTail(self, pListHead, k):
if not pListHead or k < 1:
return None
p = pListHead
q = pListHead
k -= 1
while k:
if not q.next:
return None
q = q.next
k -= 1
while q.next:
q = q.next
p = p.next
return p
第二種解法是,我們要從前往後遍歷一下整個鏈表的長度,然後也就能知道從頭部到倒數第的節點的長度了。
class Solution:
def findKthToTail_2(self, pListHead, k):
n = 0
p = pListHead
while p:
n += 1
p = p.next
if n == 0 or k < 1 or k > n:
return None
p = pListHead
while k < n:
p = p.next
k += 1
20. 鏈表中環的入口位置(快慢指針,找規律)
給定一個鏈表,若其中包含環,則輸出環的入口節點。
若其中不包含環,則輸出None。
【分析】
之前我們一定聽說過如果判斷一個單鏈表中是否存在環的問題,一個好的思路就是快慢指針法,一個指針每次走一步,另一個每一次走兩步。如果存在環,那麼兩個指針一定會相遇,如果不存在環,那麼快指針一定會到達尾節點(如何判斷尾結點? node.next is None
)。
現在的問題,是上面一個問題的進階版,如果存在環,返回入口節點;如果不存在環,返回None。
我們通過下圖展開詳細分析鏈表中存在環的情況。
圖中我們標記了三個節點,A表示鏈表頭節點,B表示環的入口節點,C表示快慢指針相遇的節點。我們用 表示 的距離,用 表示 的距離,用 表示 的距離。(我們定義慢指針爲 ,快指針爲 )
當兩個指針相遇時,有:
- 慢指針走了 距離
- 快指針走了 距離(假設快指針已經在環裏面循環了 圈,)
快指針每次走兩步,慢指針每次走一步,因此有:
於是:
也就是說,在相遇點C的位置,走 步,就必能到達節點 B 。
怎麼找到 呢?注意到頭節點到環的入口節點的長度就是,我們把一個指針重置到頭節點,兩個指針同時移動,每次移動一步,那麼相遇的時候,即爲環的入口節點!
class Solution(object):
def entryNodeOfLoop(self, head):
p = head
q = head
while p and q:
p = p.next
q = q.next
if q:
q = q.next
else: # 說明不存在環,q抵達尾節點
return None
if p == q: # 說明鏈表存在環
p = head
while p != q:
p = p.next
q = q.next
return p
return None
21. 反轉鏈表(三指針法)
定義一個函數,輸入一個鏈表的頭結點,反轉該鏈表並輸出反轉後鏈表的頭結點。
輸入:1->2->3->4->5->NULL
輸出:5->4->3->2->1->NULL
【分析】
反轉一個單鏈表,它的核心在於,我們要用一個指針保存當前節點的前驅節點。
有了前驅節點之後,思路就比較簡單了,指針指向當前節點的下一個節點,指針指向它的前驅節點,指針再往後移到當前節點,後移到它的下一個節點。當指向None時,即爲我們反轉之後鏈表的頭節點。
有一個小坑,在初始化的時候,我一開始採取pre=ListNode(None)
的形式,結果在輸出結果時,鏈表中多了一個值爲None的節點,這是不符合題意的,我們應該用pre=None
這種方式進行初始化。
class Solution:
def reverseList(self, head):
if not head:
return head
cur = head
pre = None
while cur:
post = cur.next
cur.next = pre
pre = cur
cur = post
return pre
22. 合併兩個有序的鏈表(雙指針法)
輸入兩個遞增排序的鏈表,合併這兩個鏈表並使新鏈表中的結點仍然是按照遞增排序的。
輸入:1->3->5 , 2->4->5
輸出:1->2->3->4->5->5
【分析】
首先創建一個虛擬頭節點,用來維護新鏈表的頭。然後用兩個指針指向輸入的兩個鏈表的表頭,當兩個指針都不爲None的時候,我們比較兩個指針所指向的節點的值,把較小的值對應的節點添加到新的鏈表中,並把對應的指針後移一位。
當至少有一個指針指向None時,循環終止,並將不爲None的指針指向的剩餘的鏈表,添加到新鏈表中。
class Solution(object):
def merge(self, l1, l2):
dummy = ListNode(None)
cur = dummy
while l1 and l2:
if l1.val <= l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
if l1:
cur.next = l1
if l2:
cur.next = l2
return dummy.next
23. 樹的子結構(雙重遞歸)
輸入兩棵二叉樹A,B,判斷B是不是A的子結構。
我們規定空樹不是任何樹的子結構。
樹A:
8
/ \
8 7
/ \
9 2
/ \
4 7
樹B:
8
/ \
9 2
【分析】
判斷樹B是不是樹A的子結構,需要分兩步走:
- 第一步,在樹A中找到與樹B根結點的值一樣的節點 ;
- 第二步,判斷樹A中以 爲根結點的子樹,是否包含和樹B一樣的子結構;
具體分析詳見代碼:
class Solution(object):
def isSame(self, root1, root2):
if not root2: # 樹B中無待匹配節點,說明樹B中該分支已匹配完
return True
if not root1 or root1.val != root2.val: # 樹B還有待匹配節點,樹A中無節點了 或者 根結點的值不相等
return False
# 如果當前點匹配了,遞歸判斷左右子樹是否同樣匹配
return self.isSame(root1.left, root2.left) and self.isSame(root1.right, root2.right)
def hasSubtree(self, pRoot1, pRoot2):
"""
:type pRoot1: TreeNode
:type pRoot2: TreeNode
:rtype: bool
"""
if not pRoot1 or not pRoot2: # 空子樹排除
return False
# 判斷以pRoot1爲根結點的樹是否與以pRoot2爲根結點的樹相同(可以包含,但根節點必須相同)
if self.isSame(pRoot1, pRoot2):
return True
else:
return self.hasSubtree(pRoot1.left, pRoot2) or self.hasSubtree(pRoot1.right, pRoot2)
24. 對稱的二叉樹(二叉樹鏡像 && DFS)
請實現一個函數,用來判斷一棵二叉樹是不是對稱的。
如果一棵二叉樹和它的鏡像一樣,那麼它是對稱的。
下面這棵樹就是一棵對稱二叉樹
1
/ \
2 2
/ \ / \
3 4 4 3
【分析】
先聊一聊二叉樹的鏡像,二叉樹和它的鏡像二叉樹有什麼特點呢?特點在於把原來的二叉樹每一個節點的左右節點互相交換,就能得到它的鏡像二叉樹。
我們觀察示例中的對稱二叉樹,可以發現,根結點的左、右子樹互爲鏡像二叉樹!
class Solution(object):
def isSymmetric(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
if not root:
return True
return self.dfs(root.left, root.right)
def dfs(self, p1, p2):
if not p1 or not p2: # p1,p2一個爲空一個不爲空或兩個同時爲空時成立
return not p1 and not p2 # 只有一個爲空返回False,同爲空則返回True
if p1.val != p2.val: # 兩棵樹中,對應位置的節點值不相等,直接返回False
return False
return self.dfs(p1.left, p2.right) and self.dfs(p1.right, p2.left)
25. 順時針打印矩陣(蛇形遍歷)
輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每一個數字。
輸入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
輸出:[1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7]
【分析】
我們起點是。
- 開始後,一直向右移動,即橫座標加,縱座標加,一直到,發生數組越界,換方向移動;
- 於是向下移動,即橫座標加,縱座標加,一直到,發生數組越界,換方向移動;
- 於是向左移動,即橫座標加,縱座標減,一直到,發生數組越界,換方向移動;
- 於是向上移動,即橫座標減,縱座標加,一直到,該網格已訪問,於是換方向移動;
- 週而復始
於是我們得出一個解題思路,首先指定一個移動的方向dx=[0,1,0,-1],dy=[1,0,-1,0]
,遇到邊界溢出或者元素已訪問,則調整方向。
class Solution(object):
def printMatrix(self, matrix):
"""
:type matrix: List[List[int]]
:rtype: List[int]
"""
if not matrix:
return matrix
m = len(matrix)
n = len(matrix[0])
label_mat = [[0] * n for _ in range(m)] # 標記是否已訪問
res = []
dx = [0, 1, 0, -1] # 定義 左下右上 四個方向
dy = [1, 0, -1, 0]
x, y, direction = 0, 0, 0
for i in range(0, m * n):
res.append(matrix[x][y])
label_mat[x][y] = 1
a = x + dx[direction]
b = y + dy[direction]
if a < 0 or a >= m or b < 0 or b >= n or label_mat[a][b]:
direction = (direction + 1) % 4
a = x + dx[direction]
b = y + dy[direction]
x = a
y = b
return res
26. 包含min 函數的棧(輔助棧,單調遞減棧)
設計一個支持push,pop,top等操作並且可以在O(1)時間內檢索出最小元素的堆棧。
push(x) 將元素x插入棧中
pop() 移除棧頂元素
top() 得到棧頂元素
getMin() 得到棧中最小元素
此題考察的是輔助棧的使用,我們在普通棧的基礎上,再添加一個輔助棧(單調遞減棧)。
具體實現見代碼:
class MinStack(object):
def __init__(self):
self.stack = [] # 普通棧
self.min_stack = [] # 輔助棧,單調遞減棧
def push(self, x):
"""
:type x: int
:rtype: void
"""
self.stack.append(x) # 普通棧,直接將元素入棧
# 如果輔助棧爲空或者它的棧頂元素不小於當前元素,則將元素入棧
if not self.min_stack or self.min_stack[-1] >= x:
self.min_stack.append(x)
def pop(self):
"""
:rtype: void
"""
x = self.stack.pop()
if x == self.min_stack[-1]:
self.min_stack.pop()
def top(self):
"""
:rtype: int
"""
return self.stack[-1]
def getMin(self):
"""
:rtype: int
"""
return self.min_stack[-1]
27. 棧的壓入、彈出序列(出棧順序)
輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能爲該棧的彈出順序。(假設壓入棧的所有數字均不相等。)
例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序列。
注意:若兩個序列長度不等則視爲並不是一個棧的壓入、彈出序列。若兩個序列都爲空,則視爲是一個棧的壓入、彈出序列。
【分析】
本題有點小成就感,按照自己的思路,一步一步修改,然後AC了,所以蠻爽~
我的思路:
創建一個 變量 ,表示本輪中是否發生了入棧或者出棧的操作,如果發生了,則將 置爲 False;
每一輪中,首先判斷,棧是否爲空、彈出序列是否爲空、棧頂元素與彈出序列的第一個元素是否相等;
三個條件都滿足了的話,則彈出棧頂元素stack.pop()
,並彈出 彈出序列中的第一個元素popV.pop(0)
,並將 置爲 False;
前面條件不成立的話,再進行判斷輸入序列是否爲空,不爲空的話,將輸入序列的第一個元素彈出,並添加到棧中。
最後修改 的值,flag = True if not flag else False
.
class Solution(object):
def isPopOrder(self, pushV, popV):
"""
:type pushV: list[int]
:type popV: list[int]
:rtype: bool
"""
stack = []
flag = True # 如果本輪中未發生插入或彈出操作,則停止循環
while flag:
if stack and popV and stack[-1] == popV[0]:
flag = False
stack.pop()
popV.pop(0)
elif pushV:
stack.append(pushV.pop(0))
flag = False
flag = True if not flag else False # 或 flag = not flag
if stack:
return False
return True
28. 分行從上往下打印二叉樹(BFS)
從上到下按層打印二叉樹,同一層的結點按從左到右的順序打印,每一層打印到一行。
輸入如下圖所示二叉樹[8, 12, 2, null, null, 6, null, 4, null, null, null]
8
/ \
12 2
/
6
/
4
輸出:[[8], [12, 2], [6], [4]]
【分析】
看到BFS類型的題,要第一反應構建一個遍歷隊列!
一個比較好的思路是,我們在每一層的節點遍歷完之後,插入一個None,作爲標記該層遍歷完畢,並將該層的節點值存到 中。
初始的時候,如果 不爲 None,我們令 queue=[root, None]
,然後進行BFS。
class Solution(object):
def printFromTopToBottom(self, root):
"""
:type root: TreeNode
:rtype: List[List[int]]
"""
res = []
if not root: # 異常情況排除
return res
queue = [root, None]
level = []
while queue:
node = queue.pop(0)
if not node: # 遇到我們設定的None,說明本層節點遍歷完畢
if not level: # 如果level爲空,說明隊列遍歷完畢,結束循環
break
res.append(level.copy())
level.clear()
queue.append(None) # 插入到遍歷隊列中,作爲一層結束的標記
continue # 結束本輪循環
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return res
本題有一個變種,以“之字形”,從上到下打印二叉樹,如下示例:
輸入如下圖所示二叉樹[8, 12, 2, null, null, 6, null, 4, null, null, null]
8
/ \
12 2
/ \ \
1 5 6
/ / \
7 9 4
輸出:[[8], [2, 12], [1, 5, 6], [4, 9, 7]]
只需要在上面代碼的基礎上,做一點小小的變化即可。
class Solution(object):
def printFromTopToBottom_2(self, root):
"""
:type root: TreeNode
:rtype: List[List[int]]
"""
res = []
if not root:
return res
queue = [root, None]
level = []
i = 1 # 用來控制每層添加的level,是順序還是逆序!
while queue:
node = queue.pop(0)
if not node:
if not level:
break
i += 1
res.append(level[::pow(-1, i)]) # 與上一份代碼的區別
level = []
queue.append(None)
continue
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return res
29. 二叉搜索樹的後序遍歷(DFS)
輸入一個整數數組,判斷該數組是不是某二叉搜索樹的後序遍歷的結果。
如果是則返回true,否則返回false。
對於輸入爲空,返回True
假設輸入的數組的任意兩個數字都互不相同。
【分析】
二叉搜索樹的特點在於,左子樹所有節點的值 根結點的值 右子樹所有節點的值。
後序遍歷的特點是:左子樹 、右子樹、根結點
結合這兩條性質,我們可以得出一個重要結論:
如果一棵樹是一個合法的二叉搜索樹,那麼它的後序遍歷中,最後一個元素爲該序列的根結點,並且該值可以將序列分爲左、右兩部分,左邊部分的所有值均小於最後一個元素的值,右邊部分的所有值均大於最後一個元素的值。劃分完之後,左邊部分的序列和右邊部分的序列,也仍需要滿足上述特性。
class Solution:
seq = []
def verifySequenceOfBST(self, sequence):
"""
:type sequence: List[int]
:rtype: bool
"""
self.seq = sequence
if not self.seq: # 空二叉樹
return True
return self.dfs(0, len(self.seq) - 1)
def dfs(self, l, r):
if l >= r: # 說明當前分支的節點數爲空
return True
k = l
for i in range(l, r):
if self.seq[i] >= self.seq[r]:
break
k += 1 # k爲序列中,從左往右,第一個大於最後一個元素值的下標
for j in range(k, r):
if self.seq[j] <= self.seq[r]: # 右邊部分存在不大於最後一個元素的值
return False
return self.dfs(l, k - 1) and self.dfs(k, r - 1)
30. 二叉樹中的所有路徑(DFS && 回溯法)
定義二叉樹中的路徑爲:從根結點到葉子結點的所經過的所有節點的值。
輸入:如下圖所示二叉樹[8, 12, 2, null, null, 6, null, 4, null, null, null]
8
/ \
12 2
/ \ \
1 5 6
/ / \
7 9 4
輸出:[[8, 12, 1], [8, 12, 5, 7], [8, 2, 6, 9], [8, 2, 6, 4]]
【分析】
葉子結點的特點是 not node.left and not node.right
條件成立。
我們的思路是,遞歸的遍歷一棵二叉樹,首先當前節點的值,添加到路徑中。如果該節點爲葉子結點,則將路徑添加到返回結果中,否則,如果該節點有左子樹,就它的左子樹,如果該節點有右子樹,就它的右子樹。
回溯法體現在,當前節點對應的 返回之後,從路徑中要刪除該節點的值。
class Solution(object):
res = []
path = []
def findAllPath(self, root):
"""
:type root: TreeNode
:rtype: List[List[int]]
"""
if not root:
return self.res
self.dfs(root)
return self.res
def dfs(self, root):
if not root:
return
self.path.append(root.val)
if not root.left and not root.right:
self.res.append(self.path.copy()) # 這裏必須用path.copy(),否則值會被修改
self.dfs(root.left)
self.dfs(root.right)
self.path.pop()
31. 二叉樹中和爲S的路徑(DFS && 回溯法)
輸入一棵二叉樹和一個整數,打印出二叉樹中結點值的和爲輸入整數的所有路徑。
從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。
【分析】
此題和上面那道題十分相似,相當於在所有路徑的基礎上,增加了一層過濾。
第一種思路是,獲取所有路徑,然後返回和爲S的路徑。
第二種思路,我們在到達了一條路徑的葉節點的時候,判斷當前路徑的和是否爲S,是的話,就添加到 中。
我們這裏採取思路二進行代碼實現,而且有兩種實現方式。
- 方式一:到達了一條路徑的葉節點的時候,判斷當前路徑的和是否爲S
class Solution(object):
res = []
path = []
target = 0
def findPath(self, root, sum):
"""
:type root: TreeNode
:type sum: int
:rtype: List[List[int]]
"""
if not root or not sum:
return self.res
self.target = sum
self.dfs(root)
return self.res
def dfs(self, root):
if not root:
return
self.path.append(root.val)
if not root.left and not root.right and sum(self.path) == self.target:
self.res.append(self.path.copy())
self.dfs(root.left)
self.dfs(root.right)
self.path.pop()
- 方式二:遍歷某節點時,如果它不爲空,把它添加到路徑中,並執行
S = S - node.val
,如果它爲葉節點,且S == 0
,那麼說明該條路徑的和爲S
class Solution(object):
res = []
path = []
def findPath(self, root, sum):
"""
:type root: TreeNode
:type sum: int
:rtype: List[List[int]]
"""
if not root or not sum:
return self.res
self.dfs(root, sum)
return self.res
def dfs(self, root, sum):
if not root:
return
self.path.append(root.val)
sum -= root.val
if not root.left and not root.right and sum == 0:
# 因爲進行pop操作,path會修改,所以這裏要用path.copy()
self.res.append(self.path.copy())
self.dfs(root.left, sum)
self.dfs(root.right, sum)
self.path.pop()
32. 二叉樹中根結點到某一結點的路徑(DFS && 回溯法)
輸入一棵二叉樹和一個結點,打印出從根結點到該結點到路徑。
【分析】
這一題和上面兩道都是類似的題型,我們 到某個結點時,如果它不爲空,就把它添加到路徑中,並判斷它的值和目標結點的值是否相等,是的話,返回True,否則 它的左右子樹。
class Solution:
res = []
path = []
def find_path(self, root, target):
self.res = [] # 避免多次輸入時,res中還保留上一個輸入的結果
if not root or not target:
return self.res
self.dfs(root, target)
def dfs(self, root, target):
if not root:
return
self.path.append(root.val)
if root.val == target.val:
self.res = self.path.copy()
return # 樹中無重複節點,所以只有一條路徑,找到了則返回本輪遞歸
self.dfs(root.left, target)
self.dfs(root.right, target)
self.path.pop()
33. 複雜鏈表的複製(鏈表插入與刪除)
請實現一個函數可以複製一個複雜鏈表。
在複雜鏈表中,每個結點除了有一個指針指向下一個結點外,還有一個額外的指針指向鏈表中的任意結點或者null。
【分析】
上圖是一個複雜鏈表的示例,實線表示next
指針,虛線表示random
指針,它也可以指向 None,在圖中省略。
一個直觀的思路是,分兩步完成,第一步複製原始鏈表中的每一個節點,並用next
指針連接起來;第二步是設置每個節點的random
指針,這一步比較麻煩,假設某個節點的random
指向節點S,那麼定位S的位置需要從頭節點開始查找,這種方法的時間複雜度爲 。
一種優化的思路是,分三步完成:
- 第一步:複製原始鏈表中的每一個節點,並將它鏈接到原節點之後。
- 第二步:如果原始鏈表中,某節點的
random
指針指向節點S,那麼它對應的複製節點指向S.next
。
- 第三步:分離鏈表,把奇數位置的節點用
next
指針鏈接起來,就是原始鏈表,把偶數位置的節點用next
指針鏈接起來,就是複製的新鏈表。
class Solution(object):
def copyRandomList(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
if not head:
return None
# 第一步,複製新節點在原節點之後
cur = head
while cur:
p = ListNode(cur.val)
p.next = cur.next
cur.next = p
cur = p.next
# 第二步,複製新節點的random指針
cur = head
while cur:
if cur.random:
cur.next.random = cur.random.next
cur = cur.next.next # cur每次都指向原節點,跨一個節點移動
# 第三步,分離鏈表
dummy = ListNode(None)
cur = dummy
p = head
while p:
cur.next = p.next
cur = cur.next
p = p.next.next # 跨節點移動
return dummy.next
34. 二叉搜索樹與雙向鏈表(DFS && 分情況討論)
輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。
要求不能創建任何新的結點,只能調整樹中結點指針的指向。
注意:
返回雙向鏈表中,最左側的節點。
例如:
【分析】
本題的思路是,我們設置一個pair = [l_node,r_node]
,表示當前的樹結構中,最左側的節點和最右側的節點。
分情況討論:以node
作爲根結點的樹中
- 如果它不存在左、右子樹,那麼返回
[node, node]
; - 如果它存在左、右子樹,則
root.left
,獲得左子樹的l_pair
,再root.right
,獲得右子樹的r_pair
,然後把l_pair[1]
與node
進行雙向鏈接,把r_pair[0]
與node
進行雙向鏈接,並返回[l_pair[0], r_pair[1]]
; - 如果它只存在左子樹,則
root.left
,獲得左子樹的l_pair
,然後把l_pair[1]
與node
進行雙向鏈接,並返回[l_pair[0], node]
; - 如果它只存在右子樹,則
root.right
,獲得右子樹的r_pair
,然後把r_pair[0]
與node
進行雙向鏈接,並返回[node, r_pair[1]]
;
具體代碼實現如下:
class Solution(object):
def convert(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if not root:
return root
pair = self.dfs(root)
return pair[0]
def dfs(self, node): # 以node爲根結點的樹中,返回 [最左側的節點,最右側的節點]
if not node.left and not node.right: # 1. 左、右子樹均不存在
return [node, node]
if node.left and node.right: # 2. 左、右子樹均存在
l_pair = self.dfs(node.left)
r_pair = self.dfs(node.right)
l_pair[1].right = node
node.left = l_pair[1]
node.right = r_pair[0]
r_pair[0].left = node
return [l_pair[0], r_pair[1]]
if node.left: # 3. 只有左子樹存在
l_pair = self.dfs(node.left)
l_pair[1].right = node
node.left = l_pair[1]
return [l_pair[0], node]
if node.right: # 4. 只有右子樹存在
r_pair = self.dfs(node.right)
node.right = r_pair[0]
r_pair[0].left = node
return [node, r_pair[1]]
35. 數字的全排列(DFS && 二進制標記 )
輸入一組數字(可能包含重複數字),輸出其所有的排列方式。
樣例:
輸入:[1,2,3]
輸出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
【分析】
我們先考慮一下,數組中不存在重複元素的情況!
假設輸入的數組爲,也就是說,我們有4個坑需要填。
我們按順序將數組中的元素,填到坑中,假設第一個待填的元素爲1
,那麼它有四個可以填的坑;接下來的待填的元素爲2
,可以看到它有三個可以填入的坑;然後待填的元素爲3
,可以看到它有兩個可以填入的坑;最後一個待填的元素爲4
,只剩一個坑可以填。
這裏我們用二進制數來標記哪個坑已被佔用,哪個坑未被佔用,如 11 = 0b1011
,意味着從右往左數,從0開始計數,第2個坑未被佔用。
state >> i & 1
,在 state
中,從右往左數,從 0 開始,第 個元素的值。
state + (1 << i)
表示從右往左數,從 0 開始,將 state
中的第 個元素的值 。
python 中運算符的優先級順序:(從上到下逐漸降低)
可以看到,+ 的優先級,大於 >> 和 << 的優先級,大於 & 的優先級。邏輯運算 not and or 的優先級是最低的。
因此,state + (1 << i)
中需要將左移運算用小括號括起來。
具體實現代碼如下:(輸入數組無重複)
class Solution:
res = [] # 返回的結果
holes = [] # 待填的坑
def permutation(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
if not nums:
return self.res
self.holes = [None] * len(nums) # 初始化坑的狀態
self.dfs(nums, 0, 0)
return self.res
def dfs(self, nums, idx, state):
"""
:param nums: 輸入的數組
:param idx: idx 表示當前待填入holes中的元素的下標
:param state: 當前holes的狀態,換成二進制表示,1表示已有元素,0表示暫無元素
"""
if idx == len(nums):
self.res.append(self.holes.copy()) # 需要用copy(),因爲後續holes會被修改
return
for i in range(0, len(nums)): # 設定枚舉範圍,從0開始,到len(nums)-1
if not state >> i & 1: # 從右往左,第i個位置第值是否爲1,從i=0開始
self.holes[i] = nums[idx]
self.dfs(nums, idx + 1, state + (1 << i))
我們接下來考慮,數組中存在重複元素的情況!
大致實現思路和上面一致,唯一要考慮的是,如果元素存在重複,我們增加一個約束條件,它必須填到重複元素的坑的後面。
爲此,我們需要先對輸入數組進行排序操作,這樣使得重複元素的位置相鄰,便於我們判斷當前元素是否重複。
class Solution:
res = [] # 返回的結果
path = [] # 待填的坑
def permutation(self, nums):
"""
:type nums: List[int]
:rtype: List[List[int]]
"""
if not nums:
return self.res
self.path = [None] * len(nums) # 初始化坑,即holes數組
nums = sorted(nums) # 排序,保證重複的元素相鄰
self.dfs(nums, 0, 0, 0)
return self.res
def dfs(self, nums, idx, start, state):
"""
:param nums: 輸入的數組
:param idx: idx 表示當前待填入holes中的元素的下標
:param start: 當前元素應該從哪個位置開始枚舉
:param state: 當前"坑"的狀態,換成二進制表示,1表示已有元素,0表示暫無元素
"""
if idx == len(nums):
self.res.append(self.path.copy())
return
# 如果當前元素爲第一個元素,或者當前元素與上一個元素不重複,則從第0個坑開始枚舉
if idx == 0 or nums[idx] != nums[idx - 1]:
start = 0
for i in range(start, len(nums)): # 設定枚舉範圍,從start開始,到len(nums)-1
if not state >> i & 1: # 從右往左,第i個位置第值是否爲1,從i=0開始
self.path[i] = nums[idx]
self.dfs(nums, idx + 1, i + 1, state + (1 << i))
36. 數組中出現次數超過一半的數字(消除法)
數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。
假設數組非空,並且一定存在滿足條件的數字。
輸入:[1, 2, 3, 3, 3, 1, 3]
輸出:3
【分析】
如果某個數字出現的次數超過數組長度的一半,那麼就是說,它出現的次數,比其他所有數字出現的總次數還要多。
因此,我們在遍歷數組的時候,可以保存兩個值,一個是數組中的某個元素,另一個是該元素出現的次數。
如果遍歷到的元素與保存的元素值相同,則次數加1,反之次數減1。
當次數爲零的時候,我們需要保存下一個數字,並把次數設爲1。
最後保存的元素,一定是次數超過一半的元素。
class Solution(object):
def moreThanHalfNum_Solution(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
res = nums[0]
count = 1
for i in range(1, len(nums)):
if count == 0:
res = nums[i]
count += 1
continue
if nums[i] == res:
count += 1
else:
count -= 1
return res
37. 最小的k個數(最大堆)
輸入n個整數,找出其中最小的k個數。
注意:
數據保證k一定小於等於輸入數組的長度;
輸出數組內元素請按從小到大順序排序;
樣例
輸入:[1,2,3,4,5,6,7,8] , k=4
輸出:[1,2,3,4]
【分析】
從輸入的個數中,返回最小的個數,或者最大的個數,像這種問題,我們可以用最大堆或者最小堆來實現。
- 最小堆:父堆元素小於或等於其子堆的每一個元素(中,模塊實現)
- 最大堆:父堆元素大於或等於其子堆的每一個元素
我們來詳細瞭解一下中的模塊:
- heapq.heappop(list): 彈出 所代表的最小堆中的堆頂元素(list中最小的元素)
- heapq.heappush(list,x): 將元素 x 插入到 所代表的最小堆中
- heapq.nlargest(k, list): 返回最小堆 中最大的 的元素(遞減排序)
- heapq.nsmallest(k, list): 返回最小堆 中最小的 的元素(遞增排序)
需注意,heapq.heappush(list,x) 操作,是直接對 進行修改,無返回值。
如果我們想用 模塊來實現最大堆,一種思路是,將 中的每一個元素,取其相反數,那麼heapq.heappop(list) 返回的是原 中最大值的相反數,其他操作類似,不再贅述。
具體實現代碼:
import heapq
class Solution(object):
def getLeastNumbers_Solution(self, input, k):
"""
:type input: list[int]
:type k: int
:rtype: list[int]
"""
heap = [] # 維護一個元素個數爲k的最大堆結構
for x in input:
heapq.heappush(heap, -x)
if len(heap) > k:
heapq.heappop(heap) # 彈出最小元素,即實際上最大元素的相反數
res = heapq.nlargest(k, heap) # 從大到下排列的k個值,如[-1,-2,-3]
return [-x for x in res]
此外,也可以直接調用return heapq.nsmallest(k,input)
,一行代碼搞定。
38. 連續子數組的最大和(一維動態規劃)
輸入一個 非空 整型數組,數組裏的數可能爲正,也可能爲負。
數組中一個或連續的多個整數組成一個子數組。
求所有子數組的和的最大值。
要求時間複雜度爲O(n)。
輸入:[1, -2, 3, 10, -4, 7, 2, -5]
輸出:18
【分析】
我們初始化一個元素值全爲零的 數組dp=[0] * len(nums)
,其中 表示從第 0 個到第 個元素中,連續子數組(包含第 個元素)的最大和。
當 dp[i-1]<0
時,則令 dp[i-1]=0
,任何一個數加上一個負數,都一定小於它本身,所以這裏將dp[i-1]
置爲0。
然後執行 dp[i] = dp[i-1] + nums[i]
,求出包含第 個元素在內的連續子數組的最大和。
最後將 和 中較大的元素保存在 中res = max(res, dp[i])
。(因爲輸入的數組中,可能存在負數,所以初始化res = float(’-inf’)
。)
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
res = float('-inf') # 初始化 res 爲負無窮
dp = [0] * len(nums)
for i in range(len(nums)):
if dp[i - 1] < 0:
dp[i - 1] = 0
dp[i] = dp[i - 1] + nums[i]
res = max(dp[i], res)
return res
39. 從1到n的整數中1出現的次數(分情況討論)
輸入一個整數n,求從1到n這n個整數的十進制表示中1出現的次數。
例如
輸入12,從1到12這些整數中包含“1”的數字有1,10,11和12,其中“1”一共出現了5次。
【分析】
考慮一個整數 ,如下圖所示:
我們從左往右逐次遍歷,當遍歷到 的位置的時候,它左邊的值 ,右邊的值 ,右邊的元素個數 。
當 前面的數值取 , 必定可以取到 1 ,此時共有 個 可能。
當 c 前面的數值取 時,分三種情況討論:
- c > 1時,則 c 後面的取值無約束,共有 種可能;
- c == 1時,則 c 後面的取值只能在 ,共有 種可能;
- c < 1時,則不滿足 c = 1,0 種可能。
具體實現代碼如下:
class Solution(object):
def numberOf1Between1AndN_Solution(self, n):
"""
:type n: int
:rtype: int
"""
nums = []
res = 0
if not n: # 輸入爲0
return res
while n:
nums.append(n % 10)
n //= 10
nums.reverse() # 1999 變爲[1,9,9,9]
for i in range(len(nums)):
left = 0 # 第i個元素左邊的數值,如i=2時,left=19
right = 0 # 第i個元素右邊的數值,如i=2時,right=9
t = 0 # 第i個元素右邊的元素個數
for j in range(0, i):
left = left * 10 + nums[j]
for j in range(i + 1, len(nums)):
right = right * 10 + nums[j]
t += 1
res += left * 10 ** t
if nums[i] > 1:
res += 10 ** t
elif nums[i] == 1:
res += right + 1
return res
40. 數字序列中某一位的數字(分情況討論)
數字以0123456789101112131415…的格式序列化到一個字符序列中。
在這個序列中,第5位(從0開始計數)是5,第13位是1,第19位是4,等等。
請寫一個函數求任意位對應的數字。
【分析】
我們來討論一下:
- 以內的數字,元素個數爲 ,索引區間爲 ;
- 以內的數字,元素個數爲 ,每個元素佔 位,索引區間爲 ;
- 以內的數字,元素個數爲 ,每個元素佔 位,它的索引區間爲 ;
我們根據輸入的 ,需要知道 第 個元素是對應的是一個幾位數,一位數的分界點爲10(小於分界點),二位數的分界點爲190,三位數的分界點爲2890,以此類推
假設第 n 個元素,對應的是一個三位數中的某一位,那麼我們需要求出,它是第幾個三位數,此時用(n-190)//3
進行求解;
知道是第幾個三位數之後,我們還需要知道它是該三位數的第幾個元素,此時用(n-190)%3
。
最終整理代碼如下:
class Solution(object):
def digitAtIndex(self, n):
"""
:type n: int
:rtype: int
"""
if n < 10: # 10以內的輸入單獨處理,便於後面實現
return n
pos = 10 # 用來確定第n個數字的區間,從二位數開始
i = 1 # 用來確定第n個數字的位數
while n >= pos:
last_pos = pos
pos = pos + 9 * pow(10, i) * (i + 1)
i += 1
p = (n - last_pos) // i # 向下取整,確定第p個i位數
q = (n - last_pos) % i # 第p個i位數的第q位元素,爲返回結果
num = pow(10, i - 1) + p
return int(str(num)[q])
41. 把數組排成最小的數(自定義排序)
輸入一個正整數數組,把數組裏所有數字拼接起來排成一個數,打印能拼接出的所有數字中最小的一個。
例如輸入數組[3, 32, 321],則打印出這3個數字能排成的最小數字321323。
輸入:[3, 32, 321]
輸出:321323
輸出數字的格式爲字符串
【分析】
假設輸入的數組中,每個元素都是 以內的數,如 ,那麼它拼接起來最小的數字是 ,它是怎麼排的呢?
不難發現,在數字 中,任意兩個位置的對應的數字組成的數 ,都比 要小。
於是,當我們輸入的數組中,存在元素大於的數,最終得到的最小的數中,也應該滿足此性質,即對於任意一個元素a,如果它在元素b之前, 必須滿足ab < ba。
因爲最終拼接起來的數字,很有可能會大於 的上限,所以我們將輸入的數組中的每一個元素轉成字符串類型,nums = [str(x) for x in nums]
;
在 中,有提供自定義排序規則的函數,首先需要導入模塊,from functools import cmp_to_key
,我們的 列表的每一個元素都是字符串,所以 元素 a 和元素 b 拼接後的數字爲int(a+b)
。
調用的排序函數爲nums = sorted(nums, key=cmp_to_key(lambda x, y: int(x + y) - int(y + x)))
,如果 int(x + y) - int(y + x) < 0
,那麼說明 x < y
。
整體實現代碼:
class Solution(object):
def printMinNumber(self, nums):
"""
:type nums: List[int]
:rtype: str
"""
if not nums:
return ''
nums = [str(x) for x in nums]
from functools import cmp_to_key
nums = sorted(nums, key=cmp_to_key(lambda x, y: int(x + y) - int(y + x)))
return ''.join(nums).lstrip('0') or '0' # 去除輸入中可能存在的0,如果只有'0',則返回'0'
42. 把數字翻譯成字符串(一維動態規劃)
給定一個數字,我們按照如下規則把它翻譯爲字符串:
0翻譯成”a”,1翻譯成”b”,……,11翻譯成”l”,……,25翻譯成”z”。
一個數字可能有多個翻譯。例如12258有5種不同的翻譯,它們分別是”bccfi”、”bwfi”、”bczi”、”mcfi”和”mzi”。
請編程實現一個函數用來計算一個數字有多少種不同的翻譯方法。
輸入:“12258”
輸出:5
【分析】
統計個數類型的問題,可以考慮用動態規劃法來做。
動態規劃法,需要考慮三個因素:
- 狀態表示 本題 表示前 個元素共有多少種不同的翻譯方法;
- 狀態轉移方程 第 個元素,必定可以翻譯成一個字母,所以
dp[i] = dp[i-1]
是無條件轉移的,如果把第 個元素和第 個元素,合在一起,用一個字母進行翻譯,則必須這個合在一起的組成數字,值在之間纔可以轉移,此時dp[i] += dp[i-2]
,此時考慮一種特殊的情況,時,如果它和第個元素組成的值在區間,則需要加上 ,於是我們初始化時,考慮令dp[-1]=1
; - 邊界條件 當 時,它沒有和前面的元素組成一個兩位數的情況,需要單獨提出來,我們直接初始化
dp[0]=1
。
因爲這裏,dp[0] 和 dp[-1] 都需要初始化爲1,而從 dp[1] 開始,到 dp[-1] 結束的每一個元素值都會被覆蓋掉,於是我們可以直接初始化dp數組的值全爲1。
class Solution:
def getTranslationCount(self, s):
"""
:type s: str
:rtype: int
"""
# 計數問題,可以嘗試動態規劃法
if not s:
return -1
dp = [1] * len(s) # 初始化爲1,dp[0]=1,在計算dp[1]時,會用到dp[-1],此時它的值爲1
for i in range(1, len(s)):
dp[i] = dp[i - 1]
if 10 <= int(s[i - 1:i + 1]) <= 25:
dp[i] += dp[i - 2]
return dp[-1]
43. 棋盤的最大價值(二維動態規劃)
在一個m×n的棋盤的每一格都放有一個禮物,每個禮物都有一定的價值(價值大於0)。
你可以從棋盤的左上角開始拿格子裏的禮物,並每次向右或者向下移動一格直到到達棋盤的右下角。
給定一個棋盤及其上面的禮物,請計算你最多能拿到多少價值的禮物?
輸入:
[
[2,3,1],
[1,7,1],
[4,6,1]
]
輸出:19
解釋:沿着路徑 2→3→7→6→1 可以得到拿到最大價值禮物。
【分析】
統計個數類型的問題,可以考慮用動態規劃法來做。
動態規劃法,需要考慮三個因素:
- 狀態表示 本題 表示到達第 行第 列網格時,所得到的最大禮物價值;
- 狀態轉移方程 題中規定了每次只能向下或者向右移動,所以 只能由 或者 轉移得到,取二者中較大的一個元素即可,然後再加上當前網格的禮物價值,即
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
- 邊界條件 在第 0 行或者第 0 列的網格中,只能從某一個方向轉移得到,我們可以直接初始化 dp 矩陣中的每一個元素的值爲 0 即可,使得 和 ,不會影響狀態轉移的計算。(當然,更保險的辦法是,在原始矩陣的上邊和左邊再增加一行和一列,並初始化爲0,然後行、列都是從1開始遍歷)
class Solution(object):
def getMaxValue(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
m = len(grid) # 棋盤的行數
n = len(grid[0]) # 棋盤的列數
dp = [[0] * n for _ in range(m)] # 初始化爲0
for i in range(0, m):
for j in range(0, n):
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
return dp[m - 1][n - 1]
44. 最長不含重複字符的子字符串(一維動態規劃)
請從字符串中找出一個最長的不包含重複字符的子字符串,計算該最長子字符串的長度。
假設字符串中只包含從’a’到’z’的字符。
輸入:“abcabc”
輸出:3
【分析】
統計個數類型的問題,可以考慮要動態規劃法來做。
動態規劃法,需要考慮三個因素:
- 狀態表示 本題 表示以第 個元素結尾的不含重複字符的最大子串長度;
- 狀態轉移方程 如果第 個元素,在前 個元素中未出現,我們可以直接將第 個元素加入到上一個最大子串中,此時
dp[i] = dp[i-1]+1
;如果第 個元素,在前 個元素中已出現,那麼我們需要計算,在前 個元素中,最近一次出現第 個元素的位置,並計算出二者的間距。如果 ,說明重複的元素不影響當前不含重複字符的最大子串長度,所以dp[i] = dp[i-1]+1
,如果,說明以第 個元素結尾的,最大無重複字符的子串,是從上一個重複元素的下一位開始,到當前第 位元素結束,此時dp[i] = distance
- 邊界條件 當 時,
在本題中,需要記錄第 個元素有無出現過,如果出現過,它最近一次出現的位置在哪,所以我們可以創建一個字典結構,來保存上述信息。
class Solution:
def longestSubstringWithoutDuplication(self, s):
"""
:type s: str
:rtype: int
"""
if not s:
return 0
dp = [0] * len(s) # dp[i]表示以第i個元素結尾的不含重複字符的最大子串長度
d = dict() # 用來保存26個字母,上一次出現的位置
res = 0
for i in range(0, len(s)):
if s[i] not in d.keys(): # 判斷第i個元素在之前有沒有出現過
dp[i] = dp[i - 1] + 1
else:
distance = i - d[s[i]]
if distance > dp[i - 1]:
dp[i] = dp[i - 1] + 1
else:
dp[i] = distance
d[s[i]] = i # 更新第i個元素最後出現的位置
res = max(res, dp[i])
return res
45. 醜數(三路歸併)
我們把只包含因子2、3和5的數稱作醜數(Ugly Number)。
例如6、8都是醜數,但14不是,因爲它包含因子7。
求第n個醜數的值。
注意:習慣上我們把1當做第一個醜數。
【分析】
本題是有暴力解法的,但是時間開銷特別大,我們可以從1開始依次枚舉每一個正整數,如果它是醜數,則把當前醜數的個數加1,直到達到指定的醜數個數爲止,代碼一目瞭然,不再贅述。
class Solution(object):
# 用時間換空間
def getUglyNumber(self, n):
if n <= 1:
return n
ugly_count = 0
number = 0
while True:
number += 1
if self.is_ugly(number):
ugly_count += 1
if ugly_count == n:
break
return number
def is_ugly(self, number):
while number % 2 == 0:
number //= 2
while number % 3 == 0:
number //= 3
while number % 5 == 0:
number //= 5
return True if number == 1 else False
除了上述的暴力做法以外,本題還可以用空間換時間,獲得更加優化的解法。
本題可以考慮爲一個三路歸併的問題,我們將一個只由醜數構成的集合,分成三個子集:
- 第一路是包含質因子2的所有醜數的集合
- 第二路是包含質因子3的所有醜數的集合
- 第三路是包含質因子5的所有醜數的集合
每輪進行一次比較,取出三個集合中最小的一個元素,並將它添加到醜數集合之中,再把對應集合的指針往後移動一位。
有一種很巧妙的實現方式,具體代碼如下:
class Solution(object):
# 用空間換時間
def getUglyNumber(self, n):
"""
:type n: int
:rtype: int
"""
if n <= 1:
return n
nums = [1]
i, j, k = 0, 0, 0
n -= 1
while n:
t = min(nums[i] * 2, nums[j] * 3, nums[k] * 5) # 取出三路中,最小的醜數
if t == nums[i] * 2: # 該醜數來自第一路,指針後移一位
i += 1
if t == nums[j] * 3: # 該醜數來自第二路,指針後移一位
j += 1
if t == nums[k] * 5: # 該醜數來自第三路,指針後移一位
k += 1
nums.append(t)
n -= 1
return nums[-1]
46. 正整數分解成質因子表示(分解質因子)
質數又稱素數,它是一個大於1的自然數,除了1和它自身外,不能被其他自然數整除的數叫做質數,否則稱爲合數。
輸入一個正整數n,返回它的質因子的集合,如果輸入1,則返回 1。
輸入:90
輸出: [2, 3, 3, 5]
【分析】
我們考慮一個正整數 , 滿足 ,那麼它的質因子肯定在 區間內。
我們從 開始遍歷,如果 n % 2 == 0
,那麼說明 2 是 的一個因子,我們再修改 的值 n = n // 2
。
當我們把 中所有的 取出來之後,如果 仍成立,則將i += 1
,此時再可以把 中所有的 取出來。
如果 仍成立,執行i += 1
,此時 ,顯然 n % 4 == 0
不可能成立,因爲 時,以及把所有含 的因子取了出來,依次類推,再次執行i += 1
具體實現代碼:
class Solution(object):
def get_prime_factors(self, number, res):
if number == 1:
return res.append(number)
n = 2
while number != 1: # 最終number會被整除爲1
if number % n == 0:
res.append(n)
number //= n
else:
n += 1
return res
47. 判斷一個數是否爲質數(質數判別)
輸入一個正整數n,判斷從2到n的區間內,質數的總個數。
輸入:17
輸出:False
【分析】
本題的難點,在於判斷一個數是不是質數,一個基本的思路是,從 到 的區間內進行遍歷,如果存在一個數可以被 整除,那就說明這個數不是質數,否則說明該數是質數。
實現代碼:
class Solution(object):
def is_prime(self, number):
# ceil向上取整,floor向下取整,int是向下取整,round是四捨五入
from math import ceil, sqrt
for i in range(2, ceil(sqrt(number)) + 1):
if number % i == 0:
return False
return True
然而上面這個過程可以進行優化 !
我們繼續分析,其實質數還有一個特點,就是它總是等於 6x-1 或者 6x+1,其中 x 是大於等於1的自然數。
如何論證這個結論呢,其實不難。首先 6x 肯定不是質數,因爲它能被 6 整除;其次 6x+2 肯定也不是質數,因爲它還能被2整除;依次類推,6x+3 肯定能被 3 整除;6x+4 肯定能被 2 整除。那麼,就只有 6x+1 和 6x+5 (即等同於6x-1) 可能是質數了。
因此,如果對某個大於 的正整數 ,如果 n % 6 != 1 and n % 6 != 5
,那就說明它一定不是質數。 根據這個結論,可以進行第一次過濾。
如果上面條件不滿足,說明 n % 6 == 1 or n % 6 != 5
,如 對於這些數字,我們遍歷在 5 到 區間內,所有分佈在6兩側的數字,如果能被其整除,說明該數不是質數。
具體代碼如下:
class Solution(object):
def is_prime_2(self, number):
if number <= 3: # 考慮number=2,3的情況
return number > 1
# 不在6的倍數兩側的數,一定不是質數
if number % 6 != 1 and number % 6 != 5:
return False
from math import sqrt
i = 5
while i <= sqrt(number):
if number % i == 0 or number % (i + 2) == 0:
return False
i += 6
return True
48. 字符串中第一個只出現一次的字符(哈希表)
在字符串中找出第一個只出現一次的字符。
如輸入"abaccdeff",則輸出b。
如果字符串中不存在只出現一次的字符,返回#字符。(輸入可能爲空或都是重複字符)
【分析】
本題的思路較爲簡單,直接從前往後遍歷一次字符串,第一次出現的字符,value=1
,否則 value+=1
。
記錄本題的目的是爲了鞏固對 字典結構的使用。
令 d = {‘a’: 3, ‘d’: 1, ‘b’: 2}
:
’a’ in d
,返回 True;’a’ in d.keys()
,返回 True;1 in d
,返回 False1 in d.values()
,返回 True;sorted(d)
,返回列表[‘a’, ‘b’, ‘d’]
sorted(d.keys())
,返回列表[‘a’, ‘b’, ‘d’]
sorted(d.values())
,返回列表[1, 2, 3]
sorted(d.items())
,返回列表[(‘a’, 3), (‘b’, 2), (‘d’, 1)]
sorted(d.items(),key=lambda item:item[1])
,返回列表[(‘d’, 1), (‘b’, 2), (‘a’, 3)]
本題解答代碼如下:
class Solution:
def firstNotRepeatingChar(self, s):
"""
:type s: str
:rtype: str
"""
if not s:
return '#'
d = dict()
for ch in s:
if ch in d.keys():
d[ch] += 1
else:
d[ch] = 1
d = sorted(d.items(), key=lambda item: item[1])
if d[0][1] == 1:
return d[0][0]
return '#'
49. 字符流中第一個只出現一次的字符(哈希表,隊列)
請實現一個函數用來找出字符流中第一個只出現一次的字符。
例如,當從字符流中只讀出前兩個字符”go”時,第一個只出現一次的字符是’g’。
當從該字符流中讀出前六個字符”google”時,第一個只出現一次的字符是’l’。
如果當前字符流沒有存在出現一次的字符,返回#字符。
輸入:“google”
輸出:“ggg#ll”
解釋:每當字符流讀入一個字符,就進行一次判斷並輸出當前的第一個只出現一次的字符。
【分析】
本題和上一題的區別在於,它的字符串不是固定的,如果我們按照上面的方法,每傳入一個字符,整理一遍哈希表,再從哈希表中找出第一個 的 ,每次查詢的時間複雜度爲 , 個字符的時間複雜度則爲 。
一種把時間複雜度降爲 的做法是,我們維護一個隊列,隊列的第一個元素,是當前字符流中,第一個沒有重複出現的字符。
當傳入新字符的時候,如果該字符前面未出現過,則將它存在哈希表中,對應的 ,並將其添加到隊列裏;如果該字符在前面出現過,將它對應的 。
每輪插入新字符時,都要檢查,隊列頭部的元素是否爲重複元素,是的話,則將前彈出,直到頭部元素不再爲重複元素爲止。
class Solution:
d = dict()
queue = list()
def firstAppearingOnce(self):
"""
:rtype: str
"""
if not self.queue:
return "#"
else:
return self.queue[0]
def insert(self, char):
"""
:type char: str
:rtype: void
"""
if char in self.d.keys():
self.d[char] += 1
else:
self.d[char] = 1
self.queue.append(char)
while self.queue and self.d[self.queue[0]] > 1: # 隊首元素必須爲不重複的元素
self.queue.pop(0)
50. 數組中的逆序對(二路歸併)
在數組中的兩個數字如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。
輸入一個數組,求出這個數組中的逆序對的總數。
輸入:[1,2,3,4,5,6,0]
輸出:6 因爲逆序對有(1,0), (2,0), (3,0), (4,0), (5,0), (6,0)
【分析】
本題存在暴力解法的, 個數字,兩兩組對,有 種組隊方式(因爲先後順序是固定的),我們遍歷每一種組隊情況,如果是逆序對,則把逆序對的總數加1即可。
class Solution(object):
# 暴力解法,時間複雜度爲O(n^2)
def inversePairs(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
res = 0
for i in range(len(nums)):
for j in range(i, len(nums)):
if nums[i] > nums[j]:
res += 1
return res
上面的實現方式,時間複雜度爲,我們可以進行優化,把時間複雜度優化到。
具體思路是用到二路歸併排序的思想,想象一下,我們把一個數組劃分成左、右兩部分,那麼逆序對的總個數等於左邊部分逆序對的個數 + 右邊逆序對的個數,除此之外,我們將左右兩部分進行升序排列,那麼總的逆序對的個數,還包括左邊元素與右邊元素組成逆序對的個數,即逆序對的總個數由上述三部分組成,並且三個部分之間是沒有交集的。
class Solution(object):
def merge(self, nums, l, r):
if l >= r:
return 0
mid = l + r >> 1
# 左邊的逆序對的個數 + 右邊逆序對的個數
res = self.merge(nums, l, mid) + self.merge(nums, mid + 1, r)
i, j = l, mid + 1
sorted_nums = []
# 統計歸併之前,左邊元素與右邊元素構成逆序對的個數
while i <= mid and j <= r:
if nums[i] <= nums[j]:
sorted_nums.append(nums[i])
i += 1
else:
sorted_nums.append(nums[j])
# print('{} {}'.format(nums[i:mid+1],nums[j]))
j += 1
res += mid - i + 1 # 統計左邊有多少個大於右邊當前值的元素
while i <= mid:
sorted_nums.append(nums[i])
i += 1
while j <= r:
sorted_nums.append(nums[j])
j += 1
nums[l:r + 1] = sorted_nums # 把進行歸併所對應的原數組部分,用有序數組替代
return res
# 二路歸併,時間複雜度爲O(nlogn)
def inversePairs(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
return self.merge(nums, 0, len(nums) - 1)
51. 兩個鏈表的第一個公共結點(找規律)
輸入兩個鏈表,找出它們的第一個公共結點。
當不存在公共節點時,返回None。
給出兩個鏈表如下所示:
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
輸出第一個公共節點c1
【分析】
兩個鏈表分爲兩種情況,第一種情況是存在公共節點,第二種情況是不存在公共節點,我們分別進行討論。
- 兩鏈表存在公共節點
如上圖所示,A,B兩個鏈表存在公共節點,鏈表A的長度爲 ,鏈表B的長度爲 ,我們定義兩個指針,同時從A,B兩個鏈表的頭節點開始走,當走到所在鏈表的尾結點時,再從另一個鏈表的頭節點開始走,這時我們發現,,兩個指針必定在第一個公共節點處相遇!
- 兩鏈表不存在公共節點
如上圖所示,A,B兩個鏈表不存在公共節點,鏈表A的長度爲 ,鏈表B的長度爲 ,我們定義兩個指針,同時從A,B兩個鏈表的頭節點開始走,當走到所在鏈表的尾結點時,再從另一個鏈表的頭節點開始走,這時我們發現,,兩個指針必定會同時走向空節點!
具體實現代碼如下:
class Solution(object):
def findFirstCommonNode(self, headA, headB):
"""
:type headA, headB: ListNode
:rtype: ListNode
"""
if not headA or not headB:
return None
p = headA
q = headB
while p != q: # 當 p 和 q 相遇的位置,即爲第一個公共節點的位置
p = p.next
q = q.next
if not p and not q: # 同時走到空節點,說明不存在公共節點
return None
if not p: # 只有p走到空節點,讓p再去走鏈表B
p = headB
if not q: # 只有q走到空節點,讓q再去走鏈表A
q = headA
return p
52. 數字在排序數組中出現的次數(二分法)
統計一個數字在排序數組中出現的次數。
例如輸入排序數組和數字,由於在這個數組中出現了次,因此輸出。
輸入:[1, 2, 3, 3, 3, 3, 4, 5] , 3
輸出:4
【分析】
一個簡單的思路是,我們之間遍歷一遍數組,將元素存在哈希表中,就可以直接得到某個數字出現的次數,它的時間複雜度是 。
我們觀察這個數組的特點,它是一個排序數組,如果我們想要查詢出現的次數,只需要找到第一次出現的位置 ,和最後一次出現的位置,那麼3出現的次數則爲 。
可以用二分法來解決此問題,第一次出現的位置,滿足它左邊的所有元素均小於,它右邊所有的元素均大於等於。如果nums[mid] < 3
,那麼說明最終的 應該在 的左邊,即l = mid+1
,否則 r = mid
。
最後一次出現的位置,滿足它右邊的所有元素均大於,它左邊所有的元素均小於等於。如果nums[mid] > 3
,那麼說明最終的 應該在 的左邊,即r = mid-1
,否則 l = mid
。
具體實現代碼:
class Solution(object):
def getNumberOfK(self, nums, k):
"""
:type nums: list[int]
:type k: int
:rtype: int
"""
if not nums or k not in nums:
return 0
l, r = 0, len(nums) - 1
while l < r:
mid = l + r >> 1
if nums[mid] < k: # 我們要求的值,是第一個等於k的元素的下標
l = mid + 1
else:
r = mid
temp = l # 把l的值存起來
l, r = 0, len(nums) - 1
while l < r:
mid = l + r + 1 >> 1
if nums[mid] > k: # 我們要求的值,是最後一個等於k的元素的下標
r = mid - 1
else:
l = mid
return l - temp + 1
53. 有序數組中數值和下標相等的元素(二分法)
假設一個單調遞增的數組裏的每個元素都是整數並且是唯一的。
請編程實現一個函數找出數組中任意一個數值等於其下標的元素。
如果不存在,則返回 -1
輸入:
輸出:3
【分析】
簡單的思路是直接從頭到尾遍歷一次,找到第一個數值和下標相等的元素,再返回,時間複雜度爲 ,我們下面進行優化,把時間複雜度降到
輸入的數組是一個嚴格單調遞增的整數數組,並且每一個元素都是唯一的,即 ,於是:
所以 是一個(不嚴格)單調遞增的整數數組,我們想要找到數組中第一次爲0的元素,那麼它左邊的所有元素都一定小於0,右邊的所有元素均大於等於0,我們可以用二分法進行求解。
class Solution(object):
def getNumberSameAsIndex(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if not nums:
return -1
l, r = 0, len(nums) - 1
while l < r:
mid = l + r >> 1
if nums[mid] - mid < 0:
l = mid + 1
else:
r = mid
if nums[l] - l == 0:
return l
return -1
54. 二叉搜索樹的第k小節點(中序遍歷 && DFS)
給定一棵二叉搜索樹,請找出其中的第k小的結點。
你可以假設樹和k都存在,並且1≤k≤樹的總結點數。
輸入:root = [2, 1, 3, null, null, null, null] ,k = 3
2
/ \
1 3
輸出:3
【分析】
本題給的是一棵二叉搜索樹,它的特點在於中序遍歷的結果,是單調遞增的,由此可知,第k小的節點,也就是我們第 輪中序遍歷時的對應的節點。
中序遍歷的模板是:
def in_oder(root):
if not root:
return
in_order(root.left)
# do something
in_order(root.right)
也就是說,我們想要做的操作,應該寫在 的位置,我們每到達一次這個位置,就將 k 減 1,當 k 爲 0 的時候,即爲我們中序遍歷到第k個節點的時候。
完整代碼如下:
class Solution(object):
ans = TreeNode(-1)
k = 0 # 必須將k存爲全局變量,因爲每輪遞歸回退時的k不是同一個k值
def dfs(self, root):
if not root:
return
self.dfs(root.left)
self.k -= 1
if not self.k:
self.ans = root
return # 找到第k小的節點之後,可以提前返回,不需要再往下遍歷了
self.dfs(root.right)
def kthNode(self, root, k):
"""
:type root: TreeNode
:type k: int
:rtype: TreeNode
"""
self.k = k
self.dfs(root)
return self.ans
這裏值得一提的是,我第一次寫的時候,把 k 作爲 dfs 函數中的一個參數傳進去的,這樣子是不對的。因爲在python中,每輪遞歸完之後,回退到上一次進入遞歸的位置時,它的 k 值還是原來的 k 值,沒有發生變化。因此,我們需要將每輪遞歸中共享的變量,單獨作爲全局變量提出來。
55. 平衡二叉樹(DFS優化)
輸入一棵二叉樹的根結點,判斷該樹是不是平衡二叉樹。
如果某二叉樹中任意結點的左右子樹的深度相差不超過1,那麼它就是一棵平衡二叉樹。
注意:
規定空樹也是一棵平衡二叉樹。
【分析】
在上一題中,我們掌握瞭如何求解一棵樹的深度,於是看到本題之後,我的第一想法是,進行層次遍歷,分別求解每一個節點左子樹的高度,和右子樹的高度,如果相差超過1,直接返回 False,否則進行往下遍歷,直到最後一個葉子節點爲止。
實現代碼如下:
class Solution(object):
def treeDepth(self, node):
if not node:
return 0
return max(self.treeDepth(node.left), self.treeDepth(node.right)) + 1
def isBalanced(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
if not root:
return True
queue = [root]
while queue:
node = queue.pop(0)
if abs(self.treeDepth(node.left) - self.treeDepth(node.right)) > 1:
return False
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return True
上面這種思路,有值得優化的地方,我們在求解子樹深度的時候,就已經遞歸到葉子節點,再往上返回,得到了每一個節點作爲根結點時的樹的高度,所以我們完全可以把比較操作,直接放到求解樹的深度的代碼中,詳細如下:
class Solution(object):
ans = True
def treeDepth(self, node):
if not node:
return 0
left = self.treeDepth(node.left)
right = self.treeDepth(node.right)
if abs(left - right) > 1: # 將 ans 置爲 False
self.ans = False
return max(left, right) + 1
def isBalanced(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
self.ans = True
self.treeDepth(root)
return self.ans
56. 數組中只出現一次的兩個數字(位運算)
一個整型數組裏除了兩個數字之外,其他的數字都出現了兩次。
請寫程序找出這兩個只出現一次的數字。
你可以假設這兩個數字一定存在
輸入:[1,2,3,3,4,4]
輸出:[1,2]
【分析】
我們知道,異或運算的邏輯是,相同爲0,不同爲1。如果 n 個數中,只有一個數字出現了一次,其他數字均出現了兩次,我們把所有的數進行異或操作,那麼最後得到的就是那個獨一無二的數字。
本題中,考察的是存在兩個只出現了一次的數字,假設爲,那麼所有的數進行異或運算之後,得到的結果爲 s = x ^ y。
我們找到 s 中,某一位爲1的數字,它是 x 和 y 中不同的部分,我們利用這個性質,可以將原集合劃分成兩個部分,那麼x 和 y 則必定在不同的集合中,並且集合內,除了 x 或 y 的其他所有元素,必定是重複的,這時我們再次進行異或操作,就能得出 x 的值,x ^ s 就能得出 y 的值。
class Solution(object):
def findNumsAppearOnce(self, nums):
"""
:type nums: List[int]
:rtype: List[int]
"""
if not nums:
return []
s = 0
# 假設返回的是x,y,那麼所有數字進行異或操作之後,只剩下x^y
for num in nums:
s ^= num
k = 0
while s >> k & 1 != 1: # s的二進制表示中,從右往左第k個數爲1
k += 1
x = 0
for num in nums:
if num >> k & 1 == 1:
x ^= num
return [x, s ^ x]
57. 數組中唯一隻出現一次的數字(二進制統計)
在一個數組中除了一個數字只出現一次之外,其他數字都出現了三次。
請找出那個只出現一次的數字。
你可以假設滿足條件的數字一定存在。
思考題:
如果要求只使用 O(n) 的時間和額外 O(1) 的空間,該怎麼做呢?
輸入:[1,1,1,2,2,2,3,4,4,4]
輸出:3
【分析】
本題的條件是,除了一個數組出現了一次,其餘都出現了三次,那麼就不能直接進行異或求解。
我們換個思路,某個數字出現了三次,那麼該數字的二進制表示中,如果某一位爲1,因爲出現了三次,所以累加起來應該爲3,而只出現了一次的數字,它的某一位爲1,則該位只能加1。
也就是說,我們創建一個長度爲32的數組 count,每一個元素用來統計整個數組中,當前位置所對應二進制位的1的個數,按照題目要求,count[i] % 3
要麼爲0,要麼爲1。我們把整個數組模3,即爲只出現一次的數字的二進制表示(從右往左),然後我們把它轉成十進制數即可。
具體代碼如下:
class Solution(object):
def findNumberAppearingOnce(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
count = [0] * 32 # 統計每1個二進制位上,1出現的次數
for num in nums:
k = 0
while k < 32:
count[k] += num >> k & 1
k += 1
res = 0
for i in range(32):
# 因爲其他數字都出現了三次,只有一個數字出現了一次
# 也就說明count[i]%3等於0或1
res += count[i] % 3 * 2 ** i
return res
除此之外,還有超神版代碼,僅供瞭解:
class Solution(object):
def findNumberAppearingOnce_2(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
ones, twos = 0, 0
for num in nums:
ones = (ones ^ num) & ~ twos
twos = (twos ^ num) & ~ ones
return ones
大致思路是一個狀態機表示,如果某個數字出現了三次,就會變成 0。換個問題,如果傳入的數組中,只有一個元素出現了兩次,其餘都出現了三次,那就返回 twos 即爲所求。
58. 和爲S的兩個數字(哈希表)
輸入一個數組和一個數字s,在數組中查找兩個數,使得它們的和正好是s。
如果有多對數字的和等於s,輸出任意一對即可。
你可以認爲每組輸入中都至少含有一組滿足條件的輸出。
輸入:[1,2,3,4] , sum=7
輸出:[3,4]
【分析】
本題可以構建一個哈希表來快速實現,遍歷數組中的每一個數字,如果它不在哈希表中,我們就把target - num
作爲key,存到哈希表裏,值可以隨意指定,不妨設爲num。如果遍歷到某個數字是屬於哈希表的 key 的話,直接返回 [target-num, num]
。
class Solution(object):
def findNumbersWithSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
d = dict()
for num in nums:
if num in d.keys():
return [target - num, num]
else:
d[target - num] = num
58. 和爲S的連續正整數序列(高斯求和,雙指針法)
輸入一個正數s,打印出所有和爲s的連續正數序列(至少含有兩個數)。
例如輸入15,由於1+2+3+4+5=4+5+6=7+8=15,所以結果打印出3個連續序列1~5、4~6和7~8。
輸入:15
輸出:[[1,2,3,4,5],[4,5,6],[7,8]]
【分析】
關於連續正整數的和,我們可以想到高斯求和公式,或者說等差數列求和公式,即,它的時間複雜度爲。
也就是說,我們可以定義兩個指針,一個表示起始項,另一個表示,然後不斷的枚舉即可。
分析數列的規律,可以發現兩個指針的區間不會是全部區間,對於一個正整數 ,如果它是奇數,那麼兩個指針可以到達最大的位置分別是 、,如果它是偶數,也可以同樣指定上述的範圍。此外,第二個指針的範圍必定在第一個指針之後,於是我們將暴力搜索的區間進行限制,編寫代碼如下:
class Solution(object):
# 暴力搜索,對搜索空間進行了優化
def findContinuousSequence(self, sum):
"""
:type sum: int
:rtype: List[List[int]]
"""
res = []
for i in range(1, sum // 2 + 1): # i的最後一個取值是sum/2向下取整
for j in range(i + 1, (sum + 1) // 2 + 1): # j從i+1開始,j的最後一個取值是sum/2向上取整
s = (i + j) * (j - i + 1) // 2 # 高斯求和公式
if s == sum:
res.append(list(range(i, j + 1)))
return res
實際上呢,該問題還有一個規律,那就是假設兩個指針 當前的位置已經滿足,高斯和等於目標值,如果 繼續增大得到 ,假設存在一個 ,滿足 區間內的所有正整數之和等於目標值,那麼必有 成立。根據這個特性,我們可以進一步縮小搜索區間。
用雙指針法實現上述思想:
class Solution(object):
def findContinuousSequence_2(self, sum):
"""
:type sum: int
:rtype: List[List[int]]
"""
res = []
i = 1
j = 2
while i <= sum // 2 + 1 and j <= (sum + 1) // 2 + 1:
s = (i + j) * (j - i + 1) / 2
if s == sum:
res.append(list(range(i, j + 1)))
i += 1
j += 1
elif s < sum:
j += 1
else:
i += 1
return res
59. 最長遞增子序列(動態規劃或二分法)
給定一個無序的整數數組,找到其中最長上升子序列的長度。
輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
【分析】
統計個數類型的問題,可以考慮要動態規劃法來做。
動態規劃法,需要考慮三個因素:
- 狀態表示 本題 表示以第 個元素結尾的子序列的最大長度;
- 狀態轉移方程 在本題中, 的取值,不再是像往常一樣,只與前面幾個狀態有關,而是和前面 , , , , 每一個狀態有關。所以需要進行一次遍歷,如果 ,那麼 ,如果 ,那麼 。
- 邊界條件 當 時,,因爲每一個 必定存在一個子序列,所以我們把dp數組初始化全爲1。
具體實現代碼如下:(時間複雜度爲)
class Solution:
res = 1
def lengthOfLIS(self, nums):
if not nums:
return 0
dp = [1] * len(nums)
dp[0] = 1
for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
elif nums[j] == nums[i]:
dp[i] = max(dp[i], dp[j])
self.res = max(self.res, dp[i])
return self.res
實際上存在時間複雜度爲 ,需要用到二分法,我們下面進行詳細探討。
假設我們輸入的無序數組爲 :
- 所有長度爲1的遞增子序列爲;
- 所有長度爲2的遞增子序列爲;
- 所有長度爲3的遞增子序列爲;
我們定義一個數組,其中 來保存長度爲的所有遞增子序列中的尾部元素的最小值。有點繞,我們結合上面實例來看。
- ,因爲所有長度爲1的遞增子序列中,尾部元素最小爲3;
- ,因爲所有長度爲2的遞增子序列中,尾部元素最小爲5;
- ,因爲所有長度爲3的遞增子序列中,尾部元素最小爲6;
不難發現規律,如果 數組不斷增長,那麼它一定是單調遞增的序列,這就是二分法使用的關鍵。
我們從前往後依次遍歷數組中的每一個元素,查找 在 數組中的具體位置,具體是找到 數組中,第一個大於 的下標,然後tails[idx]=num
進行替換操作,修改 當前長度爲的遞增子序列的尾部元素的最小值。
當然,有特殊情況需要進行判斷,因爲初始時 tails 數組爲空,所以當它爲空時,直接把元素添加到 tails 數組中;另外一種情況是,我們二分查找得到的 idx 是tails 數組中的最後一個元素,這時我們進行比較,如果該元素小於 tails 數組中的最後一個元素,執行替換操作,否則把該元素追加到 tails 數組的尾部。
具體實現代碼如下:(時間複雜度爲)
class Solution:
def lengthOfLIS(self, nums):
if not nums:
return 0
tails = [] # tails是一個遞增數組,tails[i]存儲所有長度爲i+1的子序列中的尾部元素的最小值
for num in nums:
l = 0
r = len(tails) - 1
while l < r:
mid = l + r >> 1
if tails[mid] < num: # 要找的元素,它的左邊全部小於它,不包含mid
l = mid + 1
else:
r = mid
if not tails or tails[l] < num: # tails數組中的所有元素均小於num,則將num添加到tails中
tails.append(num)
else: # 否則把tails中,第一個大於num的元素修改爲num
tails[l] = num
return len(tails)
60. 翻轉字符串(操作分解)
輸入一個英文句子,翻轉句子中單詞的順序,但單詞內字符的順序不變。
爲簡單起見,標點符號看成普通字母一樣。
例如輸入字符串"I am a student.",則輸出"student. a am I"。
輸入:“I am a student.”
輸出:“student. a am I”
本題解法不難,調用python的語法,return ’ '.join(s.split()[::-1])
一行代碼即可實現,但也失去了本題考察的目的。
記錄本題的目的有兩個,一是瞭解操作分解的思想,二是熟悉python字符串的操作。
- 操作分解 我們可以把翻轉字符串的操作,分成兩個子操作。第一步,對整個字符串進行翻轉,得到
.tneduts a ma I
;第二步,把裏面的每一個單詞進行翻轉,得到student. a am I
,完成本題要求。 - python 字符串處理
- 修改字符串一個或連續多個字符
s = s.replace(s[i], ‘$’)
,s = s.replace(s[i:j], ‘$$$$$$’)
- 字符串轉列表
s = ‘i o u’,list(s) = [‘i’, ’ ', ‘o’, ’ ', ‘u’]
- 字符轉ASCII碼
ord(‘a’) = 97
- ASCII碼轉字符
chr(97) = 'a’
- 修改字符串一個或連續多個字符
class Solution(object):
def reverseWords(self, s):
"""
:type s: str
:rtype: str
"""
s = s[::-1]
i = 0
while i < len(s):
# 字符串劃分模板
j = i
while j < len(s) and s[j] != ' ':
j += 1
s = s.replace(s[i:j], s[i:j][::-1]) # 將單詞進行翻轉,並覆蓋原單詞
i = j + 1
return s
本題還有一個姊妹題,給定一個字符串,一個整數n,如何把字符串的前 n 位按順序轉移到字符串的尾部。
輸入:“abcdefg” , 3
輸出:“defgabc”
同樣可以採用操作分解的思想進行實現,第一步,把前n個字符反轉,把第n位及其之後字符進行反轉,第二步,把整個字符串進行反轉。
class Solution(object):
def leftRotateString(self, s, n):
"""
:type s: str
:type n: int
:rtype: str
"""
s = s.replace(s[:n], s[:n][::-1])
s = s.replace(s[n:], s[n:][::-1])
return s[::-1]
最後再多聊幾句 操作分解 的思想,給定一個矩陣,如果要把它順時針進行翻轉90度,180度,270度,可以把這個過程分解成兩部分完成。
- 矩陣順時針旋轉90度
第一步,把對角線兩邊的元素交換,即
第二步,把每一行的元素,進行翻轉,定義首尾指針,對應兩兩交換即可。
- 矩陣逆時針旋轉90度
第一步,把對角線兩邊的元素交換,即
第二步,把每一列的元素,進行翻轉,定義首尾指針,對應兩兩交換即可。
- 矩陣順/逆時針旋轉180度
第一步,反轉矩陣中每一行的元素。
第二步,反轉矩陣中每一列的元素。
61. 滑動窗口的最大值(單調、雙向隊列)
給定一個數組和滑動窗口的大小,請找出所有滑動窗口裏的最大值。
例如,如果輸入數組[2, 3, 4, 2, 6, 2, 5, 1]及滑動窗口的大小3,那麼一共存在6個滑動窗口,它們的最大值分別爲[4, 4, 6, 6, 6, 5]。
輸入:[2, 3, 4, 2, 6, 2, 5, 1] , k=3
輸出: [4, 4, 6, 6, 6, 5]
【分析】
一個直觀的思路是,我們維護一個長度爲k的隊列,每次從中取出隊列中的最大值,時間複雜度爲O(kn),因爲每輪要從k個數中找到最大值。
class Solution(object):
# 直觀解法,時間複雜度爲O(kn),每輪要從k個數中找到最大值
def maxInWindows(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
res = []
queue = []
for num in nums:
if len(queue) < k:
queue.append(num)
if len(queue) == k:
res.append(max(queue))
queue.pop(0)
return res
實際上,本題的時間複雜度可以優化到 ,核心在於維護一個單調遞減的雙向隊列。
我們在隊列中,保存元素的下標,當有一個元素需要入隊時,我們進行幾輪判斷:
- 1. 隊首元素是否需要出隊?
判斷依據,當前元素的下標 減去 隊列頭元素的下標 是否等於 k,是的話,說明隊列頭元素需要出隊。 - 2. 當前元素的下標插入到隊列尾部之後,能否保證隊列依然是遞減的?
爲什麼要讓隊列保持遞減呢?假如某個元素值與隊列中其他元素大,那麼隊列中比它小的元素,永遠不可能成爲當前隊列中的最大元素,所以可以直接刪除掉。 - 3. 是否需要把隊列頭元素對應的最大值添加到輸出結果中?
直接比較 當前元素的下標是否大於或等於 k-1 即可,很明顯我們的滑動窗口從第 k-1 個元素開始輸出,隊列第一個元素的下標即爲當前窗口的最大值。
具體實現代碼:
class Solution(object):
def maxInWindows(self, nums, k):
"""
:type nums: List[int]
:type k: int
:rtype: List[int]
"""
res = []
queue = []
for i, num in enumerate(nums):
if queue and i - queue[0] == k: # 判斷隊列頭元素是否需要彈出
queue.pop(0)
while queue and nums[queue[-1]] <= num: # 維護隊列單調遞減
queue.pop() # 隊列尾部小於num的元素陸續出隊
queue.append(i)
if i >= k - 1: # 隊列的頭元素始終爲當前窗口內最大值的下標
res.append(nums[queue[0]])
return res
62. n個骰子的點數(遞歸或動態規劃)
將一個骰子投擲n次,獲得的總點數爲s,s的可能範圍爲n~6n。
擲出某一點數,可能有多種擲法,例如投擲2次,擲出3點,共有[1,2],[2,1]兩種擲法。
請求出投擲n次,擲出n~6n點分別有多少種擲法
輸入:n=2
輸出:[1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]
解釋:投擲2次,可能出現的點數爲2-12,共計11種。每種點數可能擲法數目分別爲1,2,3,4,5,6,5,4,3,2,1。
所以輸出[1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]。
【分析】
本題是一類特別經典的題型,所以我會重點進行分析。
對於連續重複某種操作,並且操作結果必定是已知取值空間中的一種,求 n 次操作最終的取值類型的題,如擲骰子、爬臺階等問題,我們都可以考慮用遞歸或者動態規劃來做。
我們先聊一聊遞歸與動態規劃的區別,再用兩種解法來解決本題。
-
遞歸的特點
- 一個問題的解可以分解爲幾個子問題的解;
- 這個問題與分解之後的子問題,除了數據規模不同,求解思路完全一樣
- 存在遞歸終止條件,即必須有一個明確的遞歸結束條件,稱之爲遞歸出口
-
遞歸的解法
- 找到如何將大問題分解爲小問題的規律
- 通過規律寫出遞推公式
- 通過遞推公式的臨界點推敲出終止條件
- 編寫代碼實現遞歸過程
-
DP的特點
- 動態規劃法試圖只解決每個子問題一次(遞歸則是多次)
- 一旦某個子問題的解已經算出,則將其存儲,下次需要同一個子問題的解時直接查表
-
DP的解法
- 狀態表示
- 狀態轉移方程
- 邊界處理
根據我個人的學習感受,我認爲遞歸和動態規劃的思路還是蠻接近的,能用遞歸解決的問題,基本上可以用動態規劃來解決。遞歸是一個自上而下的過程,存在多次子問題求解的冗餘計算,故時間複雜度是指數級的,優點在於代碼簡潔,實現簡單;而動態規劃是一個自底向上的過程,可以保存每個子問題的解,上層需要求解時,直接查表即可,不需要再計算一遍,優點是時間複雜度低,但是存儲子問題的解需要開闢新的空間,典型的以空間換時間的做法。
回到本題中來,我們先用遞歸進行求解。
- 大問題分解成小問題
我們假設用f(n,S)
來表示n個骰子的和爲S的情況總數,那它可以分解爲n-1個骰子的和爲S-1,n-1個骰子的和爲S-2,,n-1個骰子的和爲S-6,這6個子問題來求解。 - 遞推公式
根據上方規律,可知f(n,S)=f(n-1,S-1)+f(n-1,S-2)+f(n-1,S-3)+f(n-1,S-4)+f(n-1,S-5)+f(n-1,S-6)
- 遞歸終止條件
顯然,當n == 1 and 0 < S < 7
時,我們要返回1;如果n < 1 or S <= 0
時,返回 0。 - 編寫代碼實現遞歸過程
class Solution(object):
def numberOfDice(self, n):
"""
:type n: int
:rtype: List[int]
"""
if not n:
return 0
res = []
for i in range(n, 6 * n + 1):
res.append(self.dfs(i, n))
return res
# 遞歸兩個要素:1.遞歸表示 2.遞推公式 ,自上而下的順序
def dfs(self, s, n):
if n < 1 or s <= 0:
return 0
if n == 1 and 0 < s < 7:
return 1
res = 0
for i in range(1, 7):
res += self.dfs(s - i, n - 1)
return res
我們接下來再用動態規劃進行求解。
- 狀態表示 我們用 表示 i 個骰子和爲 j 的總情況數
- 狀態轉移方程 根據前面的分析,我們知道,,其中 的取值爲
- 邊界處理 在本題中,我們的邊界是骰子數爲1的情況,我們可以直接把 的取值全部初始爲1。
在本題中,動態規劃還需要注意的一點是二維數組的初始化,因爲骰子數爲1的和只有6種取值,骰子數爲2有11種取值,骰子數爲3有16種取值 本來我的想法是按照每個骰子的取值情況初始化數組,但是會發生數組越界的情況,如dp[3][15] += dp[2][13]的時候,而dp[2]最多隻能到dp[2][12],此時就會報數組越界了。
因此,我們直接初始化 dp數組 爲一個 n+1 行,6*n +1 列的二維矩陣。
詳細代碼如下:
class Solution(object):
def numberOfDice(self, n):
"""
:type n: int
:rtype: List[int]
"""
if not n:
return 0
dp = [[0] * (6 * n + 1) for _ in range(0, n + 1)] # 創建一個(n+1)* 6n 的二維矩陣
for i in range(1, 7): # 邊界處理,1個骰子和的取值爲1,2,3,4,5,6的情況數全爲1
dp[1][i] = 1
for i in range(2, n + 1): # 枚舉骰子個數,從2開始
for j in range(i, 6 * i + 1): # 枚舉i個骰子和的取值
for k in range(1, 7): # k取1,2,3,4,5,6
dp[i][j] += dp[i - 1][j - k]
return dp[-1][n:] # 最後一層,從第n個元素開始,即爲所求。
63. 撲克牌中的順子(抽象建模,逆向思維)
從撲克牌中隨機抽5張牌,判斷是不是一個順子,即這5張牌是不是連續的。
2~10爲數字本身,A爲1,J爲11,Q爲12,K爲13,大小王可以看做任意數字。
爲了方便,大小王均以0來表示,並且假設這副牌中大小王均有兩張。
輸入:[3,2,0,6,5]
輸出:true
【分析】
像這種類型的題目,找到了內在原理之後,編寫代碼其實很簡單,主要難點在於考慮周全存在的各種輸入情況。
想到的第一件事,應該是把輸入數組中的 0 單獨拎出來,那麼剩餘的數組必須滿足什麼條件才能組成“順子”呢?
或者我們可以逆向思維,哪些的情況,必然不能組成順子?
如果剩餘數組中的存在重複元素,那這五張牌必然不會組成順子,此外,如果最大值和最小值的差大於4,那這五張牌同樣不可能組成順子。
編寫代碼如下:
class Solution(object):
def isContinuous(self, numbers):
"""
:type numbers: List[int]
:rtype: bool
"""
if not numbers:
return False
numbers.sort()
k = 0
while not numbers[k]: # 找到第一個不爲0的元素下標
k += 1
for i in range(k + 1, len(numbers)):
if numbers[i] == numbers[i - 1]: # 有序數組,重複元素必相鄰
return False
return numbers[-1] - numbers[k] <= 4
64. 圓圈中最後剩下的數字(抽象建模,約瑟夫環)
0, 1, …, n-1這n個數字(n>0)排成一個圓圈,從數字0開始每次從這個圓圈裏刪除第m個數字。
求出這個圓圈裏剩下的最後一個數字。
輸入:n=5 , m=3
輸出:3
本題可以直接用一個環形鏈表進行模擬,但是實現的代碼複雜度比較高,我們對問題進行探索,看能否找到問題內在的規律。
觀察下圖:
我們一開始有 個數字,每輪淘汰第 個數字,所以在第一輪,被淘汰的數字是下標爲 的數字。
那麼第二輪是從下標爲 的數字開始,我們按照順序,從零開始對數組進行重新編號,結尾數字的下標爲 。
可以發現,同一個數字,新的下標 和舊的下標 存在一個映射關係:
這個發現是解決本題的關鍵!
我們定義 來表示每次在 個數中,刪除第 個數字之後,最後剩下的數字。這個數字 必定等於 刪除第 個數字之後,從下標爲 的數字開始的 個數字之中,每次刪除第 個數字之後,最後剩下的數字。
也就是說,最後一輪剩下的數字,我們可以一層一層倒着推回去,從 開始,推到 。
在 時,因爲只剩一個元素,所以最後一個數字的編號爲 ,我們按照上面的映射關係,推導該數字在 時的下標,即 (0+m) % 2
,依次類推
實現代碼如下:
class Solution(object):
def lastRemaining(self, n, m):
"""
:type n: int
:type m: int
:rtype: int
"""
dp = [0] * (n + 1)
dp[1] = 0
for i in range(1, n + 1):
dp[i] = (dp[i - 1] + m) % i
return dp[-1]
65. 股票的最大利潤(抽象建模)
假設把某股票的價格按照時間先後順序存儲在數組中,請問買賣一次該股票可能獲得的利潤是多少?
例如一隻股票在某些時間節點的價格爲[9, 11, 8, 5, 7, 12, 16, 14]。
如果我們能在價格爲5的時候買入並在價格爲16時賣出,則能收穫最大的利潤11。
輸入:[9, 11, 8, 5, 7, 12, 16, 14]
輸出:11
【分析】
暴力的解法是,從輸入的數字中,每次隨機選取兩個數字,數字之間是有先後順序的,所以一共有 組數字,然後返回差值最大的結果,時間複雜度爲 。
實際上我們可以進行優化,只進行一次遍歷即可,遍歷到第 個元素的時候,用它的值減去它前面 個元素中的最小值,即爲當前元素的最大收益,時間複雜度爲 ,空間複雜度爲 。
實現代碼如下:
class Solution(object):
def maxDiff(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) < 2:
return 0
res = 0
min_v = nums[0]
for num in nums[1:]: # 從第2個數字開始枚舉
res = max(num - min_v, res)
min_v = min(num, min_v)
return res
66. 求1+2+…+n ( a and b )
求1+2+…+n,要求不能使用乘除法、for、while、if、else、switch、case等關鍵字及條件判斷語句(A?B:C)。
輸入:10
輸出:55
【分析】
我們先來想想,常規做法有哪些。
- 高斯求和
- 循環
for num in nums: res += num
- 遞歸
f(n) = f(n-1) + n
在編程語言中,編譯器在執行代碼時,會做一些省時的操作,我們來逐個分析執行下列命令時的實際情況。
a and b
如果 a 爲 0 或 False 則不會執行 b 語句,直接返回 0 或 False。
在python3中,假設x,y不爲0,0 and y
或x and 0
會直接返回 0,x and y
返回y。a or b
如果 a 爲 非零值 或 True 則不會執行 b 語句,直接返回 a 或 True。
在python3中,假設x,y不爲0,x or 0
或0 or y
會直接返回 x或y,x or y
返回x。
在本題中,我們可以利用 a and b
來實現遞歸求和。
class Solution(object):
def getSum(self, n):
"""
:type n: int
:rtype: int
"""
res = n
res += n and self.getSum(n - 1)
return res
67. 不用加減乘除做加法(位運算)
寫一個函數,求兩個整數之和,要求在函數體內不得使用+、-、×、÷ 四則運算符號。
輸入:num1 = 1 , num2 = 2
輸出:3
【分析】
我們考慮兩個二進制數中,同一個位置的兩個元素 a 和 b 的加法情況:
一共有上述四種情況,我們把加完之後的位置用字母c表示,進位用d表示
不難發現, 都可以用 和 通過位運算得到,即 ,。
上面的討論,是針對於單個二進制的加法,實際上它也可以拓展到多個二進制位的加法。
兩個數的加法可以分爲兩步,第一步,計算兩個數字不進位的和,第二步,把上一步的結果,加上進位的值,可以不斷循環下去,直到進位爲零即可。
不進位的和,即爲兩個數的異或,sum = num1 ^ num2
進位的值,即爲兩個數的&運算,再左移一位(進一位),carry = (num1 & num2) << 1
c++代碼爲:(比較簡潔,不用考慮負數的特殊情況)
class Solution {
public:
int add(int num1, int num2){
while (num2) {
int sum = num1 ^ num2;
int carry = (num1&num2)<<1;
num1 = sum;
num2 = carry;
}
return num1;
}
};
根據我們前面的學習,Python整數類型可以表示無限位,所以需要人爲設置邊界,避免死循環,我們這裏需要把它控制在32位,故實際做的時候把sum和carry加了一層轉換,限制邊界。
python3代碼爲:
class Solution:
def add(self, num1, num2):
while num2:
sum = (num1 ^ num2) & 0xffffffff # 限制爲32位,但是對應的32位的數不變
carry = ((num1 & num2) << 1) & 0xffffffff
num1 = sum
num2 = carry
if num1 < 0x7fffffff: # 在32位的int中,如果第32位不爲1,說明它是一個正數
return num1
else:
return ~(num1 ^ 0xffffffff)
補充說明一下,如何把 num1 還原成原來的負數:
因爲我們之前限定了邊界,把 num1 轉成了正值,所以要進行處理,把它還原成原來的負數。
我們把 num1 分成兩部分,左邊部分爲32位之前的高二進制位,全部是0,用A表示;右邊部分爲剩下的32位,用B表示。
我們想做的就是把A中的0全部變成1,並保持B不變,num1 ^ 0xffffffff
表示先把後32位按位取反,最終再全部取反,負數還原完畢。
68. 構建乘積數組(操作分解)
給定一個數組A[0, 1, …, n-1],請構建一個數組B[0, 1, …, n-1],其中B中的元素B[i]=A[0]×A[1]×… ×A[i-1]×A[i+1]×…×A[n-1]。
不能使用除法,空間複雜度爲O(1)
輸入:[1, 2, 3, 4, 5]
輸出:[120, 60, 40, 30, 24]
【分析】
本題有兩重限制,一是不能使用除法(否則我們直接求出連乘積,再逐一做除法即可),二是空間複雜度爲 (否則我們可以開闢兩個數組,一個是每個元素左邊的連乘積,另一個是每個元素右邊的連乘積)
實際上,我們是可以把空間複雜度優化爲 的,我們要計算 的值,可以分成兩次完成。
- 第一輪是把 左邊的所有元素的積賦給 ,按照從前往後的順序,從下標爲 的元素開始;
- 第二輪是把 右邊的所有元素的積乘上 ,按照從後往前的順序,從下標爲 的元素開始。
我們用一個 temp 值來保存累乘的結果,最終實現代碼如下:
class Solution(object):
def multiply(self, A):
"""
:type A: List[int]
:rtype: List[int]
"""
if not A:
return []
B = [1] * len(A)
temp = 1
for i in range(1, len(A)): # B[i] 先逐項乘以左邊的A[0],A[1],...,A[i-1]
temp *= A[i - 1]
B[i] = temp
temp = 1
for i in range(len(A) - 2, -1, -1): # B[i] 再逐項乘以右邊的A[i+1],A[i+2],...,A[n-1]
temp *= A[i + 1]
B[i] *= temp
return B
69. 把字符串轉換成整數(字符與digit)
請你寫一個函數StrToInt,實現把字符串轉換成整數這個功能。
當然,不能使用atoi或者其他類似的庫函數。
輸入:“123”
輸出:123
注意:
你的函數應滿足下列條件:
(1)忽略所有行首空格,找到第一個非空格字符,可以是 ‘+/−’ 表示是正數或者負數,緊隨其後找到最長的一串連續數字,將其解析成一個整數;
(2)整數後可能有任意非數字字符,請將其忽略;
(3)如果整數長度爲0,則返回0;
(4)如果整數大於INT_MAX( − 1),請返回 − 1;如果小於INT_MIN() ,請返回;
【分析】
本題考察了兩點,一是處理各種異常輸入,二是不用任何庫函數處理字符串。
- 判斷一個字符是不是數字,我們用
if ‘0’ <= str[i] <= '9’
- 求解一個字符的數值,我們用
ord(str[i]) - ord(‘0’)
完整代碼如下:
class Solution(object):
def strToInt(self, str):
"""
:type str: str
:rtype: int
"""
if not str:
return 0
k = 0
while str[k] == ' ': # 1.去開頭空格
k += 1
str = str[k:]
is_positive = True
if str[0] == '-': # 2. 如果存在正負號,記錄下來,並去掉
is_positive = False
str = str[1:]
elif str[0] == '+':
str = str[1:]
number = 0
for i in range(len(str)): # 將數值部分的字符串轉成int存儲
if '0' <= str[i] <= '9': # 判斷字符是否爲數值
number = number * 10 + ord(str[i]) - ord('0')
else:
break
if number <= 2 ** 31 - 1:
return number if is_positive else -number
else:
return 2 ** 31 - 1 if is_positive else -2 ** 31
70. 二叉樹中兩個節點的最低公共祖先(DFS)
給定一棵二叉樹,以及樹中一定存在的兩個節點,要求返回這兩個節點的最低公共祖先。
本題我們可以拆分成兩個子問題,一是二叉搜索樹中兩個節點的最低公共祖先,二是普通二叉樹中兩個節點的最低公共祖先。
關於兩個節點的最低公共祖先,它一共只有兩種情況,第一種情況是,兩個節點分佈在最低公共祖先的兩側;第二種情況是,其中的某個節點就是最低公共祖先。
我們先來討論二叉搜索樹的情況。
一般來說,二叉樹類型的問題,考慮用遞歸來做,我們前面分析了遞歸的解題步驟。
- 大問題分解成小問題
我們假設用dfs(root,p,q)
來表示以 爲根結點的二叉搜索樹中,節點和的最低公共祖先,我們可以先進行預處理,如果節點的值大於節點的值,就把兩個節點交換。那麼,如果p.val > root.val
,說明最低公共祖先在右子樹中;如果q.val < root.val
,說明最低公共祖先在右子樹中,如果p.val < root.val < q.val
,說明最低公共祖先就是 root; - 遞推公式
根據上面分析,我們得出dfs(root,p,q)
在不同的if
條件下,等於dfs(root.left,p,q)
、dfs(root.right,p,q)
、root
中的一種。 - 遞歸終止條件
當root
爲空時,返回None
。 - 編寫代碼實現遞歸過程
class Solution:
def lowestCommonAncestor(self, root, p, q):
"""
:type root: TreeNode
:type p: TreeNode
:type q: TreeNode
:rtype: TreeNode
"""
return self.dfs(root, p, q)
def dfs(self, root, p, q):
if not root:
return None
if p.val > q.val: # 保證我們進行搜索時,p的值小於q,簡單的交換位置即可
return self.dfs(root, q, p)
if p.val <= root.val <= q.val: # 兩個結點在根結點的兩邊
return root
if p.val > root.val: # 兩個結點都在根結點的右側
return self.dfs(root.right, p, q)
if q.val < root.val: # 兩個結點都在根結點的左側
return self.dfs(root.left, p, q)
我們再來討論普通二叉樹的情況。
同樣,我們按照遞歸的思路來求解。
- 大問題分解成小問題
我們假設用dfs(root,p,q)
來表示以 爲根結點的二叉搜索樹中,節點和的最低公共祖先。節點和要麼同時分佈在左子樹中,要麼同時分佈在右子樹中,要麼分佈在根結點的兩側。當我們遍歷到某個節點等於或時,可以直接返回該節點,在前兩種情況下,該節點就是公共祖先,在第三種情況時, 即爲公共祖先。 - 遞推公式
根據上面分析,我們可以得出left=dfs(root.left,p,q)
、right=dfs(root.right,p,q)
,如果left
和right
同時不爲空,說明爲第三種情況;否則返回left
和 **right
**中不爲空的那一個節點。 - 遞歸終止條件
當root
爲空時,返回None
,說明和都不在該子樹中;
當root==p or root==q
爲空時,返回root
。 - 編寫代碼實現遞歸過程
class Solution(object):
def lowestCommonAncestor(self, root, p, q):
"""
:type root: TreeNode
:type p: TreeNode
:type q: TreeNode
:rtype: TreeNode
"""
return self.dfs(root, p, q)
def dfs(self, root, p, q):
if not root or p == root or q == root:
return root
left = self.dfs(root.left, p, q)
right = self.dfs(root.right, p, q)
if left and right:
return root
return left if left else right