66道前端演算法面試題附思路分析助你查漏補缺

wscats發表於2021-10-12

本部分主要是 CavsZhouyou 在練習《劍指 Offer》時所做的筆記,主要涉及演算法相關知識和一些相關面試題時所做的筆記,分享這份總結給大家,幫助大家對演算法的可以來一次全方位的檢漏和排查,

附筆記連結,如果對你有幫助請給我點贊鼓勵哦:https://github.com/Wscats/art...

1. 二維陣列中的查詢

題目:
在一個二維陣列中,每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函式,輸入這樣的
一個二維陣列和一個整數,判斷陣列中是否含有該整數。

思路:

(1)第一種方式是使用兩層迴圈依次遍歷,判斷是否含有該整數。這一種方式最壞情況下的時間複雜度為 O(n^2)。

(2)第二種方式是利用遞增序列的特點,我們可以從二維陣列的右上角開始遍歷。如果當前數值比所求的數要小,則將位置向下移動
,再進行判斷。如果當前數值比所求的數要大,則將位置向左移動,再進行判斷。這一種方式最壞情況下的時間複雜度為 O(n)。

2. 替換空格

題目:

請實現一個函式,將一個字串中的空格替換成“%20”。例如,當字串為 We Are Happy.則經過替換之後的字串為 We%20
Are%20Happy

思路:

使用正規表示式,結合字串的 replace 方法將空格替換為 “%20”

str.replace(/\s/g,"%20")

3. 從尾到頭列印連結串列


題目:
輸入一個連結串列,從尾到頭列印連結串列每個節點的值。

思路:

利用棧來實現,首先根據頭結點以此遍歷連結串列節點,將節點加入到棧中。當遍歷完成後,再將棧中元素彈出並列印,以此來實現。棧的
實現可以利用 Array 的 push 和 pop 方法來模擬。

4. 重建二叉樹


題目:

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸
入前序遍歷序列 {1,2,4,7,3,5,6,8} 和中序遍歷序列 {4,7,2,1,5,3,8,6},則重建二叉樹並返回。

思路:

利用遞迴的思想來求解,首先先序序列中的第一個元素一定是根元素。然後我們去中序遍歷中尋找到該元素的位置,找到後該元素的左
邊部分就是根節點的左子樹,右邊部分就是根節點的右子樹。因此我們可以分別擷取對應的部分進行子樹的遞迴構建。使用這種方式的
時間複雜度為 O(n),空間複雜度為 O(logn)。

5. 用兩個棧實現佇列


題目:

用兩個棧來實現一個佇列,完成佇列的 Push 和 Pop 操作。

思路:

佇列的一個基本特點是,元素先進先出。通過兩個棧來模擬時,首先我們將兩個棧分為棧 1 和棧 2。當執行佇列的 push 操作時,直接
將元素 push 進棧 1 中。當佇列執行 pop 操作時,首先判斷棧 2 是否為空,如果不為空則直接 pop 元素。如果棧 2 為空,則將棧 1 中
的所有元素 pop 然後 push 到棧 2 中,然後再執行棧 2 的 pop 操作。

擴充套件:

當使用兩個長度不同的棧來模擬佇列時,佇列的最大長度為較短棧的長度的兩倍。

6. 旋轉陣列的最小數字

題目:

把一個陣列最開始的若干個元素搬到陣列的末尾,我們稱之為陣列的旋轉。 輸入一個非遞減排序的陣列的一個旋轉,輸出旋轉陣列的
最小元素。 例如陣列{3,4,5,1,2}為{1,2,3,4,5}的一個旋轉,該陣列的最小值為 1。 NOTE:給出的所有元素都大於 0,若陣列大
小為 0,請返回 0。

思路:

(1)我們輸入的是一個非遞減排序的陣列的一個旋轉,因此原始陣列的值遞增或者有重複。旋轉之後原始陣列的值一定和一個值相
鄰,並且不滿足遞增關係。因此我們就可以進行遍歷,找到不滿足遞增關係的一對值,後一個值就是旋轉陣列的最小數字。

(2)二分法

相關資料可以參考:
《旋轉陣列的最小數字》

7. 斐波那契數列


題目:

大家都知道斐波那契數列,現在要求輸入一個整數 n,請你輸出斐波那契數列的第 n 項。 n<=39

思路:

斐波那契數列的規律是,第一項為 0,第二項為 1,第三項以後的值都等於前面兩項的和,因此我們可以通過迴圈的方式,不斷通過疊
加來實現第 n 項值的構建。通過迴圈而不是遞迴的方式來實現,時間複雜度降為了 O(n),空間複雜度為 O(1)。

8. 跳臺階


題目:

一隻青蛙一次可以跳上 1 級臺階,也可以跳上 2 級。求該青蛙跳上一個 n 級的臺階總共有多少種跳法。

思路:

跳臺階的問題是一個動態規劃的問題,由於一次只能夠跳 1 級或者 2 級,因此跳上 n 級臺階一共有兩種方案,一種是從 n-1 跳上,一
種是從 n-2 級跳上,因此 f(n) = f(n-1) + f(n-2)。

和斐波那契數列類似,不過初始兩項的值變為了 1 和 2,後面每項的值等於前面兩項的和。

9. 變態跳臺階

題目:

一隻青蛙一次可以跳上 1 級臺階,也可以跳上 2 級……它也可以跳上 n 級。求該青蛙跳上一個 n 級的臺階總共有多少種跳法。

思路:

變態跳臺階的問題同上一個問題的思考方案是一樣的,我們可以得到一個結論是,每一項的值都等於前面所有項的值的和。

f(1) = 1
f(2) = f(2-1) + f(2-2) //f(2-2) 表示 2 階一次跳 2 階的次數。
f(3) = f(3-1) + f(3-2) + f(3-3)
...
f(n) = f(n-1) + f(n-2) + f(n-3) + ... + f(n-(n-1)) + f(n-n)

