本文例子完整原始碼地址: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)