只出现一次的数(哈希/排序/位运算)

简介: 只出现一次的数(哈希/排序/位运算)

1.只出现一次的数(136 - 易)



题目描述:给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。说明:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?


示例 :

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


思路


法1:本题比较常规的解法是用集合进行记录,最终返回集合中的唯一元素,代码实现如下,但这里空间复杂度O(n)!


法2:先排序后比较有没有落单的,落单返回,时间复杂度O(nlogn),空间复杂度O(1)。


法3:根据异或对象相同为0,不同为1的规则,可以推导出一些性质


  • 0 ⊕ a = a
  • a ⊕ a = 0


此外,异或满足交换律以及结合律


代码实现:

// 利用set集合
public int singleNumber(int[] nums) {
    HashSet<Integer> set = new HashSet<>();
    for (int i = 0; i < nums.length; i++) {
        if (!set.contains(nums[i])) {
            set.add(nums[i]);
        } else {
            set.remove(nums[i]);
        }
    }
    return set.iterator().next();   // 返回集合中的唯一元素
}


ps:这里注意迭代器和数据结构中的链表一样,有个header指针,header->next()就是链表中第一个元素……  如下所示:

pre  1  2  3  4


开始header指针指向pre,当执行一次iterator().next()就是返回指针所指的元素,即返回1,本题中返回那个唯一元素。

// 先排序
public int singleNumber(int[] nums) {
    if (nums.length == 1) return nums[0];
    int i = 1;
    Arrays.sort(nums);
    for ( ; i < nums.length; i += 2) {
        if (nums[i] != nums[i - 1]) break;
    }
    return nums[i - 1];
}
// 位运算
public int singleNumber(int[] nums) {
    int ans = 0;
    for (int i = 0; i < nums.length; i++) {
        ans ^= nums[i];
    }
    return ans;
}


2.只出现一次的数II(137 - 中)



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


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


示例 :

输入: [2,2,3,2]
输出: 3
输入: [0,1,0,1,0,1,99]
输出: 99


思路


法1:和1不同的是,本题重复数字变成3个,可以用HashMap来实现计数,但时间复杂度O(n);


法2:和案例1解法2思路相同,先排序,后比较;


法3:参考 这里 的一个解法。我们把数字放眼到二进制形式。

假如例子是 1 2 6 1 1 2 2 3 3 3,   3 个 1, 3 个 2, 3 个 3,1 个 6
1    0 0 1
2    0 1 0 
6    1 1 0 
1    0 0 1
1    0 0 1
2    0 1 0
2    0 1 0
3    0 1 1  
3    0 1 1
3    0 1 1      
看最右边的一列 1001100111 有 6 个 1
再往前看一列 0110011111 有 7 个 1
再往前看一列 0010000 有 1 个 1
我们只需要把是 3 的倍数的对应列写 0,不是 3 的倍数的对应列写 1    
也就是 1 1 0,也就是 6。


原因的话,其实很容易想明白。如果所有数字都出现了 3 次,那么每一列的 1 的个数就一定是 3 的倍数。之所以有的列不是 3 的倍数,就是因为只出现了 1 次的数多贡献出了 1


所以所有不是 3 的倍数的列写 1(多出的数有贡献),其他列写 0(无贡献) ,就找到了这个出现 1 次的数。


本解法也可以进行推广,即是n的倍数列写1,其他列写0,找到其余元素都出现n次时,出现1次的元素。


代码实现:

class Solution {
    // hash计数
    public int singleNumber(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i : nums) {
            map.put(i, map.getOrDefault(i, 0) + 1);
        }
        for (int key : map.keySet()) {
            if (map.get(key) == 1) {
                return key;
            }
        }
        return -1;
    }
    // 排序(注意步长)
    public int singleNumber(int[] nums) {
        Arrays.sort(nums);
        int i = 2;
        for (; i < nums.length; i += 3) {
            if (nums[i] != nums[i - 2]) {
                break;
            }
        }
        return nums[i - 2];
    }
    // 位运算
    public int singleNumber(int[] nums) {
        int ans = 0;
        //考虑每一位
        for (int i = 0; i < 32; i++) {
            int count = 0;
            //考虑每一个数
            for (int j = 0; j < nums.length; j++) {
                //第j个数的i位是否为 1(统计第i位中1的个数)
                if ((nums[j] >>> i & 1) == 1) {
                    count++;
                }
            } 
            //1 << i,将1左移位,即将i位位置1
            if (count % 3 != 0) {
                ans = ans | 1 << i;
            }
        }
        return ans;
    }
}


3.只出现一次的数III(260 - 中)



题目描述:给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。


进阶:你的算法应该具有线性时间复杂度。你能否仅使用常数空间复杂度来实现?


示例 :

输入:nums = [1,2,1,3,2,5]
输出:[3,5]
解释:[5, 3] 也是有效的答案。


思路


法1:最直接的方法,统计每个数出现的次数。使用 HashMap 或者 HashSet,由于每个数字最多出现两次,我们可以使用 HashSet。


法2:先排序,后比较,但是有两个细节:(1)如何出来首尾(2)不同步长;


法3:位运算采用降维的思想,先获取两个不同元素(结果)的异或值,技巧:a & (-a)获取a二进制中的最右边的1。这样第二次遍历通过这个条件进行区分,类似进行两遍136题操作。


代码实现:

