简介
这是一个可能的解决方案。这是相当做作且不实用的,但是问题也是如此。如果我的分析中有漏洞,我将不胜感激。如果这是一个“官方”解决方案的家庭作业或挑战问题,我也很想看看原始海报是否仍然存在,因为距离被问到已经过去了一个多月。
首先,我们需要充实问题的一些不明确的细节。所需的时间复杂度是O(N),但N 是什么?大多数评论员似乎都假设N 是数组中的元素数。如果数组中的数字具有固定的最大大小,这将是可以的,在这种情况下,Michael G 的基数排序解决方案将解决问题。但是,在原始发帖人没有澄清的情况下,我将约束 #1 解释为不需要固定最大位数。因此,如果n(小写)是数组中元素的数量,m 是元素的平均长度,那么要处理的总输入大小是mn。解决方案时间的下限是O(mn),因为这是验证解决方案所需的输入通读时间。因此,我们想要一个与总输入大小N = nm 成线性关系的解决方案。
例如,我们可能有n = m,即sqrt(N) 元素的平均长度为sqrt(N)。比较排序需要O( log(N) sqrt(N) ) < O(N) 操作,但这不是胜利,因为操作本身平均需要O(m) = O(sqrt(N)) 时间,所以我们回到O( N log(N) )。
此外,如果m 是最大 长度而不是平均 长度,则基数排序将采用O(mn) = O(N)。如果假设数字落在某个有界范围内,则最大和平均长度将处于相同的顺序,但如果不是这样,我们可能会有一个小百分比的数字大且可变,而大百分比的数字数字少.例如,10% 的数字的长度可能为 m^1.1,而 90% 的长度可能为 m*(1-10%*m^0.1)/90%。平均长度为m,但最大长度为m^1.1,因此基数排序为O(m^1.1 n) > O(N)。
为了避免有人担心我对问题定义的改动太大,我的目标仍然是描述一个时间复杂度与元素数量成线性关系的算法,即O(n)。但是,我还需要对每个元素的长度执行线性时间复杂度的操作,这样平均而言,这些操作在所有元素上将是O(m)。这些操作将是计算元素哈希函数和比较所需的乘法和加法。如果这个解决方案确实解决了O(N) = O(nm) 中的问题,那么这应该是最佳复杂度,因为验证答案需要相同的时间。
问题定义中省略的另一个细节是是否允许我们在处理数据时销毁数据。为简单起见,我将这样做,但我认为可以避免这种情况。
可能的解决方案
首先,可能存在负数的约束是空的。通过一次数据,我们将记录最小元素z和元素数量n。在第二次遍历中,我们将为每个元素添加(3-z),因此现在最小的元素为 3。(请注意,恒定数量的数字可能会因此溢出,因此我们应该对数据进行恒定数量的额外遍历首先测试这些解决方案。)一旦我们有了我们的解决方案,我们只需减去(3-z) 以将其恢复为原始形式。现在我们可以使用三个特殊的标记值0、1 和2,它们本身不是元素。
第一步
使用median-of-medians selection algorithm 确定数组A 的第90 个百分位元素p,并将数组划分为两个集合S 和T,其中S 具有10% of n 元素大于p 和T 的元素小于p。这需要O(n) 步骤(平均步骤O(m) 总共需要O(N))时间。匹配p 的元素可以放入S 或T,但为了简单起见,遍历数组一次并测试p 并通过将其替换为0 来消除它。设置S 最初跨越索引0..s,其中s 大约是n 的10%,设置T 跨越剩余90% 的索引s+1..n。
第 2 步
现在我们将遍历i in 0..s,并且对于每个元素e_i,我们将计算一个散列函数h(e_i) 到s+1..n。我们将使用universal hashing 来获得均匀分布。因此,我们的散列函数将进行乘法和加法,并在每个元素的长度上花费线性时间。
我们将对碰撞使用修改后的线性探测策略:
h(e_i) 被T 的成员占用(意思是A[ h(e_i) ] < p 但不是标记1 或2)或者是0。这是一个哈希表未命中。通过交换插槽i 和h(e_i) 中的元素插入e_i。
-
h(e_i) 被S 的成员(意思是A[ h(e_i) ] > p)或标记1 或2 占用。这是一个哈希表冲突。进行线性探测,直到遇到 e_i 的重复项或 T 或 0 的成员。
如果是T 的成员,这又是一个哈希表未命中,因此通过交换到插槽i 插入e_i,如(1.)。
如果与 e_i 重复,则这是哈希表命中。检查下一个元素。如果该元素是1 或2,我们已经不止一次看到e_i,将1s 更改为2s,反之亦然,以跟踪其奇偶性变化。如果下一个元素不是1 或2,那么我们之前只见过e_i 一次。我们希望将2 存储到下一个元素中,以表明我们现在已经看到了偶数次e_i。我们寻找下一个“空”槽,即被T 的成员占用的槽,我们将移动到槽i 或0,然后将元素向上移动到索引h(e_i)+1 向下,所以我们在h(e_i) 旁边留出空间来存储我们的奇偶校验信息。请注意,我们不需要再次存储 e_i 本身,所以我们没有用完额外的空间。
所以基本上我们有一个功能性哈希表,其中槽数是我们希望哈希的元素的 9 倍。一旦我们开始获得命中,我们也开始存储奇偶校验信息,所以我们最终可能只有 4.5 倍的插槽数,仍然是一个非常低的负载因子。有几种碰撞策略可以在这里工作,但由于我们的负载因子很低,平均碰撞次数也应该很低,线性探测应该以适当的时间复杂度平均解决它们。
第三步
一旦我们将0..s 的元素散列到s+1..n 中,我们就会遍历s+1..n。如果我们找到 S 的一个元素后跟一个2,那就是我们的目标元素,我们就完成了。 S 的任何元素 e 后跟 S 的另一个元素表示 e 仅遇到一次并且可以清零。同样e 后跟1 表示我们看到e 的次数为奇数,我们可以将e 和标记1 归零。
冲洗并按需要重复
如果我们没有找到我们的目标元素,我们重复这个过程。我们的第 90 个百分位数分区会将 10% 的 n 剩余最大元素移动到 A 的开头,并将剩余元素(包括空的 0-marker 插槽)移动到末尾。我们像以前一样继续进行散列。我们最多必须这样做 10 次,因为我们每次处理 10% 的 n。
结论分析
通过中位数算法进行分区的时间复杂度为 O(N),我们做了 10 次,仍然是 O(N)。每个哈希操作平均占用O(1),因为哈希表负载很低,并且在total 中执行了O(n) 哈希操作(10 次重复中的每一次大约占 n 的 10%)。每个n 元素都有一个为它们计算的散列函数,时间复杂度与它们的长度成线性关系,所以平均来说所有元素O(m)。因此,总的散列操作是O(mn) = O(N)。所以,如果我已经正确分析了这个,那么总的来说这个算法是O(N)+O(N)=O(N)。 (如果加法、乘法、比较和交换的操作被假定为相对于输入的常数时间,它也是O(n)。)
请注意,该算法没有利用问题定义的特殊性质,即只有一个元素出现偶数次。我们没有利用问题定义的这种特殊性质,这为存在更好(更聪明)算法的可能性提供了可能性,但最终也必须是 O(N)。