算法習題筆記

《算法習題筆記》
作者:蔣輝    
日期:2019/8/9
[email protected]

題目說明

  • 1~3題爲騰訊2019春招筆試題
  • 4~7題爲頭條2019春招筆試題
  • 8~70題爲《劍指offer》第二版中的習題

代碼地址:https://github.com/jh0905/data_structure_and_algorithm (裏面包含更多專題代碼,如二分專題、揹包專題、深搜專題、二叉樹專題等)

文章目錄

1. 硬幣問題(貪心算法)

牛家村的貨幣是一種很神奇的連續貨幣,他們貨幣的最大面額是n,並且一共有面額爲1,2,3,…,n,n種面額的硬幣。牛牛每次購買商品都會帶上所有面額的硬幣,支付時會選擇給出硬幣數量最少的方案。(每種面額的硬幣有無限多個)

輸入爲兩個整數m和n,表示貨幣的最大面額和商品的價格,輸出爲牛牛最少給出的硬幣數量。

分析
  顯然這是一個貪心算法,即儘可能多的用最大面額的硬幣,如果剩餘的商品價格小於最大硬幣的話,就用對應金額的一枚硬幣來填充。分析完之後,這就是一個向上取整的問題,在Python3中,直接 return (m+n-1) // m 來實現。

2. 奇怪的數列(分情況討論)

有這麼一個數列,{1,2,3,4,5,6,7,8,...}\{-1, 2, -3, 4, -5, 6, -7, 8, ...\},可以發現,第奇數個元素的值爲負數,第偶數個元素的值爲正數,現在給出一個區間[l,r][l, r]ll表示第ll個元素,rr表示第rr個元素,請輸出區間[l,r][l, r]所有元素的累加和。

分析
  觀察發現,數列中每相鄰的兩個元素的和爲同一個數,要麼爲+1,要麼爲-1,於是我們可以將區間[l,r][l, r]裏的元素兩兩分組,這裏分組也是有兩種情況,要麼就剩下最後一個元素,要麼所有元素都配對完成,之後就是一個簡單的求和了。【考察分情況討論的能力

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先出牌,把nn張卡片擺好,那麼小B在看得到小A每張牌的擺放情況下,如果要得s分,有多少種擺牌的方法呢?

分析
  根據題意,小B要得ss分,就意味着他有ss張卡片要勝過小A的卡片,用組合數表示爲CnsC_n^s,剩下的(ns)(n-s)張卡片,則爲平局或輸掉,即有2ns2^{n-s}種可能,也就是說,一共有Cns2nsC_n^s\cdot2^{n-s}種擺法,那麼我們剩下來要做的事情,就是如何在滿足內存和時間限制的前提下,計算出這個結果的值。

  • 楊輝三角公式:(用遞歸來實現)
    Cmn=Cm1n1+Cm1n C_m^n=C_{m-1}^{n-1}+C_{m-1}^{n}
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)
  • 轉換爲對數:
    Cmn=m!n!(mn)! C_m^n=\frac{m!}{n!\cdot(m-n)!}

ln  Cmn=lnm!n!(mn)! ln \;C_m^n=ln\frac{m!}{n!\cdot(m-n)!}

  展開
ln(Cmn)=ln(m!)ln(n!)ln((mn)!)ln(Cmn)=i=1mln(i)i=1nln(i)i=1mnln(i) \begin{array}{l}{\ln \left(C_{m}^{n}\right)=\ln (m !)-\ln (n !)-\ln ((m-n) !)} \\\\ {\ln \left(C_{m}^{n}\right)=\sum_{i=1}^{m} \ln (i)-\sum_{i=1}^{n} \ln (i)-\sum_{i=1}^{m-n} \ln (i)}\end{array}

  消除相同項(大大降低了計算的複雜度)
ln(Cmn)=i=n+1mln(i)i=1mnln(i) \ln \left(C_{m}^{n}\right)=\sum_{i=n+1}^{m} \ln (i)-\sum_{i=1}^{m-n} \ln (i)

  組合數還有一個性質
Cmn=Cmmn C_m^n=C_m^{m-n}

  於是我們在正式計算之前,判斷 nm2n \leq \frac{m}{2},不滿足的話,令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.
 
以下是一個四分鐘轉換的例子:
[211110011][221210011][222220011][222220021][222220022] \left[\begin{array}{rrr}{2} & {1} & {1} \\ {1} & {1} & {0} \\ {0} & {1} & {1}\end{array}\right] \rightarrow \left[\begin{array}{rrr}{2} & {2} & {1} \\ {2} & {1} & {0} \\ {0} & {1} & {1}\end{array}\right] \rightarrow \left[\begin{array}{rrr}{2} & {2} & {2} \\ {2} & {2} & {0} \\ {0} & {1} & {1}\end{array}\right] \rightarrow \left[\begin{array}{rrr}{2} & {2} & {2} \\ {2} & {2} & {0} \\ {0} & {2} & {1}\end{array}\right] \rightarrow \left[\begin{array}{rrr}{2} & {2} & {2} \\ {2} & {2} & {0} \\ {0} & {2} & {2}\end{array}\right]

分析
  題目的意思很好理解,在一個由{0,1,2}\{0,1,2\}三個數字填滿的二維矩陣。每一輪,數字22會把它上下左右相鄰的11變成22,然後進入下一輪,上一輪被轉變的11會把它相鄰的數字11繼續轉換爲22,由此遞歸下去。這其實就是圖搜索中的寬度優先搜索過程,由於我們可能會有多個起點(元素22),所以它也可以歸類爲多源最短路問題。

關於本題的解析
  多源最短路問題解法分爲兩步:
   (1)所有起點(源)座標插入隊列 [  [i1,j1],[i2,j2],[i3,j3],...,  ][ \;[i_1,j_1], [i_2,j_2], [i_3,j_3],...,\;] 隊列具有先進先出的性質
   (2)進行 breadth  fisrt  searchbreadth\;fisrt\;search ,每次彈出隊列中的第一個元素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. 特徵提取(暴力法的優化)

小明想從貓咪視頻中挖掘一些貓咪的運動信息,爲了提取運動信息,他需要從視頻的每一幀中提取特徵。
一個貓咪特徵是一個二維的 vector  vector\;<x, y>.
x1=x2,y1=y2x_1 = x_2, y_1 = y_2 時,我們認爲<x1x_1,y1y_1>和<x2x_2,y2y_2>爲相同特徵。
如果在連續的幾個幀裏面,都出現了相同的特徵,它將構成特徵運動。
小明期望找到最長的特徵運動長度

