劍指offer思路總結

統一格式前注:

標題對應《劍指offer》題號
時間複雜度
空間複雜度
思路:包括解題思路和編程中的技巧
教訓:編程過程中需要注意的地方以及存在的慣性錯誤


1.賦值運算符函數(略)


2.實現Singleton模式(略)


3.數組中重複的數字:
代碼
時間複雜度:O(n);
空間複雜度:O(1);
思路:從0~n-1獲得啓發,各個數字值應當與所在數組下標一致,不一致則調換,當調換前的對比中出現相同值,則找到重複數字。

衍生問題:長度爲n+1的數組,數字在1~n範圍內,要求不能改變原數組順序
時間複雜度:O(nlogn)
空間複雜度:O(1)
思路:二分查找;具體就是按數組中間位置數字m,將數組分爲1~m 和m+1~n;分別統計兩部分中的數字在整個數組中出現的次數,如果次數大於這部分的數目,那麼重複值就在這一部分中,不斷重複上述過程,最終找到重複數字。
當然這種算法找不出所有的重複數字。


4.二維數組中的查找:
代碼
時間複雜度:
空間複雜度:
思路:從右上角數字切入:1.該數字等於目標數字,則找到;2.該數字大於目標數字,則刪除該數字所在列;3.該數字小於目標數字,則刪除該數字所在行;如此逐步縮小範圍,直到找到目標數字;
教訓:行數和列數的大於小於等於的設定一定要考慮清楚!!


5.替換空格:
代碼
時間複雜度:O(n)
空間複雜度:O(1)
思路:首先基於空格數規劃新數組長度。而後用兩個指針分別指向原數組末尾和新數組末尾,倒序進行插入替換。
教訓:
【1】區分清三個長度:1.給定能用空間長度(length);2.原數組長度(str_length,要測出);3.新數組長度(str_new_length,通過str_length+empty_number*2可計算出)。
【2】並且注意while的條件(1.p1沒到頭;2.p1得在p2前面);

衍生問題:兩個數組的有序合併
思路:基本一致,就是從尾到頭比較兩個數組中的數字,並把較大的數字複製到第一個數組的合適位置。


6.從尾到頭打印鏈表:
代碼
時間複雜度:
空間複雜度:
思路:兩種思路:
【1】基於棧+循環的實現(從頭到尾遍歷入棧,再順序出棧);
【2】基於遞歸的實現(就是每訪問一個節點時,先遞歸輸出它後面的節點,再輸出該節點自身);
當然更推薦第一種思路因爲遞歸層數較深時可能導致調用棧溢出。
教訓:對於vector的語法要熟練掌握!!(insert)


7.重建二叉樹:
代碼
時間複雜度:
空間複雜度:
思路:分治思想!在二叉樹的前序遍歷中,第一個數字總是樹的根節點的值,而在中序遍歷中,根節點的值在序列的中間,由此我們就確定了兩個序列的位置關係,通過遞歸思想可以不斷構建下去。
教訓:代碼思路很簡單:
【step1】輸入合理性檢測;
【step2】通過遍歷中序序列,找出根節點所在;
【step3】基於找到的位置,將兩個序列分爲四個序列(注意index+1);
【step4】左右分別遞歸調用,實現不斷的構建;


8.二叉樹的下一個節點:
代碼
時間複雜度:
空間複雜度:
思路:
考慮到要求的是中序遍歷時的下一個節點,因此可以將二叉樹分3種情況分析:
【1】如果一個節點有右子樹,則下一個節點就是其右子樹的最左子節點;
【2】如果一個節點沒有右子樹而且這個節點是其父節點的左子節點,那麼下一個節點就是其父節點;
【3】如果一個節點沒有右子樹而且這個節點是其父節點的右子節點,那麼我們可以沿着其父節點一路向上尋找,直到找到一個是它父節點的左子節點的節點;
教訓:
具體編程時,三種情況裏是要聲明指針來負責指引的;
情況分析的順序是1,3,2


9.用兩個棧實現隊列
代碼
時間複雜度:
空間複雜度:
思路:棧1是數據進來後直接往裏放的棧,因此push操作一行就行;棧2負責作爲pop的中轉,如果棧2沒有數據,那麼先要把棧1裏的數據都出棧到棧2中,再取棧頂的數(top);如果棧2有數據,那麼直接就取棧頂的數即可(top)。

衍生問題:用兩個隊列實現一個棧
思路:基本與原問題一致,也是一個隊列負責往裏放數據,pop時就是用另一個隊列來暫存之前的數。


附:遞歸的潛在問題:遞歸的本質是把一個問題分解成兩個或者多個小問題
(1)重複的計算;(2)調用棧溢出;


10.斐波那契數列
時間複雜度:O(n)
空間複雜度:O(1)
思路:遞歸思路雖然直觀,但是存在大量重複計算,所有不適用。採用自底向上的累加計算,0,1單獨處理,而後兩個數累加得下一個數,更新後如此循環,完成計算。

衍生問題:青蛙跳臺階問題、矩形覆蓋問題
思路:本質上就是斐波那契數列(P78這種分步驟的思考方法值得借鑑!)


附:
1.常見的查找方法:
順序查找、二分查找、哈希表查找、二叉排序樹查找

2.常見的排序方法:
插入排序、冒泡排序、歸併排序、快速排序等


