劍指offer線性數(shù)據(jù)結構
劍指offer是找工作開始后刷的第一本書,刷題用牛客網(wǎng)。這本書可以說是已經(jīng)總結歸納的很好了,按照 基礎知識,高質(zhì)量代碼,解決問題的思路以及優(yōu)化時間和空間的效率分成了好幾章。對于初次刷題的人很有啟發(fā)的作用,為了加深印象,決定按照 數(shù)據(jù)結構的類型/算法類型 重新整理一遍,同時加入Leetcode相同tag的題。第一遍刷題用的是C++,很多時候有些題想不到思路,得回去看書,回去翻牛客網(wǎng)的答案。總結完之后打算二刷,用Python再刷一遍。(還是Java吧,Python就是簡化版?zhèn)未a,刷了幾題之后python的確簡單)
線性數(shù)據(jù)結構
字符串類
下標的移動很關鍵. 如何移動下標使得效率高?(怎么利用冗余的信息?)
如果是插入刪除,如何使移動的次數(shù)最少?
查找如何用空間換時間?
帶特殊條件的查找類,如何控制邊界條件?
如何把問題分解為 遞歸?回溯?分治?動態(tài)規(guī)劃?
string類和char *str的區(qū)別以及string類含有什么標準函數(shù)? char* 和string的的互相變換?
回文問題
字符串的查找都見過的題以及去思科面試被兩個面試官都問過的題:判斷是不是判斷是不是回文, 雙指針即可。
變種題: 給定一個字符串,問是否能通過添加一個字母將其變?yōu)榛匚拇?/p>
思路:分析題意,本身是回文,添加一個字母一定是回文。本身不是回文,則可以允許其中一個字符不匹配,這時處理好指針的移動。結束條件,兩指針相遇。
查找子串:模式匹配算法
字符串為T,模式為P,求P是不是T的子串。
樸素/KMP/BM算法
樸素的方法為回溯的方法,如果不同,回溯,復雜度o(mn),又稱Brute Force(暴力搜索方法)。
-
KMP(Knuth-Morris-Pratt)算法,通過增加額外的表,利用子串自己本身的信息,移動兩個指針
本身的信息:每次不匹配時,如何移動指向pattern的指針。空間換時間的一種做法。先存下當前位置,后綴和前綴相同的長度。
-
BM算法(Boyer-Moore)
從后往前比較,如果相同,則雙指針向前移,如果不同,則移動的位數(shù)為該字符在子字符中出現(xiàn)的位置。 采用兩個規(guī)則,好后綴規(guī)則和壞字符規(guī)則。
壞字符規(guī)則:如果不匹配,不存在這個字符,則整體后移,存在,則移動,使其對齊。
好后綴規(guī)則:和KMP類似,直接移動直到對齊部分和前綴相同。
實現(xiàn)的時候,生成兩張表,一個為壞字符移動的表,一個為好后綴移動表。
子串問題
例題1:計算兩個字符串的最大公共字串的長度,字符不區(qū)分大小寫
int getCommonStrLength(char * pFirstStr, char * pSecondStr);
看到此題聯(lián)想起生物信息課上蛋白質(zhì)序列l(wèi)ocal alignment,其實為動態(tài)規(guī)劃Dynamic Programming問題。建立數(shù)值矩陣。
例題2:對于一個字符串,請設計一個高效算法,計算其中最長回文子串的長度。
給定字符串A以及它的長度n,請返回最長回文子串的長度。
測試樣例:
"abc1234321ab",12
返回:7
思路: 將該字符串翻轉(zhuǎn),則變成例題1.
基于哈希表的查找
題目描述:題目描述
在一個字符串(0<=字符串長度<=10000,全部由字母組成)中找到第一個只出現(xiàn)一次的字符,并返回它的位置, 如果沒有則返回 -1(需要區(qū)分大小寫).
分析:這題有點和桶排序類似,直接記錄字符出現(xiàn)的次數(shù)。字符串的數(shù)值范圍[0-256]。
哈希表的知識補充:1. 根據(jù)key值,可以直接訪問value,加快存取速度。哈希表重要的幾個因素, 如果鍵值不同,值相同,稱為沖突。散列函數(shù),處理沖突,載荷因子。
字符串含特殊字符的匹配
此類題目看似簡單,但字符串的指針的移動需要特別小心。
正則表達式的匹配
請實現(xiàn)一個函數(shù)用來匹配包括'.'和''的正則表達式。模式中的字符'.'表示任意一個字符,而*表示它前面的字符可以出現(xiàn)任意次(包含0次)。 在本題中,匹配是指字符串的所有字符匹配整個模式。例如,字符串"aaa"與模式"a.a"和"ab*ac*a"匹配,但是與"aa.a"和"aba"均不匹配
分析:此題的難點再于理清思路,比較難處理的是*,因為* 是變長的,而且影響的是前面的字符。程序中最難處理的是 .*,而且前一個字符匹配成功與否與后面的*是相關的,而且也很難確定*到底有幾個。
這時用到了遞歸。 (將復雜的循環(huán)轉(zhuǎn)換為遞歸是很方便的)
當pattern+1 == * 時, return match(str,pattern+2)|| match(str+1,pattern);返回匹配0個或者匹配1個以上的時候。
表示數(shù)值的字符串
請實現(xiàn)一個函數(shù)用來判斷字符串是否表示數(shù)值(包括整數(shù)和小數(shù))。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示數(shù)值。 但是"12e", "1a3.14", "1.2.3", "+-5"和 "12e+4.3"都不是。
分析:自動機的實現(xiàn)。自動機可以回顧研一上學的課Fundamental of CS.書籍《Logic and Language Models for Computer Science,Hamburger and Richards》 Finite Automatic Machine.
其中比較有意思的知識點事DFA和NFA, Determinstic和Non-determinstic自動機。其中NFA一個條件可以到達多種狀態(tài)。不過狀態(tài)機雖然看起來一目了然,實現(xiàn)的時候處理要很好的處理轉(zhuǎn)移給自己的狀態(tài)。
字符串中正則表達式的函數(shù)以及庫
解題的時候可以快速解答。
字符串的翻轉(zhuǎn)
例如,“student. a am I”。后來才意識到,這家伙原來把句子單詞的順序翻轉(zhuǎn)了,正確的句子應該是“I am a student.”。Cat對一一的翻轉(zhuǎn)這些單詞順序可不在行,你能幫助他么?
分析:先旋轉(zhuǎn)一遍,再找到單詞再轉(zhuǎn)一遍。
字符串轉(zhuǎn)換為整數(shù)
將一個字符串轉(zhuǎn)換成一個整數(shù)(實現(xiàn)Integer.valueOf(string)的功能,但是string不符合數(shù)字要求時返回0),要求不能使用字符串轉(zhuǎn)換整數(shù)的庫函數(shù)。 數(shù)值為0或者字符串不是一個合法的數(shù)值則返回0。
分析:按部就班 - ‘0’即可
左旋字符串
匯編語言中有一種移位指令叫做循環(huán)左移(ROL),現(xiàn)在有個簡單的任務,就是用字符串模擬這個指令的運算結果。對于一個給定的字符序列S,請你把其循環(huán)左移K位后的序列輸出。例如,字符序列S=”abcXYZdef”,要求輸出循環(huán)左移3位后的結果,即“XYZdefabc”。是不是很簡單?OK,搞定它!
分析
利用額外空間儲存的話,簡單
要是不用額外空間儲存,則翻轉(zhuǎn)即可。 前K個翻轉(zhuǎn)一次,后K個翻轉(zhuǎn)一次,再整體翻轉(zhuǎn)。
大數(shù)類題目
動態(tài)規(guī)劃類
這一類題目歸結到動態(tài)規(guī)劃當中
數(shù)組類
排序
時間復雜度最壞 | 時間復雜度最好 | 時間復雜度平均 | 穩(wěn)定性 | |
---|---|---|---|---|
冒泡排序 | n^2 | n^2 | n^2 | 穩(wěn)定 |
選擇排序 | n^2 | n^2 | n^2 | 不穩(wěn)定 |
插入排序 | n^2 | n^2 | n^2 | 穩(wěn)定 |
歸并排序 | nlogn | nlogn | nlogn | 穩(wěn)定 |
堆排序 | nlogn | nlogn | nlogn | 不穩(wěn)定 |
快速排序 | nlogn | nlogn | n^2 | 不穩(wěn)定 |
希爾排序 | n^1.5 | n^1.3 | n^2 | 不穩(wěn)定 |
桶排序 | n+m | n | n | 不穩(wěn)定 |
基數(shù)排序 | p(n+b) | p(n+b) | p(n+b) | 穩(wěn)定 |
冒泡排序,選擇排序,插入排序
這三種時間復雜大相同。
冒泡排序:二重循環(huán),和相鄰的元素比較,有點類似于水泡上升,第二重循環(huán)值為[arr.size()-1:i;-1]
選擇排序:二重循環(huán),第二重循環(huán)值為[i:arr.size()-1:1],每次選擇最小的值和i交換。
插入排序:假定前n-1個數(shù)已經(jīng)排好序,現(xiàn)在將第n個數(shù)插到前面的有序數(shù)列中,使得這n個數(shù)也是排好順序的。如此反復循環(huán),直到全部排好順序。第二重的循環(huán)值為[i+1:0,-1], 看需不需要交換
歸并排序,mergeSort
- 先split
- 再merge, merge的時候
需要每次找到左邊和右邊的邊界值,需要創(chuàng)建額外的空間,然后先保存中間結果,再copy過去。 - 需要注意的地方,由于middle =(l+r)/2,此時分治法的遞歸函數(shù)里的左中右需要很清晰的處理,左開右閉,左閉右開,元素為1的時候返回。
- 外部排序?
快速排序,分治,quicksort
重要的函數(shù)為Partition函數(shù),其中還有與其類似的利用partition思想的題目。隨機選擇一個數(shù),比它大的放左邊,比它小的放右邊。
進階random quicksort
不穩(wěn)定.
class BubbleSort {
public:
void bubbleSort(vector<int> &data) {
for (int i = 0; i < data.size(); ++i) {
for (int j = data.size() - 1; j > i; --j) {
if (data[j - 1] > data[j]) {
swap(data[j - 1], data[j]);
}
}
}
}
};
class SelectionSort {
public:
void selecSort(vector<int> &data) {
for (int i = 0; i < data.size(); ++i) {
int small = i;
for (int j = i + 1; j < data.size(); ++j) {
if (data[j] < data[small]) {
small = j;
}
swap(data[small], data[i]);
}
}
}
};
class InsertSort {
public:
void insertSort(vector<int> &data) {
for (int i = 1; i < data.size(); ++i) {
for (int j = i; j > 0; j--) {
if (data[j - 1] > data[j]) {
swap(data[j - 1], data[j]);
}
}
}
}
};
class MergeSort {
public:
void merge(vector<int> &data, int l, int m, int r) {
int temp[r - l];
int index = 0;
int leftIndex = l;
int rightIndex = m;
while (leftIndex < m && rightIndex < r) {
if (data[leftIndex] > data[rightIndex]) {
temp[index++] = data[rightIndex++];
} else {
temp[index++] = data[leftIndex++];
}
}
// copy the rest to the temp
if (leftIndex < m) {
while (leftIndex < m) {
temp[index++] = data[leftIndex++];
}
} else {
while (rightIndex < r) {
temp[index++] = data[rightIndex++];
}
}
// copy back
for (int i = l; i < r; ++i) {
data[i] = temp[i - l];
}
}
void mergeSort(vector<int> &data, int l, int r) {
// [l,r),數(shù)組大小為1的時候,即l<r-1的時候返回, 或者也可以為[l,r],則上面不等式要加等號
if (l < r - 1) {
int m = (l + r) / 2;
mergeSort(data, l, m);
mergeSort(data, m, r);
merge(data, l, m, r);
}
}
};
class QuickSort {
public:
void quickSort(vector<int> &data, int left, int right) {
if (left < right) {
int pi = partition(data, left, right);
quickSort(data, left, pi - 1);
quickSort(data, pi + 1, right);
}
}
int partition(vector<int> &data, int left, int right) {
int pivot = data[right];
if (left < right) {
int rightIndex = right;
int index = right - 1;
while (index >= left) {
if (data[index] >= pivot) {
//這段代碼是有錯誤的 比如[3,1,2],1和1交換后,rightIndex卻移了.網(wǎng)上找到一段更通用的代碼12.29
swap(data[index], data[rightIndex - 1]);
rightIndex--;
}
index--;
}
swap(data[rightIndex], data[right]);
return rightIndex;
}
}
};
希爾排序 Shell sort
在要排序的一組數(shù)中,根據(jù)某一增量分為若干子序列,并對子序列分別進行插入排序。
然后逐漸將增量減小,并重復上述過程。直至增量為1,此時數(shù)據(jù)序列基本有序,最后進行插入排序。時間復雜度減少
不穩(wěn)定
什么腦回路,先擱置。
- 增量序列+插入排序
- 增量序列為size/2這樣計算出來
- 最壞情況 O(N^2),各種間隔序列的排序沒有交換,增量序列不互質(zhì),這樣每次都不起作用(youtube上)
- Hibbard增量序列。D=2k-1,相鄰元素互質(zhì),T=(N3/2),avg(N^5/4)
- Sedgewick增量序列
- 依賴于增量序列的選擇,跳著排,不穩(wěn)定。
計數(shù)排序
有點類似與哈希表的感覺,適用于海量數(shù)據(jù)并且數(shù)據(jù)值在一定范圍內(nèi)的時候。
基數(shù)排序Radix Sort創(chuàng)建10個桶,再從個位開始直到最高位。
此種排序方法沒有接觸過,實現(xiàn)一遍。基數(shù)=10,建立桶,此為優(yōu)先,LSD least significant digit,次位優(yōu)先。時間復雜度,時間復雜度,T=O(P(n+b)) 桶的個數(shù)和n。 別的用途,多關鍵字的排序。時間復雜度取決于放入桶中以及收集。
堆排序
什么是堆?
不穩(wěn)定,堆排序是不穩(wěn)定的。理論上看很好,實際效果雖然是O(logn),但常數(shù)大。快速排序總是可以找到最壞的情況,平均時間復雜度為nlogn,實際中運用條件。
查找和排序
基本思路是 O(n) -> O(logn),以及要靈活利用數(shù)組本身的規(guī)律。
題目描述:LL今天心情特別好,因為他去買了一副撲克牌,發(fā)現(xiàn)里面居然有2個大王,2個小王(一副牌原本是54張_)...他隨機從中抽出了5張牌,想測測自己的手氣,看看能不能抽到順子,如果抽到的話,他決定去買體育彩票,嘿嘿!!“紅心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是順子.....LL不高興了,他想了想,決定大\小 王可以看成任何數(shù)字,并且A看作1,J為11,Q為12,K為13。上面的5張牌就可以變成“1,2,3,4,5”(大小王分別看作2和4),“So Lucky!”。LL決定去買體育彩票啦。 現(xiàn)在,要求你使用這幅牌模擬上面的過程,然后告訴我們LL的運氣如何, 如果牌能組成順子就輸出true,否則就輸出false。為了方便起見,你可以認為大小王是0。
分析: 總共五張牌,其實是一道很簡單的題,選擇排序的方式加大小王的處理即可。
題目描述:數(shù)組中有一個數(shù)字出現(xiàn)的次數(shù)超過數(shù)組長度的一半,請找出這個數(shù)字。例如輸入一個長度為9的數(shù)組{1,2,3,2,2,2,5,4,2}。由于數(shù)字2在數(shù)組中出現(xiàn)了5次,超過數(shù)組長度的一半,因此輸出2。如果不存在則輸出0。
題目描述:輸入n個整數(shù),找出其中最小的K個數(shù)。例如輸入4,5,1,6,2,7,3,8這8個數(shù)字,則最小的4個數(shù)字是1,2,3,4,。
題目描述:輸入一個整數(shù)數(shù)組,實現(xiàn)一個函數(shù)來調(diào)整該數(shù)組中數(shù)字的順序,使得所有的奇數(shù)位于數(shù)組的前半部分,所有的偶數(shù)位于數(shù)組的后半部分,并保證奇數(shù)和奇數(shù),偶數(shù)和偶數(shù)之間的相對位置不變。
題目描述:在數(shù)組中的兩個數(shù)字,如果前面一個數(shù)字大于后面的數(shù)字,則這兩個數(shù)字組成一個逆序?qū)Α]斎胍粋€數(shù)組,求出這個數(shù)組中的逆序?qū)Φ目倲?shù)P。并將P對1000000007取模的結果輸出。 即輸出P%1000000007
鏈表類的題目
鏈表類的題目,要很好的處理好指針,不要存在內(nèi)存泄露的情況。以及還有一類快慢指針的問題。總之目前來看是很單一的題目類型。
快慢指針題
題目描述:輸入一個鏈表,輸出該鏈表中倒數(shù)第k個結點。
分析: 一個快指針先走K步,走到尾部即可。
題目進階:給一個鏈表,若其中包含環(huán),請找出該鏈表的環(huán)的入口結點,否則,輸出null。
分析:
- 是否有環(huán), 一個指針每次走兩步,一個指針每次走一步。
- 入口節(jié)點:當快指針追上慢指針,有環(huán),此時快指針不動,計算環(huán)的大小K。
- 讓快指針從頭先走K步,然后當兩指針相遇,為環(huán)的入口節(jié)點。
處理好指針類
題目描述:在一個排序的鏈表中,存在重復的結點,請刪除該鏈表中重復的結點,重復的結點不保留,返回鏈表頭指針。 例如,鏈表1->2->3->3->4->4->5 處理后為 1->2->5
分析:刪除重復指針的話,得一直記錄前驅(qū)節(jié)點,所以這個時候是兩個指針一起工作。注意判斷條件不要亂。
類似題: 合并兩個排序的鏈表,反轉(zhuǎn)鏈表,注意指針的處理
雙向隊列類
題目描述:給定一個數(shù)組和滑動窗口的大小,找出所有滑動窗口里數(shù)值的最大值。例如,如果輸入數(shù)組{2,3,4,2,6,2,5,1}及滑動窗口的大小3,那么一共存在6個滑動窗口,他們的最大值分別為{4,4,6,6,6,5}; 針對數(shù)組{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]}。
分析: 該題看了論壇上的答案,為雙向隊列處理,方法巧妙。應該記一下。
位圖
樹形數(shù)據(jù)結構
二叉樹,很多時候都要用到遞歸和動態(tài)規(guī)劃。另外樹形題目范圍很大很廣,還有很多沒有聽過的樹形結構,各有特點,先介紹一下常見的一些樹。
樹的種類和基礎知識
基礎的樹形結構:二叉搜索樹,線索二叉樹,哈夫曼樹,二叉堆
平衡樹: AVL,紅黑樹,2-3樹,2-3-4,B樹,B+樹,B-樹,treap SBT
優(yōu)先隊列:左高樹,雙端堆,斐波那契堆
集合類:并查集
區(qū)間樹:線段樹,劃分樹,歸并樹
字母樹:字典樹,后綴樹,AC自動向量機
動態(tài)樹:伸展樹
-
其他
要求:時間富余時都實現(xiàn)一遍這些樹
樹的遍歷
先序和后序中序的相互變換類題目。
題目描述:重建二叉樹
分析:根據(jù)二叉樹的前序和中序/二叉樹的后序和中序重建二叉樹,關鍵點在于找準根節(jié)點。
問題:前序和后序,能重建二叉樹么?不能,前序和后序只反應出了父子關系,沒有反應出左右關系。
題目描述:輸入一個整數(shù)數(shù)組,判斷該數(shù)組是不是某二叉搜索樹的后序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的數(shù)組的任意兩個數(shù)字都互不相同。
分析:二叉樹后序遍歷的題目。左子樹,右子樹,根。此樹為二叉搜索樹,則比根節(jié)點小的為左子樹,比根節(jié)點大為右子樹,遞歸即可。
題目描述:序列化二叉樹
分析:看似這題簡單,但是卻要考慮很多種形態(tài)的樹,如果不用遞歸而用循環(huán)處理和棧隊列處理的話,會出現(xiàn)很多錯。錯誤代碼可以看提交歷史,最簡潔的方法還是用遞歸。序列化選用先序遍歷,這樣可以迅速的定位根節(jié)點。用循環(huán)來處理的時候,想到了棧,但是棧在處理空葉子的時候向上回退的時候回有問題,比如是左孩子還是右孩子,誰為父親節(jié)點,總之在提交的歷史代碼里出現(xiàn)了很多錯。故解題經(jīng)驗為盡量轉(zhuǎn)換為遞歸來做。遞歸的技巧,將專門放到另一章。
題目描述:給定一棵二叉搜索樹,請找出其中的第k小的結點。例如, (5,3,7,2,4,6,8) 中,按結點數(shù)值大小順序第三小結點的值為4。
分析:遞歸?動態(tài)規(guī)劃? root_K>k,搜尋左邊,root_K = K返回, root_K<K搜尋右邊。
遍歷二叉樹/遞歸類型遍歷
題目描述:從上到下按層打印二叉樹,同一層結點從左至右輸出。每一層輸出一行。
分析: 隊列,采用雙隊列來實現(xiàn)。
題目描述:請實現(xiàn)一個函數(shù)按照之字形打印二叉樹,即第一行按照從左到右的順序打印,第二層按照從右至左的順序打印,第三行按照從左到右的順序打印,其他行以此類推。
分析: 采用雙棧來實現(xiàn),后打印的節(jié)點的孩子在下一輪先打印。
題目描述:請實現(xiàn)一個函數(shù),用來判斷一顆二叉樹是不是對稱的。注意,如果一個二叉樹同此二叉樹的鏡像是同樣的,定義其為對稱的。
分析:左子樹走一步,右子樹也要走一步。如果轉(zhuǎn)化為遞歸的話,左右子樹對稱,左右子樹的孩子也對稱。
題目描述:給定一個二叉樹和其中的一個結點,請找出中序遍歷順序的下一個結點并且返回。注意,樹中的結點不僅包含左右子結點,同時包含指向父結點的指針。
分析: 左根右。右子樹存在,下一個,右子樹不存在,向上返回。直至返回至為父親的左孩子。
題目描述:輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)
分析: 遍歷類型題目的變種題,A得遍歷整棵樹,直至B為A的子結構。 首先找到根節(jié)點相同,然后左右子樹。還是遞歸。
題目描述:輸入一顆二叉樹的跟節(jié)點和一個整數(shù),打印出二叉樹中結點值的和為輸入整數(shù)的所有路徑。路徑定義為從樹的根結點開始往下一直到葉結點所經(jīng)過的結點形成一條路徑。(注意: 在返回值的list中,數(shù)組長度大的數(shù)組靠前)
分析:所有路徑,則該題為BFS,要遍歷所有可能的路徑。采用Queue.
題目描述:輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。
分析: 平衡二叉樹,深度差不超過1。抓準重點,左右遍歷。
題目描述:操作給定的二叉樹,將其變換為源二叉樹的鏡像。
分析:同樣也是指針的變換,將左右子樹分別交換,即為鏡像,同樣也可以為遞歸
指針變換類
題目描述:輸入一棵二叉搜索樹,將該二叉搜索樹轉(zhuǎn)換成一個排序的雙向鏈表。要求不能創(chuàng)建任何新的結點,只能調(diào)整樹中結點指針的指向。
分析: 根節(jié)點的前驅(qū)節(jié)點為左孩子的最右孩子,根節(jié)點的右邊節(jié)點為右孩子的最左孩子。可以先序遞歸,先變換自己的指針,再改變左子樹,改變右子樹。
第二次刷題的時候嘗試采用三種遍歷方式來做
遞歸和循環(huán)
能用遞歸的方法,如何用循環(huán)來實現(xiàn)??
借助stack和queue.