單週速通《劍指Offer》週二

劍指Offer.13 機器人的運動範圍 中等

在這裏插入圖片描述
題目中有一個計算橫縱座標數位之和的操作,這不是題目的關鍵點,將這個計算數位之和的方法封裝起來不要干擾主要的解題邏輯。

public int sums(int x,int y){
    int ans=0;
    while (x != 0) {
        ans+=x%10;
        x/=10;
    }
    while (y != 0) {
        ans+=y%10;
        y/=10;
    }
    return ans;
}

思路一:深搜

想法比較直接,就是從起點開始用深搜的方式遍歷矩陣,控制深搜邊界的同時判斷當前訪問的位置數位和是否小於等於k,並且需要一個標記數組記錄每個位置的訪問情況,防止重複計算。

雖然題目說是上下左右都可以移動,但是我們從左上角作爲起點開始只能向右或者向下移動,如果發生向左或者向上那一定是重複的搜索。

int m,n,k;
public int movingCount(int m, int n, int k) {
    this.m=m;
    this.n=n;
    this.k=k;
    //標記訪問過的位置
    boolean[][] visited=new boolean[m][n];
    return dfs(0,0,0,visited);
}

/**
     * 深搜
     * @param i 橫座標
     * @param j 縱座標
     * @param sum   座標數位和
     * @param visited   標記數組
     * @return
     */
private int dfs(int i, int j, int sum, boolean[][] visited) {
    //如果 座標越界 或者 數位和大於k 或者 已經訪問過,則停止當前方向的深搜
    if (i==m||j==n||sum>k||visited[i][j])return 0;
    //標記爲已訪問
    visited[i][j]=true;
    //向下或者向右深搜
    return 1+dfs(i+1,j,sums(i+1,j),visited)+dfs(i,j+1,sums(i,j+1),visited);
}

//計算數位和
public int sums(int x,int y){
    int ans=0;
    while (x != 0) {
        ans+=x%10;
        x/=10;
    }
    while (y != 0) {
        ans+=y%10;
        y/=10;
    }
    return ans;
}

時間複雜度:O(mn) 最壞情況,遍歷矩陣。

空間複雜度:O(mn)

思路二:廣搜

其實和深搜的思路一樣,只是換了一個搜索的方式,採用廣搜的方式尋找符合要求的位置。

//時間複雜度:O(mn)
public int movingCount(int m, int n, int k) {
    //隊列保存座標
    Queue<int[]> queue=new ArrayDeque<>();
    //標記數組
    boolean[][] visited=new boolean[m][n];
    //廣搜
    queue.add(new int[]{0,0});
    int count=0;
    visited[0][0]=true;
    while (!queue.isEmpty()) {
        int[] poll = queue.poll();
        count++;
        //向下、向右尋找符合要求的位置入隊並標記訪問狀態
        //不越界 並且 數位和小於等於k 並且 未訪問過
        if (poll[0] + 1 < m
            && sums(poll[0] + 1, poll[1]) <= k
            &&!visited[poll[0]+1][poll[1]]){
            queue.add(new int[]{poll[0]+1,poll[1]});
            visited[poll[0]+1][poll[1]]=true;
        }
        if (poll[1] + 1 < n
            && sums(poll[0], poll[1] + 1) <= k
            &&!visited[poll[0]][poll[1] + 1]){
            queue.add(new int[]{poll[0],poll[1]+1});
            visited[poll[0]][poll[1]+1]=true;
        }
    }
    return count;
}
//計算數位和
public int sums(int x,int y){
    int ans=0;
    while (x != 0) {
        ans+=x%10;
        x/=10;
    }
    while (y != 0) {
        ans+=y%10;
        y/=10;
    }
    return ans;
}

時間複雜度:O(mn) 最壞情況,遍歷矩陣。

空間複雜度:O(mn)

劍指Offer.14_I 剪繩子 中等

tPfzp4.png

思路一:動態規劃 dp[i]數組的含義:長度爲 i 的繩子剪斷後可以得到的最大乘積。

初始化:dp[1]=dp[2]=1,即長度爲 1 和 2 的繩子最大乘積是 1 。