輸入格式
第一行爲正整數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個單位,編號爲ii的建築爲h(i)h(i)個單位。
起初,機器人在編號爲0的建築處,每一步,它要跳到下一個建築。
假設機器人在第k個建築,且它的能量值爲E,下一步它將跳到第k+1個建築。
如果h(k+1)>Eh(k+1)>E,它將失去h(k+1)Eh(k+1)-E的能量,否則它將獲得Eh(k+1)E-h(k+1) 的能量。
遊戲目標是到底第N個建築,在這個過程中,機器人的能量不能爲負數。
現在的問題是,機器人初始時以多少能量值開始遊戲,纔可以保證成功完成這個遊戲。

輸入格式
第一行輸入正數NN
第二行爲N個空格隔開的整數,1N,  H(i)1051 \leq N, \;H(i) \leq 10^5
輸出格式
一個整數,表示最小的能量值

分析

  題目的要求是,機器人的能量不能爲負數,即假設機器人到達第kk個建築的時候,它的能量值爲 ϵ\epsilon,那麼它跳到第k+1個建築的時候,能量值則變爲 ϵ+[ϵh(k+1)]=2ϵh(k+1)\epsilon+[\epsilon-h(k+1)]=2\epsilon-h(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)

  【注】上面代碼使用的前提是,在查找區間裏,一定有符合題意的搜索結果!上面代碼,可以作爲二分查找的一個模板,但是要記得使用前提。

  二分查找法的時間複雜度爲O(logN)O(log N),是時間複雜度最低的算法,O(log105)52.310O(log 10^5) \approx 5*2.3 \approx 10,也就是說哪怕搜索空間擴大一個量級,搜索次數也沒擴大多少。


  二分查找模板總結

  假設目標值在閉區間[l,r][l,r]中,每次將區間長度縮小一半,當l=rl=r時,我們就找到了目標值。

  • 模板一如果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的數組裏的所有數字都在[0,n1][0,n-1]的範圍內。
數組中某些數字是重複的,但不知道有幾個數字是重複的,也不知道每個數字重複幾次。
請找出數組中任意一個重複的數字!
注意:如果某些數字不在0n10 \sim n-1範圍內,輸出 -1

分析
  數組的特性是,所有值都在[0,n1][0,n-1]內,一共有nn個數,如果沒有重複數字的話,那麼每個元素應該在它對應的下標位置。於是我們從前往後遍歷,如果當前元素不在正確位置上,那就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 \ge 1
請找出數組中任意一個重複的數,但不能修改輸入的數組

分析
  數組中所有的數都在[1,n][1,n]內,說明我們有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

分析
  已知 preorder:rootleftright\text{preorder}:\text{root}\rightarrow \text{left}\rightarrow \text{right}inorder:leftrootright\text{inorder}:\text{left}\rightarrow \text{root}\rightarrow \text{right},所以先序遍歷的第一個元素,即爲根結點的值,找到根結點的值之後,可以將中序遍歷數組分成兩部分,得到左子樹的元素個數和右子樹的元素個數,按照此思路遞歸下去。核心在於設定遞歸式,我們這裏用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
這樣的 3×43 \times 4 矩陣中包含一條字符串"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,可理解爲矩陣中,部分網格存在障礙物,機器人無法移動

  如上圖所示,機器人在一個13×1413 \times 14的網格里,起點位置爲(0,0)(0,0),按照題意要求,我們將矩陣下標的數位之和小於或等於33的網格,用綠色標識,其他網格用障礙物標識。

  很明顯可以看到,滿足條件的網格被分爲四塊區間,中間被障礙物阻隔開,如果機器人初始位置在(0,0)(0,0)的話,則只能在一塊綠色區域中移動,其他位置到達不了。

  前面說過這是一個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。

分析
  本題是一個經典的整數劃分問題,裏面有一些重要的結論,我們下面具體來分析!

  考慮把一個整數NN分成mm段,即N=n0+n1+n2++nmN=n_0+n_1+n_2+\cdots+n_m

  如果存在 ni5n_i \geq5 ,則 3×(ni3)>ni3\times(n_i-3)>n_i 式子變形可得 ni>4.5n_i>4.5,必成立。 這說明了一個重要的結論,如果要讓劃分的段的乘積儘可能大,則每一段的長度一定要小於55。那我們劃分的段的長度,只能在2342,3,4中取得。

  長度44也可以劃分爲2×22\times 2所以進一步縮小範圍,我們劃分的段的長度只能在2,3中取得。

  然而,2×2×2<3×32\times2\times2<3\times3我們再次得出一個結論,劃分完的段中,長度爲22的段最多隻有22個。

  因此,結論如下:

  把一個整數NN劃分爲mm段,最多有22個長度爲22的段,其餘全部爲33

  我們令 n=Nmod  3n = N\mod 3

  • 如果 n=0n = 0,則把NN全部劃分爲長度爲3的段;
  • 如果 n=1n=1,則把NN劃分出2個長度爲2的段,其餘長度全部爲3;
  • 如果 n=2n=2,則把NN劃分出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。

分析
  首先了解一下補碼的概念,簡單明瞭的說,在計算機中,如果兩個數互爲補碼,那就意味着它們的二進制數之和爲 10000...00001 0000...000011的後面一共有323200

  我們知道,22的補碼是2-222 的二進制表示爲 00000000  00000000  00000000  0000001000000000 \;00000000 \;00000000 \;000000102-2 的表示則爲11111111  11111111  11111111  1111111011111111\; 11111111\; 11111111\; 11111110,二者之和,滿足上述性質。

  說回本題,思路很簡單,我們只需要把輸入整數轉爲無符號整數即可,python中用 num&0xffffffff\text{num\&0xffffffff}來實現。

  對於一個無符號整數num\text{num}num&1\text{num\&1}表示取num\text{num}二進制表示最右邊一位的值。num>>1\text{num>>1}表示將num\text{num}右移一位。

  具體代碼如下:

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

  其次呢,這裏說的刪除重複的節點,是指把所有重複的節點都刪除,而不是保留一個,注意理解題意。

  然後,這題可以用雙指針法來做,這是一個排序的鏈表,所以重複節點一定相鄰。讓一個指針p\text{p}指向鏈表中,按從前往後遍歷的順序,未重複出現的第一個節點,所以這裏p\text{p}初始時指向dummy\text{dummy}節點,指針q\text{q}指向p\text{p}的下一個節點。

  while循環,如果 q\text{q} 存在,且 q\text{q} 指向的節點值與 p\text{p} 的下一個節點指向的值相等,則 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