再次總結可得

        | 1 ,(n=0 )
f(n) =  | 1 ,(n=1 )
        | 2\*f(n-1),(n>=2)

10. 矩形覆蓋

題目:

我們可以用 2*1 的小矩形橫著或者豎著去覆蓋更大的矩形。請問用 n 個 2*1 的小矩形無重疊地覆蓋一個 2\*n 的大矩形,總共
有多少種方法?

思路:

依舊是斐波那契數列的應用

11. 二進位制中 1 的個數


題目:

輸入一個整數,輸出該數二進位制表示中 1 的個數。其中負數用補碼錶示。

思路:

一個不為 0 的整數的二進位制表示,一定會有一位為 1。我們找到最右邊的一位 1,當我們將整數減去 1 時,最右邊的一位 1 變為 0,它後
面的所有位都取反,因此將減一後的值與原值相與,我們就會能夠消除最右邊的一位 1。因此判斷一個二進位制中 1 的個數,我們可以判
斷這個數可以經歷多少次這樣的過程。

如:1100&1011=1000

12. 數值的整數次方


題目:

給定一個 double 型別的浮點數 base 和 int 型別的整數 exponent。求 base 的 exponent 次方。

思路:

首先我們需要判斷 exponent 正負和零取值三種情況,根據不同的情況通過遞迴來實現。

13. 調整陣列順序使奇數位於偶數前面


題目:

輸入一個整數陣列,實現一個函式來調整該陣列中數字的順序,使得所有的奇數位於陣列的前半部分,所有的偶數位於位於陣列的後半
部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。

思路:

由於需要考慮到調整之後的穩定性,因此我們可以使用輔助陣列的方式。首先對陣列中的元素進行遍歷,每遇到一個奇數就將它加入到
奇數輔助陣列中,每遇到一個偶數,就將它將入到偶數輔助陣列中。最後再將兩個陣列合並。這一種方法的時間複雜度為 O(n),空間
複雜度為 O(n)。

14. 連結串列中倒數第 k 個節點


題目:

輸入一個連結串列,輸出該連結串列中倒數第 k 個結點。

思路:

使用兩個指標,先讓第一個和第二個指標都指向頭結點,然後再讓第二個指標走 k-1 步,到達第 k 個節點。然後兩個指標同時向後
移動,當第二個指標到達末尾時,第一個指標指向的就是倒數第 k 個節點了。

15. 反轉連結串列


題目:

輸入一個連結串列,反轉連結串列後,輸出連結串列的所有元素。

思路:

通過設定三個變數 pre、current 和 next,分別用來儲存前繼節點、當前節點和後繼結點。從第一個節點開始向後遍歷,首先將當
前節點的後繼節點儲存到 next 中,然後將當前節點的後繼節點設定為 pre,然後再將 pre 設定為當前節點,current 設定為 ne
xt 節點,實現下一次迴圈。

16. 合併兩個排序的連結串列


題目:

輸入兩個單調遞增的連結串列,輸出兩個連結串列合成後的連結串列,當然我們需要合成後的連結串列滿足單調不減規則。

思路:

通過遞迴的方式,依次將兩個連結串列的元素遞迴進行對比。

17. 樹的子結構


題目:

輸入兩棵二叉樹 A、B,判斷 B 是不是 A 的子結構。(ps:我們約定空樹不是任意一個樹的子結構)

思路:

通過遞迴的思想來解決

第一步首先從樹 A 的根節點開始遍歷,在左右子樹中找到和樹 B 根結點的值一樣的結點 R 。
第二步兩棵樹同時從 R 節點和根節點以相同的遍歷方式進行遍歷,依次比較對應的值是否相同,當樹 B 遍歷結束時,結束比較。

18. 二叉樹的映象


題目:

操作給定的二叉樹,將其變換為源二叉樹的映象。

思路:

從根節點開始遍歷,首先通過臨時變數儲存左子樹的引用,然後將根節點的左右子樹的引用交換。然後再遞迴左右節點的子樹交換。

19. 順時針列印矩陣


題目:

輸入一個矩陣,按照從外向裡以順時針的順序依次列印出每一個數字,
例如,如果輸入如下矩陣: 1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
則依次列印出數字 1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10

思路:

(1)根據左上角和右下角可以定位出一次要旋轉列印的資料。一次旋轉列印結束後,往對角分別前進和後退一個單位,可以確定下一
次需要列印的資料範圍。

(2)使用模擬魔方逆時針解法,每列印一行,則將矩陣逆時針旋轉 90 度,列印下一行,依次重複。

20. 定義一個棧,實現 min 函式


題目:

定義棧的資料結構,請在該型別中實現一個能夠得到棧最小元素的 min 函式。

思路:

使用一個輔助棧,每次將資料壓入資料棧時,就把當前棧裡面最小的值壓入輔助棧當中。這樣輔助棧的棧頂資料一直是資料棧中最小
的值。

21. 棧的壓入彈出


題目:

輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如
序列 1,2,3,4,5 是某棧的壓入順序,序列 4,5,3,2,1 是該壓棧序列對應的一個彈出序列,但 4,3,5,1,2 就不可能是該壓棧序
列的彈出序列。(注意:這兩個序列的長度是相等的)

思路:

我們可以使用一個輔助棧的方式來實現,首先遍歷壓棧順序,依次將元素壓入輔助棧中,每次壓入元素後我們首先判斷該元素是否與出
棧順序中的此刻位置的元素相等,如果不相等,則將元素繼續壓棧,如果相等,則將輔助棧中的棧頂元素出棧,出棧後,將出棧順序中
的位置後移一位繼續比較。當壓棧順序遍歷完成後,如果輔助棧不為空,則說明該出棧順序不正確。

22. 從上往下列印二叉樹


題目:

