从零学算法2007

本文探讨了通过排序和数据结构(哈希表或队列)解决从双倍数组还原原数组问题的方法,时间复杂度为O(nlogn)。
摘要由CSDN通过智能技术生成

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;
      }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值