狀態轉移:dp[i] = Max(dp[i],Max((i-j)*j,dp[i-j]*j)),遍歷 [3,n] 長度的繩子,每種繩子都有j∈[0,i) 個位置可以進行剪斷,剪斷分爲兩種方式:1. 只在 j 位置剪斷一次,得到乘積 (i-j)*j 。2. 將剪斷後的部分 (i-j) 繼續剪斷,並選擇可以形成最大的乘積方法 dp[i-j] ,所以是 dp[i-j]*j 。從兩種方式裏選最大的。

//時間複雜度:O(n^2)  空間複雜度:O(n)
public int cuttingRope(int n) {
    int[] dp = new int[n + 1];
    //初始化
    dp[1]=dp[2]=1;
    //[3,n] 種不通長度的繩子
    for (int i = 3; i < n + 1; i++) {
        //每個繩子有 [0,i) 個可剪斷的位置
        for (int j = 0; j < i; j++) {
            dp[i] = Math.max(dp[i],Math.max((i-j)*j,j*dp[i-j]));
        }
    }
    return dp[n];
}

優化動態規劃 學習自 @Krahets 的題解中(我並沒有購買這本神書)。

爲使乘積最大,只有長度爲 2 和 3 的繩子不應再切分,且 3 比 2 更優 (詳情見下表)
在這裏插入圖片描述

//時間複雜度:O(n)  空間複雜度:O(n)
public int cuttingRope(int n) {
    if (n < 4) return n - 1;
    int[] dp = new int[n + 1];
    dp[2] = 2;
    dp[3] = 3;
    for (int i = 4; i <= n; i++) {
        dp[i] = Math.max(2 * dp[i - 2], 3 * dp[i - 3]);
    }
    return dp[n];
}

劍指Offer.14_II 剪繩子II 中等

在這裏插入圖片描述
(一個更簡單的辦法,直接使用 BigDecimal 大數類型,方法不變和上一題一樣。)

上題的優化解法,已經基本給出了本題的思路。本題的變化在於 n 的取值更大,可能出現整形溢出的情況。

思路一:貪心算法 我們首先考慮對於一段長n的繩子,我們可以切出的結果包含什麼?

1 會包含嗎? 不會,因爲 1 * (k - 1) < k , 只要把 1 和任何一個其他的片段組合在一起就有個更大的值

2 可以

3 可以

4 可以嗎? 它拆成兩個 2 的效果和本身一樣,因此也不考慮

5 以上可以嗎? 不可以,這些繩子必須拆,因爲總有一種拆法比不拆更優,比如拆成 k / 2 和 k - k / 2

綜上, 最後的結果只包含 2 和 3 (當然當總長度爲 2 和 3 時單獨處理), 那麼很顯然 n >= 5 時, 3*(n - 3) >= 2 * (n - 2) ,因此我們優先拆成 3 ,最後剩餘的拆成 2 。最後的結果一定是由若干個 3 和 1 或 2 個 2 組成。

//時間複雜度:O(logn) 空間複雜度:O(1)
public int cuttingRope(int n) {
    if (n<4) return n-1;
    long res = 1;
    while (n > 4) {
        res *= 3;
        res %= 1000000007;
        n -= 3;
    }
    //別忘了最後一段繩子,長度 n
    return (int) (res*n%1000000007);
}

劍指Offer.15 二進制中 1 的個數 簡單

在這裏插入圖片描述
思路一:逐位統計 用 & 運算得到最後一位是 1 還是 0,每次統計完將 n 用 >>> 無符號右移 1 位。

public int hammingWeight(int n) {
    int count = 0;
    for (int i = 0; i < 32; i++) {
        count += n & 1;
        n >>>= 1;
    }
    return count;
}

思路二:減 1 統計 看例子:

n = 11010001
n = (n - 1)&n = 11010000 & 11010001 = 11010000
n = (n - 1)&n = 11001110 & 11010000 = 11000000
n = (n - 1)&n = 10111111 & 11000000 = 10000000
n = (n - 1)&n = 01111111 & 10000000 = 00000000 = 0
共計算了 4 次,得到結果:有 4 個 1 。