從上往下列印出二叉樹的每個節點,同層節點從左至右列印。

思路:

本質上是二叉樹的層序遍歷,可以通過佇列來實現。首先將根節點入隊。然後對佇列進行出隊操作,每次出隊時,將出隊元素的左右子
節點依次加入到佇列中,直到佇列長度變為 0 時,結束遍歷。

23. 二叉搜尋樹的後序遍歷


題目:

輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷的結果。如果是則輸出 Yes,否則輸出 No。假設輸入的陣列的任意兩
個數字都互不相同。

思路:

對於一個合法而二叉樹的後序遍歷來說,最末尾的元素為根元素。該元素前面的元素可以劃分為兩個部分,一部分為該元素的左子樹,
所有元素的值比根元素小,一部分為該元素的右子樹,所有的元素的值比該根元素大。並且每一部分都是一個合法的後序序列,因此我
們可以利用這些特點來遞迴判斷。

24. 二叉樹中和為某一值路徑


題目:

輸入一顆二叉樹和一個整數,列印出二叉樹中結點值的和為輸入整數的所有路徑。路徑定義為從樹的根結點開始往下一直到葉結點所經
過的結點形成一條路徑。

思路:

通過對樹進行深度優先遍歷,遍歷時儲存當前節點的值並判斷是否和期望值相等,如果遍歷到葉節點不符合要求則回退處理。

25. 複雜連結串列的複製


題目:

輸入一個複雜連結串列(每個節點中有節點值,以及兩個指標,一個指向下一個節點,另一個特殊指標指向任意一個節點),返回結果為
複製後複雜連結串列的 head。(注意,輸出結果中請不要返回引數中的節點引用,否則判題程式會直接返回空)

思路:

(1)第一種方式,首先對原有連結串列每個節點進行復制,通過 next 連線起來。然後當連結串列複製完成之後,再來設定每個節點的 ra
ndom 指標,這個時候每個節點的 random 的設定都需要從頭結點開始遍歷,因此時間的複雜度為 O(n^2)。

(2)第二種方式,首先對原有連結串列每個節點進行復制,並且使用 Map 以鍵值對的方式將原有節點和複製節點儲存下來。當連結串列復
制完成之後,再來設定每個節點的 random 指標,這個時候我們通過 Map 中的鍵值關係就可以獲取到對應的複製節點,因此
不必再從頭結點遍歷,將時間的複雜度降低為了 O(n),但是空間複雜度變為了 O(n)。這是一種以空間換時間的做法。

(3)第三種方式,首先對原有連結串列的每個節點進行復制,並將複製後的節點加入到原有節點的後面。當連結串列複製完成之後,再進行
random 指標的設定,由於每個節點後面都跟著自己的複製節點,因此我們可以很容易的獲取到 random 指向對應的複製節點
。最後再將連結串列分離,通過這種方法我們也能夠將時間複雜度降低為 O(n)。

26. 二叉搜尋樹與雙向連結串列


題目:

輸入一棵二叉搜尋樹,將該二叉搜尋樹轉換成一個排序的雙向連結串列。要求不能建立任何新的結點,只能調整樹中結點指標的指向。

思路:

需要生成一個排序的雙向列表,那麼我們應該通過中序遍歷的方式來調整樹結構,因為只有中序遍歷,返回才是一個從小到大的排序
序列。

基本的思路是我們首先從根節點開始遍歷,先將左子樹調整為一個雙向連結串列,並將左子樹雙向連結串列的末尾元素的指標指向根節點,並
將根節點的左節點指向末尾節點。再將右子樹調整為一個雙向連結串列,並將右子樹雙向連結串列的首部元素的指標指向根元素,再將根節點
的右節點指向首部節點。通過對左右子樹遞迴調整,因此來實現排序的雙向連結串列的構建。

27. 字串的排列


題目:

輸入一個字串,按字典序列印出該字串中字元的所有排列。例如輸入字串 abc,則列印出由字元 a,b,c 所能排列出來的所有
字串 abc,acb,bac,bca,cab 和 cba。輸入描述:輸入一個字串,長度不超過 9(可能有字元重複),字元只包括大小寫字母。

思路:

我們可以把一個字串看做是兩個部分,第一部分為它的第一個字元,第二部分是它後面的所有字元。求整個字串的一個全排列,可
以看做兩步,第一步是求所有可能出現在第一個位置的字元,即把第一個字元和後面的所有字元交換。第二步就是求後面所有字元的一
個全排列。因此通過這種方式,我們可以以遞迴的思路來求出當前字串的全排列。

詳細資料可以參考:
《字串的排列》

28. 陣列中出現次數超過一半的數字


題目:

陣列中有一個數字出現的次數超過陣列長度的一半。請找出這個數字。例如輸入一個長度為 9 的陣列{1,2,3,2,2,2,5,4,2}。由於數
字 2 在陣列中出現了 5 次,超過陣列長度的一半,因此輸出 2。如果不存在則輸出 0。

思路:

(1)對陣列進行排序,排序後的中位數就是所求數字。這種方法的時間複雜度取決於我們採用的排序方法的時間複雜度,因此最快為
O(nlogn)。

(2)由於所求數字的數量超過了陣列長度的一半,因此排序後的中位數就是所求數字。因此我們可以將問題簡化為求一個陣列的中
位數問題。其實陣列並不需要全排序,只需要部分排序。我們通過利用快排中的 partition 函式來實現,我們現在陣列中隨
機選取一個數字,而後通過 partition 函式返回該數字在陣列中的索引 index,如果 index 剛好等於 n/2,則這個數字
便是陣列的中位數,也即是要求的數,如果 index 大於 n/2,則中位數肯定在 index 的左邊,在左邊繼續尋找即可,反之
在右邊尋找。這樣可以只在 index 的一邊尋找,而不用兩邊都排序,減少了一半排序時間,這種方法的時間複雜度為 O(n)。

