部分參考上文和牛客網討論
為了在秋招的手撕代碼環節中不出紕漏,把劍指offer從頭刷一遍
1. 二維數組中查找數字。
題目:在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。
思路:從右上角即第0行第n列來入手,如果右上角的數字大于目標,那么最右邊一列都不滿足,則考慮前一列(col--),如果這個數小于目標,則最上面一行都不滿足,則考慮下一行(row++),如果剛好是目標就可以輸出了。
bool Find(int target, vector<vector<int> > array) {
if(array.empty()) return false; //空數組直接返回false
int rows=array.size();
int cols=array[0].size();
int row=0;
int col=cols-1;
while(row<rows&&col>=0)
{
if(array[row][col]==target) return true;
else if(array[row][col]>target)
{
col--;
}
else row++;
}
return false;
}
2.替換空格。
題目:請實現一個函數,將一個字符串中的每個空格替換成“%20”。例如,當字符串為We Are Happy.則經過替換之后的字符串為We%20Are%20Happy。
思路:從后向前替換(從后往前,每個空格后面的字符只需要移動一次。從前往后,當遇到第一個空格時,要移動第一個空格后所有的字符一次;當遇到第二個空格時,要移動第二個空格后所有的字符一次;以此類推。所以總的移動次數會更多。本質上其實是先計算出替換后的長度后再使用原字符串填充,與前后填充的順序無關),先遍歷一遍統計空格的數目,然后擴張字符串使得可以放下替換后的字符,然后從后向前依次復制,非空格字符直接復制,空格字符用題目要求的替換。
//C風格的字符串最后一個字符是\n,而且是記在字符串長度里的。
void replaceSpace(char *str,int length) {
int i=0;
int cnt=0;
if(length==0||str==nullptr) return ; //空字符串
for(i=0;i<length;i++) //統計空格數
{
if(str[i]==' ')
cnt++;
}
if(cnt==0) return; //如果沒有空格自然不用替換
int newlength=length+2*cnt; //新字符串的長度
int index_new=newlength-1;
int index_old=length-1;
while(index_old>=0&&index_new>index_old) //沒有到頭或者兩個指針追上了
{
if(str[index_old]==' ')
{
//依次替換三個字符,并且把index_old--
str[index_new--]='0';
str[index_new--]='2';
str[index_new--]='%';
index_old--;
}
else
{
//如果不是空格直接復制就可以了
str[index_new--]=str[index_old--];
}
}
}
3. 從尾到頭打印鏈表。
- 題目:輸入一個鏈表,按鏈表值從尾到頭的順序返回一個ArrayList。
- 思路:1. 取出鏈表到一個新數組中 2.將數組倒置即可
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> value;
ListNode *p=NULL;
p=head; #取鏈表頭地址
while(p!=NULL){
value.push_back(p->val); //根據鏈表地址找到對應的值,使用push_back()函數為value數組尾端添加值
p=p->next; //找到鏈表下一項的地址
}
//reverse(value.begin(),value.end()); //可使用C++自帶的翻轉函數對value值進行翻轉
int temp=0;
int i=0,j=value.size()-1;
while(i<j){
temp=value[i]; //也可以用swap函數,swap(value[i],value[j]);
value[i]=value[j];
value[j]=temp;
i++;
j--;
}
return value;
}
4.重建二叉樹
- 題目:輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重復的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹并返回。
- 知識點:關于二叉樹的前序、中序、后序三種遍歷
-
思路:本題的關鍵在于如何應用給定的中序遍歷序列和前序遍歷序列。先從實例分析如何重建二叉樹
- 前序遍歷序列的第一個數“1”為根節點,處在中序遍歷序列的第4位
- 按照中序遍歷序列,則{4,7,2}為左子樹的中序遍歷結果 ,{5,3,8,6}為右子樹的中序遍歷
- 由于根節點處于中序遍歷序列第4位,則前序序列第2位到第4位即{2,4,7}為左子樹的前序遍歷,第5位到末端{3,5,6,8}為右子樹的前序遍歷。
- 由此又重新構成了兩組新的前序和中序遍歷序列,遞歸調用函數即可重建二叉樹。
- 注意當最后得到的子樹只有一個節點時,返回節點即可
public:
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
int vinlen=vin.size();
if(vinlen==0)
return NULL; //判斷中序長度,其實就是為了在函數無子樹時返回空,為遞歸做準備
vector<int> left_pre,right_pre,left_vin,right_vin;
TreeNode* head=new TreeNode(pre[0]); //創建根節點,根節點肯定是前序遍歷的第一個數
int gen=0;
for(int i=0;i<vinlen;i++)//找到中序遍歷根節點所在位置,存放于變量gen中
{
if (vin[i]==pre[0])
{
gen=i;
break;
}
}
//對于中序遍歷,根節點左邊的節點位于二叉樹的左邊,根節點右邊的節點位于二叉樹的右邊
//利用上述這點,對二叉樹節點進行歸并
for(int i=0;i<gen;i++)
{
left_vin.push_back(vin[i]); //中序前gen個為左子樹中序
left_pre.push_back(pre[i+1]);//前序第2個到1+gen個為左子樹前序
}
for(int i=gen+1;i<vinlen;i++)
{
right_vin.push_back(vin[i]);
right_pre.push_back(pre[i]);
}
//和shell排序的思想類似,取出前序和中序遍歷根節點左邊和右邊的子樹
//遞歸,再對其進行上述所有步驟,即再區分子樹的左、右子子數,直到葉節點
head->left=reConstructBinaryTree(left_pre,left_vin);
head->right=reConstructBinaryTree(right_pre,right_vin);
return head;
}
5.用兩個棧實現一個隊列
- 題目:用兩個棧來實現一個隊列,完成隊列的Push和Pop操作。 隊列中的元素為int類型。
- 知識點:隊列和棧的區別,簡而言之就是棧為先進后出,隊列為先進先出
-
思路:棧為先進后出,要實現隊列的Push功能不需要進行改變,而為了實現出的功能,則需要將整個棧倒過來出棧,從而達到Pop效果。因此劃分兩個棧,stack1,stack2。
- 入隊時,判斷stack1是否為空,如不為空,將元素壓入stack1;如為空,先將stack2元素倒回stack1,再將新元素壓入stack1。
- 出隊時,判斷stack2是否為空,如不為空,則直接彈出頂元素;如為空,則將stack1的元素逐個“倒入”stack2,把stack1最后一個元素彈出并出隊。
void push(int node) {
if(stack1.empty()){ //棧1空有可能數據全在棧2
while(!stack2.empty()){
stack1.push(stack2.top());
stack2.pop();
}
}
stack1.push(node);
}
int pop() {
if(stack2.empty()){ //棧2空有可能數據全在棧1
while(!stack1.empty()){
stack2.push(stack1.top());
stack1.pop();
}
}
int t=stack2.top();
stack2.pop();
return t;
}
6.重旋轉數組的最小數字
- 題目:把一個數組最開始的若干個元素搬到數組的末尾,我們稱之為數組的旋轉。 輸入一個非減排序的數組的一個旋轉,輸出旋轉數組的最小元素。 例如數組{3,4,5,1,2}為{1,2,3,4,5}的一個旋轉,該數組的最小值為1。 NOTE:給出的所有元素都大于0,若數組大小為0,請返回0。
-
知識點:劍指Offer中有這道題目的分析。這是一道二分查找的變形的題目。
旋轉之后的數組實際上可以劃分成兩個有序的子數組:前面子數組的大小都大于后面子數組中的元素
注意到實際上最小的元素就是兩個子數組的分界線。本題目給出的數組一定程度上是排序的,因此我們試著用二分查找法尋找這個最小的元素。 -
思路:
- 我們用兩個指針left,right分別指向數組的第一個元素和最后一個元素。按照題目的旋轉的規則,第一個元素應該是大于最后一個元素的(沒有重復的元素)。
但是如果不是旋轉,第一個元素肯定小于最后一個元素。 - 找到數組的中間元素。
中間元素大于第一個元素,則中間元素位于前面的遞增子數組,此時最小元素位于中間元素的后面。我們可以讓第一個指針left指向中間元素。
移動之后,第一個指針仍然位于前面的遞增數組中。
中間元素小于第一個元素,則中間元素位于后面的遞增子數組,此時最小元素位于中間元素的前面。我們可以讓第二個指針right指向中間元素。
移動之后,第二個指針仍然位于后面的遞增數組中。
這樣可以縮小尋找的范圍。 - 按照以上思路,第一個指針left總是指向前面遞增數組的元素,第二個指針right總是指向后面遞增的數組元素。
最終第一個指針將指向前面數組的最后一個元素,第二個指針指向后面數組中的第一個元素。
也就是說他們將指向兩個相鄰的元素,而第二個指針指向的剛好是最小的元素,這就是循環的結束條件。
到目前為止以上思路很耗的解決了沒有重復數字的情況,這一道題目添加上了這一要求,有了重復數字。
因此這一道題目比上一道題目多了些特殊情況:
我們看一組例子:{1,0,1,1,1} 和 {1,1, 1,0,1} 都可以看成是遞增排序數組{0,1,1,1,1}的旋轉。
這種情況下我們無法繼續用上一道題目的解法,去解決這道題目。因為在這兩個數組中,第一個數字,最后一個數字,中間數字都是1。
第一種情況下,中間數字位于后面的子數組,第二種情況,中間數字位于前面的子數組。
因此當兩個指針指向的數字和中間數字相同的時候,我們無法確定中間數字1是屬于前面的子數組(綠色表示)還是屬于后面的子數組(紫色表示)。
也就無法移動指針來縮小查找的范圍。因此采用遍歷的方式取得最小值
- 我們用兩個指針left,right分別指向數組的第一個元素和最后一個元素。按照題目的旋轉的規則,第一個元素應該是大于最后一個元素的(沒有重復的元素)。
public:
int minNumberInRotateArray(vector<int> rotateArray) {
int size = rotateArray.size();
if(size == 0){
return 0;
}//if
int left = 0,right = size - 1;
int mid = 0;
// rotateArray[left] >= rotateArray[right] 確保旋轉
while(rotateArray[left] >= rotateArray[right]){
// 分界點
if(right - left == 1){
mid = right;
break;
}//if
mid = left + (right - left) / 2;
// rotateArray[left] rotateArray[right] rotateArray[mid]三者相等
// 無法確定中間元素是屬于前面還是后面的遞增子數組
// 只能順序查找
if(rotateArray[left] == rotateArray[right] && rotateArray[left] == rotateArray[mid]){
return MinOrder(rotateArray,left,right);
}//if
// 中間元素位于前面的遞增子數組
// 此時最小元素位于中間元素的后面
if(rotateArray[mid] >= rotateArray[left]){
left = mid;
}//if
// 中間元素位于后面的遞增子數組
// 此時最小元素位于中間元素的前面
else{
right = mid;
}//else
}//while
return rotateArray[mid];
}
private:
// 順序尋找最小值
int MinOrder(vector<int> &num,int left,int right){
int result = num[left];
for(int i = left + 1;i < right;++i){
if(num[i] < result){
result = num[i];
}//if
}//for
return result;
}
7.斐波拉切數列
- 題目:大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項為0)。n<=39 。
- 知識點:斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368........這個數列從第3項開始,每一項都等于前兩項之和。
-
思路:
- 最容易想到的方法就是遞歸,寫法簡單,但是遞歸在大數量之下的計算量無法想象,響應時間肯定滿足不了要求。
- 所以循環迭代雖然是最簡單但最靠譜的方法。由此,需要時間基本上沒有優勢,如何節省資源就成了本題的關鍵問題
public:
int Fibonacci(int n) {
int f = 0, g = 1; //假設存在0項為0,0+1=1;依然滿足條件
while(n-->0) { //防止輸入負數
g += f; //求和得到第三項的值
f = g - f; //為了得到第二項的值,用新得到的第三項值g減去第一項值f得到新的f值即為第二項值
}
return f;//返回新的第一項的值
}
上面的代碼非常的巧妙,即先算出n+1項裴波那切數列值,反過來求第n項并輸出,代碼簡潔且功能正常
8.跳臺階問題
- 題目: 一只青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先后次序不同算不同的結果)。
-
思路:非常明顯的遞歸問題,跳上n級臺階的方法即跳上(n-1)級臺階的方法加上跳上(n-2)級臺階的方法。因此構成了類裴波那切數列,因此解題思路又同上題了。
然后通過實際的情況可以得出:只有一階的時候 f(1) = 1 ,只有兩階的時候可以有 f(2) = 2,f(n) = f(n-1)+f(n-2) ,(n>2,n為整數)
兩種方式實現:
- 遞歸方式實現:(運行時間578ms,占用內存488k)
public:
int jumpFloor(int number) {
if (number <= 0) {
return false;
} else if (number == 1) {
return 1;
} else if (number ==2) {
return 2;
} else {
return jumpFloor(number-1)+jumpFloor(number-2);
}
}
- 動態規劃實現:(運行時間3ms,占用內存460k)
public:
int jumpFloor(int number) {
int f = 1, g = 1; //假設存在0項為0,1+1=2;依然滿足條件
while(number-->0) { //防止輸入負數
g += f; //求和得到第三項的值
f = g - f; //為了得到第二項的值,用新得到的第三項值g減去第一項值f得到新的f值即為第二項值
}
return f;//返回新的第一項的值
}
對比可見,動態規劃不僅時間大大縮短,而且占用內存更小,而且不需要判斷語句,代碼優化可以說非常關鍵
9.變態跳臺階問題
- 題目:一只青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
-
思路:
F(N)=F(N-1)+F(N-2)+F(N-3)+...+F(1)
把N-1帶入則可以得到:
F(N-1)=F(N-2)+F(n-3)+...+F(1)
兩式相減呢?:
F(N)=2*F(N-1)
則這是一個等比數列啊,且F(1)=1,所以最后的結果就是一個冪次。
F(N)=2^(N-1)
所以本題其實有非常多的寫法:
- 位移(雖然速度快簡單,但是為0或者值過大會溢出)(3ms,480k)
public:
int jumpFloorII(int number) {
int a=1; return a<<(number-1);
}
- 調用math模塊,直接使用冪函數pow() 函數(3ms,488k)
double pow(double x, double y);用來計算以x 為底的 y 次方值,然后將結果返回。設返回值為 ret,則 ret = x^y。
#include <math.h>
class Solution {
public:
int jumpFloorII(int number) {
return pow(2,(number-1));
}
};
3.不調用math函數的話,遞歸累乘即可(3ms,476k)
class Solution {
public:
int jumpFloorII(int number) {
if (number <= 0) {
return -1;
} else if (number == 1) {
return 1;
} else {
return 2 * jumpFloorII(number - 1);
}
}
};
可見由于冪為基本運算,無論采取哪種方法,時間均拉不開差距
10.矩形覆蓋
- 題目:我們可以用2x1的小矩形橫著或者豎著去覆蓋更大的矩形。請問用n個2x1的小矩形無重疊地覆蓋一個2xn的大矩形,總共有多少種方法?
-
思路:仔細想這個問題其實跟跳臺階一樣,如果還剩兩塊磚沒鋪,即如果剩一塊磚,就只有一種鋪法如果剩下2x2的方塊沒鋪,就會有2種鋪法,一種跟剩一塊磚一樣(例如豎著鋪兩次),另外一種即跟剩一塊磚情況垂直(例如橫著鋪兩次)。因此實際上還是f(n) = f(n-1)+f(n-2)
代碼跟題8相同,需要注意測試用例設置了n=0
class Solution {
public:
int rectCover(int number) {
if(number==0)
{
return 0;
}
else
{
int f = 1, g = 1; //假設存在0項為1,1+1=2;依然滿足條件
while(number-->0) { //防止輸入負數
g += f; //求和得到第三項的值
f = g - f; //為了得到第二項的值,用新得到的第三項值g減去第一項值f得到新的f值即為第二項值
}
return f;//返回新的第一項的值
}
}
};