實現這個方式,每次計算之後計數器 +1 ,直至 n 變爲 0,則統計完成。

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

劍指Offer.16 數值的整數次方 中等

在這裏插入圖片描述
思路一:暴力法 這道題暴力法是不能通過leetcode判題機,會得到一個t。但是方法本身是可以得到正確答案的,所以我們需要對他進行優化。暴力法的想法很簡單的:2^3=2*2*2。

如果n爲負,則n=-n同時x=1/x,例如2^(-3)=1/2*1/2*1/2。但是這裏要注意n的取值範圍,主要是 正整數和負整數的不同範圍限制 。

public double myPow(double x, int n) {
    if (x==0)return 0;
    if (n==0)return 1;
    double ans=1;
    long N=n;
    if (N<0){
        N=-N;
        x=1/x;
    }
    for (int i=0;i<N;i++){
        ans*=x;
    }
    return ans;
}

時間複雜度:O(n)

思路二:二分法 當我們得到x^(n/2)的時候,我們不需要再去乘上n/2個x了,而是x^(n/2)*x^(n/2)=x^n。

這個想法用遞歸很容易實現,但是需要注意的是n的奇偶性,如果n爲奇數則需要再乘上一個x。

public double myPow(double x, int n) {
    switch (n){
        case 1:return x;
        case 0:return 1;
        case -1:return 1/x;
    }
    double half=myPow(x,n/2);
    //奇偶性處理
    double rest=myPow(x,n%2);
    return half*half*rest;
}

時間複雜度:O(logn)

劍指Offer.17 打印從 1 到最大的 n 位數 簡單

在這裏插入圖片描述
思路一:冪運算 上一題剛搞過冪運算,直接拿來用。n 位數的需要申請 10^n-1 大小的數組。

//時間複雜度:O(10^n)
public int[] printNumbers(int n) {
    int size = myPow(10, n);
    int[] res = new int[size - 1];
    //打印從 1 到 10^n-1
    for (int i = 0; i < res.length; i++) {
        res[i] = i + 1;
    }
    return res;
}

private int myPow(int x, int n) {
    switch (n) {
        case 1:
            return x;
        case 0:
            return 1;
        case -1:
            return 1 / x;
    }
    int half = myPow(x, n / 2);
    int rest = myPow(x, n % 2);
    return half * half * rest;
}

劍指Offer.18 刪除鏈表的節點 簡單

tPhPn1.png

思路一:遍歷 cur 指針遍歷鏈表,同時用一個 pre 指針指向 cur 節點的前驅, cur 指針找到 val 節點之後,刪除 cur 節點即可。

//時間複雜度:O(n)
public ListNode deleteNode(ListNode head, int val) {
    if (head == null) return head;
    if (head.val == val) return head.next;
    ListNode cur = head.next, pre = head;
    while (cur != null) {
        if (cur.val != val) {
            cur = cur.next;
            pre = pre.next;
        } else {
            pre.next = cur.next;
            break;
        }
    }
    return head;
}

劍指Offer.19 正則表達式匹配 困難

tPhF76.png
在這裏插入圖片描述
思路一:回溯法 這種匹配思路其實就是不斷地減掉s和p的可以匹配首部,直至一個或兩個字符串被減爲空的時候,根據最終情況來得出結論。

如果只是兩個普通字符串進行匹配,按序遍歷比較即可:

if( s.charAt(i) == p.charAt(i) )

如果正則表達式字符串p只有一種"."一種特殊標記,依然是按序遍歷比較即可 :

if( s.charAt(i) == p.charAt(i) || p.charAt(i) == '.' )

上述兩種情況實現時還需要判斷字符串長度和字符串判空的操作。

但是,"*"這個特殊字符需要特殊處理,當p的第i個元素的下一個元素是星號時會有兩種情況:

  1. i元素需要出現0次,我們就保持s不變,將p的減掉兩個元素,調用isMatch。例如s:bc、p:a*bc,我們就保持s不變,減掉p的"a*",調用isMatch(s:bc,p:bc)。
  2. i元素需要出現一次或更多次,先比較i元素和s首元素,相等則保持p不變,s減掉首元素,調用isMatch。例如s:aabb、p:a*bb,就保持p不變,減掉s的首元素,調用isMatch(s:abb,p:a*bb)。

