數據結構 —— 劍指Offer算法題突破口總結

注意:本文適用于已刷過題目,想短短幾分鐘快速簡單回顧的情況。沒看過《劍指offer》的讀者建議先閱讀下。

斐波那契數列

1、1、2、3、5、8、13、21、34、……
在數學上,斐波納契數列以如下被以遞歸的方法定義:F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
解法比較:

  1. 遞歸
int F(int n)  
{  
    if(n <= 0)  
        return 0;  
    else if(n == 1)   
        return 1;  
    else  
        return F(n-1) + F(n-2);  
}  

缺點:效率非常低,時間復雜度是指數級的。重復的計算量是無比巨大的。
2.迭代法。

long Fib(int n)
{
    int i;
    unsigned long a = 0, b = 1, c;
    if (n <= 1) {
        return n;
    } else {
        for (i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

求Fibonacci數列第n項時雖然要用到前面兩項的值,但它們僅作為臨時計算的中間值,不作為結果輸出,因此無保留的必要,完全可以轉化成迭代法求解

青蛙跳臺階

一次可跳1階或2階,求n階跳法,變相斐波那契數列。

青蛙跳臺階變態版本

一只青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
f(n)=2f(n-1)*
圖解:


效率更高的解法:

       if(number<=0)
           return 0;
        if(number==1)
            return 1;
         int sum=1<<(number-1);//位運算代替*2  1<<n 意思為2的n次方
        return sum;

矩形覆蓋

同青蛙跳臺階理

二進制中1的個數

      int count = 0;
        while(n!= 0){
            count++;
            n = n & (n - 1);
         }
        return count;

例:一個二進制數1100,從右邊數起第三位是處于最右邊的一個1。減去1后,第三位變成0,它后面的兩位0變成了1,而前面的1保持不變,因此得到的結果是1011.我們發現減1的結果是把最右邊的一個1開始的所有位都取反了。這個時候如果我們再把原來的整數和減去1之后的結果做與運算,從原來整數最右邊一個1那一位開始所有位都會變成0。如1100&1011=1000.也就是說,把一個整數減去1,再和原整數做與運算,會把該整數最右邊一個1變成0.那么一個整數的二進制有多少個1,就可以進行多少次這樣的操作。

數值的整數次方

給定一個double類型的浮點數base和int類型的整數exponent。求base的exponent次方。
考慮:

  • 指數為0的情況
  • 指數為負數->對指數求絕對值,算出次方結果后取倒數,倒數還要判斷底數為0的情況。
  • 浮點數的比較是有誤差的,因此不能用簡單的a==0來比較。一般的比較方式是,相減的差在一個很小的區間內,我們就認為是相等的
(float1- float2 > -0.0000001) && (float1 -float2 < 0.0000001)

最佳解決:


可用>>1代替/2

 if(exponent==0)
            return 1;
         if(exponent==1)
            return base;
         if(exponent==-1)
             return 1.0/base;
        if((exponent&1)==1){
            //奇數
            double result=Power(base,(exponent-1)/2);
            result*=result;
            result*=base;
            return result;
        }else{
            //偶數
            double result=Power(base,exponent/2);
            result*=result;
            return result;
        }

從尾到頭打印鏈表

采用棧臨時存儲遍歷的節點值,遍歷完后棧輸出。

替換空格

將長度為1的空格替換為長度為3的“%20”,字符差的長度變長。如果允許我們開辟一個新的數組來存放替換空格后的字符串,那么這道題目就非常簡單。但是如果面試官要求在原先的字符串上操作,并且保證原字符串有足夠長的空間來存放替換后的字符串,那么我們就得另想方法。
如果從前往后替換字符串,那么保存在空格后面的字符串肯定會被覆蓋,那么我們就考慮從后往前進行替換。
首先遍歷原字符串,找出字符串的長度以及其中的空格數量,
根據原字符串的長度和空格的數量我們可以求出最后新字符串的長度。
設置兩個指針point1和point2分別指向原字符串和新字符串的末尾位置。
如果point1指向內容不為空格,那么將內容賦值給point2指向的位置,如果point1指向為空格,那么從point2開始賦值“02%”
直到point1==point2時表明字符串中的所有空格都已經替換完畢。

二維數組中的查找

數組每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。
一張圖說明,查找7為例:


旋轉數組的最小數字

例:{3,4,5,1,2}為{1,2,3,4,5}的旋轉數組,求最小數字。
可用二分查找法。
注意:存在該情況{1,2,3,4,5}仍然是數組的選擇。
特殊情況:{1,0,1,1}(a[start]==a[end]==a[mid])此時只能采用順序查找。

求鏈表中倒數第k個數

首先要先判斷k和鏈表的長度比較,如果k<=鏈表長度,
則先指針1走k步,指針2和指針1一起走。指針1指向NULL的時候,指針2所指就是倒數第k個數。

從上往下打印二叉樹

采用隊列作為臨時存儲容器

二叉樹深度

  int TreeDepth(TreeNode* pRoot)
    {
        if(pRoot==NULL)
            return 0;
        return max(TreeDepth(pRoot->left),TreeDepth(pRoot->right))+1;
    }

連續子數組的和

例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和為8(從第0個開始,到第3個為止)子向量的長度至少是1。【動態規劃思想】

 public int FindGreatestSumOfSubArray(int[] array) {
         int sum=array[0];
         int max=array[0];
        for(int i=1;i<array.length;i++){
            if(sum<=0){
                sum=array[i];
            }else
                sum+=array[i];
            if(max<sum)
                max=sum;
        }
        return max;
    }

兩個棧實現隊列

stack1只管push
stack2為空時把stack1內容彈出,一一入stack2棧,stack2棧頂為要pop的元素,若stack2不為空,直接出棧。

包含min函數的棧

例存儲為stack,采用棧minStack輔助存min,當push進stack更小值時,minStack存入當前push的值,若push的值比minStack棧頂值大,則minStack再次入棧棧頂的值。stack pop時,minStack比較大小,若小于pop的值,minStack跟著pop。

數字在排序數組中出現的次數

二分查找出頭和尾,尾下標-頭下標+1為出現次數。
思路簡單,但是要注意越界的坑!

   public int GetNumberOfK(int[] array, int k) {
        int end = findEnd(array, 0, array.length-1, k);
        int start = findStart(array, 0, array.length-1, k);
        if (end == -1 || start == -1)
            return 0;
        return end-start+1;
    }

    public int findStart(int[] array, int start, int end, int k) {
        if (start > end||end>=array.length||start<0)
            return -1;
        int mid = (start + end) / 2;
        if (array[mid] == k) {
            if (mid - 1 < 0)
                return mid;
            if (array[mid - 1] != k)
                return mid;
            else
                return findStart(array, start, mid - 1, k);
        } else if (array[mid] > k)
            return findStart(array, start, mid - 1, k);
        else
            return findStart(array, mid + 1, end, k);
    }

    public int findEnd(int[] array, int start, int end, int k) {
        if (start > end||end>=array.length||start<0)
            return -1;
        int mid = (start + end) / 2;
        if (array[mid] == k) {
            if (mid + 1 >= array.length)
                return mid;
            if (array[mid + 1] != k)
                return mid;
            else
                return findEnd(array, mid + 1, end, k);
        } else if (array[mid] > k)
            return findEnd(array, start, mid - 1, k);
        else
            return findEnd(array, mid + 1, end, k);
    }

數組中出現次數超一半的數字

沒什么好說的,出現次數超過一半,那么排序后該數字必然會在數組中位線處。
另一種思路:符合條件的數字,則它出現的次數比其他所有數字出現的次數和還要多。在遍歷數組時保存兩個值:一是數組中一個數字,一是次數。遍歷下一個數字時,若它與之前保存的數字相同,則次數加1,否則次數減1;若次數為0,則保存下一個數字,并將次數置為1。遍歷結束后,記得統計下數組,所保存的數字在數組中出現的次數是否超過數組的一半長度,不超過直接return 0,否則返回保存的數字。

反轉鏈表

三個指針pPrev、p、pNext、tmp,初始化時分別指向NULL、head、head->next、head->next->next。重復進行p->next=pPrev操作。再一一挪到下一個指向。

if(pHead==NULL)
        return NULL;
    ListNode* pPrev=NULL;
    ListNode* p=pHead;
    ListNode* pNext=p->next;
    while(p!=NULL)
    {
        ListNode* tmp;
        if(pNext!=NULL)
            tmp=pNext->next;
        p->next=pPrev;
        pPrev=p;
        p=pNext;
        pNext=tmp;

    }
    return pPrev;

合并兩個排序的鏈表

分別從link1和link2的頭部開始比較,誰小誰就作為新鏈表的next點。
需要注意的情況:

  • 兩個鏈表都為NULL
  • 有一個鏈表為NULL
  • 兩個鏈表長度不一致
  • 第一次比較時newLink表頭為NULL,賦值給頭,下一次比較賦值給newLink的next,且要有臨時變量存儲newLink的頭指針(最后返回臨時變量),因為在循環中不斷newLink=newLink->next后返回newLink,此時newLink指向鏈表的中部而不是頭。

調整數組順序使奇數位于偶數前面

  • 不穩定算法(不強調相對順序):頭尾兩個指針,頭遇到偶數停止,尾遇到奇數停止,交換,一直到頭指針下標大于尾指針下標。
  • 穩定算法(調整完保證奇數和奇數,偶數和偶數之間的相對位置不變):
    采用插入排序思想。找到奇數時,往前0~奇數位置范圍新找偶數,交換位置。

丑數

只包含因子2、3和5的數稱作丑數,習慣上把1當做是第一個丑數。
//1,2,3,4,5,6,8,9,10,12,15,16,18....
分析:

//用數組存儲計算出來的丑數,返回a[index-1]即可(數組從0開始)
(1)index=1,num=a[index-1]=a[0]=1
(2)index=2,num=min(a[index-2]*2,a[index-2]*3,a[index-2]*5);
//前一個數乘以3個因子的最小值  也就是1*2和1*3和1*5比較,min為2。
(3)index=3,num=min(a[index-3]*2,a[index-3]*3,a[index-3]*5,a[index-2]*2,a[index-2]*3,a[index-2]*5);
//前2個數乘以3個因子的最小值。
也就是1*2和1*3和1*5和2*2和2*3和2*5比較,min為2。
但是此時的丑數要大于前一個丑數!!,所以取次小值3。

最好的辦法是循環計算得到 第一個 *2大于下一個目標數的丑數的下標
                                         第一個 *3大于下一個目標數的丑數的下標
                                         第一個 *5大于下一個目標數的丑數的下標
分別保存下來,然后求這三個數的最小值。
              if (index <= 0)
            return 0;
        if (index == 1)
            return 1;
        int a[index];
        a[0] = 1;
        int index1 = 0, index2 = 0, index3 = 0;
        for (int i = 1; i < index; i++) {
            int min = getMin(a[index1] * 2, a[index2] * 3, a[index3] * 5);
            a[i] = min;
            while (a[index1] * 2 <= a[i])
                index1++;
            while (a[index2] * 3 <= a[i])
                index2++;
            while (a[index3] * 5 <= a[i])
                index3++;
        }
        return a[index - 1];

第一次只出現一次的字符

java用哈希表,先掃描一遍,統計各個字符出現的次數。
再掃描第二遍,去哈希表取對應的次數,一旦等于1,跳出循環,返回下標。
c用數組代替哈希表,總共只有256個字符,數組長度256即可。

二叉樹的鏡像

沒什么好說,上關鍵代碼。

 void Mirror(TreeNode* root)
    {
        if(root==NULL)
            return ;
        if(root!=NULL)
        {
            TreeNode* tmp=root->left;
            root->left=root->right;
            root->right=tmp;
            if(root->left)
                Mirror(root->left);
            if(root->right)
                Mirror(root->right);

        }
    }

棧的壓入彈出序列

輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序列。(注意:這兩個序列的長度是相等的)
思路:
模擬堆棧操作:將原數列依次壓棧,棧頂元素與所給出棧隊列相比,如果相同則出棧,接著比較出棧隊列下一位。如果不同則繼續壓棧,直到原數列中所有數字壓棧完畢。
檢測棧中是否為空,若空,說明出棧隊列可由原數列進行棧操作得到。否則,說明出棧隊列不能由原數列進行棧操作得到。


兩個鏈表的第一個公共結點

兩個單向鏈表存在公共點,類似“Y”

  • 方法一:遍歷兩個鏈表,存入棧。一一出棧,最后一個相同的結點,即為第一個公共結點。
  • 方法二:遍歷兩個鏈表,獲取長度,長的鏈表先走len1-len2步,然后兩個鏈表一起遍歷,第一個相同的結點為第一個公共結點。

不用加減乘除做加法

寫一個函數,求兩個整數之和,要求在函數體內不得使用+、-、*、/四則運算符號。

 int Add(int num1, int num2)
    {
        while(num2!=0)
        {  
            int n=num1^num2;
            num2=(num1&num2)<<1;
            num1=n;

        }
        return num1;
    }

根據先序和中序重建二叉樹

遞歸思想
假定已知二叉樹如下:

7
/ \
10 2
/ \ /
4 3 8
\ /
1 11
那么它的先序遍歷和中序遍歷的結果如下:
preorder = {7,10,4,3,1,2,8,11}
inorder = {4,10,3,1,7,11,8,2}
需要關注幾個重要的點:
1)先序遍歷的第一個結點總是根結點。先序遍歷時父結點總是在子結點前遍歷。
2)可以觀察到在中序遍歷中,7是第4個值(從0開始算起)。由于中序遍歷順序為:左子樹,根結點,右子樹。所以7左邊的{4,10,3,1} 這四個結點屬于左子樹,而根結點7右邊的{11,8,2}屬于右子樹。
3)可以從上面的結論得到遞歸式。在構建了根結點7后,我們可以根據中序遍歷{4, 10, 3, 1} 和{11,8,2}分別構建它的左子樹和右子樹。我們同時需要相應的先序遍歷結果用于發現規律。我們可以由先序遍歷知道左右子樹的先序遍歷分別是{10,4, 3, 1}和{2, 8, 11}。左右子樹也分別為二叉樹,由此可以遞歸來解決問題。

  public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        
        return build(pre,0,pre.length-1,in,0,in.length-1);
        
    }
     public TreeNode build(int [] pre,int prestart,int preend,int [] in,int instart,int inend) {
        
        if(prestart>preend||instart>inend)
            return null;
         TreeNode root=new TreeNode(pre[prestart]);
         for(int i=instart;i<=inend;i++){
             if(in[i]==pre[prestart]){
                    root.left=build(pre,prestart+1,prestart+i-instart,in,instart,i-1);
                    root.right=build(pre,prestart+i-instart+1,preend,in,i+1,inend);
                 break;
             }
         }
      
        return root;
    }

