只出现一次的数字:从暴力美学到位运算神技的进化之路

只出现一次的数字:从暴力美学到位运算神技的进化之路

面试官:"找出数组中那个落单的数字!"

你:😏 "简单,循环计数不就完事了?"

面试官:"要求时间复杂度 O(n),空间复杂度 O(1)。"

你:😱 "等等......容我三思!"

别慌! 这篇博客带你层层解剖这道经典题 (LeetCode 136/137/260) ,从"老实人暴力法"到"位运算魔法",全程幽默+代码注释,保证你笑着学会!👇


问题一:只出现一次的数字

题目 :数组中除一个数字外,其他都成对出现 ,找出那个"单身狗"。 要求:时间复杂度 O(n),空间复杂度 O(1)。

心路历程

  1. 第一反应:双循环点名法

"每个数字都点名,喊到一次的就是单身狗!"

痛点 :数组长度超 1000?电脑风扇开始直升机模式 → O(n²) 太慢!

  1. 灵光一闪:空间换时间

"不让双重循环?我用 HashMap 记小本本!"

痛点:时间降到 O(n),代价是空间 O(n),空间复杂度 O(n) 超 1000?内存说:我压力很大!

  1. 绝地求生:不用额外空间?

"Set 开关灯法:单身狗进来,情侣狗出去!"

痛点:Set 也算额外空间!题目白纸黑字**"O(1) 空间"** → Set 也算额外空间!💢

  1. 终极顿悟:位运算降临

"异或操作:成对数字相互抵消,单身狗闪耀登场!"

  • 神技原理
    • a ^ a = 0(情侣相见湮灭成灰 ☠️)
    • a ^ 0 = a(单身狗闪耀登场 🌟)
  • 空间 O(1):只用一个变量,内存感动哭了 😭

🐢 1. 双重循环法:老实人的暴力美学

思路:每个数字全员点名喊到!喊到一次的就是落单的。

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            int count = 0; // 计数器:统计 nums[i] 出现次数
            for (int j = 0; j < nums.length; j++) {
                if (nums[i] == nums[j]) count++; // 全员点名:相同数字就计数+1
            }
            if (count == 1) return nums[i]; // 只喊到1次?恭喜,单身狗就是你!
        }
        return -1; // 单身狗不存在?离谱!
    }
}
  • 优点:逻辑直白,幼儿园小朋友都能懂 👍
  • 缺点 :慢得像乌龟赛跑!🐢 数组长一点就卡成 PPT(时间复杂度 O(n²)适用场景: 数组长度 ≤10,或者你想体验计算机的愤怒 😈

📦 2. 哈希表法:空间换时间的土豪

思路:给每个数字发"签到牌"🎫,签到时计数,最后找只签一次的数字。

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> countMap = new HashMap<>(); // 签到柜台:数字 → 出现次数
        for (int num : nums) {
            // getOrDefault:如果 num 没签过,默认0;签过就+1
            countMap.put(num, countMap.getOrDefault(num, 0) + 1); 
        }
        for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
            if (entry.getValue() == 1) return entry.getKey(); // 找到只签1次的靓仔!
        }
        return -1;
    }
}
  • 优点 :速度起飞 🚀 时间复杂度 O(n)
  • 缺点 :额外开了个"签到柜台"(空间复杂度 O(n) ),内存说:我压力很大! 适用场景: 数据量大但内存管够,典型土豪行为 💰

⚖️ 3. 集合法:反复横跳的魔术师

思路:像开关灯💡,数字出现一次就开灯,出现两次就关灯,最后亮着的就是单身狗!

java 复制代码
import java.util.HashSet;

class Solution {
    public int singleNumber(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for (int num : nums) {
            if (set.contains(num)) set.remove(num); // 有对象?踢出去!
            else set.add(num); // 单身?进来!
        }
        return set.iterator().next(); // 最后剩下的就是天选之子!
    }
}
  • 优点 :代码简洁如诗 📜 空间比哈希表省(最坏 O(n)
  • 缺点 :反复添加删除,Set 表示很忙 🤹 骚操作点: 像不停开关灯,最后亮着的房间就是目标!💡

🧮 4. 求和法:数学课代表的推理

思路 :单身狗 = 2 × (所有不重复数字和) - 数组总和

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        Set<Integer> set = new HashSet<>();
        int arraySum = 0;
        for (int num : nums) {
            arraySum += num; // 数组总和
            set.add(num); // 所有数字进集合(去重)
        }
        int setSum = 0;
        for (int num : set) setSum += num; // 集合总和
        return 2 * setSum - arraySum; // 魔法公式!✨
    }
}
  • 原理单身狗 = 2*(所有不重复数字和) - 数组总和
  • 为什么? :因为成对的数字在 数组总和 里算了两次,减去后刚好抵消!
  • 缺点:代码啰嗦,数学不好容易绕晕 😵‍💫,且可能整数溢出(后面解决)