分析
  本題是一個雙指針的問題,一個指針ii指向數組頭部,一個指針jj指向數組尾部。

  我們要保證ii之前的每一個元素都是奇數,jj之後的每一個元素都是偶數。 於是兩個指針開始移動,當ii遇到偶數時停止,當jj遇到奇數時停止,然後把兩個指針的元素交換(交換前提是)。

  整個過程中,始終要保證 i<=ji<=j

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個結點。
注意:k>1k>1,如果kk大於鏈表長度,那麼返回None

輸入:[1,2,3,4,5][1,2,3,4,5]k=2k=2
輸出:44

分析
  本題有兩種做法,第一種做法是我比較喜歡的做法,雙指針法,我們思考一下,如果求倒數第kk個節點,那麼我們只需要定義兩個指針,一個指針指向鏈表頭,另一個指針指向鏈表頭的下一個節點,也就是說兩個指針之間的間隔爲k1k-1,然後兩個指針一起向後移動,當指針qq移到鏈表尾部的時候,指針pp也就到了倒數第kk個節點的位置。

  第一步,我們把指針qq後移k1k-1位,這裏存在一個kk可能大於表長的問題,所以在後移時進行判斷,qq 是否會移到None的位置;

  第二步,同時將指針ppqq向後移到,當指針qq移到鏈表尾的時候,指針qq 到達了倒數第kk個節點的位置。

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

  第二種解法是,我們要從前往後遍歷一下整個鏈表的長度,然後也就能知道從頭部到倒數第kk的節點的長度了。

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表示快慢指針相遇的節點。我們用 xx 表示 ABA\rightarrow B 的距離,用 yy 表示 BCB\rightarrow C 的距離,用 zz 表示 CBC\rightarrow B 的距離。(我們定義慢指針爲 pp ,快指針爲 qq

  當兩個指針相遇時,有:

  • 慢指針走了 x+yx+y 距離
  • 快指針走了 x+(y+z)n+yx+(y+z)\cdot n+y 距離(假設快指針已經在環裏面循環了 nn 圈,n1n\geq 1

  快指針每次走兩步,慢指針每次走一步,因此有:
x+(y+z)n+y2=x+y\frac{x+(y+z)\cdot n + y}{2}=x+y

x=(n1)(y+z)+zx = (n-1)\cdot(y+z)+z

  於是:
x+y=(n1)(y+z)+zx+y=(n-1)\cdot(y+z)+z

=n(y+z)            =n \cdot (y+z)\;\;\;\;\;\;

  也就是說,在相遇點C的位置,走 xx 步,就必能到達節點 B 。

  怎麼找到 xx 呢?注意到頭節點到環的入口節點的長度就是xx,我們把一個指針重置到頭節點,兩個指針同時移動,每次移動一步,那麼相遇的時候,即爲環的入口節點!

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

分析

  反轉一個單鏈表,它的核心在於,我們要用一個指針保存當前節點的前驅節點。

  有了前驅節點之後,思路就比較簡單了,post\text{post}指針指向當前節點的下一個節點,cur\text{cur}指針指向它的前驅節點,pre\text{pre}指針再往後移到當前節點,cur\text{cur}後移到它的下一個節點。當cur\text{cur}指向None時,pre\text{pre}即爲我們反轉之後鏈表的頭節點。

  有一個小坑,在初始化pre\text{pre}的時候,我一開始採取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

分析
  首先創建一個虛擬頭節點dummy\text{dummy},用來維護新鏈表的頭。然後用兩個指針指向輸入的兩個鏈表的表頭,當兩個指針都不爲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根結點的值一樣的節點 p\text{p}
  • 第二步,判斷樹A中以 p\text{p} 爲根結點的子樹,是否包含和樹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]

分析
  我們起點是matrix[0][0]\text{matrix[0][0]}

  • 開始後,一直向右移動,即橫座標加00,縱座標加11,一直到matrix[0][5]\text{matrix[0][5]},發生數組越界,換方向移動;
  • 於是向下移動,即橫座標加11,縱座標加00,一直到matrix[3][4]\text{matrix[3][4]},發生數組越界,換方向移動;
  • 於是向左移動,即橫座標加00,縱座標減11,一直到matrix[2][-1]\text{matrix[2][-1]},發生數組越界,換方向移動;
  • 於是向上移動,即橫座標減11,縱座標加00,一直到matrix0][0]\text{matrix0][0]},該網格已訪問,於是換方向移動;
  • 週而復始 \cdots

  於是我們得出一個解題思路,首先指定一個移動的方向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了,所以蠻爽~

  我的思路:

  創建一個bool\text{bool} 變量 flag\text{flag},表示本輪中是否發生了入棧或者出棧的操作,如果發生了,則將 flag\text{flag} 置爲 False;

  每一輪中,首先判斷,棧是否爲空、彈出序列是否爲空、棧頂元素與彈出序列的第一個元素是否相等;

  三個條件都滿足了的話,則彈出棧頂元素stack.pop(),並彈出 彈出序列中的第一個元素popV.pop(0),並將 flag\text{flag} 置爲 False;

  前面條件不成立的話,再進行判斷輸入序列是否爲空,不爲空的話,將輸入序列的第一個元素彈出,並添加到棧中。

  最後修改 flag\text{flag} 的值,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,作爲標記該層遍歷完畢,並將該層的節點值存到 res\text{res} 中。

  初始的時候,如果 root\text{root} 不爲 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條件成立。

  我們的思路是,遞歸的遍歷一棵二叉樹,首先當前節點的值,添加到路徑中。如果該節點爲葉子結點,則將路徑添加到返回結果中,否則,如果該節點有左子樹,就dfs\text{dfs}它的左子樹,如果該節點有右子樹,就dfs\text{dfs}它的右子樹。

  回溯法體現在,當前節點對應的 dfs\text{dfs} 返回之後,從路徑中要刪除該節點的值。

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,是的話,就添加到 res\text{res} 中。

  我們這裏採取思路二進行代碼實現,而且有兩種實現方式。

  • 方式一:到達了一條路徑的葉節點的時候,判斷當前路徑的和是否爲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 && 回溯法)