11.旋轉數組的最小數字
時間複雜度:O(logn)
空間複雜度:O(1)
思路:分三種情況討論
【1】常規場景1,此時就是基於二分查找思想,如果mid位置的值大於p2的,那麼最小數在後半部分,如果mid位置的值小於p1的,那麼最小數在前半部分;
【2】特殊場景2: p1位置的值和p2的相等,此時最小就是p1位置的值(注意這一場景是作爲while的判斷條件的,如果出現,則while被整個跳過,函數會返回初始化爲index_mid=p1的位置的值)
【3】特殊場景3:此時p1和mid和p2位置上的值大小相同,這樣就只能進行整體遍歷找到最小值;
教訓:
整體思路是很清晰的,要注意的是特殊場景2的處理,在編程中是作爲while的條件的
while(rotateArray[p1]>=rotateArray[p2])
爲什麼這樣寫?是因爲index_mid初始化爲了p1,這樣當出現場景2時,while就被跳過,將直接返回index_mid位置上的值,這是符合場景2的思想的;


附:回溯法
回溯法可以視爲蠻力法的升級版,其非常適合由多個步驟組成的問題,並且每個步驟有多個選項。
用回溯法解決的問題的所有選項可以形象的用樹狀結構表示。
過程:如果在葉結點的狀態不滿足約束條件,那麼只好回溯到它的上一個節點再嘗試其他選項。如果上一個節點的所有可能的選項都已經試過,並且不能達到滿足約束條件的終結狀態,則再次回溯到上一個節點。如果所有節點的所有選項都已經嘗試過仍然不能達到滿足約束條件的終結狀態,則該問題無解。

通常回溯法算法適合用遞歸實現。


12.矩陣中的路徑
代碼
時間複雜度:
空間複雜度:
思路:
step1.輸入合理性檢測
step2.初始化一個標記矩陣,用於標記矩陣中元素是否已訪問
step3.用兩層循環來找到字符串起始字符在矩陣中的位置
step4.在遞歸函數裏針對上下左右進行遞歸操作(注意其中的各種邊界判斷條件!)


13.機器人的運動範圍
代碼
時間複雜度:
空間複雜度:
思路:
和前面12題中矩陣中路徑的思路一致,而且更爲簡單,只是多了個閾值檢測而已。


附:應用動態規劃求解問題的特點(4個):
【1】求解的是一個問題的最優解;
【2】整體問題的最優解是依賴各個子問題的最優解;
【3】我們把大問題分解成若干個小問題,這些小問題之間還有相互重疊的更小的子問題;
【4】由於子問題在分解大問題的過程中重複出現,爲避免重複求解子問題,可以從下往上先計算小問題的最優解並存儲下來,再以此爲基礎求取最大問題的最優解(“從上往下分析問題,從下往上求解問題”)


14.剪繩子
時間複雜度:
空間複雜度:
思路:可以採用動態規劃或者貪婪算法來解決。
動態規劃:從下往上累進式計算;
貪婪算法:當n>=5時,我們儘可能多地剪長度爲3的繩子;當剩下的繩子長度爲4時,把繩子剪成兩段長度爲2的繩子。


15.二進制中1的個數
代碼
時間複雜度:
空間複雜度:
思路:
常規解法:就是保持目標數不動,1不斷進位,實現逐位的1次數統計;中規中矩,循環次數等於整數二進制的位數;
巧妙解法:把一個整數減去1,再和原整數做&運算,會把這一整數的最右邊的1變爲0.那麼一個整數的二進制表示中有多少個1,就可以進行多少次這樣的操作。相較於常規解法,該方法具有更少的循環次數。

衍生問題:
(1)用一條語句判斷一個整數是不是2的整數次方
思路:如果這個整數是2的整數次方,那麼它的二進制表示中只有一位是1,因此可以採用前面的思路,將這個數減1再和自己做&運算,這樣結果應該是0;
(2)輸入兩個整數m和n,計算需要改變m的二進制表示中的多少位才能得到n
思路:step1.求兩個數的異或;step2.統計異或結果中1的位數(同樣採用前面的思路);


16.數值的整數次方
代碼
時間複雜度:
空間複雜度:
思路:次方很好算,主要是特殊情況要考慮全面。特殊情況包括:【1】base爲0且exponent<0,這種情況只能爲0;【2】當base不爲0,但是exponent時,需要先取絕對值,計算完再取倒數。
次方的計算也有優化之處,如P112的公式所分析的,通過迭代,n次迭代就不用計算n次了。

我們要注意:由於計算機表示小數(包括float和double型小數)都有誤差,我們不能直接用等號(==)判斷兩個小數是否相等。如果兩個小數的差的絕對值很小,比如小於0.0000001,就可以認爲它們相等。
在這裏插入圖片描述


17.打印從1到最大的n位數

時間複雜度:
空間複雜度:
思路:採用基於遞歸的全排列方法,數字的每一位都可能是0~9中的一個數,然後設置下一位。遞歸的結束條件是我們已經設置了數字的最後一位。


18.刪除鏈表的節點
題1:在O(1)時間內刪除鏈表節點
時間複雜度:O(1)
空間複雜度:O(1)
思路:總體思路就是把下一個節點的內容複製到需要刪除的節點上覆蓋原有的內容,再把下一個節點刪除,這樣就相當於把當前需要刪除的節點刪除了。此外還需要考慮待刪除節點爲:1.尾節點;2.鏈表只有一個節點的情況。

題2:刪除鏈表中重複的節點(很考驗編程能力!!)
代碼
時間複雜度:
空間複雜度:
思路:pPreNode指向前一節點,pNode指向當前節點,pNext指向下一個節點。(pTodelete指向pNode)