(3)由於該數字的出現次數比所有其他數字出現次數的和還要多,因此可以考慮在遍歷陣列時儲存兩個值:一個是陣列中的一個數
字,一個是次數。當遍歷到下一個數字時,如果下一個數字與之前儲存的數字相同,則次數加 1,如果不同,則次數減 1,如果
次數為 0,則需要儲存下一個數字,並把次數設定為 1。由於我們要找的數字出現的次數比其他所有數字的出現次數之和還要大,
則要找的數字肯定是最後一次把次數設為 1 時對應的數字。該方法的時間複雜度為 O(n),空間複雜度為 O(1)。

詳細資料可以參考:
《出現次數超過一半的數字》

29. 最小的 K 個數


題目:

輸入 n 個整數,找出其中最小的 K 個數。例如輸入 4,5,1,6,2,7,3,8 這 8 個數字,則最小的 4 個數字是 1,2,3,4 。

思路:

(1)第一種思路是首先將陣列排序,排序後再取最小的 k 個數。這一種方法的時間複雜度取決於我們選擇的排序演算法的時間複雜
度,最好的情況下為 O(nlogn)。

(2)第二種思路是由於我們只需要獲得最小的 k 個數,這 k 個數不一定是按序排序的。因此我們可以使用快速排序中的 part
ition 函式來實現。每一次選擇一個樞紐值,將陣列分為比樞紐值大和比樞紐值小的兩個部分,判斷樞紐值的位置,如果該樞
紐值的位置為 k-1 的話,那麼樞紐值和它前面的所有數字就是最小的 k 個數。如果樞紐值的位置小於 k-1 的話,假設樞
紐值的位置為 n-1,那麼我們已經找到了前 n 小的數字了,我們就還需要到後半部分去尋找後半部分 k-n 小的值,進行劃
分。當該樞紐值的位置比 k-1 大時,說明最小的 k 個值還在左半部分,我們需要繼續對左半部分進行劃分。這一種方法的平
均時間複雜度為 O(n)。

(3)第三種方法是維護一個容量為 k 的最大堆。對陣列進行遍歷時,如果堆的容量還沒有達到 k ,則直接將元素加入到堆中,這
就相當於我們假設前 k 個數就是最小的 k 個數。對 k 以後的元素遍歷時,我們將該元素與堆的最大值進行比較,如果比最
大值小,那麼我們則將最大值與其交換,然後調整堆。如果大於等於堆的最大值,則繼續向後遍歷,直到陣列遍歷完成。這一
種方法的平均時間複雜度為 O(nlogk)。

詳細資料可以參考:
《尋找最小的 k 個數》

30. 連續子陣列的最大和


題目:

HZ 偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計
算連續子向量的最大和,當向量全為正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的
正數會彌補它呢?例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和為 8(從第 0 個開始,到第 3 個為止)。你會不會被他忽悠
住?(子向量的長度至少是 1)

思路:

(1)第一種思路是直接暴力求解的方式,先以第一個數字為首往後開始疊加,疊加的過程中儲存最大的值。然後再以第二個數字為首
往後開始疊加,並與先前儲存的最大的值進行比較。這一種方法的時間複雜度為 O(n^2)。

(2)第二種思路是,首先我們觀察一個最大和的連續陣列的規律,我們可以發現,子陣列一定是以正數開頭的,中間包含了正負數。
因此我們可以從第一個數開始向後疊加,每次儲存最大的值。疊加的值如果為負數,則將疊加值初始化為 0,因為後面的數加上負
數只會更小,因此需要尋找下一個正數開始下一個子陣列的判斷。一直往後判斷,直到這個陣列遍歷完成為止,得到最大的值。
使用這一種方法的時間複雜度為 O(n)。

詳細資料可以參考:
《連續子陣列的最大和》

31. 整數中 1 出現的次數(待深入理解)


題目:

求出 1~13 的整數中 1 出現的次數,並算出 100~1# 300 的整數中 1 出現的次數?為此他特別數了一下 1~13 中包含 1 的數字有 1、10、11、
12、13 因此共出現 6 次,但是對於後面問題他就沒轍了。ACMer 希望你們幫幫他,並把問題更加普遍化,可以很快的求出任意非負整
數區間中 1 出現的次數。

思路:

(1)第一種思路是直接遍歷每個數,然後將判斷每個數中 1 的個數,一直疊加。

(2)第二種思路是求出 1 出現在每位上的次數,然後進行疊加。

詳細資料可以參考:
《從 1 到 n 整數中 1 出現的次數:O(logn)演算法》

32. 把陣列排成最小的數


題目:

輸入一個正整數陣列,把陣列裡所有數字拼接起來排成一個數,列印能拼接出的所有數字中最小的一個。例如輸入陣列{3,32,321
},則列印出這三個數字能排成的最小數字為 321323。

思路:

(1)求出陣列的全排列,然後對每個排列結果進行比較。

(2)利用排序演算法實現,但是比較時,比較的並不是兩個元素的大小,而是兩個元素正序拼接和逆序拼接的大小,如果逆序拼接的
結果更小,則交換兩個元素的位置。排序結束後,陣列的順序則為最小數的排列組合順序。

詳細資料可以參考:
《把陣列排成最小的數》

33. 醜數(待深入理解)


題目:

把只包含質因子 2、3 和 5 的數稱作醜數。例如 6、8 都是醜數,但 14 不是,因為它包含因子 7。 習慣上我們把 1 當做是第一個醜數。求
按從小到大的順序的第 N 個醜數。

思路:

(1)判斷一個數是否為醜數,可以判斷該數不斷除以 2,最後餘數是否為 1。判斷該數不斷除以 3,最後餘數是否為 1。判斷不斷除以
5,最後餘數是否為 1。在不考慮時間複雜度的情況下,可以依次遍歷找到第 N 個醜數。