二叉搜索樹的后序遍歷序列

判斷一個序列是否是二叉搜索樹的后序遍歷序列。二叉搜索樹的后序遍歷滿足以下條件,例:{1,3,2,5,7,6,4}。最后一位4為根節點,{1,3,2}為左子樹,{5,7,6}為右子樹。再繼續遞歸,2為根節點,1為左子樹,3為右子樹。6為根節點,5為左子樹,7為右子樹。

 public boolean VerifySquenceOfBST(int[] sequence) {
        if (sequence.length <= 0)
            return false;
        return build(sequence, 0, sequence.length - 1);
    }

    public boolean build(int[] sequence, int start, int end) {
        if (start > end || end < 0 || start >= sequence.length) {
            return true;
        }
        int root = sequence[end];
        int j = end - 1;
        while (j >= start && sequence[j] > root) {
            j--;//找到第一個小于根節點的下標
        }
        for (int i = start; i <= j; i++) {
            if (sequence[i] > root)
                return false;
                    //判斷左子樹序列是否都小于根節點,不小于則不滿足
        }
                //檢查左子樹和右子樹  
        return build(sequence, start, j) && build(sequence, j + 1, end - 1);
    }

樹的子結構

遞歸比較

 bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{

    if(pRoot1==NULL||pRoot2==NULL)
        return false;

    return find(pRoot1,pRoot2)||HasSubtree(pRoot1->left,pRoot2)||HasSubtree(pRoot1->right,pRoot2);
  //用短路或代替flag
}
bool find(TreeNode* pRoot1, TreeNode* pRoot2)
{

    if(pRoot2==NULL)
        return true;
    if(pRoot1==NULL)
        return false;
    if(pRoot1->val==pRoot2->val)
        return find(pRoot1->left, pRoot2->left)&&find(pRoot1->right, pRoot2->right);
         return false;

}