//核心維護pre和cur兩個指針,本質上還是快慢指針思想的體現
ListNode *deleteDuplicates(ListNode *head) {
    if (! head || ! head->next) 
    	return head;
    
    ListNode *start = new ListNode(0);
    start->next = head;
    ListNode *pre = start;
    while (pre->next) 
    {
        ListNode *cur = pre->next;
        while (cur->next && cur->next->val == cur->val) 
        	cur = cur->next;
        if (cur != pre->next) 
        	pre->next = cur->next;
        else 
        	pre = pre->next;
    }
    return start->next;
}

19.正則表達式匹配

時間複雜度:
空間複雜度:
思路:

解這題需要把題意仔細研究清楚,反正我試了好多次才明白的。
首先,考慮特殊情況:
     1>兩個字符串都爲空,返回true
     2>當第一個字符串不空,而第二個字符串空了,返回false(因爲這樣,就無法
        匹配成功了,而如果第一個字符串空了,第二個字符串非空,還是可能匹配成
        功的,比如第二個字符串是“a*a*a*a*”,由於‘*’之前的元素可以出現0次,
        所以有可能匹配成功)
之後就開始匹配第一個字符,這裏有兩種可能:匹配成功或匹配失敗。但考慮到pattern
下一個字符可能是‘*’, 這裏我們分兩種情況討論:pattern下一個字符爲‘*’或
不爲‘*’:
      1>pattern下一個字符不爲‘*’:這種情況比較簡單,直接匹配當前字符。如果
        匹配成功,繼續匹配下一個;如果匹配失敗,直接返回false。注意這裏的
        “匹配成功”,除了兩個字符相同的情況外,還有一種情況,就是pattern的
        當前字符爲‘.’,同時str的當前字符不爲‘\0’。
      2>pattern下一個字符爲‘*’時,稍微複雜一些,因爲‘*’可以代表0個或多個。
        這裏把這些情況都考慮到:
           a>當‘*’匹配0個字符時,str當前字符不變,pattern當前字符後移兩位,
            跳過這個‘*’符號;
           b>當‘*’匹配1個或多個時,str當前字符移向下一個,pattern當前字符
            不變。(這裏匹配1個或多個可以看成一種情況,因爲:當匹配一個時,
            由於str移到了下一個字符,而pattern字符不變,就回到了上邊的情況a;
            當匹配多於一個字符時,相當於從str的下一個字符繼續開始匹配)
之後再寫代碼就很簡單了。

20.表示數值的字符串
代碼
時間複雜度:
空間複雜度:
思路:表示數值的字符串遵循模式A[.[B]][e|EC]或者.B[e|EC],其中A爲數值的整數部分,B緊跟着小數點爲數值的小數部分,C緊跟着e或者E爲數值的指數部分。
因此實現的時候分爲三個部分:首先是對整數部分進行分析,接着是對小數部分進行分析,然後是對指數部分進行分析


21.調整數組順序使奇數位於偶數前面
代碼
時間複雜度:
空間複雜度:
思路:新建一個vector,先遍歷一遍,遇到奇數就push_back;再遍歷一遍,遇到偶數就push_back


22.鏈表中倒數第k個節點
代碼
時間複雜度:
空間複雜度:
思路:兩個指針都先指向頭指針,而後第一個先走k步,而後兩個指針同時後移直到末尾,此時第二個指針所指就是倒數第k個節點。
教訓:
step1.輸入合理性檢測(包括鏈表爲空、k爲非正數)
step2.一個次數爲k的循環,做好準備的同時,也處理了當鏈表長度小於k的情況
step3.就是兩個指針同步後移直到末尾

衍生問題:求鏈表的中間節點
思路:兩個指針,一個一次走一步,一個一次走兩步,這樣當走的快的指針走到鏈表的末尾時,走的慢的指針正好在鏈表的中間。


23.鏈表中環的入口節點
代碼
時間複雜度:
空間複雜度:
思路:
step1.利用快慢兩個指針,能夠在有環的前提下獲得一個環內的相遇節點;
step2.基於這個環內相遇節點,統計環內的節點數目;
step3.兩個指針策略,都先指向頭指針,一個先移動環內節點數目的步數;
step4.而後兩個指針再同步移動,此時相交的節點位置就是環口位置。


24.反轉鏈表
時間複雜度:
空間複雜度:
思路:定義三個指針,分別指向當前遍歷到的節點、它的前一個節點和後一個節點;
教訓:在while循環中,要聲明一個pNext來指向pNode的下一個節點,需要注意的是,每次改變的都是pNodeBefore和pNode之間的連接,返回的最終是pNodeAfter指針!(見P143圖)


25.合併兩個排序的鏈表
代碼
時間複雜度:
空間複雜度:
思路:核心思想是遞歸!具體來說就是兩個鏈表中頭結點比較,小的那個放入前面,然後調整小的這個鏈表傳入遞歸函數的頭指針,遞歸繼續進行比較。(此外爲保證算法魯棒性,還要針對兩個鏈表是否爲空進行檢測)
教訓:
step1.首先對兩個鏈表進行空鏈表檢測
step2.定義一個最終結果的頭指針resultHead
step3.按照兩個鏈表頭指針所指值的大小關係,給resultHead賦值,而後resultHead->next=Merge
繼續進行後續的遞歸操作。


26.樹的子結構
添加鏈接描述
時間複雜度:
空間複雜度:
思路:分兩個步驟
step1.在樹A中找到和樹B的根節點的值一樣的節點R;
step2.判斷樹A中以R爲根節點的子樹是不是包含和樹B一樣的結構
教訓:採用遞歸的方法,注意對nullptr的判斷!


27.二叉樹的鏡像
代碼
時間複雜度:
空間複雜度:
思路:(核心當然採用的是遞歸方法)很直觀,採用的是前序遍歷,如果遍歷到的節點有子節點,就交換它的兩個子節點。當交換完所有非葉子節點的左右子節點之後,就得到了樹的鏡像。


