LeetCode 2007.从双倍数组中还原原数组:哈希表——从nlogn到n

2024-04-18 17:20

本文主要是介绍LeetCode 2007.从双倍数组中还原原数组:哈希表——从nlogn到n,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【LetMeFly】2007.从双倍数组中还原原数组:哈希表——从nlogn到n

力扣题目链接:https://leetcode.cn/problems/find-original-array-from-doubled-array/

一个整数数组 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

方法一:哈希表 + 排序

使用哈希表记录每个元素出现(剩余)的次数。对原始数组排个序,接着遍历原始数组:

  • 如果这个元素已经被“消耗”了,则跳过;
  • 否则,“移除”这个元素。这个元素的二倍必须在哈希表中:
    • 若在,则找到“一对”,记入答案数组中,并将二倍元素移除;
    • 否则,无法还原,返回空数组。

为什么要排序?因为排序后遍历结果保证是从小到大,一个元素要么已经被“消耗”(则其为二倍元素),要么(其为一倍元素)必须消耗一个二倍元素。

  • 时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),其中 n = l e n ( c h a n g e d ) n=len(changed) n=len(changed)时间复杂度的瓶颈在于排序
  • 空间复杂度 O ( n ) O(n) O(n),或者说 O ( C ) O(C) O(C),其中 C = r a n g e ( c h a n g e d ) = 1 0 5 C=range(changed)=10^5 C=range(changed)=105

AC代码

C++
class Solution {
public:vector<int> findOriginalArray(vector<int>& changed) {sort(changed.begin(), changed.end());vector<int> a(200000 + 1);  // 手动unordered_map  // 2倍防越界for (int t : changed) {a[t]++;}vector<int> ans;for (int t : changed) {if (!a[t]) {continue;}a[t]--;if (!a[t * 2]) {return {};}a[t * 2]--;ans.push_back(t);}return ans;}
};

为什么使用数组模拟哈希表?因为内置的unordered_map没试,unordered_multiset非常慢会超时。

方法二:哈希表 + 反推

方法一时间复杂度的瓶颈是排序,排序是为了在遍历过程中判断一个元素是“一倍元素”还是“二倍元素”。

有没有办法不排序呢?当然有,那就是遍历过程中,“遇到1/2元素则先跳过”:

遍历到元素 t t t,如果 t 2 \frac{t}{2} 2t还存在,就先跳过 t t t,遍历到 t 2 \frac{t}{2} 2t时再处理 t t t

这样就需要一个计数器记录还剩下多少元素(防止两个 t t t和一个 t 2 \frac{t}{2} 2t的情况),以及特判 0 0 0的情况(因为 0 2 = 0 \frac{0}{2}=0 20=0不能跳过)。

完了吗?没完,还需要加个while循环

给定一组样例[2,4,2,1],如果按照上述思路则会:跳过 2 2 2、跳过 4 4 4、跳过 2 2 2,处理 1 1 1,最终剩下一个 2 2 2 4 4 4,本应是一对确误判有所剩余。

这是因为对于 4 4 4本来应该在遍历到 2 2 2时处理,结果遍历到 2 2 2时一看有 1 1 1跳过了。

因此遇到 t 2 \frac{t}{2} 2t不存在的 t t t时,应当在 t t t有剩余时,不断“反推”,令 t = 2 t t=2t t=2t并继续“抵消”,直到 t t t无剩余。

这样对于[2,4,2,1]

处理到 1 1 1 t = 1 t=1 t=1 t 2 = 0.5 \frac{t}{2}=0.5 2t=0.5不存在,因此抵消 t t t 2 t 2t 2t(配对成功一个 1 1 1 2 2 2

t = 2 t = 2 t=2t=2 t=2t=2,抵消 2 2 2 4 4 4(配对成功一个 2 2 2 4 4 4

t = 2 t = 4 t = 2t = 4 t=2t=4,发现 4 4 4已经不存在了,结束

最终得到原始数组[1, 2]

完了吗?没完,单单 2 t 2t 2t不存在了还不能结束循环,要判断 4 t 4t 4t是否存在:

给定样例[4,8,2,1],处理到 4 4 4 8 8 8 2 2 2时都会跳过,而处理到 1 1 1时:

t = 1 t=1 t=1发现 1 1 1 2 2 2

t = 2 t = 2 t=2t=2 t=2t=2发现 2 2 2不存在了,循环结束

则会剩下一个本应是一对的 4 4 4 8 8 8

因此,在while循环中,不能单单地令 t = 2 t t=2t t=2t做这一步的反推。

2 t 2t 2t已经不存在时,应该令 t = 4 t t=4t t=4t

4 t 4t 4t也不存在,再结束while循环

至此,算法达成。

  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( n ) O(n) O(n),或者说 O ( C ) O(C) O(C),其中 C = r a n g e ( c h a n g e d ) = 1 0 5 C=range(changed)=10^5 C=range(changed)=105

AC代码

C++

降低了时间复杂度,代价是代码变得很长且很容易有没考虑周全的地方。

class Solution {
public:vector<int> findOriginalArray(vector<int>& changed) {vector<int> a(400001);for (int t : changed) {a[t]++;}int remain = changed.size();vector<int> ans;if (a[0] % 2) {return {};}remain -= a[0];ans.resize(a[0] / 2);a[0] = 0;for (int t : changed) {if (t % 2 == 0 && a[t / 2]) {continue;}while (a[t]) {int thisCnt = a[t];a[t] = 0;remain -= thisCnt;if (a[t * 2] < thisCnt) {return {};}a[t * 2] -= thisCnt;remain -= thisCnt;ans.insert(ans.end(), thisCnt, t);if (a[t * 2]) {t *= 2;}else {t *= 4;}}}return remain ? vector<int>() : ans;}
};

同步发文于CSDN和我的个人博客,原创不易,转载经作者同意后请附上原文链接哦~
Tisfy:https://letmefly.blog.csdn.net/article/details/137924488

这篇关于LeetCode 2007.从双倍数组中还原原数组:哈希表——从nlogn到n的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/915408

相关文章

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

vue如何监听对象或者数组某个属性的变化详解

《vue如何监听对象或者数组某个属性的变化详解》这篇文章主要给大家介绍了关于vue如何监听对象或者数组某个属性的变化,在Vue.js中可以通过watch监听属性变化并动态修改其他属性的值,watch通... 目录前言用watch监听深度监听使用计算属性watch和计算属性的区别在vue 3中使用watchE

哈希leetcode-1

目录 1前言 2.例题  2.1两数之和 2.2判断是否互为字符重排 2.3存在重复元素1 2.4存在重复元素2 2.5字母异位词分组 1前言 哈希表主要是适合于快速查找某个元素(O(1)) 当我们要频繁的查找某个元素,第一哈希表O(1),第二,二分O(log n) 一般可以分为语言自带的容器哈希和用数组模拟的简易哈希。 最简单的比如数组模拟字符存储,只要开26个c

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

hdu 1166 敌兵布阵(树状数组 or 线段树)

题意是求一个线段的和,在线段上可以进行加减的修改。 树状数组的模板题。 代码: #include <stdio.h>#include <string.h>const int maxn = 50000 + 1;int c[maxn];int n;int lowbit(int x){return x & -x;}void add(int x, int num){while

leetcode-24Swap Nodes in Pairs

带头结点。 /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode(int x) { val = x; }* }*/public class Solution {public ListNode swapPairs(L