(2)使用一個陣列來儲存已排序好的醜數,後面的醜數由前面生成。

34. 第一個只出現一次的字元


題目:

在一個字串(1<=字串長度<=10000,全部由大寫字母組成)中找到第一個只出現一次的字元,並返回它的位置。

思路:

(1)第一種思路是,從前往後遍歷每一個字元。每遍歷一個字元,則將字元與後邊的所有字元依次比較,判斷是否含有相同字元。這
一種方法的時間複雜度為 O(n^2)。

(2)第二種思路是,首先對字串進行一次遍歷,將字元和字元出現的次數以鍵值對的形式儲存在 Map 結構中。然後第二次遍歷時
,去 Map 中獲取對應字元出現的次數,找到第一個只出現一次的字元。這一種方法的時間複雜度為 O(n)。

35. 陣列中的逆序對


題目:

在陣列中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個陣列,求出這個陣列中的逆序對
的總數 P。

思路:

(1)第一種思路是直接求解的方式,順序掃描整個陣列。每掃描到一個數字的時候,逐個比較該數字和它後面的數字的大小。如果
後面的數字比它小,則這兩個數字就組成了一個逆序對。假設陣列中含有 n 個數字。由於每個數字都要和 O(n)個數字作比
較,因此這個演算法的時間複雜度是 O(n^2)。

(2)第二種方式是使用歸併排序的方式,通過利用歸併排序分解後進行合併排序時,來進行逆序對的統計,這一種方法的時間複雜
度為 O(nlogn)。

詳細資料可以參考:
《陣列中的逆序對》

36. 兩個連結串列的第一個公共結點


題目:

輸入兩個連結串列,找出它們的第一個公共結點。

思路:

(1)第一種方法是在第一個連結串列上順序遍歷每個結點,每遍歷到一個結點的時候,在第二個連結串列上順序遍歷每個結點。如果在第二
個連結串列上有一個結點和第一個連結串列上的結點一樣,說明兩個連結串列在這個結點上重合,於是就找到了它們的公共結點。如果第一
個連結串列的長度為 m,第二個連結串列的長度為 n。這一種方法的時間複雜度是 O(mn)。

(2)第二種方式是利用棧的方式,通過觀察我們可以發現兩個連結串列的公共節點,都位於連結串列的尾部,以此我們可以分別使用兩個棧
,依次將連結串列元素入棧。然後在兩個棧同時將元素出棧,比較出棧的節點,最後一個相同的節點就是我們要找的公共節點。這
一種方法的時間複雜度為 O(m+n),空間複雜度為 O(m+n)。

(3)第三種方式是,首先分別遍歷兩個連結串列,得到兩個連結串列的長度。然後得到較長的連結串列與較短的連結串列長度的差值。我們使用兩個
指標來分別對兩個連結串列進行遍歷,首先將較長連結串列的指標移動 n 步,n 為兩個連結串列長度的差值,然後兩個指標再同時移動,
判斷所指向節點是否為同一節點。這一種方法的時間複雜度為 O(m+n),相同對於上一種方法不需要額外的空間。

詳細資料可以參考:
《兩個連結串列的第一個公共結點》

37. 數字在排序陣列中出現的次數


題目:

統計一個數字:在排序陣列中出現的次數。例如輸入排序陣列{ 1, 2, 3, 3, 3, 3, 4, 5}和數字 3 ,由於 3 在這個陣列中出
現了 4 次,因此輸出 4 。

思路:

(1)第一種方法是直接對陣列順序遍歷的方式,通過這種方法來統計數字的出現次數。這種方法的時間複雜度為 O(n)。

(2)第二種方法是使用二分查詢的方法,由於陣列是排序好的陣列,因此相同數字是排列在一起的。統計數字出現的次數,我們需要
去找到該段數字開始和結束的位置,以此來確定數字出現的次數。因此我們可以使用二分查詢的方式來確定該數字的開始和結束
位置。如果我們第一次我們陣列的中間值為 k ,如果 k 值比所求值大的話,那麼我們下一次只需要判斷前面一部分就行了,如
果 k 值比所求值小的話,那麼我們下一次就只需要判斷後面一部分就行了。如果 k 值等於所求值的時候,我們則需要判斷該值
是否為開始位置或者結束位置。如果是開始位置,那麼我們下一次需要到後半部分去尋找結束位置。如果是結束位置,那麼我們
下一次需要到前半部分去尋找開始位置。如果既不是開始位置也不是結束位置,那麼我們就分別到前後兩個部分去尋找開始和結
束位置。這一種方法的平均時間複雜度為 O(logn)。

38. 二叉樹的深度


題目:

輸入一棵二叉樹,求該樹的深度。從根結點到葉結點依次經過的結點(含根、葉結點)形成樹的一條路徑,最長路徑的長度為樹的深
度。

思路:

根節點的深度等於左右深度較大值加一,因此可以通過遞迴遍歷來實現。

39. 平衡二叉樹


題目:

輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。

思路:

(1)在遍歷樹的每個結點的時候,呼叫函式得到它的左右子樹的深度。如果每個結點的左右子樹的深度相差都不超過 1 ,那麼它
就是一棵平衡的二叉樹。使用這種方法時,節點會被多次遍歷,因此會造成效率不高的問題。

(2)在求一個節點的深度時,同時判斷它是否平衡。如果不平衡則直接返回 -1,否則返回樹高度。如果一個節點的一個子樹的深
度為-1,那麼就直接向上返回 -1 ,該樹已經是不平衡的了。通過這種方式確保了節點只能夠被訪問一遍。

40. 陣列中只出現一次的數字


題目:

一個整型陣列裡除了兩個數字之外,其他的數字都出現了兩次。請寫程式找出這兩個只出現一次的數字。

思路:

(1)第一種方式是依次遍歷陣列,記錄下數字出現的次數,從而找出兩個只出現一次的數字。