28.對稱的二叉樹
代碼
時間複雜度:
空間複雜度:
思路:構造一種前序遍歷算法的對稱遍歷算法,這樣兩個算法都遍歷一次,如果序列相同,則認爲此二叉樹爲對稱二叉樹。
教訓:具體實現時,沿用的遞歸的方法,比較就相當於一顆樹的左子節點和另一顆樹的右子節點間的比較。


29.順時針打印矩陣
在這裏插入圖片描述
代碼
時間複雜度:
空間複雜度:
思路:以上面的圖爲核心,打印分爲四個階段
主要注意在後兩個階段加入的top和bottom,left和right的比較,這裏的理解是,前兩階段操作,對於不同的矩陣情況,一般都有,而後兩種則不一定有,所有要加入條件判斷。

書上的理解是:第一步總是需要的,第二步的前提條件是終止行號大於起始行號;第三步需要的前提條件是圈內至少有兩行兩列,這就解釋了爲什麼要有top和bottom的對比;同理,第四步需要的前提條件是至少有三行兩列,這就解釋了爲什麼要有left和right的對比。


30.包含min函數棧
時間複雜度:
空間複雜度:
思路:我們需要一個數據棧和一個輔助棧,數據棧就是常規的數據存儲,輔助棧每次把當前最小值入棧(簡單來說,當來一個數時,如果這個數入棧後是棧中最小的數,那麼輔助棧同樣存放的是這個數,而如果這個數並不是棧中最小的數,那麼輔助棧會把棧頂的數再存一次)
教訓:注意value和my_min_value輔助棧的棧頂數字比較前,先判斷該棧是否爲空(這是很嚴謹的,因爲值比較前,是必須要有值的,不然連比較都沒法實現)


31.棧的壓入、彈出序列
代碼
時間複雜度:
空間複雜度:
思路:
1.如果下一個彈出的數字剛好是棧頂的數字,那麼直接彈出;
2.如果下一個彈出的數字不在棧頂,則把壓棧序列中還沒有入棧的數字壓入輔助棧,直到把下一個需要彈出的數字壓入棧頂位置;
3.如果所有的數字都壓入棧後,仍然沒有找到下一個彈出的數字,那麼該序列不可能是一個彈出序列;
教訓:
具體編程時,依照思路的步驟,重點關注條件判斷的處理!


32.從上到下打印二叉樹
代碼
時間複雜度:
空間複雜度:
思路:就是對樹的常規逐層遍歷,採用deque的數據結構,可以後端插入,前端彈出

衍生問題1:分行從上到下打印二叉樹
代碼
思路:核心仍然是利用隊列queue這一數據結構,爲了把每一行單獨打印到一行裏,需要兩個變量:一個變量表示在當前層中還沒有打印的節點數this_line,另一個變量表示下一層節點的數目next_line。
教訓:開頭節點的處理在while循環裏要處理好,重點關注!!

衍生問題2:之字形打印二叉樹
代碼
思路:按之字形順序打印二叉樹需要兩個棧。我們在打印某一層的節點時,把下一層的子節點保存到相應的棧裏:
【1】如果當前打印的是奇數層,則先保存左子節點再保存右子節點到第一個棧裏;
【2】如果當前打印的是偶數層,則先保存右子節點再保存左子節點到第二個棧裏


33.二叉搜索樹的後續遍歷序列
代碼
時間複雜度:
空間複雜度:
思路:
首先明確,二叉搜索樹是有大小性質的:左子樹節點<根節點<右子樹節點
採用的是遞歸的思路,首先根節點位於序列的尾部,而後我們可以根據根節點,到序列中通過大小比較,將序列劃分爲左子樹節點序列和右子樹節點序列,而後就可以分左右子樹遞歸進行判斷。
教訓:
務必注意length-1這一循環邊界!同時要注意劃分位置mid_index的取值!!!


34.二叉樹中和爲某一值的路徑
代碼
時間複雜度:
空間複雜度:
思路:因爲路徑總是從根節點出發的,所以說對二叉樹的遍歷需要首先訪問根節點,3種遍歷方法中只有前序遍歷滿足。
此外這種訪問方式需要典型的棧數據結構的支持,但是在實際編程中,我們採用的是vector,這是從結果存放角度考慮的,使用的是vector的push_back方法和pop_back方法。
教訓:
這裏採用的遞歸思路很有意思,遞歸函數是進來就把節點存入隊列,然後再判斷是否和等於預定值以及是否到葉結點,這樣做可行是因爲該遞歸函數是沒有返回值的,可以說存放到result的唯一途徑就是這一個組合判斷。(注意傳入函數的是result和each_line的引用!!)


35.複雜鏈表的複製
代碼
時間複雜度:O(n)
空間複雜度:
思路:思路上是很直觀的,分爲3個步驟:
【step1】把鏈表中每個複製的節點連接到原節點後面;
【step2】把原鏈表各節點的random連接,複製到複製節點的random;
【step3】調整連接,把奇數位置的節點連接起來就是原鏈表,偶數位置的節點連接起來就是複製出來的鏈表;
教訓:
在編程過程中,注意第三步時,pNode要調整到pnext後面,這是因爲在while條件中,只能用pNode !=nullptr,這是因爲pnext可能一開始就是空的,此時是不能用pnext->next !=nullptr來作爲判斷條件的,要特別注意!!!


