劍指offer思路總結

統一格式前注:

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


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


2.實現Singleton模式(略)git


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

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


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; i n t &gt; &lt;int&gt; ());
pop_heap(max.begin(),max.end(),less &lt; i n t &gt; &lt;int&gt; ());
push_heap(min.begin(),min.end(),greater &lt; i n t &gt; &lt;int&gt; ());
pop_heap(min.begin(),min.end(),greater &lt; i n t &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。