5. 异或法:终极位操作の神技!

思路 :用异或操作 ^ ------ 成对数字相互抵消

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums) {
            single ^= num; // 全程高能异或!
        }
        return single; // 结束,撒花🎉
    }
}
  • 优点

    • 速度 O(n) ,空间 O(1) !(原地工作,内存狂喜)
    • 代码只有 3 行,面试官看了直呼内行!🤯
  • 原理 :异或(^)有三定律:

    1. a ^ a = 0(自己搞自己=消失)
    2. a ^ 0 = a(和 0 搞=还是自己)
    3. 交换律:a ^ b = b ^ a 所有成对的数字异或后全变 0,最后剩下单身狗!
  • 面试装逼必备:一行代码秒杀 LeetCode 136!

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        return Arrays.stream(nums).reduce(0, (a, b) -> a ^ b);
    }
}

🏆 终极对决:方法大乱斗(表格版)

方法 时间复杂度 空间复杂度 优点 缺点
双重循环 🔄 O(n²) O(1) 逻辑简单 慢到怀疑人生
哈希表 📦 O(n) O(n) 速度稳 内存警告
集合 ⚖️ O(n) O(n) 代码优雅 依赖集合特性
求和法 🧮 O(n) O(n) 数学之美 啰嗦且可能溢出整数
异或法 ✨ O(n) O(1) 又快又省 位运算抽象难懂

🚦 怎么选?看这里!

  • 小白入门 👶:先玩 集合法 (代码好懂),再试 哈希表(理解计数)
  • 效率狂魔 ⚡:无脑选 异或法!面试装逼必备 👔
  • 数学爱好者 🎓:求和法 让你重温数学荣光(然后还是得用异或)

💡 记住这个灵魂总结

🌟 成对数字的克星 = 异或操作! 🌟 一行代码干翻全场:

java 复制代码
Arrays.stream(nums).reduce(0, (a, b) -> a ^ b);

下次面试官问你找"单身狗",别再说暴力循环了,甩出异或大法,TA 的眼神会告诉你:

这波稳了! 😎


进阶一:137. 只出现一次的数字 II

题目进阶,要求其余元素出现次数由 2次 增加到 3次

也就是说过了很久单身狗还是单身狗,但其他的都变成一家三口了

1.依旧是暴力美学(代码不变,但更慢了!)

依然基础思路,先双循环,统计每个数字出现次数。

代码不需要有任何修改(想想这是为什么)

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            int count = 0;
            for (int j = 0; j < nums.length; j++) {
                if (nums[i] == nums[j]) count++; // 每个数都全员点名!
            }
            if (count == 1) return nums[i]; // 喊到1次的出列!
        }
        return -1; // 单身狗不存在?离谱!
    }
}

2.哈希的艺术(代码不变,内存警告升级 ⚠️)

这个的代码也不需要做任何修改,和之前的代码是一样的。

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> countMap = new HashMap<>();

        for (int num : nums) {
            countMap.put(num, countMap.getOrDefault(num,0) + 1);
        }

        for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
            if (entry.getValue() == 1) {
                return entry.getKey();
            }
        }

        return -1;
    }
}

3.集合的艺术 (这个时候的集合法其实是不太适用的,相对的,求和法只需改变最后的参数即可)

关键点 :在具体测试的过程中,我发现,会存在整数溢出的问题。

所以先把数组中的元素都转成 long 类型,再进行计算。

最后返回的时候做一个类型的强制转化,(int)

java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        Set<Integer> set = new HashSet<>();
        long arraySum = 0;
        for (int num : nums) {
            arraySum += num; // 使用 long 类型存储数组总和
            set.add(num); // 所有数字进集合(去重)
        }
        long setSum = 0;
        for (int num : set)
            setSum += num; // 使用 long 类型存储集合总和
        return (int) ((3 * setSum - arraySum) / 2); // 魔法公式!✨
    }
}