36.二叉搜索樹與雙向鏈表
代碼
時間複雜度:
空間複雜度:
思路:核心思路是“中序遍歷+遞歸”
中序遍歷的特點是按照從小到大的順序遍歷二叉樹的每一個節點。當我們遍歷到根節點時,它的左子樹已經轉換爲一個排序的鏈表了,並且處於鏈表中的最後一個節點是當前值最大的節點,此時需要的就是把根節點和這個最大值節點連接起來,接着再去遍歷右子樹,把根節點和右子樹中最小的節點連接起來。通過遞歸執行這一過程,實現雙向鏈表的構建。
教訓:
首先,在遍歷完整個二叉樹後,爲了返回雙向鏈表的頭指針,是需要while遍歷一下的;
此外,在遞歸函數中,注意根節點和左邊最大節點的連接操作,以及pLastNodeInList指針的更新;
pLastNodeInList是一個指向已轉好鏈表中最後元素的指針,因此在傳入時是按照指針傳入的(
)**


37.序列化二叉樹(其實就是遍歷和重建二叉樹)
時間複雜度:
空間複雜度:
思路:


38.字符串的排列
代碼
時間複雜度:
空間複雜度:
思路:實際上思路是很直觀的,每次迭代都只考慮一位數字。
具體的,第一步求所有可能出現在第一個位置的字符,即把第一個字符和後面所有的字符交換;
第二步固定第一個字符,求後面所有字符的排列。這時候我們仍然把後面的所有字符分成兩個部分:後面字符的第一個字符,以及這個字符後面的所有字符,然後把第一個字符逐一和後面的字符交換。
教訓:
之所以編程的和書上有區別,是因爲書上傳入的是str的指針,因此在迭代過程中的值交換,在出迭代時要還原回來,而實際編程中採用的是值傳遞,因此不需要還原這一步驟。

衍生問題1:如果不是求字符的所有排列,而是求字符的所有組合
思路:我們可以吧求n個字符組成長度爲m的問題分解成兩個子問題,分別求n-1個字符中長度爲m-1的組合,以及求n-1個字符中長度爲m的組合。這兩個子問題都可以用遞歸的方式解決。

衍生問題2:正方體三組相對的面上的4個頂點的和都相等
思路:這相當於得到8個數字的所有排列,然後判斷有沒有某一個排列符合題目給定的條件,即三組相等。

衍生問題3:8皇后問題
思路:我們用一個長度爲8的數組array,數組中第i個數字表示位於第i行的皇后的列號。數組先用0~7進行初始化,而後進行全排列。(因爲我們用不同的數字初始化數字,所有任意兩個皇后肯定不同列,因此只需要判斷每一種排列是否在同一條對角線上,即對於數組的兩個下標i和j,是否有i-j=array[i]-array[j]或者j-i=array[j]-array[i])



經驗Tips:
1.對於C++程序員來說,要養成採用引用(指針)傳遞複雜類型參數的習慣,這對提高代碼的時間效率有好處;
2.遞歸儘管寫法上很簡潔,但是考慮到如果小問題中有互相重疊的部分,則時間效率上很差;對於這種題目,我們可以用遞歸的思路來分析問題,但是寫代碼時可以用數組來保存中間結果,再基於循環來實現。絕大部分動態規劃算法的分析和代碼實現都是分這兩個步驟實現的。
3.代碼的時間效率是可以反映一個人的基本功的,比如同樣是查找算法,有O(n) 的時間複雜度,也有O(logn)和O(1)。


39.數組中出現次數超過一半的數字

思路2代碼
時間複雜度:都是O(n)
空間複雜度:
思路:
1.基於快排的思路:在數組中隨機選擇一個數字,然後調整數組中數字的順序,使得比選中的數字小的數字位於選中數字左邊,比選中數字大的數字位於選中數字右邊,(1)如果該選中數字下標剛好是n/2,則這個數字就是中位數(2)如果該數字下標大於n/2,則中位數位於其左邊,可以在左邊重複上述查找查找;(3)如果該數字下標小於n/2,則中位數位於其右邊,可以在右邊重複上述查找查找;(此外當然還要考慮非法輸入的問題)
2.基於統計的思路:我們遍歷給定數組,遍歷過程中保持兩個數,一個是數組中的數字,一個是次數;當我們遍歷到下一個數字時,如果下一個數字和我們之前保存的數字相同,則次數加1,否則次數減1.如果次數爲零,則我們需要保存下一個數字,並把次數設置爲1。
由於我們要找的數字出現的次數比其他所有數字出現的次數之和還要多,那麼要找的數字肯定是最後一次把次數設置爲1時對應的數字。
兩種思路時間複雜度相同,區別在於前者會改變原數組中數字的順序,而後者則不改變原數組中數字的順序。


40.最小的k個數(可以考察大數據問題)
代碼
時間複雜度:
空間複雜度:
思路:
1.基於快排的思路,利用上面的Partition函數,只是這裏和第k位比較,而不是和middle比較;
2.基於最大堆這一數據結構,我們每次在O(1)時間內得到已有的k個數字中的最大值,但是需要O(logk)時間完成刪除及插入操作;
附:對堆的操作介紹


41.數據流中的中位數
代碼
時間複雜度:
空間複雜度:
思路:採用了基於vector實現的最大堆和最小堆(最大堆的第一個數是最小值,最小堆的第一個數字是最大值)。
思路就是左邊爲最大堆,右邊爲最小堆,左邊保持恆小於右邊。
當已有數目爲偶數時,新來的數要放入最小堆;當已有數目爲奇數時,新來的數要放入最大堆。
如果待放入最大堆的數小於max[0],則要進行堆的調整(壓入和彈出),否則應放入最小堆中;反之同理。