輸入一棵二叉樹和一個結點,打印出從根結點到該結點到路徑。

分析
  這一題和上面兩道都是類似的題型,我們 dfs\text{dfs} 到某個結點時,如果它不爲空,就把它添加到路徑中,並判斷它的值和目標結點的值是否相等,是的話,返回True,否則 dfs\text{dfs} 它的左右子樹。

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的位置需要從頭節點開始查找,這種方法的時間複雜度爲 O(n2)O(n^2)

  一種優化的思路是,分三步完成:

  • 第一步:複製原始鏈表中的每一個節點,並將它鏈接到原節點之後。
  • 第二步:如果原始鏈表中,某節點的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]
  • 如果它存在左、右子樹,則 dfs\text{dfs} root.left,獲得左子樹的l_pair,再 dfs\text{dfs} root.right,獲得右子樹的r_pair,然後把l_pair[1]node進行雙向鏈接,把r_pair[0]node進行雙向鏈接,並返回[l_pair[0], r_pair[1]]
  • 如果它只存在左子樹,則 dfs\text{dfs} root.left,獲得左子樹的l_pair,然後把l_pair[1]node進行雙向鏈接,並返回[l_pair[0], node]
  • 如果它只存在右子樹,則 dfs\text{dfs} 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]
]

分析
  我們先考慮一下,數組中不存在重複元素的情況!

  假設輸入的數組爲[1, 2, 3, 4]\text{[1, 2, 3, 4]},也就是說,我們有4個坑需要填。

  我們按順序將數組中的元素,填到坑中,假設第一個待填的元素爲1,那麼它有四個可以填的坑;接下來的待填的元素爲2,可以看到它有三個可以填入的坑;然後待填的元素爲3,可以看到它有兩個可以填入的坑;最後一個待填的元素爲4,只剩一個坑可以填。

  這裏我們用二進制數來標記哪個坑已被佔用,哪個坑未被佔用,如 11 = 0b1011,意味着從右往左數,從0開始計數,第2個坑未被佔用。

  state >> i & 1,在 state 中,從右往左數,從 0 開始,第 ii 個元素的值。

  state + (1 << i) 表示從右往左數,從 0 開始,將 state 中的第 ii 個元素的值 +1+1

  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]

分析
  從輸入的n\text{n}個數中,返回最小的k\text{k}個數,或者最大的k\text{k}個數,像這種問題,我們可以用最大堆或者最小堆來實現。

  • 最小堆:父堆元素小於或等於其子堆的每一個元素(python\text{python}中,heapq\text{heapq}模塊實現)
  • 最大堆:父堆元素大於或等於其子堆的每一個元素

  我們來詳細瞭解一下python\text{python}中的heapq\text{heapq}模塊:

  • heapq.heappop(list): 彈出 list\text{list} 所代表的最小堆中的堆頂元素(list中最小的元素)
  • heapq.heappush(list,x): 將元素 x 插入到 list\text{list} 所代表的最小堆中
  • heapq.nlargest(k, list): 返回最小堆 list\text{list} 中最大的 k\text{k} 的元素(遞減排序)
  • heapq.nsmallest(k, list): 返回最小堆 list\text{list} 中最小的 k\text{k} 的元素(遞增排序)

  需注意,heapq.heappush(list,x) 操作,是直接對 list\text{list} 進行修改,無返回值。

  如果我們想用 heapq\text{heapq} 模塊來實現最大堆,一種思路是,將 list\text{list} 中的每一個元素,取其相反數,那麼heapq.heappop(list) 返回的是原 list\text{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\text{dp} 數組dp=[0] * len(nums),其中 dp[i]\text{dp[i]} 表示從第 0 個到第 ii 個元素中,連續子數組(包含第 ii 個元素)的最大和。

  dp[i-1]<0 時,則令 dp[i-1]=0,任何一個數加上一個負數,都一定小於它本身,所以這裏將dp[i-1]置爲0。

  然後執行 dp[i] = dp[i-1] + nums[i],求出包含第 ii 個元素在內的連續子數組的最大和。

  最後將 res\text{res}dp[i]\text{dp[i]} 中較大的元素保存在 res\text{res}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次。

分析
  考慮一個整數 abcde\text{abcde} ,如下圖所示:

  我們從左往右逐次遍歷,當遍歷到 cc 的位置的時候,它左邊的值 left=a×10+b\text{left} = a \times10+b,右邊的值 right=d×10+e\text{right} = d \times10+e,右邊的元素個數 t=2t=2

  cc 前面的數值取 0left-10\sim \text{left-1}cc 必定可以取到 1 ,此時共有 left×10t\text{left}\times10^t 個 可能。

  當 c 前面的數值取 left\text{left} 時,分三種情況討論:

  • c > 1時,則 c 後面的取值無約束,共有 10t10^t 種可能;
  • c == 1時,則 c 後面的取值只能在 0right0\sim \text{right},共有 right+1\text{right}+1 種可能;
  • 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,等等。
請寫一個函數求任意位對應的數字。

分析

  我們來討論一下:

  • 090\sim9 以內的數字,元素個數爲 1010 ,索引區間爲 090\sim9
  • 109910\sim99 以內的數字,元素個數爲 9090,每個元素佔 22 位,索引區間爲 1018910\sim189
  • 100999100\sim999 以內的數字,元素個數爲 900900,每個元素佔 33 位,它的索引區間爲 1902889190\sim2889
  • \cdots\cdots

  我們根據輸入的 nn,需要知道 第 nn 個元素是對應的是一個幾位數,一位數的分界點爲10(小於分界點),二位數的分界點爲190,三位數的分界點爲2890,以此類推\cdots

  假設第 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
輸出數字的格式爲字符串

分析
  假設輸入的數組中,每個元素都是1010 以內的數,如 [1,3,2,5,7,0][1,3,2,5,7,0],那麼它拼接起來最小的數字是 012357012357,它是怎麼排的呢?

  不難發現,在數字 012357012357 中,任意兩個位置的對應的數字組成的數 ij\text{ij},都比 ji\text{ji} 要小。

  於是,當我們輸入的數組中,存在元素大於1010的數,最終得到的最小的數中,也應該滿足此性質,即對於任意一個元素a,如果它在元素b之前, 必須滿足ab < ba。

  因爲最終拼接起來的數字,很有可能會大於 int\text{int} 的上限,所以我們將輸入的數組中的每一個元素轉成字符串類型,nums = [str(x) for x in nums]

  python3\text{python3} 中,有提供自定義排序規則的函數,首先需要導入模塊,from functools import cmp_to_key,我們的 nums\text{nums} 列表的每一個元素都是字符串,所以 元素 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]\text{dp[i]} 表示前 ii 個元素共有多少種不同的翻譯方法;
  • 狀態轉移方程i\text{i} 個元素,必定可以翻譯成一個字母,所以 dp[i] = dp[i-1] 是無條件轉移的,如果把第 i\text{i} 個元素和第 i-1\text{i-1} 個元素,合在一起,用一個字母進行翻譯,則必須這個合在一起的組成數字,值在102510\sim25之間纔可以轉移,此時dp[i] += dp[i-2],此時考慮一種特殊的情況,i = 1\text{i = 1}時,如果它和第00個元素組成的值在[10,25][10,25]區間,則需要加上 11 ,於是我們初始化時,考慮令 dp[-1]=1
  • 邊界條件i=0\text{i=0} 時,它沒有和前面的元素組成一個兩位數的情況,需要單獨提出來,我們直接初始化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]\text{dp[i][j]} 表示到達第 i\text{i} 行第 j\text{j} 列網格時,所得到的最大禮物價值;
  • 狀態轉移方程 題中規定了每次只能向下或者向右移動,所以 dp[i][j]\text{dp[i][j]} 只能由 dp[i-1][j]\text{dp[i-1][j]} 或者 dp[i][j-1]\text{dp[i][j-1]} 轉移得到,取二者中較大的一個元素即可,然後再加上當前網格的禮物價值,即 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
  • 邊界條件 在第 0 行或者第 0 列的網格中,只能從某一個方向轉移得到,我們可以直接初始化 dp 矩陣中的每一個元素的值爲 0 即可,使得 dp[-1][j]=0\text{dp[-1][j]=0}dp[i][-1]=0\text{dp[i][-1]=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]\text{dp[i]} 表示以第 i\text{i} 個元素結尾的不含重複字符的最大子串長度;
  • 狀態轉移方程 如果第 i\text{i} 個元素,在前 i-1\text{i-1} 個元素中未出現,我們可以直接將第 i\text{i} 個元素加入到上一個最大子串中,此時 dp[i] = dp[i-1]+1;如果第 i\text{i} 個元素,在前 i-1\text{i-1} 個元素中已出現,那麼我們需要計算,在前 i-1\text{i-1} 個元素中,最近一次出現第 i\text{i} 個元素的位置,並計算出二者的間距distance\text{distance}。如果 distance>dp[i-1]\text{distance>dp[i-1]},說明重複的元素不影響當前不含重複字符的最大子串長度,所以 dp[i] = dp[i-1]+1,如果distancedp[i-1]\text{distance}\leq\text{dp[i-1]},說明以第 i\text{i} 個元素結尾的,最大無重複字符的子串,是從上一個重複元素的下一位開始,到當前第 i\text{i} 位元素結束,此時dp[i] = distance
  • 邊界條件i=0\text{i=0} 時,dp[0] = 1\text{dp[0] = 1}

  在本題中,需要記錄第 i\text{i} 個元素有無出現過,如果出現過,它最近一次出現的位置在哪,所以我們可以創建一個字典結構,來保存上述信息。

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]

