【好書推薦】《劍指Offer》之硬技能(程式設計題1~6)

OKevin發表於2019-05-31

本文例子完整原始碼地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

前一篇《【好書推薦】《劍指Offer》之軟技能》中提到了面試中的一些軟技能,簡歷的如何寫等。《劍指Offer》在後面的章節中主要是一些程式設計題並配以講解。就算不面試,這些題多做也無妨。可惜的是書中是C++實現,我又重新用Java實現了一遍,如果有錯誤或者更好的解法,歡迎提出交流。

1.賦值運算子函式

  Java不支援賦值運算子過載,略。

2.實現Singleton模式

  餓漢模式

 1 /**
 2 * 餓漢模式
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Singleton {
 7 
 8    private static Singleton singleton = new Singleton();
 9 
10    private Singleton() {
11 
12    }
13    public static Singleton getInstance() {
14        return singleton;
15    }
16 }

  優點:執行緒安全、不易出錯、效能較高。

  缺點:在類初始化的時候就例項化了一個單例,佔用了記憶體。

  飽漢模式一

 1 /**
 2 * 飽漢模式一
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Singleton {
 7 
 8    private static Singleton singleton ;
 9 
10    private Singleton() {
11 
12    }
13    public static synchronized Singleton getInstance() {
14        if (singleton == null) {
15            singleton = new Singleton();
16        }
17        return singleton;
18    }
19 }

  飽漢模式二

 1 /**
 2 * 飽漢模式二
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Singleton {
 7 
 8    private static Singleton singleton ;
 9 
10    private Singleton() {
11 
12    }
13    public static Singleton getInstance() {
14        if (singleton == null) {
15            synchronized (Singleton.class) {
16               if (singleton == null) {
17                  singleton = new Singleton();
18               }
19            }
20        }
21        return singleton;
22    }
23 }

  優點:執行緒安全,節省記憶體,在需要時才例項化物件,比在方法上加鎖效能要好。

  缺點:由於加鎖,效能仍然比不上餓漢模式。

  列舉模式

 1 /**
 2 * 列舉模式
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public enum Singleton {
 7    INSTANCE;
 8 
 9    Singleton() {
10 
11    }
12 }

  在《Effective Java》書中,作者強烈建議通過列舉來實現單例。另外列舉從底層保證了執行緒安全,這點感興趣的讀者可以深入瞭解下。儘管列舉方式實現單例看起來比較“另類”,但從多個方面來看,這是最好且最安全的方式。

3.陣列中重複的數字

題目:給定一個陣列,找出陣列中重複的數字。

  解法一:時間複雜度O(n),空間複雜度O(n)

 1 /**
 2 * 找出陣列中重複的數字
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Solution {
 7 
 8    public void findRepeat(Integer[] array) {
 9        Set<Integer> noRepeat = new HashSet<>();
10        for (Integer number : array) {
11            if (!noRepeat.contains(number)) {
12                noRepeat.add(number);
13            } else {
14                System.out.println("重複數字:" + number);
15            }
16        }
17    }
18 }

  *Set底層實現也是一個Map

  通過Map雜湊結構,可以找到陣列中重複的數字,此演算法時間複雜度為O(n),空間複雜度為O(n)(需要額外定義一個Map)。

  解法二:時間複雜度O(n^2),空間複雜度O(1)

 1 /**
 2 * 找出陣列中重複的數字
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Solution {
 7 
 8    public void findRepeat(Integer[] array) {
 9        for (int i = 0; i < array.length; i++) {
10            Integer num = array[i];
11            for (int j = i + 1; j < array.length; j++) {
12                if (num.equals(array[j])) {
13                    System.out.println("重複數字:" + array[j]);
14                }
15            }
16        }
17    }
18 }

  解法二通過遍歷的方式找到重複的陣列元素,解法一相比於解法二是典型的“以空間換取時間”的演算法

變形:給定一個長度為n的陣列,陣列中的數字值大小範圍在0~n-1,找出陣列中重複的數字。

  變形後的題目也可採用上面兩種方法,數字值大小範圍在0~n-1的特點,不借助額外空間(空間複雜度O(1)),遍歷一次(時間複雜度為O(n))的演算法

 1 /**
 2 * 找出陣列中重複的數字,陣列中的數字值大小範圍在0~n-1
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Solution {
 7    public void findRepeat(Integer[] array) {
 8        for (int i = 0; i < array.length; i++) {
 9            while (array[i] != i) {
10                if (array[i].equals(array[array[i]])) {
11                    System.out.println("重複數字:" + array[i]);
12                    break;
13                }
14                Integer temp = array[i];
15                array[i] = array[temp];
16                array[temp] = temp;
17            }
18        }
19    }
20 }

  分析:變形後的題目中條件出現了,陣列中的值範圍在陣列長度n-1以內,且最小為0。也就是說,陣列中的任意值在作為陣列的下標都不會越界,這是一個潛在的條件。根據這個潛在的條件,我們可以把每個值放到對應的陣列下標,使得陣列下標=陣列值。例如:4,2,1,4,3,3。遍歷第一個值4,此時下標為0,陣列下標≠陣列值,比較array[0]與array[4]不相等->交換,4放到了正確的位置上,得到3,2,1,4,4,3。此時第一個值為3,陣列下標仍然≠陣列值,比較array[0]與array[3]不想等->交換,3放到了正確的位置,得到4,2,1,3,4,3。此時陣列下標仍然≠陣列值,比較array[0]與array[4]相等,退出當前迴圈。依次類推,開始陣列下標int=1的迴圈。

4.二維陣列中的查詢

題目:給定一個二維陣列,每一行都按照從左到右依次遞增的順序排序,每一列都按照從上到下依次遞增的順序排序。輸入一個二維陣列和一個整數,判斷該整數是否在二維陣列中。

  解法一:遍歷n*m大小的二維陣列,時間複雜度O(n*m),空間複雜度O(1)

 1 /**
 2 * 二維陣列中查詢
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Solution {
 7 
 8    public boolean isExist(Integer[][] twoArray, Integer target) {
 9        for (int i = 0; i < twoArray.length; i++) {
10            for (int j = 0; j < twoArray[i].length; j++) {
11                if (twoArray[i][j].equals(target)) {
12                    return true;
13                }
14            }
15        }
16        return false;
17    }
18 }

  優點:簡單暴力。

  缺點:效能不是最優的,時間複雜度較高,沒有充分利用題目中“有序”的條件。

  解法二:時間複雜度O(n+m),空間複雜度O(1)

 1 /**
 2 * 二維陣列中查詢
 3 * @author OKevin
 4 * @date 2019/5/27
 5 **/
 6 public class Solution {
 7 
 8    public boolean isExist(Integer[][] twoArray, Integer target) {
 9        int x = 0;
10        int y = twoArray[0].length - 1;
11        for (int i = 0; i < twoArray.length-1 + twoArray[0].length-1; i++) {
12            if (twoArray[x][y].equals(target)) {
13                return true;
14            }
15            if (twoArray[x][y] > target) {
16                y--;
17                continue;
18            }
19            if (twoArray[x][y] < target) {
20                x++;
21            }
22        }
23        return false;
24    }
25 }

  分析:通過舉一個例項,找出規律,從右上角開始查詢。

  Integer[][] twoArray = new Integer[4][4];

  twoArray[0] = new Integer[]{1, 2, 8, 9};

  twoArray[1] = new Integer[]{2, 4, 9, 12};

  twoArray[2] = new Integer[]{4, 7, 10, 13};

  twoArray[3] = new Integer[]{6, 8, 11, 15};