涉及的語法:對堆操作的介紹
push_heap(max.begin(),max.end(),less&lt;int&gt;&lt;int&gt;());
pop_heap(max.begin(),max.end(),less&lt;int&gt;&lt;int&gt;());
push_heap(min.begin(),min.end(),greater&lt;int&gt;&lt;int&gt;());
pop_heap(min.begin(),min.end(),greater&lt;int&gt;&lt;int&gt;());


42.連續子數組的最大和
代碼
時間複雜度:O(n)
空間複雜度:O(1)
思路:從頭開始累加,如果當累加下一個數時,前面的和<=0,此時再累加下一個數肯定是小於這個數的,因此此時應當將和置零,從當前值開始重新進行累加;在計算過程中,保持最大的和記錄,最後返回的即爲最大值。
本質上是動態規劃法的循環實現:
在這裏插入圖片描述


43.1~n整數中1出現的次數
代碼
時間複雜度:O(ln(N)/ln(10)+1)
空間複雜度:
思路:逐個判斷每一位的情況。
【1】如果這一位是0,則這一位出現1的次數由更高位決定;
【2】如果這一位爲1,則這一位出現1的次數不僅受更高位影響,而且還受低位影響;
【3】如果這一位數字大於1,則這一位出現1的次數僅由更高位決定;

int NumberOf1Between1AndN_Solution(int n)
    {
        if(n<0)
            return 0;
         
        int icount=0;
        int ifactor=1;
         
        int ilowernum=0;     //低位數字
        int icurrentnum=0;   //當前位數字
        int ihighernum=0;       //高位數字
         
        while(n / ifactor !=0)
        {
            ilowernum= n-(n/ifactor)*ifactor;              //這3組公式很關鍵!!
            icurrentnum=(n/ifactor)%10;
            ihighernum = n/(ifactor*10);
             
            switch(icurrentnum)
            {
                case 0:
                    icount += ihighernum*ifactor;
                    break;
                case 1:
                    icount +=ihighernum*ifactor+ilowernum+1;
                    break;
                default:
                    icount +=(ihighernum+1)*ifactor;
                    break;
            }
            ifactor *=10;
        }
        return icount;
    }

44.數字序列中某一位的數字
時間複雜度:
空間複雜度:
思路:這一問題的核心就是找規律,10,90,900,…找到規律就容易處理了。
step1.首先是一個計算該位數有多少數字的函數,核心公式就是10^(digitic-1)*9;
step2.如果說第n位比這個大,那麼n減去該位所擁有的數字,然後再循環比較;
step3.如果說第n位小於這個位所有的數的數目了,那麼就說明n是在該位數之中,而後可以用一個函數單獨找出這個數字;


45.把數組排成最小的數
代碼
時間複雜度:
空間複雜度:
思路:爲簡單起見,同時爲了避免隱形大數問題,我們定義compare函數,將兩個數轉化爲字符串來進行比較。
具體思路就是先將各個字符串排序,而後拼接起來即可(這裏用到了內置的sort函數,當然compare方法要自己寫)

    string PrintMinNumber(vector<int> numbers) {
         
        string str="";  //用於返回目標字符串
        //輸入檢查
        if(numbers.size()==0)
            return str;
         
        int n=numbers.size(); //獲取目標數組的長度,即有多少個數要進行處理
         
        sort(numbers.begin(),numbers.end(),compare);  //對給定數組進行排序
         
        for(int i=0;i<n;i++)   //對排好序的數組進行拼接
        {
            str += to_string(numbers[i]);
        }
         
        return str;
    }
         
    static bool compare(int a,int b)
    {
        string str1= to_string(a);
        string str2= to_string(b);
             
        return (str1+str2)<(str2+str1);           
    }

46.把數組翻譯成字符串
時間複雜度:
空間複雜度:
思路:
思路1:遞歸思想,我們定義函數f(i)表示從第i位數字開始的不同翻譯的數目,那麼f(i)=f(i+1)+g(i,i+1)*f(i+2)。當第i位和第i+1位兩位數字在10~25的範圍內時,函數g(i,i+1)的值爲1,否則爲0。
潛在問題:重複計算比較嚴重

思路2:自下而上解決問題,這樣就可以消除重複的子問題。
具體實現方面,採用一個和字符串等長的數組,從最後一位開始,最後一位初始化爲1,而後向前計算,每位等於上一位的值,或者是上一位和上上一位之和。最後返回的是該數組的第一位數。

//Leetcode NO.91:思路就是動態規劃,只是其中加入了一些判斷條件
class Solution {
public:
    int numDecodings(string s) {
        if(s.size()==0 || (s.size()> 0 && s[0]=='0'))
            return 0;
        
        vector<int> result(s.size()+1, 0);
        result[0]=1;
        
        for(int i=1;i<s.size()+1;i++)
        {
            result[i]=(s[i-1]=='0') ? 0 : result[i-1];
            if(i>1 && (s[i-2]=='1' || (s[i-2]=='2' && s[i-1]<='6')))
               result[i] +=result[i-2];
        }
        return result.back();
    }
};

47.禮物的最大價值
時間複雜度:
空間複雜度:
思路:採用動態規劃思想。(本質上求解思路是從右下到左上的,即與題目相反)
用一個1*n的數組存儲和,代表的是到達該位置時所能獲得的最大價值。(這個一維數組是從二維矩陣簡化來的,因爲在逐層計算時,到達座標(i,j)的格子時能夠拿到的禮物的最大價值只依賴於(i-1,j)和(i,j-1)這兩個格子,因此第i-2行以及更上面的所有格子禮物的最大價值實際上沒有必要保存下來,因此採用了一個一維數組代替)

