缺失和重复的元素.md 8.4 KB
Newer Older
D
dragon_li 已提交
1
# 如何寻找缺失和重复的元素
2

L
labuladong 已提交
3 4 5 6




L
labuladong 已提交
7 8


9 10
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
11
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
12 13 14 15
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
16
![](https://labuladong.github.io/algo/images/souyisou1.png)
17

L
labuladong 已提交
18
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V1.9,点击这里体验 [刷题全家桶](https://labuladong.gitee.io/algo/images/others/%E5%85%A8%E5%AE%B6%E6%A1%B6.jpg)。**
19 20


L
labuladong 已提交
21 22 23 24 25 26

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [645. Set Mismatch](https://leetcode.com/problems/set-mismatch/) | [645. 错误的集合](https://leetcode.cn/problems/set-mismatch/) | 🟢
27 28 29

**-----------**

L
labuladong 已提交
30
今天就聊一道很看起来简单却十分巧妙的问题,寻找缺失和重复的元素。之前的一篇文章 [常用的位操作](https://labuladong.github.io/article/fname.html?fname=常用的位操作) 中也写过类似的问题,不过这次的和上次的问题使用的技巧不同。
L
labuladong 已提交
31

L
labuladong 已提交
32
这是力扣第 645 题「错误的集合」,我来描述一下这个题目:
L
labuladong 已提交
33 34 35

给一个长度为 `N` 的数组 `nums`,其中本来装着 `[1..N]``N` 个元素,无序。但是现在出现了一些错误,`nums` 中的一个元素出现了重复,也就同时导致了另一个元素的缺失。请你写一个算法,找到 `nums` 中的重复元素和缺失元素的值。

L
labuladong 已提交
36
```java
L
labuladong 已提交
37
// 返回两个数字,分别是 {dup, missing}
L
labuladong 已提交
38
int[] findErrorNums(int[] nums);
L
labuladong 已提交
39 40 41 42 43 44 45 46
```

比如说输入:`nums = [1,2,2,4]`,算法返回 `[2,3]`

其实很容易解决这个问题,先遍历一次数组,用一个哈希表记录每个数字出现的次数,然后遍历一次 `[1..N]`,看看那个元素重复出现,那个元素没有出现,就 OK 了。

但问题是,这个常规解法需要一个哈希表,也就是 O(N) 的空间复杂度。你看题目给的条件那么巧,在 `[1..N]` 的几个数字中恰好有一个重复,一个缺失,**事出反常必有妖**,对吧。

L
labuladong 已提交
47
O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想办法如何降低空间复杂度,是否可以在 O(1) 的空间复杂度之下找到重复和缺失的元素呢?
L
labuladong 已提交
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

### 思路分析

这个问题的特点是,每个元素和数组索引有一定的对应关系。

我们现在自己改造下问题,**暂且将 `nums` 中的元素变为 `[0..N-1]`,这样每个元素就和一个数组索引完全对应了,这样方便理解一些**

如果说 `nums` 中不存在重复元素和缺失元素,那么每个元素就和唯一一个索引值对应,对吧?

现在的问题是,有一个元素重复了,同时导致一个元素缺失了,这会产生什么现象呢?**会导致有两个元素对应到了同一个索引,而且会有一个索引没有元素对应过去**

那么,如果我能够通过某些方法,找到这个重复对应的索引,不就是找到了那个重复元素么?找到那个没有元素对应的索引,不就是找到了那个缺失的元素了么?

那么,如何不使用额外空间判断某个索引有多少个元素对应呢?这就是这个问题的精妙之处了:

L
labuladong 已提交
63
**通过将每个索引对应的元素变成负数,以表示这个索引被对应过一次了**,算法过程如下 GIF 所示:
L
labuladong 已提交
64

L
labuladong 已提交
65
![](https://labuladong.github.io/algo/images/dupmissing/1.gif)
L
labuladong 已提交
66 67 68

如果出现重复元素 `4`,直观结果就是,索引 `4` 所对应的元素已经是负数了:

L
labuladong 已提交
69
![](https://labuladong.github.io/algo/images/dupmissing/2.jpg)
L
labuladong 已提交
70 71 72

对于缺失元素 `3`,直观结果就是,索引 `3` 所对应的元素是正数:

L
labuladong 已提交
73
![](https://labuladong.github.io/algo/images/dupmissing/3.jpg)
L
labuladong 已提交
74 75 76

对于这个现象,我们就可以翻译成代码了:

L
labuladong 已提交
77 78 79
```java
int[] findErrorNums(int[] nums) {
    int n = nums.length;
L
labuladong 已提交
80 81
    int dup = -1;
    for (int i = 0; i < n; i++) {
L
labuladong 已提交
82
        int index = Math.abs(nums[i]);
L
labuladong 已提交
83 84
        // nums[index] 小于 0 则说明重复访问
        if (nums[index] < 0)
L
labuladong 已提交
85
            dup = Math.abs(nums[i]);
L
labuladong 已提交
86 87 88 89 90 91 92 93 94 95
        else
            nums[index] *= -1;
    }

    int missing = -1;
    for (int i = 0; i < n; i++)
        // nums[i] 大于 0 则说明没有访问
        if (nums[i] > 0)
            missing = i;
    
L
labuladong 已提交
96
    return new int[]{dup, missing};
L
labuladong 已提交
97 98 99 100 101
}
```

这个问题就基本解决了,别忘了我们刚才为了方便分析,假设元素是 `[0..N-1]`,但题目要求是 `[1..N]`,所以只要简单修改两处地方即可得到原题的答案:

L
labuladong 已提交
102 103 104
```java
int[] findErrorNums(int[] nums) {
    int n = nums.length;
L
labuladong 已提交
105 106 107
    int dup = -1;
    for (int i = 0; i < n; i++) {
        // 现在的元素是从 1 开始的
L
labuladong 已提交
108
        int index = Math.abs(nums[i]) - 1;
L
labuladong 已提交
109
        if (nums[index] < 0)
L
labuladong 已提交
110
            dup = Math.abs(nums[i]);
L
labuladong 已提交
111 112 113 114 115 116 117 118 119
        else
            nums[index] *= -1;
    }

    int missing = -1;
    for (int i = 0; i < n; i++)
        if (nums[i] > 0)
            // 将索引转换成元素
            missing = i + 1;
L
labuladong 已提交
120 121

    return new int[]{dup, missing};
L
labuladong 已提交
122 123 124 125 126 127 128 129 130 131 132 133 134
}
```

其实,元素从 1 开始是有道理的,也必须从一个非零数开始。因为如果元素从 0 开始,那么 0 的相反数还是自己,所以如果数字 0 出现了重复或者缺失,算法就无法判断 0 是否被访问过。我们之前的假设只是为了简化题目,更通俗易懂。

### 最后总结

对于这种数组问题,**关键点在于元素和索引是成对儿出现的,常用的方法是排序、异或、映射**

映射的思路就是我们刚才的分析,将每个索引和元素映射起来,通过正负号记录某个元素是否被映射。

排序的方法也很好理解,对于这个问题,可以想象如果元素都被从小到大排序,如果发现索引对应的元素如果不相符,就可以找到重复和缺失的元素。

L
labuladong 已提交
135
异或运算也是常用的,因为异或性质 `a ^ a = 0, a ^ 0 = a`,如果将索引和元素同时异或,就可以消除成对儿的索引和元素,留下的就是重复或者缺失的元素。可以看看前文 [常用的位运算](https://labuladong.github.io/article/fname.html?fname=常用的位操作),介绍过这种方法。
L
labuladong 已提交
136

137
**_____________**
L
labuladong 已提交
138

L
labuladong 已提交
139
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
140 141

![](https://labuladong.github.io/algo/images/souyisou2.png)
L
labuladong 已提交
142 143


朱力 已提交
144 145
======其他语言代码======

B
brucecat 已提交
146 147 148 149 150 151
[645.错误的集合](https://leetcode-cn.com/problems/set-mismatch)



### java

朱力 已提交
152 153 154 155 156 157 158
[zhuli](https://github.com/1097452462 "zhuli")提供的Java代码:
```java
class Solution {
    public int[] findErrorNums(int[] nums) {
        int n = nums.length;
        int dup = -1;
        for (int i = 0; i < n; i++) {
D
dragon_li 已提交
159
	    // 元素是从 1 开始的
朱力 已提交
160 161 162 163 164 165 166 167 168 169 170
            int index = Math.abs(nums[i]) - 1;
            // nums[index] 小于 0 则说明重复访问
            if (nums[index] < 0)
                dup = Math.abs(nums[i]);
            else
                nums[index] *= -1;
        }
        int missing = -1;
        for (int i = 0; i < n; i++)
            // nums[i] 大于 0 则说明没有访问
            if (nums[i] > 0)
D
dragon_li 已提交
171
		// 将索引转换成元素
朱力 已提交
172 173 174 175
                missing = i + 1;
        return new int[]{dup, missing};
    }
}
D
dragon_li 已提交
176
```
B
brucecat 已提交
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211



### javascript

```js
/**
 * @param {number[]} nums
 * @return {number[]}
 */
var findErrorNums = function (nums) {
    let n = nums.length;
    let dup = -1;
    for (let i = 0; i < n; i++) {
        // 现在的元素是从1开始的
        let index = Math.abs(nums[i]) - 1;
        if (nums[index] < 0) {
            dup = Math.abs(nums[i]);
        } else {
            nums[index] *= -1;
        }
    }

    let missing = -1;
    for (let i = 0; i < n; i++) {
        if (nums[i] > 0) {
            // 将索引转换成元素
            missing = i + 1;
        }
    }

    return [dup, missing]
};
```