此時存在一些需要思考的情況,例如s:abb、p:a*abb,會用兩種方式處理:

  1. 按照上述第二種情況比較i元素和s首元素,發現相等就會減掉s的首字符,調用isMatch(s:bb,p:a*abb)。在按照上述第一種情況減去p的兩個元素,調用isMatch(s:bb,p:abb),最終導致false。
  2. 直接按照上述第一種情況減去p的兩個元素,調用isMatch(s:abb,p:abb),最終導致true。

所以說這算是一種暴力方法,會將所有的情況走一邊,看看是否存在可以匹配的情況。

public boolean isMatch(String s, String p) {
    //如果正則串p爲空字符串s也爲空這匹配成功,如果正則串p爲空但是s不是空則說明匹配失敗
    if (p.isEmpty())return s.isEmpty();
    //判斷s和p的首字符是否匹配,注意要先判斷s不爲空
    boolean headMatched=!s.isEmpty()&&(s.charAt(0)==p.charAt(0)||p.charAt(0)=='.');
    if (p.length()>=2&&p.charAt(1)=='*'){//如果p的第一個元素的下一個元素是*
        //則分別對兩種情況進行判斷
        return isMatch(s,p.substring(2))||
            (headMatched&&isMatch(s.substring(1),p));
    }else if (headMatched){//否則,如果s和p的首字符相等
        return isMatch(s.substring(1),p.substring(1));
    }else {
        return false;
    }
}

時間複雜度:O((n+m)*2^(n+m/2)) n和m分別是s和p的長度

思路二:動態規劃法 本題的dp數組的含義就是:dp[i][j]就是s的前i個元素是否可以被p的前j個元素所匹配。

我們知道了dp數組的含義之後就知道了dp數組的幾個細節:

  1. dp[0][0]一定是true,因爲s爲空且p也爲空的時候一定是匹配的;dp[1][0]一定是false,因爲s有一個字符但是p爲空的時候一定是不匹配的。
  2. 這個boolean類型的dp數組的大小應該是dp[s.length+1][p.length+1],因爲我們不僅僅要分別取出s和p的所有元素,還要表示分別取s和p的0個元素時候(都爲空)的情況。
  3. 當寫到dp[s.length][p.length]的時候,我們就得到了最終s和p的匹配情況。
  4. dp[1][0]~dp[s.length][0]這一列都是false,因爲s不爲空但是p爲空一定不能匹配。

所以創建好dp數組之後,初始化dp[0][0]=true、dp[0][1]=false、dp[1][0]~dp[s.length][0]都是false。然後將第一行即dp[0][2]到dp[0][p.length]的元素初始化。

第一行初始化思路:如果不爲空的p想要匹配上爲空的s,因爲此時p已經不爲空,則需要p是"a*"、“b*”、“c*”。。。這種形式的才能匹配上。

然後填寫數組的其餘部分,這個過程中如果p.charAt(j)==’*'依然是遵循上題中的兩種情況;否則就判斷兩個字符串的i和j號字符是否相等,相等則分別減除當前字符繼續判斷,不相等則直接等於false。