這個一維數組存放的是上一行n-j個值和本行j個值,而且是在不斷更新的。(就是說這個計算過程是反着的)


48.最長不含重複字符的子字符串
時間複雜度:
空間複雜度:
思路:採用動態規劃算法
首先定義函數f(i)表示以第i個字符爲結尾的不包含重複字符的子字符串的總長長度。
場景1.如果第i個字符之前沒有出現過,那麼f(i)=f(i-1)+1;
場景2.如果第i個字符之前已經出現過,則我們先計算第i個字符和它上次出現在字符串中的位置的距離,並記爲d;
場景2.1 第一種情況是d小於或者等於f(i-1),此時第i個字符上次出現在f(i-1)對應的最長子字符串之中,因此f(i)=d;
場景2.2 第二種情況是d大於f(i-1),此時第i個字符上次出現在f(i-1)對應的最長字符串之前,因此仍然有f(i)=f(i-1)+1;


49.醜數
代碼
時間複雜度:
空間複雜度:
思路:空間換時間的思路。
用一個數組來存放從小到大順序形式的醜數,用三個指針標記三個位置,這三個位置代表三種醜數新加入數的位置,因爲在這三個指針前的數再乘該醜數,結果已經存在於數組中,因此沒有重複計算的必要。


50.第一個只出現一次的字符
問題1:字符串中第一個只出現一次的字符(只有大小寫字母)
代碼
時間複雜度:O(n)
空間複雜度:O(1)
思路:利用哈希表。
第一輪遍歷設定兩個數組,分別統計大小和小寫字母出現的次數,與此同時用兩個數組記錄相應的下標;
這樣在第二輪遍歷時,就不需要對str進行遍歷了,而只需要遍歷大小爲26的哈希表,注意ASCII表中(a=97>A=65)。

問題2:輸入兩個字符串,從第一個字符串中刪除第二個字符串中出現過的所有字符。
思路:利用長度爲26的哈希表即可;

問題3:刪除字符串中所有重複出現的字符。
思路:同樣是利用哈希表;

問題4:英語中的變位詞檢查。
思路:同樣是利用哈希表,初始化爲0,第一個單詞遍歷時,出現的字母對應位置+1;第二個單詞遍歷時,出現的字母對應位置-1,最後統計所有位置的和爲0,則說明兩個單詞互爲變位詞。

問題5:字符流中第一個只出現一次的字符
思路:同樣是利用哈希表,初始化值爲-1,首次出現時把值賦爲該字符的位置,再次出現使,把它賦爲-2,這樣當我們要尋找到目前爲止從字符流中讀出的所有字符中第一個不重複的字符時,只需要掃描整個數組,並從中找出最小的大於等於0的值對應的字符即可。


51.數組中的逆序對

時間複雜度:
空間複雜度:
思路:


52.兩個鏈表的第一個公共節點
代碼
時間複雜度:O(m+n)
空間複雜度:
思路:
step1.首先基於子函數,分別統計兩個鏈表的長度;
step2.對較長的鏈表,先走差值步;
step3.而後兩個鏈表同步比較和後移,直到找到第一個相等的數;


53.在排序數組中查找數字
代碼
問題1:數字在排序數組中出現的次數
時間複雜度:O(logn)
空間複雜度:
思路:利用二分查找算法,總體思路是先用二分查找找出目標數第一次出現所在位置start,而後再用二分查找找出目標數最後一次出現所在位置end,而後次數就等於numebrs=end-start+1。
教訓:如果數組中不包含k,則返回-1作爲標誌,這個在主函數中作爲numbers計算前的判斷條件。

這裏還遇到一個圖靈完備性的問題,簡單來說就是如果大循環是if(data.size() !=0),那麼return應該保證if成立與否都應該有值,也就是說這種寫法時,return在內部是不夠的,在if外還應該有return。(這個東西從語法上來說是沒問題的,只是因爲完備性考慮,所以當考慮不完全時,就會出現編譯錯誤)

問題2: 0~n-1中缺失的數字
思路:本質上還是利用二分查找,問題轉換爲在排序數組中找出第一個值和下標不相等的元素。

問題3:數組中數值和下標相等的元素
思路:同樣是利用二分查找,如果第i個數字的值大於i,那麼它右邊的數字都大於對應的下標,我們都可以忽略。下一輪查找我們只需要從它的左邊的數字中查找即可。


54.二叉搜索樹的第k大節點
代碼
時間複雜度:
空間複雜度:
思路:考慮到二叉搜索樹是大小有序的二叉樹,因此本質上考察的是中序遍歷算法。(遞歸實現,k的指針傳遞要注意)
具體實現方面:先一路檢測左子樹直到葉子結點,而後判斷這個k是否符合,不符合則減1,因爲是從最小值開始的,而後再檢查右子樹。
教訓:“先左–再判斷k–再右”


55.二叉樹的深度
代碼
時間複雜度:
空間複雜度:
思路:一種層層分割的思想,如果一棵樹只有一個節點,則深度爲1。如果根節點只有左子樹而沒有右子樹,則樹的深度爲左子樹深度+1;如果根節點只有右子樹而沒有左子樹,則樹的深度爲右子樹深度+1;如果根節點既有左子樹又有右子樹,則樹的深度就是兩者中深度較大的+1。

    int TreeDepth(TreeNode* pRoot)
    {
        //輸入檢測
        if(pRoot==nullptr)
            return 0;
         
        int nleft=TreeDepth(pRoot->left);
        int nright=TreeDepth(pRoot->right);
         
        return (nleft>nright)?(nleft+1):(nright+1); //思路就是層層分割,遞歸進行,返回的就是左右子樹中相對深度較大的
    }