4.位运算的艺术

思路:统计所有数字每个二进制位"1"出现的次数。

  • 如果某位"1"出现次数 不是 3 的倍数 → 单身狗在该位是 1!
java 复制代码
class Solution {
    public int singleNumber(int[] nums) {
        int[] count = new int[32]; // 存储每个二进制位"1"出现的次数
        for (int num : nums) {
            for (int i = 0; i < 32; i++) { // 遍历32位(int类型)
                if (((num >> i) & 1) == 1) { // 检查第 i 位是否为 1
                    count[i]++; // 如果是,计数+1
                }
            }
        }
        int result = 0;
        for (int i = 0; i < 32; i++) {
            if (count[i] % 3 != 0) { // 不是3的倍数?单身狗在此位是1!
                result |= (1 << i);  // 将此位设为1
            }
        }
        return result;
    }
}

进阶二:260. 只出现一次的数字 III

题目 :有 两个 数字只出现一次,其他都出现两次。

1.依旧是双重暴力循环

java 复制代码
class Solution {
    public int[] singleNumber(int[] nums) {
        int[] result = new int[2];
        int index = 0;

        for (int i = 0; i < nums.length; i++) {
            int count = 0;
            for (int j = 0; j < nums.length; j++) {
                if (nums[i] == nums[j]) count++; // 每个数都全员点名!
            }
            if (count == 1) result[index++] = nums[i];// 喊到1次的出列!
        }
        return result; // 单身狗不存在?离谱!
    }
}

2.哈希的艺术

java 复制代码
class Solution {
    public int[] singleNumber(int[] nums) {
        Map<Integer, Integer> countMap = new HashMap<>();
        for (int num : nums) {
            countMap.put(num, countMap.getOrDefault(num, 0) + 1);
        }
        int[] result = new int[2];
        int index = 0;
        for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
            if (entry.getValue() == 1) {
                result[index++] = entry.getKey();
            }
        }
        return result;
    }
}

3.位运算神技升级:分组异或

思路

  1. 所有数字异或 → 得到 diff = dog1 ^ dog2
  2. 找到 diff 中任意一个为 1 的位(比如最低位)。
  3. 根据该位是 0 或 1,将数字分成两组 → 每组各含一只狗!
java 复制代码
class Solution {
    public int[] singleNumber(int[] nums) {
        int xor = 0;
        for (int num : nums) {
            xor ^= num; // Step 1: 得到 dog1 ^ dog2
        }
        int diff = xor & (-xor); // Step 2: 取最低位的1(用于分组)
        int[] result = new int[2]; // 存放两只狗
        for (int num : nums) {
            if ((num & diff) == 0) { // 分组:该位为0的进组1
                result[0] ^= num;    // 组内异或 → 得到 dog1
            } else {                 // 该位为1的进组2
                result[1] ^= num;    // 组内异或 → 得到 dog2
            }
        }
        return result;
    }
}

🎯 终极总结:单身狗三连击指南

问题 推荐方法 核心技巧
单只狗(136) 异或法 a ^ a = 0a ^ 0 = a
三胎家庭(137) 位计数 统计每位1的个数 % 3 ≠ 0
两只狗(260) 分组异或 diff & -diff 分组

记住:暴力循环是备用轮胎,位运算是兰博基尼! 🚗💨

相关推荐
宇寒风暖2 小时前
Flask 框架全面详解
笔记·后端·python·学习·flask·知识
你的人类朋友2 小时前
❤️‍🔥为了省内存选择sqlite,代价是什么
数据库·后端·sqlite
还是鼠鼠2 小时前
tlias智能学习辅助系统--SpringAOP-进阶-通知顺序
java·后端·mysql·spring·mybatis·springboot
Pitayafruit3 小时前
Spring AI 进阶之路01:三步将 AI 整合进 Spring Boot
spring boot·后端·ai编程
用户21411832636024 小时前
零成本搭建 AI 应用!Hugging Face 免费 CPU 资源实战指南
后端
程序员曦曦4 小时前
15:00开始面试,15:06就出来了,问的问题有点变态。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
澡点睡觉4 小时前
golang的包和闭包
开发语言·后端·golang
outsider_友人A5 小时前
前端也想写后端(1)初识 Nest.js
后端·nestjs·全栈
涡能增压发动积7 小时前
Browser-Use Agent使用初体验
人工智能·后端·python