class Solution {
    // hashmap
    public int[] singleNumber(int[] nums) {
        HashMap<Integer, Integer> map = new HashMap<>();
        int[] ans = new int[2];
        int i = 0;
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            if (entry.getValue() == 1) {
                ans[i++] = entry.getKey();
            }
        }
        return ans;
    }
    // hashset
    public int[] singleNumber(int[] nums) {
        HashSet<Integer> set = new HashSet<>();
        int[] ans = new int[2];
        int index = 0;
        for (int num : nums) {
            if (!set.contains(num)) {
                set.add(num);
            } else {
                set.remove(num);
            }
        }
        for (int i : set) {
            ans[index++] = i;
        }
        return ans;
    }
    // 排序(注意不同的步长)
    public int[] singleNumber(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        int[] ans = new int[2];
        int index = 0;
        for (int i = 0; i < n - 1;) {
            if (nums[i] != nums[i + 1]) {
                ans[index++] = nums[i];
                i++;
            } else {
                i += 2;
            }
        }
        // 最后一个如果只出现一次,最后一个元素会被上述for循环略过
        if(nums[n - 2] != nums[n - 1]) {
            ans[index++] = nums[n - 1];
        }
        return ans;
    }
    // 位运算
    public int[] singleNumber(int[] nums) {
        int[] ans = new int[2];
        int xor = 0;
        for (int num : nums) {
            xor ^= num;
        }
        // 获取两个数异或后二进制中最低非0位(即区分两个数)
        xor &= -xor;
        // 遍历数组与xor进行与操作,即把问题降维到136题
        for (int num : nums) {
            if ((num & xor) == 0) {
                ans[0] ^= num;
            } else {
                ans[1] ^= num;
            }
        }
        return ans;
    }
}


4.第一个只出现一次的字符(剑指50)



题目描述:在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。


示例 :

s = "abaccdeff"
返回 "b"
s = "" 
返回 " "


思路:本题考查hash表:下面两种时间复杂度相同,不同的是有序hash需要遍历一次s串。


代码实现:

class Solution {
    // hash表
    public char firstUniqChar(String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        char[] cs = s.toCharArray();
        for (char c : cs) {
            map.put(c, map.getOrDefault(c, 0) + 1);
        }
        for (char c : cs) {
            if (map.get(c) == 1) {
                return c;
            }
        }
        return ' ';
    }
    // 有序hash表
    public char firstUniqChar(String s) {
        HashMap<Character, Integer> map = new LinkedHashMap<>();
        char[] cs = s.toCharArray();
        for (char c : cs) {
            map.put(c, map.getOrDefault(c, 0) + 1);
        }
        for (Map.Entry<Character, Integer> entry : map.entrySet()) {
            if (entry.getValue() == 1) {
                return entry.getKey();
            }
        }
        return ' ';
    }
}


补充:


ps:注意先位运算再进行逻辑运算。java优先级口诀:单目乘除为(位)关系,逻辑三目后赋值。


  • 单目:单目运算符+ –(负数)  ++  -- 等
  • 乘除:算数单目运算符*  /  %  +  -
  • 为:位移单目运算符<<  >>  >>>
  • 带符号右移>>(相当于除以2):正数右移高位补0,负数右移高位补1。
  • 无符号右移>>>:无论正数负数,高位统统补0。
  • 关系:关系单目运算符>  <  >=  <=  ==  !=
  • 逻辑:逻辑单目运算符&&  ||  &  |  ^
  • 三目:三目单目运算符A > B  ?  X  :  Y
  • 后:无意义,仅仅为了凑字数
  • 赋值:赋值 =
相关文章
|
5天前
|
搜索推荐 编译器 Linux
一个可用于企业开发及通用跨平台的Makefile文件
一款适用于企业级开发的通用跨平台Makefile,支持C/C++混合编译、多目标输出(可执行文件、静态/动态库)、Release/Debug版本管理。配置简洁,仅需修改带`MF_CONFIGURE_`前缀的变量,支持脚本化配置与子Makefile管理,具备完善日志、错误提示和跨平台兼容性,附详细文档与示例,便于学习与集成。
307 116
|
20天前
|
域名解析 人工智能
【实操攻略】手把手教学,免费领取.CN域名
即日起至2025年12月31日,购买万小智AI建站或云·企业官网,每单可免费领1个.CN域名首年!跟我了解领取攻略吧~
|
7天前
|
数据采集 人工智能 自然语言处理
Meta SAM3开源:让图像分割,听懂你的话
Meta发布并开源SAM 3,首个支持文本或视觉提示的统一图像视频分割模型,可精准分割“红色条纹伞”等开放词汇概念,覆盖400万独特概念,性能达人类水平75%–80%,推动视觉分割新突破。
504 45
Meta SAM3开源:让图像分割,听懂你的话
|
14天前
|
安全 Java Android开发
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
崩溃堆栈全是 a.b.c?Native 错误查不到行号?本文详解 Android 崩溃采集全链路原理,教你如何把“天书”变“说明书”。RUM SDK 已支持一键接入。
695 222
|
2天前
|
Windows
dll错误修复 ,可指定下载dll,regsvr32等
dll错误修复 ,可指定下载dll,regsvr32等
137 95
|
12天前
|
人工智能 移动开发 自然语言处理
2025最新HTML静态网页制作工具推荐:10款免费在线生成器小白也能5分钟上手
晓猛团队精选2025年10款真正免费、无需编程的在线HTML建站工具,涵盖AI生成、拖拽编辑、设计稿转代码等多种类型,均支持浏览器直接使用、快速出图与文件导出,特别适合零基础用户快速搭建个人网站、落地页或企业官网。
1711 158
|
存储 人工智能 监控
从代码生成到自主决策:打造一个Coding驱动的“自我编程”Agent
本文介绍了一种基于LLM的“自我编程”Agent系统,通过代码驱动实现复杂逻辑。该Agent以Python为执行引擎,结合Py4j实现Java与Python交互,支持多工具调用、记忆分层与上下文工程,具备感知、认知、表达、自我评估等能力模块,目标是打造可进化的“1.5线”智能助手。
953 62