2007.从双倍数组中还原原数组
一个整数数组 original 可以转变成一个 双倍 数组 changed ,转变方式为将 original 中每个元素 值乘以 2 加入数组中,然后将所有元素 随机打乱 。
给你一个数组 changed ,如果 change 是 双倍 数组,那么请你返回 original数组,否则请返回空数组。original 的元素可以以 任意 顺序返回。
示例 1:
输入:changed = [1,3,4,2,6,8]
输出:[1,3,4]
解释:一个可能的 original 数组为 [1,3,4] :
- 将 1 乘以 2 ,得到 1 * 2 = 2 。
- 将 3 乘以 2 ,得到 3 * 2 = 6 。
- 将 4 乘以 2 ,得到 4 * 2 = 8 。
其他可能的原数组方案为 [4,3,1] 或者 [3,1,4] 。
示例 2:
输入:changed = [6,3,0,1]
输出:[]
解释:changed 不是一个双倍数组。
示例 3:
输入:changed = [1]
输出:[]
解释:changed 不是一个双倍数组。
提示:
1 <= changed.length <= 105
0 <= changed[i] <= 105
- 排序 + 哈希表:例如例子 1,我们最开始可以确定的是最小值 1 肯定不会是某个数的倍数,所以 1 可以尝试加进 original,对应的, 1 的两倍 2 肯定就不在 original,此时数组中只剩下 [3,4,6,8],还是同理,3 肯定在 original,所以 6 肯定不在 original…
- 为了方便取最小值,所以我们先对数组排序,遍历排序后的数组,我们用一个哈希表 map 记录某个数的倍数出现的次数,如果此时的数字 c 不在 map 中,说明不是某个数的倍数,又因为经过排序 c 为此时最小值,所以 c 可以直接加入 original;否则说明 c 为某个数的倍数,令它在 map 中记录的次数减 1,如果减到 0 就直接移除它
-
public int[] findOriginalArray(int[] changed) { int n = changed.length; Map<Integer, Integer> map = new HashMap<>(); Arrays.sort(changed); int[] res = new int[n / 2]; int ri = 0; for(int c : changed){ // 如果不是某个数的倍数 if(!map.containsKey(c)){ // 如果 res 都被塞满了说明有太多数不是某个数的倍数 // 比如 [1,3,5,7],所以可以确定无法还原了 if(ri >= res.length)return new int[0]; res[ri++] = c; // c 的倍数出现的次数加 1 // 相当于 map.put(c * 2, map.getOrDefault(c * 2, 0) + 1); map.merge(c * 2, 1, Integer::sum); // 是某个数的倍数 }else{ // 出现次数减 1 int count = map.merge(c, -1, Integer::sum); if(count == 0)map.remove(c); } } return res; }
- 排序 + 队列:其实我们可以理解,上面的先加到 original 的数 c,也先记录 2c 的出现次数,并且如果可以还原,也会先消除 2c,这种先进先出的概念使得我们可以使用更轻量级的队列代替哈希表,如果 c 等于队列头就说明它是某个数的倍数就消除它,否则就加入 original
-
public int[] findOriginalArray(int[] changed) { Queue<Integer> q = new ArrayDeque<>(); Arrays.sort(changed); int[] res = new int[changed.length / 2]; int ri = 0; for(int c : changed){ if(!q.isEmpty()){ // q.peek() < c 说明数组中没有某个数的两倍 // 比如 [1,5,6,7],遍历到 5 就可以确定无法还原了 // 因为没有 1 的两倍 2 if(q.peek() < c)return new int[0]; // 能消就消,不能消就到下面继续处理,加入 original if (q.peek() == c){ q.poll(); continue; } } if(ri >= res.length) return new int[0]; res[ri++] = c; // 把 c 的两倍加入 q q.offer(c * 2); } return res; }
- 上述两种方法的时间复杂度都因为排序被限制在了 O(nlogn),我们也可以不排序。类似消消乐的思想,比如
[1,1,2,2,2,4,8,16,3,6,24,48,9,9,18,18]
,我们根据 x,2x,4x… 的规律可以划分为 4 组 - 1, 1, 2, 2, 2, 4, 8, 16
- 3, 6
- 24, 48
- 9, 9, 18, 18
- 上面第二三两组不在一起是因为没有 12 使得他们满足划分的规律
- 每一组可以进行相同的处理
- 比如第一组,先用 1 消 2 得到
[2,4,8,16]
,把两个 1 加入 original;再用 2 消 4 得到[8,16]
,4 加入 original;再用 8 消除 16,把 8 加入 original。剩余三组同理。 - 综上,我们遇到 2x 先跳过,遇到 x 的时候再开始处理,把它那一组都处理完。具体的,我们先用哈希表记录每个数出现的次数,然后遍历每个数,如果 2/x 在哈希表中就先跳过,否则消除 x,2x,4x…,消的同时记得把数字加入 original 即可。
- 由于 0 的两倍还是 0,所以我们特殊处理一下
-
public int[] findOriginalArray(int[] changed) { Map<Integer, Integer> map = new HashMap<>(); for(int x : changed)map.merge(x, 1, Integer::sum); if(map.getOrDefault(0 , 0) % 2 == 1)return new int[0]; map.remove(0); int[] res = new int[changed.length / 2]; int ri = 0; for(int x : map.keySet()){ // 如果是 2x 就跳过 if(x % 2 == 0 && map.containsKey(x / 2))continue; // 否则先当你是 x while (map.containsKey(x)) { int cntX = map.get(x); int cnt2X = map.getOrDefault(x*2, 0); // 如果 2x 的个数都不足以消除 x,那就无法还原 if(cnt2X < cntX)return new int[0]; // 否则就暂时可以还原,先把每个 x 加入 original for(int i = 0; i < cntX; i++)res[ri++] = x; // 如果还有剩下的 2x 就更新它的个数,并且下次处理从 2x 开始 if(cnt2X > cntX){ map.put(x * 2, cnt2X - cntX); x *= 2; // 如果 2x 被消完了,下次处理从 4x 开始 }else{ x *= 4; } } } return res; }