本文例子完整源碼地址: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
持續更新,敬請關注公眾號:coderbuff,回復關鍵字“sword”獲取相關電子書。
這是一個能給程序員加buff的公眾號 (CoderBuff)