5.替換空格

題目:將字串中的空格替換為“20%”。

  解法一:根據Java提供的replaceAll方法直接替換

 1 /**
 2 * 字串空格替換
 3 * @author OKevin
 4 * @date 2019/5/28
 5 **/
 6 public class Solution {
 7    public String replaceSpace(String str) {
 8        return str.replaceAll(" ", "20%");
 9    }
10 }

  這種解法沒什麼可說。但可以瞭解一下replaceAll的JDK實現。replaceAll在JDK中的實現是根據正規表示式匹配要替換的字串。

  解法二:利用空間換時間的方式替換

 1 /**
 2 * 字串空格替換
 3 * @author OKevin
 4 * @date 2019/5/28
 5 **/
 6 public class Solution {
 7    public String replaceSpace(String str, String target) {
 8        StringBuilder sb = new StringBuilder();
 9        for (char c : str.toCharArray()) {
10            if (c == ' ') {
11                sb.append(target);
12                continue;
13            }
14            sb.append(c);
15        }
16        return sb.toString();
17    }
18 }

6.從尾到頭列印連結串列

題目:輸入一個連結串列的頭節點,從尾到頭反過來列印出每個節點的值。

*由於《劍指Offer》採用C++程式語言,這題需要我們先構造出一個節點,模擬出連結串列的結構。

  定義節點

 1 /**
 2 * 連結串列節點定義
 3 * @author OKevin
 4 * @date 2019/5/29
 5 **/
 6 public class Node {
 7    /**
 8     * 指向下一個節點
 9     */
10    private Node next;
11    /**
12     * 表示節點的值域
13     */
14    private Integer data;
15 
16    public Node(){}
17 
18    public Node(Integer data) {
19        this.data = data;
20    }
21    //省略getter/setter方法
22 }

  解法一:利用棧先進後出的特點,遍歷連結串列放入棧中,再從棧推出資料

 1 /**
 2 * 逆向列印連結串列的值
 3 * @author OKevin
 4 * @date 2019/5/29
 5 **/
 6 public class Solution {
 7    public void tailPrint(Node head) {
 8        Stack<Node> stack = new Stack<>();
 9        while (head != null) {
10            stack.push(head);
11            head = head.getNext();
12        }
13        while (!stack.empty()) {
14            System.out.println(stack.pop().getData());
15        }
16    }
17 }

  這種解法“不幸”地藉助了額外的空間。

  解法二:既然使用棧的結構,實際上也就可以使用遞迴的方式逆向列印連結串列

 1 /**
 2 * 逆向列印連結串列的值
 3 * @author OKevin
 4 * @date 2019/5/29
 5 **/
 6 public class Solution {
 7    public void tailPrint(Node head) {
 8        if (head.getNext() != null) {
 9            tailPrint(head.getNext());
10        }
11        System.out.println(head.getData());
12    }
13 }

  使用遞迴雖然避免了藉助額外的記憶體空間,但如果連結串列過長,遞迴過深易導致呼叫棧溢位。

  測試程式:

 1 /**
 2 * @author OKevin
 3 * @date 2019/5/29
 4 **/
 5 public class Main {
 6    /**
 7     * 1->2->3->4->5
 8     * @param args
 9     */
10    public static void main(String[] args) {
11        Node node1 = new Node(1);
12        Node node2 = new Node(2);
13        Node node3 = new Node(3);
14        Node node4 = new Node(4);
15        Node node5 = new Node(5);
16        node1.setNext(node2);
17        node2.setNext(node3);
18        node3.setNext(node4);
19        node4.setNext(node5);
20 
21        Node head = node1;
22 
23        Solution solution = new Solution();
24        solution.tailPrint(head);
25    }
26 }

   本文例子完整原始碼地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

  持續更新,敬請關注公眾號

 

 

這是一個能給程式設計師加buff的公眾號 (CoderBuff)

相關文章