劍指offer刷題記錄(C++版本)(之一)

劍指offer刷題記(C++版本)

部分參考上文和牛客網討論

為了在秋招的手撕代碼環節中不出紕漏,把劍指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. 前序遍歷序列的第一個數“1”為根節點,處在中序遍歷序列的第4位
    2. 按照中序遍歷序列,則{4,7,2}為左子樹的中序遍歷結果 ,{5,3,8,6}為右子樹的中序遍歷
    3. 由于根節點處于中序遍歷序列第4位,則前序序列第2位到第4位即{2,4,7}為左子樹的前序遍歷,第5位到末端{3,5,6,8}為右子樹的前序遍歷。
    4. 由此又重新構成了兩組新的前序和中序遍歷序列,遞歸調用函數即可重建二叉樹。
    5. 注意當最后得到的子樹只有一個節點時,返回節點即可
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中有這道題目的分析。這是一道二分查找的變形的題目。
    旋轉之后的數組實際上可以劃分成兩個有序的子數組:前面子數組的大小都大于后面子數組中的元素
    注意到實際上最小的元素就是兩個子數組的分界線。本題目給出的數組一定程度上是排序的,因此我們試著用二分查找法尋找這個最小的元素。
  • 思路
    1. 我們用兩個指針left,right分別指向數組的第一個元素和最后一個元素。按照題目的旋轉的規則,第一個元素應該是大于最后一個元素的(沒有重復的元素)。
      但是如果不是旋轉,第一個元素肯定小于最后一個元素。
    2. 找到數組的中間元素。
      中間元素大于第一個元素,則中間元素位于前面的遞增子數組,此時最小元素位于中間元素的后面。我們可以讓第一個指針left指向中間元素。
      移動之后,第一個指針仍然位于前面的遞增數組中。
      中間元素小于第一個元素,則中間元素位于后面的遞增子數組,此時最小元素位于中間元素的前面。我們可以讓第二個指針right指向中間元素。
      移動之后,第二個指針仍然位于后面的遞增數組中。
      這樣可以縮小尋找的范圍。
    3. 按照以上思路,第一個指針left總是指向前面遞增數組的元素,第二個指針right總是指向后面遞增的數組元素。
      最終第一個指針將指向前面數組的最后一個元素,第二個指針指向后面數組中的第一個元素。
      也就是說他們將指向兩個相鄰的元素,而第二個指針指向的剛好是最小的元素,這就是循環的結束條件。
      到目前為止以上思路很耗的解決了沒有重復數字的情況,這一道題目添加上了這一要求,有了重復數字
      因此這一道題目比上一道題目多了些特殊情況:
      我們看一組例子:{1,0,1,1,1} 和 {1,1, 1,0,1} 都可以看成是遞增排序數組{0,1,1,1,1}的旋轉。
      這種情況下我們無法繼續用上一道題目的解法,去解決這道題目。因為在這兩個數組中,第一個數字,最后一個數字,中間數字都是1。
      第一種情況下,中間數字位于后面的子數組,第二種情況,中間數字位于前面的子數組。
      因此當兩個指針指向的數字和中間數字相同的時候,我們無法確定中間數字1是屬于前面的子數組(綠色表示)還是屬于后面的子數組(紫色表示)。
      也就無法移動指針來縮小查找的范圍。因此采用遍歷的方式取得最小值
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項開始,每一項都等于前兩項之和。
  • 思路
    1. 最容易想到的方法就是遞歸,寫法簡單,但是遞歸在大數量之下的計算量無法想象,響應時間肯定滿足不了要求。
    2. 所以循環迭代雖然是最簡單但最靠譜的方法。由此,需要時間基本上沒有優勢,如何節省資源就成了本題的關鍵問題
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為整數)
    兩種方式實現:
  1. 遞歸方式實現:(運行時間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);
        }
    }
  1. 動態規劃實現:(運行時間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)
    所以本題其實有非常多的寫法:
  1. 位移(雖然速度快簡單,但是為0或者值過大會溢出)(3ms,480k)
public:
    int jumpFloorII(int number) {
                int a=1; return a<<(number-1);
    }
  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;//返回新的第一項的值
      }
    }
};
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容