算法题 只出现一次的数字

136. 只出现一次的数字

问题描述

给定一个非空整数数组 nums,其中某个元素只出现一次,其余每个元素均出现两次。找出那个只出现一次的元素。要求:

  • 线性时间复杂度(O(n))
  • 常数空间复杂度(O(1))

示例

输入: [2,2,1]
输出: 1

输入: [4,1,2,1,2]
输出: 4

算法思路:位运算(异或)

利用异或运算(XOR)的三个性质:

  1. 归零律:任何数与自身异或结果为 0a ^ a = 0
  2. 恒等律:任何数与 0 异或结果为自身(a ^ 0 = a
  3. 交换律/结合律:异或操作满足交换律和结合律(a ^ b ^ a = (a ^ a) ^ b = 0 ^ b = b

核心思想

  • 将数组中所有数字进行异或操作
  • 成对出现的数字异或结果为 0
  • 最终结果即为只出现一次的数字

代码实现

class Solution {
    public int singleNumber(int[] nums) {
        int result = 0;
        // 遍历数组,对所有元素进行异或操作
        for (int num : nums) {
            result ^= num;
        }
        return result;
    }
}

算法分析

  • 时间复杂度:O(n)
    • 只需遍历数组一次
  • 空间复杂度:O(1)
    • 仅使用一个额外变量存储结果

关键点

  1. 异或运算特性
    • 相同数字异或结果为 0
    • 不同数字异或保留差异位
  2. 成对消除
    • 出现两次的数字相互抵消
    • 最终剩余唯一出现一次的数字
  3. 边界处理
    • 题目保证数组非空,无需额外判空
    • 当数组只有一个元素时直接返回该元素

算法过程

输入: [4, 1, 2, 1, 2]
计算过程:
  0 ^ 4 = 4
  4 ^ 1 = 5  (二进制 100 ^ 001 = 101)
  5 ^ 2 = 7  (101 ^ 010 = 111)
  7 ^ 1 = 6  (111 ^ 001 = 110)
  6 ^ 2 = 4  (110 ^ 010 = 100)
返回: 4

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    int[] nums1 = {2, 2, 1};
    System.out.println("Test 1: " + solution.singleNumber(nums1)); // 1
    
    // 测试用例2:标准示例
    int[] nums2 = {4, 1, 2, 1, 2};
    System.out.println("Test 2: " + solution.singleNumber(nums2)); // 4
    
    // 测试用例3:单个元素
    int[] nums3 = {5};
    System.out.println("Test 3: " + solution.singleNumber(nums3)); // 5
    
    // 测试用例4:负数
    int[] nums4 = {-1, 3, -1, 3, 0};
    System.out.println("Test 4: " + solution.singleNumber(nums4)); // 0
    
    // 测试用例5:大数
    int[] nums5 = {1000000, 1, 1000000, 2, 2};
    System.out.println("Test 5: " + solution.singleNumber(nums5)); // 1
}

常见问题

  1. 为什么异或能解决这个问题?

    • 异或操作满足交换律和结合律,相同数字异或结果为0,最终剩余唯一出现一次的数字。
  2. 如何处理负数?

    • 异或操作直接作用于整数的二进制补码形式,对正负数处理机制相同。
  3. 如果数组中有多个只出现一次的数字会怎样?

    • 题目限定只有一个单独出现的数字,其余都出现两次。若有多个单独数字,算法会返回它们的异或结果(不符合题目要求)。
  4. 能否用其他位运算解决?

    • 异或是最优解。若使用其他方法(如哈希表),空间复杂度会达到O(n),不符合要求。
  5. 为什么初始化result=0

    • 因为任何数与0异或等于自身,0作为初始值不影响最终结果(0 ^ a = a)。

137. 只出现一次的数字 II

问题描述

给定一个整数数组 nums,除某个元素仅出现一次外,其余每个元素都恰出现三次。找出并返回那个只出现一次的元素。

示例

输入:nums = [2,2,3,2]
输出:3

输入:nums = [0,1,0,1,0,1,99]
输出:99

要求

  • 时间复杂度 O(n)
  • 空间复杂度 O(1)

算法思路

方法一:位运算(按位统计)

  1. 核心思想
    对于整数(32位),统计每一位上1出现的总次数:

    • 出现三次的数字在每一位上1的个数一定是3的倍数
    • 只出现一次的数字在每一位上1的个数对3取模即为该位的值
  2. 操作步骤

    • 遍历32位(int类型)
    • 对每一位统计所有数字在该位上1的总和
    • 若总和 mod 3 ≠ 0,说明只出现一次的数在该位为1

方法二:有限状态自动机

  1. 核心思想
    使用两个变量(onestwos)模拟三种状态:

    • 00:出现0次
    • 01:出现1次
    • 10:出现2次
    • 11 无效(自动转为 00
  2. 状态转移

    ones = (ones ^ num) & ~twos
    twos = (twos ^ num) & ~ones
    
    • 遇到0时状态不变
    • 遇到1时状态按 00→01→10→00 循环

代码实现

方法一:位运算(通用解法)

class Solution {
    public int singleNumber(int[] nums) {
        int res = 0;
        // 遍历32位
        for (int i = 0; i < 32; i++) {
            int sum = 0;  // 统计第i位1的总数
            // 遍历所有数字
            for (int num : nums) {
                // 检查第i位是否为1:右移i位后与1
                sum += (num >> i) & 1;
            }
            // 若该位1的总数不是3的倍数,则只出现一次的数在该位为1
            if (sum % 3 != 0) {
                // 将结果中该位置1:用或运算
                res |= (1 << i);
            }
        }
        return res;
    }
}

注释

  1. (num >> i) & 1:提取 num 的第 i 位值(0或1)
  2. sum % 3 != 0:该位上的1无法被3整除,说明目标数该位为1
  3. res |= (1 << i):将结果中该位设为1

方法二:有限状态自动机(优化解法)

class Solution {
    public int singleNumber(int[] nums) {
        int ones = 0;  // 记录出现1次的位
        int twos = 0;  // 记录出现2次的位
        for (int num : nums) {
            /*
             * 更新ones:
             *  1. 与num异或:记录出现奇数次的位
             *  2. & ~twos:确保twos为1的位不参与更新
             */
            ones = (ones ^ num) & ~twos;
            
            /*
             * 更新twos:
             *  1. 与num异或:记录出现偶数次的位
             *  2. & ~ones:确保ones更新后的状态不影响当前计算
             */
            twos = (twos ^ num) & ~ones;
        }
        return ones;  // 最终ones存储只出现一次的数
    }
}

状态转移

  • 初始状态(ones, twos) = (00)
  • 遇到第一个num
    • ones = (00 ^ num) & ~00 = num
    • twos = (00 ^ num) & ~num = 00 → 状态 01
  • 遇到第二个num
    • ones = (num ^ num) & ~00 = 00
    • twos = (00 ^ num) & ~00 = num → 状态 10
  • 遇到第三个num
    • ones = (00 ^ num) & ~num = 00
    • twos = (num ^ num) & ~00 = 00 → 状态 00

算法分析

方法时间复杂度空间复杂度优势
位运算O(32n)→O(n)O(1)逻辑直观,通用性强
状态机O(n)O(1)位操作高效,无循环嵌套

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 示例测试
    int[] nums1 = {2, 2, 3, 2};
    System.out.println("Test 1: " + solution.singleNumber(nums1)); // 3
    
    int[] nums2 = {0, 1, 0, 1, 0, 1, 99};
    System.out.println("Test 2: " + solution.singleNumber(nums2)); // 99
    
    // 边界测试
    int[] nums3 = {1}; // 单个元素
    System.out.println("Test 3: " + solution.singleNumber(nums3)); // 1
    
    int[] nums4 = {-1, -1, -1, 2}; // 含负数
    System.out.println("Test 4: " + solution.singleNumber(nums4)); // 2
    
    int[] nums5 = {3, 3, 3, 1, 1, 1, 5}; // 多个重复数
    System.out.println("Test 5: " + solution.singleNumber(nums5)); // 5
}

关键点

  1. 位运算核心

    • 利用整数二进制表示独立处理每一位
    • 出现次数与位值分离统计
  2. 状态机精髓

    • 双变量编码三种状态(00/01/10)
    • 位操作实现状态转移原子性
  3. 负数处理

    • Java使用补码表示负数
    • 按位操作对所有整数统一处理
  4. 通用性扩展

    • 方法一可直接扩展至"所有数字出现k次,仅一个出现m次"(修改 mod k
    • 方法二状态机需调整状态转移规则

260. 只出现一次的数字 III

问题描述

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。找出只出现一次的那两个元素。要求时间复杂度为 O(n),空间复杂度为 O(1)。

示例

输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:3 和 5 只出现一次,其他数字均出现两次

算法思路

核心思想:位运算 + 分组异或

  1. 整体异或
    对所有元素进行异或操作,得到的结果 xorAll 等于两个目标数(假设为 ab)的异或值:
    xorAll = a ^ b

  2. 分离关键位
    xorAll 中任意选择一个值为 1 的位(通常选择最低位的 1),该位表明 ab 在此位的值不同(一个为 0,一个为 1)。

  3. 分组异或
    根据关键位将数组分为两组:

    • 关键位为 0 的组:包含 a 和所有在此位为 0 的重复元素
    • 关键位为 1 的组:包含 b 和所有在此位为 1 的重复元素
      分别对两组进行异或操作,即可得到 ab

代码实现

class Solution {
    public int[] singleNumber(int[] nums) {
        // 步骤1:计算所有数字的异或结果
        int xorAll = 0;
        for (int num : nums) {
            xorAll ^= num;
        }
        
        // 步骤2:找到最低位的1(分离关键位)
        int lowbit = xorAll & -xorAll;
        
        // 步骤3:初始化两个结果
        int a = 0, b = 0;
        
        // 步骤4:分组异或
        for (int num : nums) {
            // 根据关键位分组
            if ((num & lowbit) == 0) {
                a ^= num;  // 关键位为0的组
            } else {
                b ^= num;  // 关键位为1的组
            }
        }
        
        return new int[]{a, b};
    }
}

注释

  1. 整体异或计算

    int xorAll = 0;
    for (int num : nums) {
        xorAll ^= num;  // 最终得到 a ^ b
    }
    
  2. 获取最低位的 1

    int lowbit = xorAll & -xorAll;
    
    • -xorAll 是补码表示(按位取反后加 1)
    • 示例:若 xorAll = 6(二进制 110),则 -xorAll = -6(补码 010)
      110 & 010 = 010 → 最低位的 1 是第 2 位
  3. 分组异或

    if ((num & lowbit) == 0) {
        a ^= num;  // 关键位为0的组
    } else {
        b ^= num;  // 关键位为1的组
    }
    
    • 重复元素会被分到同一组并相互抵消
    • 目标数 ab 被分到不同组

算法分析

  • 时间复杂度:O(n)
    遍历数组两次(整体异或 + 分组异或)
  • 空间复杂度:O(1)
    仅使用常数空间(xorAlllowbitab

算法过程

输入:nums = [1,2,1,3,2,5]

  1. 整体异或
    1^2^1^3^2^5 = (1^1)^(2^2)^3^5 = 0^0^3^5 = 3^5 = 6(二进制 110)

  2. 分离关键位
    lowbit = 6 & -6 = 2(二进制 010)

  3. 分组异或

    数字二进制& 010分组组内异或结果
    1001000 (0)a组a=0^1=11^1=00^3=3
    2010010 (≠0)b组b=0^2=22^2=00^5=5
    1001000 (0)a组
    3011010 (≠0)b组
    2010010 (≠0)b组
    5101000 (0)a组
    输出[3, 5]

边界处理

  • 负数处理
    Java 的补码机制确保位操作对负数有效
  • 数组空值
    题目保证至少有两个元素,无需特殊处理
  • 关键位唯一性
    最低位的 1 一定存在(因为 a ≠ b

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 示例测试
    int[] nums1 = {1,2,1,3,2,5};
    System.out.println(Arrays.toString(solution.singleNumber(nums1))); // [3,5]
    
    // 负数测试
    int[] nums2 = {-1,0,0,-1,2,3};
    System.out.println(Arrays.toString(solution.singleNumber(nums2))); // [2,3]
    
    // 大数测试
    int[] nums3 = {100000, -100000, 100000, 99999};
    System.out.println(Arrays.toString(solution.singleNumber(nums3))); // [-100000,99999]
    
    // 最小数组测试
    int[] nums4 = {1, -1};
    System.out.println(Arrays.toString(solution.singleNumber(nums4))); // [1,-1]
}

关键点

  1. 异或性质

    • x ^ x = 0
    • 0 ^ x = x
    • 异或满足交换律和结合律
  2. 分组依据

    • 利用 ab 在某一二进制位的差异进行分组
    • 重复元素因相同位值被分到同一组
  3. 位操作技巧

    • x & -x 快速获取最低位的 1
    • (num & lowbit) == 0 判断特定位值
  4. 通用性扩展
    此方法可扩展至寻找更多个只出现一次的数字(需增加分组维度)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值