衍生問題:平衡二叉樹的判斷
基於後序遍歷的版本代碼
思路:這種藉助了後序遍歷的思想。在遍歷一個節點之前就已經遍歷了它的左右子樹。只要在遍歷每個節點的時候記錄它的深度,就可以一邊遍歷,一邊判斷每個節點是不是平衡的。
藉助上一問的遞歸版本代碼
思路:這種比較好理解,就是不斷遞歸計算深度,然後每次遞歸比較一下左右兩邊深度。


56.數組中數字出現的次數
代碼
時間複雜度:
空間複雜度:
思路:利用的是異或操作的抵消效果。
step1.首先對數組進行一輪異或遍歷,由於數組中有兩個只出現一次的數,因此結果中必然是有位是1的;
step2.對於step1中遍歷的結果,找到結果中是1的第一個位置(這裏是位運算),該位置將用於對數組進行劃分;
step3.然後再次對數組進行遍歷,根據上面位爲1的特點,數組將被劃分爲兩個數組,每個數組裏只有一個出現次數爲1的數字,這樣在這個數組上進行異或遍歷後,結果就是那個只出現一次的數。


57.和爲s的數字
代碼
時間複雜度:O(n)
空間複雜度:O(1)
思路:(被耍了一道。。)從兩頭開始,如果和大於sum,則 j–,如果和小於sum,則 i++。如果等於sum,則直接返回。問題裏要什麼乘積最小的,實際上就是第一組。。

衍生問題:和爲s的連續正數序列
代碼
時間複雜度:
空間複雜度:
思路:思路與前面兩個數和爲s基本一致,也是兩個下標移動的策略。
教訓:
這裏不是從兩頭開始, 而是從1,2開始,而且while的條件也變爲 i<(1+sum)/2,要注意的就是對每一輪累加的優化,以及找到後的存放。


58.左旋轉字符串
代碼
時間複雜度:
空間複雜度:
思路:概括爲“拆-本翻-整翻”
step1.根據給定的翻轉個數,將字符串分爲前後兩個部分;
step2.先分別翻轉前後部分;
step3.再翻轉整個字符串,這樣就得到了結果。
教訓:
注意輸入合理性檢測,要考慮全面,返回的是str。

        if(str.size()==0 || n<0 || n>str.size())
            return str;

59.隊列的最大值
代碼
時間複雜度:
空間複雜度:
思路:用一個兩端開口的隊列(deque)存放潛在最大值的下標
step1.首先初始化第一個劃窗,找出最大的放入deque以及result隊列;
step2.接着遍歷,如果已有數字小於待存入的數字,那麼已有數字從隊列頭部彈出;如果隊列頭部的數字已經不在劃窗中(靠下標之差確定),則該頭部數字從隊列頭部彈出;
教訓:必須想清楚基本原理!vector和deque不能弄混了。


60.n個骰子的點數
暫無代碼
時間複雜度:
空間複雜度:
思路:兩個6n+1長度的數組,交替進行更新


61.撲克牌中的順子
代碼
時間複雜度:
空間複雜度:
思路:三步驟,將5張牌視爲一個長度爲5的數組;
step1.首先把數組排序;
step2.其次統計數組中0的個數;
step3.最後統計排序之後的數組中相鄰數字之間的空缺總數。如果空缺的總數小於或者等於0的個數,那麼這個數組就是連續的,反之則是不連續;


62.圓圈中最後剩下的數字
思路1代碼
時間複雜度:O(mn)
空間複雜度:O(n)
思路:採用循環鏈表的數據結構,按部就班的進行循環刪減

思路2代碼
時間複雜度:O(n)
空間複雜度:O(1)
思路:基於找規律,得到如下公式
在這裏插入圖片描述

class Solution {
public:
    int LastRemaining_Solution(int n, int m)
    {
        if(n < 1 || m < 1){
            return -1;
        }
        int last = 0;
        for(int i = 2; i <= n; i++){
            last = (last + m) % i;
        }
        return last;
    }
};

63.股票的最大利潤
時間複雜度:O(n)
空間複雜度:
思路:最大利潤就是數組中所有數對的最大差值。在賣出價固定時,買入價越低,獲得的利潤越大。
如果我們在掃描到數組中的第 i 個數字時,只要我們能夠記住之前 i-1 個數字中的最小值,就能計算出在當前價位賣出時可能得到的最大利潤。
核心:編程過程中,最重要的是保存前面遍歷過的值中的最小值。(這樣只需要遍歷一遍即可)


64.求1+2+3+…+n
時間複雜度:
空間複雜度:
思路:不太有實際價值


65.不用加減乘除做加法
代碼
時間複雜度:
空間複雜度:O(1)
思路:三步走。
step1.兩個數進行異或操作,這一步的目的是進行不進位的相加;
step2.兩個數進行與操作,並且向右移動一位,這一步的目的是進行一位的進位操作;
step3.更新兩個數,其中num1用第一步的結果更新,num2用第二步的結果更新;
不斷循環,直到num2=0,即不再有進位爲止。


66.構建乘積數組(要求不能用除法)
代碼
時間複雜度:O(n)
空間複雜度:O(n)
思路:
思路1.直觀的暴力解,但是算法複雜度爲O(n2);
思路2.採用“正三角和倒三角相乘”的方法。具體來說,首先從上到下計算正三角的各行累乘結果,而後再從下到上計算倒三角的各行累乘結果,注意的是從始至終都只有一個vector,也就是最終返回的vector。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章