分析
  我們考慮一個正整數 nnnn 滿足 n2n\geq2,那麼它的質因子肯定在 2n2\sim n 區間內。

  我們從 i=2i=2 開始遍歷,如果 n % 2 == 0,那麼說明 2 是 nn 的一個因子,我們再修改 nn 的值 n = n // 2

  當我們把 nn 中所有的 22 取出來之後,如果 n>in>i 仍成立,則將i += 1,此時再可以把 nn 中所有的 33 取出來。

  如果 n>in>i 仍成立,執行i += 1,此時 i=4i = 4,顯然 n % 4 == 0不可能成立,因爲 i=2i = 2 時,以及把所有含 22 的因子取了出來,依次類推,再次執行i += 1 \cdots

  具體實現代碼:

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的區間內,質數的總個數。 n>=2n >= 2

輸入:17
輸出:False

分析
  本題的難點,在於判斷一個數是不是質數,一個基本的思路是,從 22sqrt(number)\text{sqrt(number)} 的區間內進行遍歷,如果存在一個數可以被 number\text{number} 整除,那就說明這個數不是質數,否則說明該數是質數。

  實現代碼:

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) 可能是質數了。

  因此,如果對某個大於 44 的正整數 nn,如果 n % 6 != 1 and n % 6 != 5,那就說明它一定不是質數。 根據這個結論,可以進行第一次過濾。

  如果上面條件不滿足,說明 n % 6 == 1 or n % 6 != 5,如 5,7,11,13,17,19,23,25,29,31,35,37,5,7,11,13,17,19,23,25,29,31,35,37,\cdots對於這些數字,我們遍歷在 5 到 sqrt(number)\text{sqrt(number)} 區間內,所有分佈在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

  記錄本題的目的是爲了鞏固對 python3\text{python3} 字典結構的使用。

   d = {‘a’: 3, ‘d’: 1, ‘b’: 2}:

  • ’a’ in d,返回 True
  • ’a’ in d.keys(),返回 True
  • 1 in d,返回 False
  • 1 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”
解釋:每當字符流讀入一個字符,就進行一次判斷並輸出當前的第一個只出現一次的字符。

分析
   本題和上一題的區別在於,它的字符串不是固定的,如果我們按照上面的方法,每傳入一個字符,整理一遍哈希表,再從哈希表中找出第一個 value=1\text{value=1}key\text{key},每次查詢的時間複雜度爲 nnnn 個字符的時間複雜度則爲 O(n2)O(n^2)

   一種把時間複雜度降爲 O(n)O(n) 的做法是,我們維護一個隊列,隊列的第一個元素,是當前字符流中,第一個沒有重複出現的字符。

   當傳入新字符的時候,如果該字符前面未出現過,則將它存在哈希表中,對應的 value=1\text{value=1} ,並將其添加到隊列裏;如果該字符在前面出現過,將它對應的 value+=1\text{value+=1}

   每輪插入新字符時,都要檢查,隊列頭部的元素是否爲重複元素,是的話,則將前彈出,直到頭部元素不再爲重複元素爲止。

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)

