今天来总结一下Missing Number一系列问题
1、Find All Numbers Disappeared in an Array
Given an array of integers where 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and others appear once.
Find all the elements of [1, n] inclusive that do not appear in this array.
Could you do it without extra space and in O(n) runtime? You may assume the returned list does not count as extra space.
Example:
Input: [4,3,2,7,8,2,3,1] Output: [5,6]
1 class Solution { 2 public List<Integer> findDisappearedNumbers(int[] nums) { 3 Set<Integer> set = new HashSet<>(); 4 List<Integer> ans = new LinkedList<>(); 5 for ( int num : nums ) set.add(num); 6 for ( int i = 1 ; i <= nums.length ; i ++ ) 7 if ( !set.contains(i) ) ans.add(i); 8 return ans; 9 } 10 }
运行时间41ms,击败5.76%的提交。这种方法属于奇技淫巧了,没有算法思想。
第三个想法,首先我们发现数组中元素都是介于1——n之间的,减1之后就是介于0——n-1之间,很容易和数组下标联想起来。我们参考[4,3,2,7,8,2,3,1],这个数组中missing number是4、5,也就是对于下标是3、4的两个元素肯定是无法访问到的。因此这个想法就是遍历整个数组,令nums[nums[i]-1] = -nums[nums[i]-1]。然后再遍历数组,找到值大于0的就可以了。代码如下:
1 public List<Integer> findDisappearedNumbers(int[] nums) { 2 List<Integer> ans = new LinkedList<>(); 3 for ( int i = 0 ; i < nums.length ; i ++ ){ 4 int val = Math.abs(nums[i]) - 1; 5 if ( nums[val] > 0 ) nums[val] = -nums[val]; 6 } 7 for ( int i = 0 ; i < nums.length ; i ++ ){ 8 if (nums[i] > 0 ) ans.add(i+1); 9 } 10 return ans; 11 }
运行时间9ms,击败82.5%的提交。这种方法叫“取反法”。
第四个想法:参考《数组统计分析》中的解法,采用交换元素,最终想要达成的目的是令所有出现过的元素都实现nums[i]=i+1,这个目标。采用交换法关键就是在于如何判断交换结束,这里有两种情况不能交换:nums[i]==i+1或者nums[nums[i]-1]==nums[i]。第一种情况说明这个位置元素已经正确放置,第二种情况说明元素i没有在正确位置但是其正确位置已经放置好i了。分析一下这两个式子好像可以达成一致,都用nums[nums[i]-1]==nums[i]来判断。代码如下:
1 class Solution { 2 public List<Integer> findDisappearedNumbers(int[] nums) { 3 List<Integer> ans = new LinkedList<>(); 4 for ( int i = 0 ; i < nums.length ; i ++ ){ 5 while ( nums[i] != nums[nums[i]-1] ){ 6 swap(nums,i,nums[i]-1); 7 } 8 } 9 for ( int i = 0 ; i < nums.length ; i ++ ){ 10 if ( nums[i] != i + 1 ) ans.add(i+1); 11 } 12 return ans; 13 } 14 private void swap(int[] nums, int start, int end) { 15 int temp = nums[start]; 16 nums[start] = nums[end]; 17 nums[end] = temp; 18 } 19 }
运行时间8ms。交换法虽然表面看起来是O(n^2),但是均摊分析之后,时间复杂度还是O(n):如果满足交换条件,则每次都会使一个元素处在正确位置,因为总共有n个元素,所以至多需要n-1次交换(交换完n-1个元素,第n个元素自动满足)即可使所有的元素处在正确位置,也即while循环至多执行O(n)次,每次的平摊代价是O(1)。所以上述交换操作的复杂度为O(n)。
第五个想法:参考待字闺中公众号《数组统计分析》,取余法。第一次循环先把每个元素对应的位置加上n;第二次循环再把每个位置除以n,如果该位置是0,就代表这个元素没出现过;如果为2,就代表出现两次。代码如下:
1 class Solution { 2 public List<Integer> findDisappearedNumbers(int[] nums) { 3 List<Integer> ans = new LinkedList<>(); 4 int n = nums.length; 5 for ( int i = 0 ; i < n ; i ++ ) 6 nums[ ( nums[i] - 1 ) % n ] += n; 7 for ( int i = 0 ; i < n ; i ++ ) 8 nums[i] = ( nums[i] - 1 ) / n; 9 for ( int i = 0 ; i < n ; i ++ ) 10 if ( nums[i] == 0 ) ans.add(i+1); 11 return ans; 12 } 13 }
运行时间9ms。这个方法是最一般的方法,最后的nums数组中保存着每个元素出现的次数,第i位置保存着i+1元素出现的次数。这种方法就可以求出现次数最多的,次数为0的,等等。但是一定要注意这里数字出现要么是0~n-1或者1~n。如果是0~n,标红的地方就应该是nums[i]。
总结:三种方法可以总结一下,取余法、取反法、交换法。其中取余法是最值得玩味的一个算法,非常灵活,可以计算出每个元素出现的次数,而且时间复杂度是O(n),空间复杂度是O(1)。很值得学习总结。参考点击这里。
类似的题目有[leetcode] 442. Find All Duplicates in an Array
[leetcode] 287. Find the Duplicate Number
注意上面两个题目的不同。两个题目都可以用取余法完成,时间复杂度都不错。