(2)第二種方式,根據位運算的異或的性質,我們可以知道兩個相同的數字異或等於 0,一個數和 0 異或還是它本身。由於陣列中
的其他數字都是成對出現的,因此我們可以將陣列中的所有數依次進行異或運算。如果只有一個數出現一次的話,那麼最後剩下
的就是落單的數字。如果是兩個數只出現了一次的話,那麼最後剩下的就是這兩個數異或的結果。這個結果中的 1 表示的是 A 和
B 不同的位。我們取異或結果的第一個 1 所在的位數,假如是第 3 位,接著通過比較第三位來將陣列分為兩組,相同數字一定會
被分到同一組。分組完成後再按照依次異或的思路,求得剩餘數字即為兩個只出現一次的數字。

41. 和為 S 的連續正數序列


題目:

小明很喜歡數學,有一天他在做數學作業時,要求計算出 9~16 的和,他馬上就寫出了正確答案是 100。但是他並不滿足於此,他在想究
竟有多少種連續的正數序列的和為 100(至少包括兩個數)。沒多久,他就得到另一組連續正數和為 100 的序列:18,19,20,21,22。
現在把問題交給你,你能不能也很快的找出所有和為 S 的連續正數序列?Good Luck!輸出描述:輸出所有和為 S 的連續正數序列。序
列內按照從小至大的順序,序列間按照開始數字從小到大的順序。

思路:

維護一個正數序列陣列,陣列中初始只含有值 1 和 2,然後從 3 依次往後遍歷,每遍歷到一個元素則將這個元素加入到序列陣列中,然後
判斷此時序列陣列的和。如果序列陣列的和大於所求值,則將第一個元素(最小的元素彈出)。如果序列陣列的和小於所求值,則繼續
往後遍歷,將元素加入到序列中繼續判斷。當序列陣列的和等於所求值時,列印出此時的正數序列,然後繼續往後遍歷,尋找下一個連
續序列,直到陣列遍歷完成終止。

詳細資料可以參考:
《和為 s 的連續正數序列》

42. 和為 S 的兩個數字


題目:

輸入一個遞增排序的陣列和一個數字 S,在陣列中查詢兩個數,是的他們的和正好是 S,如果有多對數字的和等於 S,輸出兩個數
的乘積最小的。輸出描述:對應每個測試案例,輸出兩個數,小的先輸出。

思路:

首先我們通過規律可以發現,和相同的兩個數字,兩個數字的差值越大,乘積越小。因此我們只需要從陣列的首尾開始找到第一對和
為 s 的數字對進行了。因此我們可以使用雙指標的方式,左指標初始指向陣列的第一個元素,右指標初始指向陣列的最後一個元素
。然後首先判斷兩個指標指向的數字的和是否為 s ,如果為 s ,兩個指標指向的數字就是我們需要尋找的數字對。如果兩數的和
比 s 小,則將左指標向左移動一位後繼續判斷。如果兩數的和比 s 大,則將右指標向右移動一位後繼續判斷。

詳細資料可以參考:
《和為 S 的字串》

43. 左旋轉字串


題目:

組合語言中有一種移位指令叫做迴圈左移(ROL),現在有個簡單的任務,就是用字串模擬這個指令的運算結果。對於一個給定的
字元序列 S,請你把其迴圈左移 K 位後的序列輸出。例如,字元序列 S=”abcXYZdef”,要求輸出迴圈左移 3 位後的結果,即 “X
YZdefabc”。是不是很簡單?OK,搞定它!

思路:

字串裁剪後拼接

44. 翻轉單詞順序列


題目:

牛客最近來了一個新員工 Fish,每天早晨總是會拿著一本英文雜誌,寫些句子在本子上。同事 Cat 對 Fish 寫的內容頗感興趣,有
一天他向 Fish 借來翻看,但卻讀不懂它的意思。例如,“student. a am I”。後來才意識到,這傢伙原來把句子單詞的順序翻轉了
,正確的句子應該是“I am a student.”。Cat 對一一的翻轉這些單詞順序可不在行,你能幫助他麼?

思路:

通過空格將單詞分隔,然後將陣列反序後,重新拼接為字串。

45. 撲克牌的順子


題目:

LL 今天心情特別好,因為他去買了一副撲克牌,發現裡面居然有 2 個大王,2 個小王(一副牌原本是 54 張^\_^)...他隨機從中抽出
了 5 張牌,想測測自己的手氣,看看能不能抽到順子,如果抽到的話,他決定去買體育彩票,嘿嘿!!“紅心 A,黑桃 3,小王,大王
,方片 5”,“Oh My God!”不是順子..... LL 不高興了,他想了想,決定大\小王可以看成任何數字,並且 A 看作 1,J 為 11,
Q 為 12,K 為 13。上面的 5 張牌就可以變成“1,2,3,4,5”(大小王分別看作 2 和 4),“So Lucky!”。LL 決定去買體育彩票啦。
現在,要求你使用這幅牌模擬上面的過程,然後告訴我們 LL 的運氣如何。為了方便起見,你可以認為大小王是 0。

思路:

首先判斷 5 個數字是不是連續的,最直觀的方法是把陣列排序。值得注意的是,由於 0 可以當成任意數字,我們可以用 0 去補滿數
組中的空缺。如果排序之後的陣列不是連續的,即相鄰的兩個數字相隔若干個數字,但只要我們有足夠的。可以補滿這兩個數字的空
缺,這個陣列實際上還是連續的。

於是我們需要做 3 件事情:首先把陣列排序,再統計陣列中 0 的個數,最後統計排序之後的陣列中相鄰數字之間的空缺總數。如
果空缺的總數小於或者等於 0 的個數,那麼這個陣列就是連續的:反之則不連續。最後,我們還需要注意一點:如果陣列中的非 0
數字重複出現,則該陣列不是連續的。換成撲克牌的描述方式就是如果一副牌裡含有對子,則不可能是順子。