分析
  本題存在暴力解法的,nn 個數字,兩兩組對,有 Cn2C_n^2 種組隊方式(因爲先後順序是固定的),我們遍歷每一種組隊情況,如果是逆序對,則把逆序對的總數加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

  上面的實現方式,時間複雜度爲O(n2)O(n^2),我們可以進行優化,把時間複雜度優化到O(nlogn)O(nlogn)

  具體思路是用到二路歸併排序的思想,想象一下,我們把一個數組劃分成左、右兩部分,那麼逆序對的總個數等於左邊部分逆序對的個數 + 右邊逆序對的個數,除此之外,我們將左右兩部分進行升序排列,那麼總的逆序對的個數,還包括左邊元素與右邊元素組成逆序對的個數,即逆序對的總個數由上述三部分組成,並且三個部分之間是沒有交集的。

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的長度爲 x+zx+z,鏈表B的長度爲 y+zy+z,我們定義兩個指針,同時從A,B兩個鏈表的頭節點開始走,當走到所在鏈表的尾結點時,再從另一個鏈表的頭節點開始走,這時我們發現,x+z+y=y+z+xx+z+y = y+z+x,兩個指針必定在第一個公共節點處相遇!

  • 兩鏈表不存在公共節點

  如上圖所示,A,B兩個鏈表不存在公共節點,鏈表A的長度爲 aa,鏈表B的長度爲 bb,我們定義兩個指針,同時從A,B兩個鏈表的頭節點開始走,當走到所在鏈表的尾結點時,再從另一個鏈表的頭節點開始走,這時我們發現,a=ba = 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][1, 2, 3, 3, 3, 3, 4, 5]和數字33,由於33在這個數組中出現了44次,因此輸出44

輸入:[1, 2, 3, 3, 3, 3, 4, 5] , 3
輸出:4

分析
  一個簡單的思路是,我們之間遍歷一遍數組,將元素存在哈希表中,就可以直接得到某個數字出現的次數,它的時間複雜度是 O(n)O(n)

  我們觀察這個數組的特點,它是一個排序數組,如果我們想要查詢33出現的次數,只需要找到33第一次出現的位置 ii,和33最後一次出現的位置jj,那麼3出現的次數則爲 ji+1j-i+1

  可以用二分法來解決此問題,33第一次出現的位置,滿足它左邊的所有元素均小於33,它右邊所有的元素均大於等於33。如果nums[mid] < 3,那麼說明最終的 ll 應該在 mid\text{mid} 的左邊,即l = mid+1,否則 r = mid

  33最後一次出現的位置,滿足它右邊的所有元素均大於33,它左邊所有的元素均小於等於33。如果nums[mid] > 3,那麼說明最終的 rr 應該在 mid\text{mid} 的左邊,即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,1,1,3,5][-3, -1, 1, 3, 5]
輸出:3

分析
  簡單的思路是直接從頭到尾遍歷一次,找到第一個數值和下標相等的元素,再返回,時間複雜度爲 O(n)O(n),我們下面進行優化,把時間複雜度降到 O(logn)O(logn)

  輸入的數組是一個嚴格單調遞增的整數數組,並且每一個元素都是唯一的,即 nums[i]-nums[i-1]1\text{nums[i]-nums[i-1]}\geq1,於是:
(nums[i]-i) - (nums[i-1]-(i-1))=nums[i]-nums[i-1]-10\text{(nums[i]-i) - (nums[i-1]-(i-1))=nums[i]-nums[i-1]-1}\geq0

  所以 nums[i]-i\text{nums[i]-i} 是一個(不嚴格)單調遞增的整數數組,我們想要找到數組中第一次爲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小的節點,也就是我們第 kk 輪中序遍歷時的對應的節點。

  中序遍歷的模板是:

def in_oder(root):
	if not root:
		return
	in_order(root.left)
	# do something
	in_order(root.right) 

  也就是說,我們想要做的操作,應該寫在 do something\text{do something} 的位置,我們每到達一次這個位置,就將 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 個數中,只有一個數字出現了一次,其他數字均出現了兩次,我們把所有的數進行異或操作,那麼最後得到的就是那個獨一無二的數字。

  本題中,考察的是存在兩個只出現了一次的數字,假設爲x,yx,y,那麼所有的數進行異或運算之後,得到的結果爲 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]]

