程序猿不得不知道的算法--只出现一次的数

5 篇文章 0 订阅

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

说明:

你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?

先找出题目中的重点要求:

  1、线性时间复杂度:要求我们的代码时间复杂度最高为O(n),不能有嵌套循环等。

  2、不使用额外空间:要求空间复杂度最高为O(1)。

除此之外,还有重要的信息:

  • 除了某个元素只出现一次以外,其余每个元素均出现两次。

  这个条件非常关键,一开始自己审题不清楚没注意到均出现两次这个关键点,按照其他元素出现多次的情况处理了,这样导致思路受限很多。

开始解题:

方法一(比较法):

  思路:先对数组进行排序,然后对 nums[i] 和 nums[i + 1]进行比较,如相等,i += 2,继续下一组比较,直到取到不相等的一组。

  注意:首先这个数组的长度肯定是奇数(目标数字只出现一次,其他所有数字出现两次),所以如果上述步骤没有找到不相等的一组数,那么肯定是数组的最后一个数字是单独出现的。

  代码如下:

public static int singleNumber(int[] nums) {
            Arrays.sort(nums);  // 排序数组
        for (int i = 0; i < nums.length - 1; i += 2) {
            // 找到不相等的一组,直接返回
            if (nums[i] != nums[i + 1]) {
                return nums[i];
            }
        }
        // 如果没有找到不相等的一组数据,直接返回数组的最后一个数字
        return nums[nums.length - 1];
    }

 

 

 

方法二(去重法):

  思路:利用HashSet的特性,删除重复的数组元素,最后剩下一个单独的元素,返回即可。

  直接上代码:

     public static int singleNumber(int[] nums) {
         Set<Integer> set = new HashSet<>();
         for (int i = 0; i < nums.length; i++) {
             if (!set.add(nums[i])) { // add成功返回true,如果set中已有相同数字,则add方法会返回false
                 set.remove(nums[i]); // 删除重复出现的数字
            }
         }
         return set.iterator().next();
}
 

方法三(求差法):

  思路:先对数组排序,显而易见的,单独出现一次的数据必然是出现在数组下标为偶数的位置(下标从0开始),那么所有奇数下标的元素之和减去偶数下标的元素之和,就是需要求得的结果。

  代码如下:

 public static int singleNumber(int[] nums) {
         int num = 0;
         Arrays.sort(nums);
         for (int i = 0; i < nums.length; i++) {
             // 偶数下标位置 num += nums[i],奇数下标位置 num -= nums[i]
            num = i % 2 == 0 ? num + nums[i] : num - nums[i];
        }
         return num;
    }

 

 

 

方法四(异或法)

  思路:根据异或运算的特点,相同的数字经过异或运算后结果为0,除单独出现一次的数字外,其他数字都是出现两次的,那么这些数字经过异或运算后结果一定是0。而任何数字与0进行异或运算都是该数字本身。所以对数组所有元素进行异或运算,运算结果就是题目的答案。

  上代码:

public static int singleNumber(int[] nums) {
         int num = 0;
         for (int i = 0; i < nums.length; i++) {
             num = num ^ nums[i];
         }
         return num;
     }

 

总结:

  其实严格来讲,只有第四种方式是题目想要的解法,其他三种方法都是有瑕疵的。

  方法一、方法三都用到了Arrays.sort(int[] a)的方法,我们先来看JDK提供的数组排序方法:

public static void sort(int[] a) {
         DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
     }

  调用了DualPivotQuicksort类的静态方法:

  方法二呢,是利用了HashSet集合不可重复的特性,同样我们来看HashSet的add方法:

 

      /**
       * Adds the specified element to this set if it is not already present.
       * More formally, adds the specified element <tt>e</tt> to this set if
       * this set contains no element <tt>e2</tt> such that
       * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
       * If this set already contains the element, the call leaves the set
       * unchanged and returns <tt>false</tt>.
      *
      * @param e element to be added to this set
      * @return <tt>true</tt> if this set did not already contain the specified
      * element
      */
     public boolean add(E e) {
         return map.put(e, PRESENT)==null;
     }

 

  其实HashSet的底层是通过HashMap来实现的,HashSet中的元素都是HashMap中的key,再来看HashMap的put方法:

 

    /**
      * Associates the specified value with the specified key in this map.
      * If the map previously contained a mapping for the key, the old
      * value is replaced.
      *
      * @param key key with which the specified value is to be associated
      * @param value value to be associated with the specified key
      * @return the previous value associated with <tt>key</tt>, or
      *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
      *         (A <tt>null</tt> return can also indicate that the map
      *         previously associated <tt>null</tt> with <tt>key</tt>.)
      */
     public V put(K key, V value) {
         return putVal(hash(key), key, value, false, true);
     }
 
     /**
      * Implements Map.put and related methods
      *
      * @param hash hash for key
      * @param key the key
      * @param value the value to put
      * @param onlyIfAbsent if true, don't change existing value
      * @param evict if false, the table is in creation mode.
      * @return previous value, or null if none
      */
     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                    boolean evict) {
         Node<K,V>[] tab; Node<K,V> p; int n, i;
         if ((tab = table) == null || (n = tab.length) == 0)
             n = (tab = resize()).length;
         if ((p = tab[i = (n - 1) & hash]) == null)
             tab[i] = newNode(hash, key, value, null);
         else {
             Node<K,V> e; K k;
             if (p.hash == hash &&
                 ((k = p.key) == key || (key != null && key.equals(k))))
                 e = p;
             else if (p instanceof TreeNode)
                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
             else {
                 for (int binCount = 0; ; ++binCount) {
                     if ((e = p.next) == null) {
                         p.next = newNode(hash, key, value, null);
                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                             treeifyBin(tab, hash);
                         break;
                     }
                     if (e.hash == hash &&
                         ((k = e.key) == key || (key != null && key.equals(k))))
                         break;
                     p = e;
                 }
             }
             if (e != null) { // existing mapping for key
                 V oldValue = e.value;
                 if (!onlyIfAbsent || oldValue == null)
                     e.value = value;
                 afterNodeAccess(e);
                 return oldValue;
             }
         }
         ++modCount;
         if (++size > threshold)
             resize();
         afterNodeInsertion(evict);
         return null;
     }

 

  请注意:上面 putVal 方法中,调用的 resize() 、 putTreeVal() 等方法本身也是O(n2)的时间复杂度。不符合题目要求的线性时间复杂度

  所以严格来说,只有第四种方式符合题目要求,但我们拓宽思路,能多用几种还算不错的方式实现需求,也是有百利而无一害的,大家也不必非要钻这个牛角尖。

  另外,从效率上来讲,第四种效率是最高的,经过测试高出前三种方式1-2个数量级。只是在普通的业务代码中,用到异或运算的地方并不多,不太容易想到这种方式,这个就考验大家的基础功底了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火柴有猿

您的鼓励,将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值