詳細資料可以參考:
《撲克牌的順子》

46. 圓圈中最後剩下的數字(約瑟夫環問題)


題目:

0, 1, … , n-1 這 n 個數字排成一個圈圈,從數字 0 開始每次從圓圏裡刪除第 m 個數字。求出這個圈圈裡剩下的最後一個數
字。

思路:

(1)使用環形連結串列進行模擬。

(2)根據規律得出(待深入理解)

詳細資料可以參考:
《圓圈中最後剩下的數字》

47. 1+2+3+...+n


題目:

求 1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case 等關鍵字及條件判斷語句(A?B:C)。

思路:

由於不能使用迴圈語句,因此我們可以通過遞迴來實現。並且由於不能夠使用條件判斷運算子,我們可以利用 && 操作符的短路特
性來實現。

48. 不用加減乘除做加法


題目:

寫一個函式,求兩個整數之和,要求在函式體內不得使用 +、-、×、÷ 四則運算子號。

思路:

通過位運算,遞迴來實現。

49. 把字串轉換成整數。


題目:

將一個字串轉換成一個整數,要求不能使用字串轉換整數的庫函式。數值為 0 或者字串不是一個合法的數值則返回 0。輸入描
述:輸入一個字串,包括數字字母符號,可以為空。輸出描述:如果是合法的數值表達則返回該數字,否則返回 0。

思路:

首先需要進行符號判斷,其次我們根據字串的每位通過減 0 運算轉換為整數和,依次根據位數疊加。

50. 陣列中重複的數字


題目:

在一個長度為 n 的陣列裡的所有數字都在 0 到 n-1 的範圍內。陣列中某些數字是重複的,但不知道有幾個數字重複了,也不知
道每個數字重複了幾次。請找出陣列中任意一個重複的數字。

思路:

(1)首先將陣列排序,排序後再進行判斷。這一種方法的時間複雜度為 O(nlogn)。

(2)使用 Map 結構的方式,依次記錄下每一個數字出現的次數,從而可以判斷是否出現重複數字。這一種方法的時間複雜度為 O
(n),空間複雜度為 O(n)。

(3)從陣列首部開始遍歷,每遍歷一個數字,則將該數字和它的下標相比較,如果數字和下標不等,則將該數字和它對應下標的值
交換。如果對應的下標值上已經是正確的值了,那麼說明當前元素是一個重複數字。這一種方法相對於上一種方法來說不需要
額外的記憶體空間。

51. 構建乘積陣列


題目:

給定一個陣列 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]。不能使用除法。

思路:

(1)  C[i]=A[0]×A[1]×...×A[i-1]=C[i-1]×A[i-1]
      D[i]=A[i+1]×...×A[n-1]=D[i+1]×A[i+1]
      B[i]=C[i]×D[i]
       將乘積分為前後兩個部分,分別迴圈求出後,再進行相乘。

(2)上面的方法需要額外的記憶體空間,我們可以引入中間變數的方式,來降低空間複雜度。(待深入理解)

詳細資料可以參考:
《構建乘積陣列》

52. 正規表示式的匹配


題目:

請實現一個函式用來匹配包括'.'和'_'的正規表示式。模式中的字元'.'表示任意一個字元,而'_'表示它前面的字元可以出現任
意次(包含 0 次)。 在本題中,匹配是指字串的所有字元匹配整個模式。例如,字串"aaa"與模式"a.a"和"ab*ac*a"匹配,
但是與"aa.a"和"ab\*a"均不匹配。

思路:

(1)狀態機思路(待深入理解)

詳細資料可以參考:
《正規表示式匹配》

53. 表示數值的字串


題目:

請實現一個函式用來判斷字串是否表示數值(包括整數和小數)。例如,字串"+100","5e2","-123","3.1416"和"-1E-
16"都表示數值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。、

思路:

利用正規表示式實現

54. 字元流中第一個不重複的字元


題目:

請實現一個函式用來找出字元流中第一個只出現一次的字元。例如,當從字元流中只讀出前兩個字元 "go" 時,第一個只出現一次
的字元是 "g" 。當從該字元流中讀出前六個字元 "google" 時,第一個只出現一次的字元是 "l"。 輸出描述:如果當前字元流
沒有存在出現一次的字元,返回#字元。

思路:

同第 34 題

55. 連結串列中環的入口結點


題目:

一個連結串列中包含環,如何找出環的入口結點?

思路:

首先使用快慢指標的方式我們可以判斷連結串列中是否存在環,當快慢指標相遇時,說明連結串列中存在環。相遇點一定存在於環中,因此我
們可以從使用一個指標從這個點開始向前移動,每移動一個點,環的長度加一,當指標再次回到這個點的時候,指標走了一圈,因此
通過這個方法我們可以得到連結串列中的環的長度,我們將它記為 n 。

然後我們設定兩個指標,首先分別指向頭結點,然後將一個指標先移動 n 步,然後兩個指標再同時移動,當兩個指標相遇時,相遇
點就是環的入口節點。

詳細資料可以參考:
《連結串列中環的入口結點》
《《劍指 offer》——連結串列中環的入口結點》

56. 刪除連結串列中重複的結點


題目:

在一個排序的連結串列中,存在重複的結點,請刪除該連結串列中重複的結點,重複的結點不保留,返回連結串列頭指標。例如,連結串列 1->2->3-

> 3->4->4->5 處理後為 1->2->5

思路:

解決這個問題的第一步是確定刪除的引數。當然這個函式需要輸入待刪除連結串列的頭結點。頭結點可能與後面的結點重複,也就是說頭
結點也可能被刪除,所以在連結串列頭額外新增一個結點。