分析
  關於連續正整數的和,我們可以想到高斯求和公式,或者說等差數列求和公式,即s=a1+an2×ns = \frac{a_1+a_n}{2}\times n,它的時間複雜度爲O(1)O(1)

  也就是說,我們可以定義兩個指針,一個表示起始項a1a_1,另一個表示ana_n,然後不斷的枚舉即可。

  分析數列的規律,可以發現兩個指針的區間不會是全部區間,對於一個正整數 nn,如果它是奇數,那麼兩個指針可以到達最大的位置分別是 n2\lfloor{\frac{n}{2}}\rfloorn2\lceil{\frac{n}{2}}\rceil,如果它是偶數,也可以同樣指定上述的範圍。此外,第二個指針的範圍必定在第一個指針之後,於是我們將暴力搜索的區間進行限制,編寫代碼如下:

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

  實際上呢,該問題還有一個規律,那就是假設兩個指針 i, j\text{i, j} 當前的位置已經滿足,高斯和等於目標值,如果 i\text{i} 繼續增大得到 i\text{i}^\prime,假設存在一個 j\text{j}^\prime,滿足 i,j\text{i}^\prime, \text{j}^\prime區間內的所有正整數之和等於目標值,那麼必有 j>j\text{j}^\prime > \text{j} 成立。根據這個特性,我們可以進一步縮小搜索區間。

  用雙指針法實現上述思想:

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[i]\text{dp[i]} 表示以第 i\text{i} 個元素結尾的子序列的最大長度;
  • 狀態轉移方程 在本題中, dp[i]\text{dp[i]} 的取值,不再是像往常一樣,只與前面幾個狀態有關,而是和前面 dp[0]\text{dp[0]}, dp[1]\text{dp[1]}, dp[2]\text{dp[2]}, \cdots, dp[i-1]\text{dp[i-1]}每一個狀態有關。所以需要進行一次遍歷,如果 nums[i]>nums[j]\text{nums[i]>nums[j]},那麼 dp[i]=max(dp[i], dp[j] + 1)\text{dp[i]=max(dp[i], dp[j] + 1)},如果 nums[i]==nums[j]\text{nums[i]==nums[j]},那麼 dp[i] = max(dp[i], dp[j])\text{dp[i] = max(dp[i], dp[j])}
  • 邊界條件i=0\text{i=0} 時,dp[0] = 1\text{dp[0] = 1},因爲每一個 dp[i]\text{dp[i]} 必定存在一個子序列,所以我們把dp數組初始化全爲1。

  具體實現代碼如下:(時間複雜度爲O(n2)O(n^2)

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

  實際上存在時間複雜度爲 O(nlogn)O(nlogn),需要用到二分法,我們下面進行詳細探討。

  假設我們輸入的無序數組爲 nums=[4,5,6,3]\text{nums=[4,5,6,3]}

  • 所有長度爲1的遞增子序列爲[[4],[5],[6],[3]][[4], [5], [6], [3]]
  • 所有長度爲2的遞增子序列爲[[4,5],[4,6],[5,6]][[4, 5], [4, 6], [5, 6]]
  • 所有長度爲3的遞增子序列爲[[4,5,6]][[4, 5, 6]]

  我們定義一個數組tails\text{tails},其中 tails[i]\text{tails[i]} 來保存長度爲ii的所有遞增子序列中的尾部元素的最小值。有點繞,我們結合上面實例來看。

  • tails[0] = 3\text{tails[0] = 3},因爲所有長度爲1的遞增子序列中,尾部元素最小爲3;
  • tails[1] = 5\text{tails[1] = 5},因爲所有長度爲2的遞增子序列中,尾部元素最小爲5;
  • tails[2] = 6\text{tails[2] = 6},因爲所有長度爲3的遞增子序列中,尾部元素最小爲6;

  不難發現規律,如果 tails\text{tails} 數組不斷增長,那麼它一定是單調遞增的序列,這就是二分法使用的關鍵。

  我們從前往後依次遍歷數組中的每一個元素num\text{num},查找 num\text{num}tails\text{tails} 數組中的具體位置,具體是找到 tails\text{tails} 數組中,第一個大於 num\text{num} 的下標idx\text{idx},然後tails[idx]=num進行替換操作,修改 當前長度爲idx+1\text{idx+1}的遞增子序列的尾部元素的最小值。

  當然,有特殊情況需要進行判斷,因爲初始時 tails 數組爲空,所以當它爲空時,直接把元素num\text{num}添加到 tails 數組中;另外一種情況是,我們二分查找得到的 idx 是tails 數組中的最後一個元素,這時我們進行比較,如果該元素小於 tails 數組中的最後一個元素,執行替換操作,否則把該元素追加到 tails 數組的尾部。

  具體實現代碼如下:(時間複雜度爲O(nlogn)O(nlogn)

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度
    [1][2][3][4][5][6][7][8][9][0][1][2][3][4][5][6][1][5][9][3][2][6][0][4][3][7][1][5][4][8][2][6][3][9][5][1][4][0][6][2][5][1][7][3][6][2][8][4] \begin{array}{l}{[1][2][3][4]} \\ {[5][6][7][8]} \\ {[9][0][1][2]} \\ {[3][4][5][6]}\end{array}\rightarrow\begin{array}{l}{[1][5][9][3]} \\ {[2][6][0][4]} \\ {[3][7][1][5]} \\ {[4][8][2][6]}\end{array} \rightarrow \begin{array}{l}{[3][9][5][1]} \\ {[4][0][6][2]} \\ {[5][1][7][3]} \\ {[6][2][8][4]}\end{array}

  第一步,把對角線兩邊的元素交換,即 matrix[i][j] = matrix[j][i]\text{matrix[i][j] = matrix[j][i]}

  第二步,把每一行的元素,進行翻轉,定義首尾指針,對應兩兩交換即可。

  • 矩陣逆時針旋轉90度
    [1][2][3][4][5][6][7][8][9][0][1][2][3][4][5][6][1][5][9][3][2][6][0][4][3][7][1][5][4][8][2][6][4][8][2][6][3][7][1][5][2][6][0][4][1][5][9][3] \begin{array}{l}{[1][2][3][4]} \\ {[5][6][7][8]} \\ {[9][0][1][2]} \\ {[3][4][5][6]}\end{array}\rightarrow\begin{array}{l}{[1][5][9][3]} \\ {[2][6][0][4]} \\ {[3][7][1][5]} \\ {[4][8][2][6]}\end{array} \rightarrow \begin{array}{l}{[4][8][2][6]} \\ {[3][7][1][5]} \\ {[2][6][0][4]} \\ {[1][5][9][3]}\end{array}

  第一步,把對角線兩邊的元素交換,即 matrix[i][j] = matrix[j][i]\text{matrix[i][j] = matrix[j][i]}

  第二步,把每一列的元素,進行翻轉,定義首尾指針,對應兩兩交換即可。

  • 矩陣順/逆時針旋轉180度
    [1][2][3][4][5][6][7][8][9][0][1][2][3][4][5][6][4][3][2][1][8][7][6][5][2][0][1][9][6][5][4][3][6][5][4][3][2][0][1][9][8][7][6][5][4][3][2][1] \begin{array}{l}{[1][2][3][4]} \\ {[5][6][7][8]} \\ {[9][0][1][2]} \\ {[3][4][5][6]}\end{array}\rightarrow\begin{array}{l}{[4][3][2][1]} \\ {[8][7][6][5]} \\ {[2][0][1][9]} \\ {[6][5][4][3]}\end{array} \rightarrow \begin{array}{l}{[6][5][4][3]} \\ {[2][0][1][9]} \\ {[8][7][6][5]} \\ {[4][3][2][1]}\end{array}

  第一步,反轉矩陣中每一行的元素。

  第二步,反轉矩陣中每一列的元素。

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

  實際上,本題的時間複雜度可以優化到 O(n)O(n),核心在於維護一個單調遞減的雙向隊列。

  我們在隊列中,保存元素的下標,當有一個元素需要入隊時,我們進行幾輪判斷:

  • 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,\cdots,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

  我們接下來再用動態規劃進行求解。

  • 狀態表示 我們用dp[i][j]\text{dp[i][j]} 表示 i 個骰子和爲 j 的總情況數
  • 狀態轉移方程 根據前面的分析,我們知道,dp[i][j] += dp[i-1][j-k]\text{dp[i][j] += dp[i-1][j-k]},其中 k\text{k} 的取值爲1,2,3,4,5,6{1,2,3,4,5,6}
  • 邊界處理 在本題中,我們的邊界是骰子數爲1的情況,我們可以直接把 dp[1][1],dp[1][2],dp[1][3],dp[1][4],dp[1][5],dp[1][6]\text{dp[1][1],dp[1][2],dp[1][3],dp[1][4],dp[1][5],dp[1][6]}的取值全部初始爲1。

  在本題中,動態規劃還需要注意的一點是二維數組的初始化,因爲骰子數爲1的和只有6種取值,骰子數爲2有11種取值,骰子數爲3有16種取值\cdots \cdots 本來我的想法是按照每個骰子的取值情況初始化數組,但是會發生數組越界的情況,如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

  本題可以直接用一個環形鏈表進行模擬,但是實現的代碼複雜度比較高,我們對問題進行探索,看能否找到問題內在的規律。

  觀察下圖:

  我們一開始有 nn 個數字,每輪淘汰第 mm 個數字,所以在第一輪,被淘汰的數字是下標爲 m1m-1 的數字。

  那麼第二輪是從下標爲 mm 的數字開始,我們按照順序,從零開始對數組進行重新編號,結尾數字的下標爲 n2n-2

  可以發現,同一個數字,新的下標 j\text{j} 和舊的下標 i\text{i} 存在一個映射關係:
i=(j+m)%ni = (j+m) \% n

  這個發現是解決本題的關鍵!

  我們定義 f(n,m)f(n,m) 來表示每次在 nn 個數中,刪除第 mm 個數字之後,最後剩下的數字。這個數字 必定等於 刪除第 mm 個數字之後,從下標爲 mm 的數字開始的 n1n-1 個數字之中,每次刪除第 mm 個數字之後,最後剩下的數字。

  也就是說,最後一輪剩下的數字,我們可以一層一層倒着推回去,從 i=1i = 1 開始,推到 i=ni = n

  在 i=1i = 1 時,因爲只剩一個元素,所以最後一個數字的編號爲 00 ,我們按照上面的映射關係,推導該數字在 i=2i = 2 時的下標,即 (0+m) % 2,依次類推 \cdots\cdots

  實現代碼如下:

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

分析
  暴力的解法是,從輸入的數字中,每次隨機選取兩個數字,數字之間是有先後順序的,所以一共有 Cn2C_n^2 組數字,然後返回差值最大的結果,時間複雜度爲 O(n2)O(n^2)

  實際上我們可以進行優化,只進行一次遍歷即可,遍歷到第 i\text{i} 個元素的時候,用它的值減去它前面 i-1\text{i-1} 個元素中的最小值,即爲當前元素的最大收益,時間複雜度爲 O(n)O(n),空間複雜度爲 O(1)O(1)

  實現代碼如下:

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

分析
  我們先來想想,常規做法有哪些。

  • 高斯求和 1+n2×n\frac{1+n}{2}\times n
  • 循環 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 yx and 0會直接返回 0,x and y返回y。
  • a or b
    如果 a 爲 非零值 或 True 則不會執行 b 語句,直接返回 a 或 True。
    在python3中,假設x,y不爲0,x or 00 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 的加法情況:

  • a:0101\text{a}:0 \qquad1 \qquad0 \qquad1
  • b:0011\text{b}:0 \qquad0 \qquad1 \qquad1

  一共有上述四種情況,我們把加完之後的位置用字母c表示,進位用d表示

  • c:0110\text{c}:0 \qquad1 \qquad1 \qquad0
  • d:0001\text{d}:0 \qquad0 \qquad0 \qquad1

  不難發現,c, d\text{c, d} 都可以用 a\text{a}b\text{b} 通過位運算得到,即 c=a&bc = a\&bd=abd=a^{\wedge} b

  上面的討論,是針對於單個二進制的加法,實際上它也可以拓展到多個二進制位的加法。

  兩個數的加法可以分爲兩步,第一步,計算兩個數字不進位的和,第二步,把上一步的結果,加上進位的值,可以不斷循環下去,直到進位爲零即可。

  不進位的和,即爲兩個數的異或,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 還原成原來的負數:

  因爲我們之前限定了邊界0xffffffff\text{0xffffffff},把 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]

【分析】
  本題有兩重限制,一是不能使用除法(否則我們直接求出連乘積,再逐一做除法即可),二是空間複雜度爲 O(1)O(1)(否則我們可以開闢兩個數組,一個是每個元素左邊的連乘積,另一個是每個元素右邊的連乘積)

  實際上,我們是可以把空間複雜度優化爲 O(1)O(1) 的,我們要計算 B[i]\text{B[i]} 的值,可以分成兩次完成。

  • 第一輪是把 A[i]\text{A[i]} 左邊的所有元素的積賦給 B[i]B[i],按照從前往後的順序,從下標爲 11 的元素開始;
  • 第二輪是把 A[i]\text{A[i]} 右邊的所有元素的積乘上 B[i]B[i],按照從後往前的順序,從下標爲 n-2\text{n-2} 的元素開始。

  我們用一個 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(2312^{31} − 1),請返回2312^{31} − 1;如果小於INT_MIN(231−2^{31}) ,請返回231−2^{31}

分析
  本題考察了兩點,一是處理各種異常輸入,二是不用任何庫函數處理字符串。

  • 判斷一個字符是不是數字,我們用 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) 來表示以 root\text{root} 爲根結點的二叉搜索樹中,節點p\text{p}q\text{q}的最低公共祖先,我們可以先進行預處理,如果節點p\text{p}的值大於節點q\text{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) 來表示以 root\text{root} 爲根結點的二叉搜索樹中,節點p\text{p}q\text{q}的最低公共祖先。節點p\text{p}q\text{q}要麼同時分佈在左子樹中,要麼同時分佈在右子樹中,要麼分佈在根結點的兩側。當我們遍歷到某個節點等於p\text{p}q\text{q}時,可以直接返回該節點,在前兩種情況下,該節點就是公共祖先,在第三種情況時,root\text{root} 即爲公共祖先。
  • 遞推公式
    根據上面分析,我們可以得出 left=dfs(root.left,p,q)right=dfs(root.right,p,q),如果 leftright 同時不爲空,說明爲第三種情況;否則返回 left 和 **right**中不爲空的那一個節點。
  • 遞歸終止條件
    root 爲空時,返回 None,說明p\text{p}q\text{q}都不在該子樹中;
    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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章