給定一個非空整數數組,除了某個元素只出現一次之外,其他每一個元素均出現兩次。找出那個只出現了一次的元素。算法
說明:數組
你的算法應該具備線性時間複雜度。 你能夠不使用額外空間來實現嗎?app
示例 1:ide
輸入: [2,2,1]
輸出: 1
示例 2:測試
輸入: [4,1,2,1,2]
輸出: 4
以上是原題ui
OK,先找出題目中的重點要求:this
一、線性時間複雜度:要求咱們的代碼時間複雜度最高爲O(n),不能有嵌套循環等。spa
二、不使用額外空間:要求空間複雜度最高爲O(1)。code
除此以外,還有重要的信息:orm
這個條件很是關鍵,一開始本身審題不清楚沒注意到均出現兩次這個關鍵點,按照其餘元素出現屢次的狀況處理了,這樣致使思路受限不少。
開始解題:
方法一(比較法):
思路:先對數組進行排序,而後對 nums[i] 和 nums[i + 1]進行比較,如相等,i += 2,繼續下一組比較,直到取到不相等的一組。
注意:首先這個數組的長度確定是奇數(目標數字只出現一次,其餘全部數字出現兩次),因此若是上述步驟沒有找到不相等的一組數,那麼確定是數組的最後一個數字是單獨出現的。
代碼以下:
1 public static int singleNumber(int[] nums) { 2 Arrays.sort(nums); // 排序數組 3 for (int i = 0; i < nums.length - 1; i += 2) { 4 // 找到不相等的一組,直接返回 5 if (nums[i] != nums[i + 1]) { 6 return nums[i]; 7 } 8 } 9 // 若是沒有找到不相等的一組數據,直接返回數組的最後一個數字 10 return nums[nums.length - 1]; 11 }
方法二(去重法):
思路:利用HashSet的特性,刪除重複的數組元素,最後剩下一個單獨的元素,返回便可。
直接上代碼:
1 public static int singleNumber(int[] nums) { 2 Set<Integer> set = new HashSet<>(); 3 for (int i = 0; i < nums.length; i++) { 4 if (!set.add(nums[i])) { // add成功返回true,若是set中已有相同數字,則add方法會返回false 5 set.remove(nums[i]); // 刪除重複出現的數字 6 } 7 } 8 return set.iterator().next();9 }
方法三(求差法):
思路:先對數組排序,顯而易見的,單獨出現一次的數據必然是出如今數組下標爲偶數的位置(下標從0開始),那麼全部奇數下標的元素之和減去偶數下標的元素之和,就是須要求得的結果。
代碼以下:
1 public static int singleNumber(int[] nums) { 2 int num = 0; 3 Arrays.sort(nums); 4 for (int i = 0; i < nums.length; i++) { 5 // 偶數下標位置 num += nums[i],奇數下標位置 num -= nums[i] 6 num = i % 2 == 0 ? num + nums[i] : num - nums[i]; 7 } 8 return num; 9 }
方法四(異或法):
思路:根據異或運算的特色,相同的數字通過異或運算後結果爲0,除單獨出現一次的數字外,其餘數字都是出現兩次的,那麼這些數字通過異或運算後結果必定是0。而任何數字與0進行異或運算都是該數字自己。因此對數組全部元素進行異或運算,運算結果就是題目的答案。
上代碼:
1 public static int singleNumber(int[] nums) { 2 int num = 0; 3 for (int i = 0; i < nums.length; i++) { 4 num = num ^ nums[i]; 5 } 6 return num; 7 }
總結:
其實嚴格來說,只有第四種方式是題目想要的解法,其餘三種方法都是有瑕疵的。
方法1、方法三都用到了Arrays.sort(int[] a)的方法,咱們先來看JDK提供的數組排序方法:
1 public static void sort(int[] a) { 2 DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0); 3 }
調用了DualPivotQuicksort類的靜態方法:
1 /** 2 * Sorts the specified range of the array using the given 3 * workspace array slice if possible for merging 4 * 5 * @param a the array to be sorted 6 * @param left the index of the first element, inclusive, to be sorted 7 * @param right the index of the last element, inclusive, to be sorted 8 * @param work a workspace array (slice) 9 * @param workBase origin of usable space in work array 10 * @param workLen usable size of work array 11 */ 12 static void sort(int[] a, int left, int right, 13 int[] work, int workBase, int workLen) { 14 // Use Quicksort on small arrays 15 if (right - left < QUICKSORT_THRESHOLD) { 16 sort(a, left, right, true); 17 return; 18 } 19 20 /* 21 * Index run[i] is the start of i-th run 22 * (ascending or descending sequence). 23 */ 24 int[] run = new int[MAX_RUN_COUNT + 1]; 25 int count = 0; run[0] = left; 26 27 // Check if the array is nearly sorted 28 for (int k = left; k < right; run[count] = k) { 29 if (a[k] < a[k + 1]) { // ascending 30 while (++k <= right && a[k - 1] <= a[k]); 31 } else if (a[k] > a[k + 1]) { // descending 32 while (++k <= right && a[k - 1] >= a[k]); 33 for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) { 34 int t = a[lo]; a[lo] = a[hi]; a[hi] = t; 35 } 36 } else { // equal 37 for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) { 38 if (--m == 0) { 39 sort(a, left, right, true); 40 return; 41 } 42 } 43 } 44 45 /* 46 * The array is not highly structured, 47 * use Quicksort instead of merge sort. 48 */ 49 if (++count == MAX_RUN_COUNT) { 50 sort(a, left, right, true); 51 return; 52 } 53 } 54 55 // Check special cases 56 // Implementation note: variable "right" is increased by 1. 57 if (run[count] == right++) { // The last run contains one element 58 run[++count] = right; 59 } else if (count == 1) { // The array is already sorted 60 return; 61 } 62 63 // Determine alternation base for merge 64 byte odd = 0; 65 for (int n = 1; (n <<= 1) < count; odd ^= 1); 66 67 // Use or create temporary array b for merging 68 int[] b; // temp array; alternates with a 69 int ao, bo; // array offsets from 'left' 70 int blen = right - left; // space needed for b 71 if (work == null || workLen < blen || workBase + blen > work.length) { 72 work = new int[blen]; 73 workBase = 0; 74 } 75 if (odd == 0) { 76 System.arraycopy(a, left, work, workBase, blen); 77 b = a; 78 bo = 0; 79 a = work; 80 ao = workBase - left; 81 } else { 82 b = work; 83 ao = 0; 84 bo = workBase - left; 85 } 86 87 // Merging 88 for (int last; count > 1; count = last) { 89 for (int k = (last = 0) + 2; k <= count; k += 2) { 90 int hi = run[k], mi = run[k - 1]; 91 for (int i = run[k - 2], p = i, q = mi; i < hi; ++i) { 92 if (q >= hi || p < mi && a[p + ao] <= a[q + ao]) { 93 b[i + bo] = a[p++ + ao]; 94 } else { 95 b[i + bo] = a[q++ + ao]; 96 } 97 } 98 run[++last] = hi; 99 } 100 if ((count & 1) != 0) { 101 for (int i = right, lo = run[count - 1]; --i >= lo; 102 b[i + bo] = a[i + ao] 103 ); 104 run[++last] = right; 105 } 106 int[] t = a; a = b; b = t; 107 int o = ao; ao = bo; bo = o; 108 } 109 }
代碼較長,想看的同窗點開看一下,能夠發現這個方法的時間複雜度是O(n3),不符合題目的要求線性時間複雜度。
方法二呢,是利用了HashSet集合不可重複的特性,一樣咱們來看HashSet的add方法:
1 /** 2 * Adds the specified element to this set if it is not already present. 3 * More formally, adds the specified element <tt>e</tt> to this set if 4 * this set contains no element <tt>e2</tt> such that 5 * <tt>(e==null ? e2==null : e.equals(e2))</tt>. 6 * If this set already contains the element, the call leaves the set 7 * unchanged and returns <tt>false</tt>. 8 * 9 * @param e element to be added to this set 10 * @return <tt>true</tt> if this set did not already contain the specified 11 * element 12 */ 13 public boolean add(E e) { 14 return map.put(e, PRESENT)==null; 15 }
其實HashSet的底層是經過HashMap來實現的,HashSet中的元素都是HashMap中的key,再來看HashMap的put方法:
1 /** 2 * Associates the specified value with the specified key in this map. 3 * If the map previously contained a mapping for the key, the old 4 * value is replaced. 5 * 6 * @param key key with which the specified value is to be associated 7 * @param value value to be associated with the specified key 8 * @return the previous value associated with <tt>key</tt>, or 9 * <tt>null</tt> if there was no mapping for <tt>key</tt>. 10 * (A <tt>null</tt> return can also indicate that the map 11 * previously associated <tt>null</tt> with <tt>key</tt>.) 12 */ 13 public V put(K key, V value) { 14 return putVal(hash(key), key, value, false, true); 15 } 16 17 /** 18 * Implements Map.put and related methods 19 * 20 * @param hash hash for key 21 * @param key the key 22 * @param value the value to put 23 * @param onlyIfAbsent if true, don't change existing value 24 * @param evict if false, the table is in creation mode. 25 * @return previous value, or null if none 26 */ 27 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 28 boolean evict) { 29 Node<K,V>[] tab; Node<K,V> p; int n, i; 30 if ((tab = table) == null || (n = tab.length) == 0) 31 n = (tab = resize()).length; 32 if ((p = tab[i = (n - 1) & hash]) == null) 33 tab[i] = newNode(hash, key, value, null); 34 else { 35 Node<K,V> e; K k; 36 if (p.hash == hash && 37 ((k = p.key) == key || (key != null && key.equals(k)))) 38 e = p; 39 else if (p instanceof TreeNode) 40 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 41 else { 42 for (int binCount = 0; ; ++binCount) { 43 if ((e = p.next) == null) { 44 p.next = newNode(hash, key, value, null); 45 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 46 treeifyBin(tab, hash); 47 break; 48 } 49 if (e.hash == hash && 50 ((k = e.key) == key || (key != null && key.equals(k)))) 51 break; 52 p = e; 53 } 54 } 55 if (e != null) { // existing mapping for key 56 V oldValue = e.value; 57 if (!onlyIfAbsent || oldValue == null) 58 e.value = value; 59 afterNodeAccess(e); 60 return oldValue; 61 } 62 } 63 ++modCount; 64 if (++size > threshold) 65 resize(); 66 afterNodeInsertion(evict); 67 return null; 68 }
請注意:上面 putVal 方法中,調用的 resize() 、 putTreeVal() 等方法自己也是O(n2)的時間複雜度。不符合題目要求的線性時間複雜度。
因此嚴格來講,只有第四種方式符合題目要求,但咱們拓寬思路,能多用幾種還算不錯的方式實現需求,也是有百利而無一害的,你們也沒必要非要鑽這個牛角尖。
另外,從效率上來說,第四種效率是最高的,通過測試高出前三種方式1-2個數量級。只是在普通的業務代碼中,用到異或運算的地方並很少,不太容易想到這種方式,這個就考驗你們的基礎功底了。