接下來我們從頭遍歷整個連結串列。如果當前結點的值與下一個結點的值相同,那麼它們就是重複的結點,都可以被刪除。為了保證刪除
之後的連結串列仍然是相連的而沒有中間斷開,我們要把當前的前一個結點和後面值比當前結點的值要大的結點相連。我們要確保 prev
要始終與下一個沒有重複的結點連線在一起。

57. 二叉樹的下一個結點

題目:

給定一棵二叉樹和其中的一個結點,如何找出中序遍歷順序的下一個結點?樹中的結點除了有兩個分別指向左右子結點的指標以外,
還有一個指向父節點的指標。

思路:

這個問題我們可以分為三種情況來討論。

第一種情況,當前節點含有右子樹,這種情況下,中序遍歷的下一個節點為該節點右子樹的最左子節點。因此我們只要從右子節點
出發,一直沿著左子節點的指標,就能找到下一個節點。

第二種情況是,當前節點不含有右子樹,並且當前節點為父節點的左子節點,這種情況下中序遍歷的下一個節點為當前節點的父節
點。

第三種情況是,當前節點不含有右子樹,並且當前節點為父節點的右子節點,這種情況下我們沿著父節點一直向上查詢,直到找到
一個節點,該節點為父節點的左子節點。這個左子節點的父節點就是中序遍歷的下一個節點。

58. 對稱二叉樹

題目:

請實現一個函式來判斷一棵二叉樹是不是對稱的。如果一棵二叉樹和它的映象一樣,那麼它是對稱的。

思路:

我們對一顆二叉樹進行前序遍歷的時候,是先訪問左子節點,然後再訪問右子節點。因此我們可以定義一種對稱的前序遍歷的方式
,就是先訪問右子節點,然後再訪問左子節點。通過比較兩種遍歷方式最後的結果是否相同,以此來判斷該二叉樹是否為對稱二叉
樹。

59. 按之字形順序列印二叉樹(待深入理解)

題目:

請實現一個函式按照之字形順序列印二叉樹,即第一行按照從左到右的順序列印,第二層按照從右到左的順序列印,即第一行按照
從左到右的順序列印,第二層按照從右到左順序列印,第三行再按照從左到右的順序列印,其他以此類推。

思路:

按之字形順序列印二叉樹需要兩個棧。我們在列印某一行結點時,把下一層的子結點儲存到相應的棧裡。如果當前列印的是奇數層
,則先儲存左子結點再儲存右子結點到一個棧裡;如果當前列印的是偶數層,則先儲存右子結點再儲存左子結點到第二個棧裡。每
一個棧遍歷完成後進入下一層迴圈。

詳細資料可以參考:
《按之字形順序列印二叉樹》

60. 從上到下按層列印二叉樹,同一層結點從左至右輸出。每一層輸出一行。

題目:

從上到下按層列印二叉樹,同一層的結點按從左到右的順序列印,每一層列印一行。

思路:

用一個佇列來儲存將要列印的結點。為了把二叉樹的每一行單獨列印到一行裡,我們需要兩個變數:一個變數表示在當前的層中還
沒有列印的結點數,另一個變數表示下一次結點的數目。

61. 序列化二叉樹(帶深入理解)

題目:

請實現兩個函式,分別用來序列化和反序列化二叉樹。

思路:

陣列模擬

62. 二叉搜尋樹的第 K 個節點

題目:

給定一顆二叉搜尋樹,請找出其中的第 k 小的結點。

思路:

對一顆樹首先進行中序遍歷,在遍歷的同時記錄已經遍歷的節點數,當遍歷到第 k 個節點時,這個節點即為第 k 大的節點。

63. 資料流中的中位數(待深入理解)

題目:

如何得到一個資料流中的中位數?如果從資料流中讀出奇數個數值,那麼中位數就是所有值排序之後位於中間的數值。如果資料
流中讀出偶數個數值,那麼中位數就是所有數值排序之後中間兩個數的平均值。

64. 滑動視窗中的最大值(待深入理解)

題目:

給定一個陣列和滑動視窗的大小,找出所有滑動視窗裡數值的最大值。例如,如果輸入陣列{2,3,4,2,6,2,5,1}及滑動視窗的
大小 3,那麼一共存在 6 個滑動視窗,他們的最大值分別為{4,4,6,6,6,5}; 針對陣列{2,3,4,2,6,2,5,1}的滑動視窗有以下
6 個: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2
,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

思路:

使用佇列的方式模擬

65. 矩陣中的路徑(待深入理解)

題目:

請設計一個函式,用來判斷在一個矩陣中是否存在一條包含某字串所有字元的路徑。路徑可以從矩陣中的任意一個格子開始,每
一步可以在矩陣中向左,向右,向上,向下移動一個格子。如果一條路徑經過了矩陣中的某一個格子,則該路徑不能再進入該格子
。例如 a b c e s f c s a d e e 矩陣中包含一條字串"bcced"的路徑,但是矩陣中不包含"abcb"路徑,因為字串的
第一個字元 b 佔據了矩陣中的第一行第二個格子之後,路徑不能再次進入該格子。

66. 機器人的運動範圍(待深入理解)

題目:

地上有一個 m 行和 n 列的方格。一個機器人從座標 0,0 的格子開始移動,每一次只能向左,右,上,下四個方向移動一格,但是不能
進入行座標和列座標的數位之和大於 k 的格子。 例如,當 k 為 18 時,機器人能夠進入方格(35,37),因為 3+5+3+7 = 18。但是
,它不能進入方格(35,38),因為 3+5+3+8 = 19。請問該機器人能夠達到多少個格子?

劍指 offer 相關資料可以參考:

推薦

最後如果文章和筆記能帶您一絲幫助或者啟發,請不要吝嗇你的贊和收藏,你的肯定是我前進的最大動力 ?

附筆記連結,閱讀往期更多優質文章可移步檢視,喜歡的可以給我點贊鼓勵哦:https://github.com/Wscats/art...

相關文章