順時針打印矩陣

如果覺得注意邊界情況太麻煩,可以采用魔方的逆時針旋轉的方法,一直做取出第一行的操作
例如
1 2 3
4 5 6
7 8 9
輸出并刪除第一行后,再進行一次逆時針旋轉,就變成:
6 9
5 8
4 7
繼續重復上述操作即可。

最小的k個數

快排后輸出前k個。

平衡二叉樹

空樹或它的左右兩個子樹的高度差的絕對值不超過1,且左右子樹都是平衡二叉樹。

bool IsBalanced_Solution(TreeNode* pRoot) {
        if(pRoot==NULL)
            return true;
        int height=getHeight(pRoot->left)-getHeight(pRoot->right);
        if(height>1||height<-1)
            return false;
        return IsBalanced_Solution(pRoot->left)&&IsBalanced_Solution(pRoot->right);
    }
    int  getHeight(TreeNode* pRoot){
        if(pRoot==NULL)
            return 0;
        return max(getHeight(pRoot->left),getHeight(pRoot->right))+1;
    }

本文持續更新中。。。。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 第一章 緒論 什么是數據結構? 數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。 第二章...
    SeanCheney閱讀 5,821評論 0 19
  • 1、線性表、棧和隊列等數據結構所表達和處理的數據以線性結構為組織形式。棧是一種特殊的線性表,這種線性表只能在固定的...
    霧熏閱讀 2,469評論 0 10
  • 數據 元素又稱為元素、結點、記錄是數據的基本單位 數據項是具有獨立含義的最小標識單位 數據的邏輯結構 數據的邏輯結...
    PPPeg閱讀 13,784評論 0 15
  • 數據結構和算法--二叉樹的實現 幾種二叉樹 1、二叉樹 和普通的樹相比,二叉樹有如下特點: 每個結點最多只有兩棵子...
    sunhaiyu閱讀 6,529評論 0 14
  • 一縷涼風,輕輕的卷起窗簾,柔柔的,涼涼的。剛剛沖泡的茶氤氳著茶香,電腦里依舊是我喜愛的歌在單曲循環,慵懶的坐...
    那些年聆聽的閱讀 161評論 0 0