public boolean isMatch(String s, String p) {
    //需要分別取出s和p爲空的情況,所以dp數組大小+1
    boolean[][] dp=new boolean[s.length()+1][p.length()+1];
    //初始化dp[0][0]=true,dp[0][1]和dp[1][0]~dp[s.length][0]默認值爲false所以不需要顯式初始化
    dp[0][0]=true;
    //填寫第一行dp[0][2]~dp[0][p.length]
    for (int k=2;k<=p.length();k++){
        //p字符串的第2個字符是否等於'*',此時j元素需要0個,所以s不變p減除兩個字符
        dp[0][k]=p.charAt(k-1)=='*'&&dp[0][k-2];
    }
    //填寫dp數組剩餘部分
    for (int i=0;i<s.length();i++){
        for (int j=0;j<p.length();j++){
            //p第j個字符是否爲*
            if (p.charAt(j)=='*'){
                //兩種情況:1.s不變[i+1],p移除兩個元素[j+1-2]。
                // 2.比較s的i元素和p的j-1(因爲此時j元素爲*)元素,相等則移除首元素[i+1-1],p不變。
                dp[i+1][j+1]=dp[i+1][j-1]||
                    (dp[i][j+1]&&headMatched(s,p,i,j-1));
            }else {
                //s的i元素和p的j元素是否相等,相等則移除s的i元素[i+1-1]和p的j元素[j+1-1]
                dp[i+1][j+1]=dp[i][j]&&headMatched(s,p,i,j);
            }
        }
    }
    return dp[s.length()][p.length()];
}
//判斷s第i個字符和p第j個字符是否匹配
public boolean headMatched(String s,String p,int i,int j){
    return s.charAt(i)==p.charAt(j)||p.charAt(j)=='.';
}

時間複雜度:O(n*m) n和m分別是s和p的長度

有了第一題總結的"經驗"之後,這道題邏輯上不難理解,但是細節上尤其各種下標值非常的噁心。

劍指Offer.20 表示數值的字符串 中等

在這裏插入圖片描述
礙於篇幅,就贅述這種噁心人的題目了!

之前寫過一篇博文專門記錄過這道題:[LeetCode No.65]——什麼是面向測試編程?看看本這道題就知道了!口區

劍指Offer.21 調整數組順序使奇數位於偶數前面 簡單

在這裏插入圖片描述
思路一:雙指針,快排變形LeetCode NO.75 顏色分類 中變形三路快排思路的簡化版。雙指針,分別找順序第一個偶數,和逆序第一個奇數,然後交換,一次遍歷即可完成交換。

//時間複雜度:O(n)
public int[] exchange(int[] nums) {
    int i = 0, j = nums.length - 1;
    while (i < j) {
        //順序找第一個偶數
        while (i<j&&(nums[i]&1)==1)i++;
        //逆序找第一個奇數
        while (i<j&&(nums[j]&1)==0)j--;
        //交換位置
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    return nums;
}

劍指Offer.22 鏈表中倒數第 k 個節點 簡單

tPhVhD.png

  1. 用兩個指針 slow、fast 分別指向鏈表的開頭(啞節點)。
  2. 先讓 fast 指針逐步移動到距離 slow 指針 k 的位置上,也就是上 slow 指針和 fast 指針 n 個間隔。
  3. 讓 slow 指針和 fast 指針同時向後移動,直至 fast 指針爲 null。
  4. 此時 slow 指針指向的就是倒數第 k 個節點。
//時間複雜度:O(n)
public ListNode getKthFromEnd(ListNode head, int k) {
    ListNode fast = head, slow = head;
    for (int i = 1; i <= k; i++) {
        fast = fast.next;
    }
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    return slow;
}

劍指Offer.24 翻轉鏈表 簡單

在這裏插入圖片描述
思路一:三指針迭代 curr 指向當前待翻轉的節點、pre 指向前驅、next 記錄後繼。

//時間複雜度:O(N)
public ListNode reverseList(ListNode head) {
    ListNode curr = head, pre = null;
    while (curr != null) {
        ListNode next = curr.next;
        curr.next = pre;
        pre = curr;
        curr = next;
    }
    return pre;
}

劍指Offer.25 合併兩個排序的鏈表 簡單

tPhm1H.png

思路一:遍歷 遍歷比較每個節點,根據節點大小,連接成一個新鏈表。最後不要忘了,較長鏈表的剩餘部分直接拼接在新鏈表尾部。

//時間複雜度:O(n)    較短的那個鏈表長度爲 n
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(-1);
    ListNode p = dummy;
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            p.next = l1;
            l1 = l1.next;
        } else {
            p.next = l2;
            l2 = l2.next;
        }
        p = p.next;
    }
    p.next = l1 != null ? l1 : l2;
    return dummy.next;
}

本人菜鳥,有錯誤請告知,感激不盡!

更多題解源碼和學習筆記:githubCSDNM1ng

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章