有效地找到不在大小为 40、400 或 4000 的集合中的整数
Posted
技术标签:
【中文标题】有效地找到不在大小为 40、400 或 4000 的集合中的整数【英文标题】:Efficiently find an integer not in a set of size 40, 400, or 4000 【发布时间】:2019-05-19 00:11:53 【问题描述】:与经典问题find an integer not among four billion given ones相关但不完全相同。
为了澄清,整数我真正的意思只是其数学定义的一个子集。也就是说,假设只有有限数量的整数。说在 C++ 中,它们是int
,在[INT_MIN, INT_MAX]
的范围内。
现在给定一个std::vector<int>
(不重复)或std::unordered_set<int>
,其大小可以是40、400、4000左右,但不能太大,如何高效生成一个保证不在给定中的数字那些?
如果不担心溢出,那么我可以将所有非零值相乘,然后将乘积加 1。但确实存在。攻击者的测试用例可能故意包含INT_MAX
。
我更赞成简单、非随机的方法。有吗?
谢谢!
更新:为了消除歧义,假设一个未排序的 std::vector<int>
保证没有重复项。所以我问是否有比 O(n log(n)) 更好的东西。另请注意,测试用例可能同时包含INT_MIN
和INT_MAX
。
【问题讨论】:
对向量进行排序可以在O(n log(n))
中完成,不知道能不能得到更高效的方法
向量排序了吗?对于集合,它是微不足道的,因为它是排序的。
@user463035818 如果你能对O(log(n))
中的一个向量进行排序,你确实应该变得很富有了。
@Walter meh 这是一个错字,不幸的是我没有数百万
排序后,你会:Last + 1?
【参考方案1】:
随机生成 x (INT_MIN..INT_MAX) 并针对所有人进行测试。在失败时测试 x++(40/400/4000 的情况非常罕见)。
【讨论】:
他正在寻找一种有效的方法来做到这一点,这将是 O(n^2)。是的,大多数时候它会好得多,但在最坏的情况下仍然非常低效(并且 OP 特别要求 O,这是最坏的情况) @dquijada 不,不是。大 O 表示法描述了函数的渐近行为,但它确实不意味着它是最坏的情况。换句话说,一个算法可能有不同的上限,具体取决于您正在研究的输入。 @Acorn 通常使用 big-O 符号来描述算法的最坏情况时间复杂度,即对于 所有 输入可能具有的最坏时间。当然,输入的一个子集可能会以较小的复杂性运行,但最坏情况意味着考虑所有可能性。 Big-O 也可以用来描述平均-case 复杂度。但是,据我所知,“这个算法需要 O(f(n))”这个句子的“默认含义”是 O(f(n)) 是 worst-case情景,而不是一般情况。 @Bakuriu Big O 表示法与分析的复杂性类型无关。它甚至可能不是时间复杂度。当然,您可以争辩说,人们可能假设我们谈论的是最坏情况的时间复杂度,dquijada 说“OP 专门要求 O,这是最坏的情况 ",这根本不是真的。 OP 没有具体说明最坏情况(恰恰相反:我们只能假设),“O”也没有表示最坏情况(这根本不是真的)。 此外,在这个特定问题中,这个解决方案实际上在实践中非常有效,因为你几乎从来没有遇到最坏的情况(和/或如果你检测到你可能遇到了,你可以简单地回退到另一个解决方案它,例如经过几次尝试;很容易给你一个最坏情况的 O(n) 算法)。因此,通过说它是 O(n^2) 并声称 OP 想要一个好的最坏情况算法来驳斥这个解决方案是完全错误的。【参考方案2】:第 1 步: 对向量进行排序。
这可以在 O(n log(n)) 内完成,你可以在网上找到几种不同的算法,使用你最喜欢的一种。
第 2 步: 找到不在向量中的第一个 int。
轻松地从 INT_MIN 迭代到 INT_MIN + 40/400/4000 检查向量是否具有当前 int:
伪代码:
SIZE = 40|400|4000 // The one you are using
for (int i = 0; i < SIZE; i++)
if (array[i] != INT_MIN + i)
return INT_MIN + i;
解决方案是 O(n log(n) + n) 意思是:O(n log(n))
编辑: 只是阅读您的编辑要求比 O(n log(n)) 更好的东西,对不起。
【讨论】:
感谢您的回答。一个小问题:虽然它是伪代码,但当i
从INT_MIN
开始时,array[i]
似乎没有任何意义。
您可以使用 O(log N) 中的二进制搜索进行搜索。
@rici:您将如何对“丢失的数字”进行二进制搜索?如果您知道丢失的数字,您可以在 log(n) 时间内在排序数组中找到它所属的位置。我认为你不能比线性做得更好,尽管可能具有较低的常数因子。在均匀分布的情况下,线性搜索平均会在O(n / max_n)
时间出现差距,但最坏的情况是n
(直到最后都没有差距。)
如果您使用选择排序进行排序,您可以即时检查间隙,并且很有可能会在第一遍或第二遍中找到不重复的内容。 (特别是如果您还要检查随机猜测候选者。)对于小尺寸,O(n) 表示法不太有意义/有用。
@peter:与进行线性搜索的方式相同,将i + min
与vec[i]
进行比较。如果不相等,则i
前面有缺失值,所以你一分为二;否则你一分为二。二分搜索适用于任何单调谓词。【参考方案3】:
随机方法在这里确实非常有效。
如果我们想使用确定性方法,并且假设大小 n 不太大,例如 4000,那么我们可以创建一个大小为 @ 的向量 x 987654321@(或者大一点,比如4096,方便计算),初始化为0。
对于范围内的每个i
,我们只需设置 x[array[i] modulo m] = 1。
然后在 x 中进行简单的 O(n) 搜索将提供一个不在 array
中的值注意:模运算不完全是“%”运算
编辑:我提到在这里选择 4096 的大小会使计算变得更容易。更具体地说,这意味着模运算是通过简单的&
运算来执行的
【讨论】:
【参考方案4】:您可以只返回输入中未包含的N+1
候选整数中的第一个。最简单的候选者是数字0
到N
。这需要O(N)
空间和时间。
int find_not_contained(container<int> const&data)
const int N=data.size();
std::vector<char> known(N+1, 0); // one more candidates than data
for(int i=0; i< N; ++i)
if(data[i]>=0 && data[i]<=N)
known[data[i]]=1;
for(int i=0; i<=N; ++i)
if(!known[i])
return i;
assert(false); // should never be reached.
随机方法可以更节省空间,但在最坏的情况下可能需要更多的数据传递。
【讨论】:
我喜欢这个。它避免了排序和搜索。 这是查找不在给定集合中的最小整数的标准算法 @DreamConspiracy 这是有道理的。不过我不知道。 有趣的事实:在具有分散存储的机器上,例如带有 AVX512F 的 x86,该算法可以向量化。 (至少对于 x86 样式的散点图,其中掩码控制实际散布的元素)。与直方图问题不同的是,您必须对命中相同索引的多个矢量元素进行冲突检测,您只需要存储一个1
就可以了。 (x86 只能以 32 位或 64 位粒度散布,因此您必须将 known
提升为 int
,对于较大的 N
,这可能不值得。如果 N
足够大,您将想要使用位图而不是 char 数组。)
最后的搜索可以很容易地矢量化,使用适用于手动优化strlen
的任何技巧。 (例如,现代 x86 应该能够检查每个时钟周期 16 到 64 个字节,并使用 SSE2 或 AVX2 进行良好调整的循环,具体取决于硬件。)【参考方案5】:
如果允许使用以下算法对输入向量重新排序,则可以使用 O(1) 辅助空间在 O(N) 时间内找到最小的未使用整数。 [注1](如果向量包含重复数据,该算法也有效。)
size_t smallest_unused(std::vector<unsigned>& data)
size_t N = data.size(), scan = 0;
while (scan < N)
auto other = data[scan];
if (other < scan && data[other] != other)
data[scan] = data[other];
data[other] = other;
else
++scan;
for (scan = 0; scan < N && data[scan] == scan; ++scan)
return scan;
第一遍保证如果[0, N)
范围内的一些k
在位置k
之后找到,那么它现在出现在位置k
。这种重新排列是通过交换来完成的,以避免丢失数据。扫描完成后,值与其索引不同的第一个条目不会在数组中的任何位置引用。
该断言可能不是 100% 明显的,因为可以从较早的索引中引用条目。但是,在这种情况下,条目不能是不等于其索引的第一个条目,因为较早的条目将满足该标准。
要看到这个算法是 O(N),应该观察到第 6 行和第 7 行的交换只有在目标条目不等于其索引时才会发生,并且在交换之后目标条目相等到它的索引。所以最多可以执行N
交换,并且第5 行的if
条件将是true
最多N
次。另一方面,如果if
条件为假,scan
将递增,这也只会发生N
次。所以if
语句最多被评估2N
次(即O(N))。
注意事项:
-
我在这里使用了无符号整数,因为它使代码更清晰。该算法可以很容易地针对有符号整数进行调整,例如通过将有符号整数从
[INT_MIN, 0)
映射到无符号整数 [INT_MAX, INT_MAX - INT_MIN)
(减法是数学的,而不是根据不允许表示结果的 C 语义。) 2 的补码,这是相同的位模式。当然,这会改变数字的顺序,这会影响“最小未使用整数”的语义;也可以使用保序映射。
【讨论】:
要处理 OP 的要求,最小的正数未使用就足够了,因此处理有符号整数的更简单方法是在数字为负数时不交换。 @taemyr:如果您使用负整数的“2 的补码”映射会发生这种情况,这不需要额外的测试(假设 C 中的强制转换是无操作的)。通常,如果您想在某个范围内找到最小的可用数字,您可以忽略该范围之外的任何条目,并从有用的条目中减去该范围的开头。我打算添加这个观察结果,但我最感兴趣的是原始算法的优雅。【参考方案6】:对于在std::unordered_set<int>
(而不是std::vector<int>
)中提供整数的情况,您可以简单地遍历整数值的范围,直到遇到一个不存在于unordered_set<int>
。在std::unordered_set<int>
中搜索整数的存在非常简单,因为std::unodered_set
确实提供了通过其find()
成员函数进行的搜索。
这种方法的空间复杂度将是 O(1)。
如果您从int
(即std::numeric_limits<int>::min()
)的最低可能值开始遍历,您将获得最低int
未包含在std::unordered_set<int>
:
int find_lowest_not_contained(const std::unordered_set<int>& set)
for (auto i = std::numeric_limits<int>::min(); ; ++i)
auto it = set.find(i); // search in set
if (it == set.end()) // integer not in set?
return *it;
类似地,如果您从int
(即std::numeric_limits<int>::max()
)的最大可能值开始遍历,您将获得最低int
而不是包含在std::unordered_set<int>
:
int find_greatest_not_contained(const std::unordered_set<int>& set)
for (auto i = std::numeric_limits<int>::max(); ; --i)
auto it = set.find(i); // search in set
if (it == set.end()) // integer not in set?
return *it;
假设int
s被散列函数一致映射到unordered_set<int>
的桶中,则可以在恒定时间内完成对unordered_set<int>
的搜索操作。运行时复杂度将是 O(M ),其中 M 是您正在寻找非包含值的整数范围的大小。 M 的上限为 unordered_set<int>
的大小(即在您的情况下为 M )。
确实,通过这种方法,选择任何大于unordered_set
大小的整数范围,都可以保证遇到unordered_set<int>
中不存在的整数值。
【讨论】:
以上是关于有效地找到不在大小为 40、400 或 4000 的集合中的整数的主要内容,如果未能解决你的问题,请参考以下文章