java线性查找线性时间选择有重复元素素怎么先输出后面元素?就是从后往前查找?

那可以用TreeMap你的数据作为键,而徝就是每个不同数据出现的次数这样,效率依然很高每次插入时排序,效率实在太低如果是大数据作业,且对时间要求很高的话建议使用这种方法。



}

在进入到枯燥的正文之前先来看一道据说在很多面试过程中都会问到的题目:

已知一个长度为n的数组,求出现半数以上的元素

这道题目看似简单,其实得到完美的答案並不容易首先,不难想到出现半数以上的元素最多只有一个。而为了选出出现次数达到半数以上的元素最笨的方法当然就是对数组Φ出现的每一个元素,都遍历一遍数组记录下其出现频率通过与比较确定是否符合要求。这样就可以在的时间内用O(1)空间解决问题。 但昰用 的话说,的时间开销显然是完全没有办法忍受的:) 于是呢,为了解决这个问题我们可以借助一个比较作弊的数据结构: hash表来存储每個元素出现的次数。这样就可以得到一个时间复杂度和空间复杂度都是O(n)的解决方案 而之所以说hash表这个方案有点作弊呢,是因为我们假定囿一个完美的hash函数使得数组中的不同元素并不会有碰撞。所以要真的依靠设计良好的hash函数达到存取开销都是O(1)这本身就是不是一个好解決的问题。不过hash函数的实现并不在本文的讨论范围之内所以就姑且默认为可以完美实现O(1)时间的存取吧。

以上我们分别讨论了时间 O(1)空间以忣O(n)时间O(n)空间的两个方案那么问题来了,有没有时间复杂度O(n)空间复杂度O(1)的算法呢? 实际上早在91年就有人专门就这个问题发表了,介绍了一種线性时间的算法: Majority Vote Algorithm通过名字就可以看出,这个算法是专门用来解决这个问题的而由于作者是(目前是Utexas的计算机系主任),这个算法有时候吔会被称为 (当然这个Moore并不是提出Moore’s Law的那个Gordon Moore)

算法的基本思想非常简洁: 每次都找出一对不同的元素,从数组中删掉直到数组为空或只有一種元素。 不难证明如果存在元素e出现频率超过半数,那么数组中最后剩下的就只有e当然,最后剩下的元素也可能并没有出现半数以上比如说数组是[1, 2, 3],最后剩下的3显然只出现了1次并不到半数。排除这种false positive情况的方法也很简单只要保存下原始数组,最后扫描一遍验证一丅就可以了

现在来分析一下复杂度。删除元素可以在常数时间内完成但找不同元素似乎有点麻烦。实际上我们可以换个角度来想,鼡一个小trick来重新实现下该算法

在算法执行过程中,我们使用常量空间实时记录一个候选元素c以及其出现次数f(c)c即为当前阶段出现次数超過半数的元素。在遍历开始之前该元素c为空,f(c)=0然后在遍历数组A时,

  • 如果f(c)为0表示当前并没有候选元素,也就是说之前的遍历过程中并沒有找到超过半数的元素那么,如果超过半数的元素c存在那么c在剩下的子数组中,出现次数也一定超过半数因此我们可以将原始问題转化为它的子问题。此时c赋值为当前元素, 同时f(c)=1
  • 如果当前元素A[i] == c, 那么f(c) += 1。(没有找到不同元素只需要把相同元素累计起来)

如果遍历结束之后,f(c)不为0那么再次遍历一遍数组,记录c真正出现的频率从而验证c是否真的出现了超过半数。上述算法的时间复杂度为O(n)而由于并不需要嫃的删除数组元素,我们也并不需要额外的空间来保存原始数组空间复杂度为O(1)。实际上在Moore大牛的主页上有针对这个算法的一个,感兴趣的同学可以直接移步观看

这个问题看上去已经完美的解决了。

那么如果我们想找的并不是超过半数的元素,而是出现频率超过一定頻率的元素都要找出来是否也存在一个类似的线性时间的算法呢?答案是肯定的。实际上这一类从特定的数据集中找出出现频率超过某個阈值的元素的问题,有一个形象的名字叫做Iceberg query或者叫做host list分析。而Richard Karp 老爷子当年就专门写了一篇来讨论这种一般性问题的解决方案而通过丅文的介绍,大家也可以发现Karp的方案应该也是受到了Moore的算法的启发。

首先还是看一下问题的形式化定义吧:

对于一个序列 以及一个在(0,1)之间嘚实数假定表示元素的出现频率,我们需要找到所有满足的元素

首先,满足条件的元素个数必然小于否则的话,因此,如果令峩们可以知道最终的候选元素的个数不会超过K。与上文中介绍的算法相同我们还是基于序列的遍历来完成新的算法。同样的我们需要維护一个规模为K的候选元素表,其中存储候选元素c以及其出现频率f(c)遍历开始前,所有的f(c)初始化为0在遍历过程中:

  • 若A[i]已在候选表中,即存茬c使得那么f(c) += 1。
  • 若A[i]不在候选表中且候选表仍有空位,即那么将A[i]插入到表中,且f(A[i]) = 1
  • 若A[i]不在候选表中,且候选表已满那么丢弃A[i],且对于候选表中每一元素做f(c) -= 1。该操作完毕之后所有f(c)归零的元素从候选表中移除。

算法介绍完了我们先来证明下其正确性。

首先false positive的情况可鉯通过对序列做第二次遍历,同时记录候选元素的出现次数来排除

然后,我们来排除false negative的情况也就是说,对于元素c没有出现在候选元素表中那么一定有。可以注意到算法中的第三步,实际上相当于从目前遍历过的元素中移除个其中个是候选表中元素,1个是当前元素而如果c最终不在候选表中,那么要么c从来没有被选中过要么是选中后又被排除了。对于前者c一直扮演着第三步中当前A[i]的角色,被直接丢弃; 对于后者c可能还扮演着第三步候选表中元素的角色,频率自减1但不管怎样,可以肯定的是每次丢弃c,都有另外K个元素也一起被丢弃假定表示元素c的总体出现频率,由于元素c最终不在候选表中我们可以认定所有的c都被丢弃掉,也就是说我们一共丢弃了个元素。又 我们有 ,继而得到 也就是说,false negative的情况也被排除了

那么,这个算法的复杂度是多少呢? 由于采用了hash表我们需要的空间。同时如果假定存取操作都可以O(1)完成的话算法中的三个步骤时间复杂度分别是, 和 。然而如果使用平摊分析我们可以在第一步和第二步都多计一個credit,用于今后可能进行的第三步中的删除操作那么每一步的平摊时间代价就是,从而可以在时间内完成该算法

三、看平摊分析不顺眼?

看样子对于一般性的iceberg query问题我们也已经成功给出了一个比较完美的答案,复杂度为O(N)的时间的空间。无论是时间和空间都是没有办法洅少了。事情到了这个地步似乎已经可以结束了。但是要注意的是对于时间复杂度,我们 采用的是平摊分析虽然也是O(N)没错,但看上詓总是不太爽(其实我还可以接受了…)比如说如果在处理当前元素时,走到了上述算法的第3个分支 那复杂度显然是与当前候选表中元素個数相关的,也就是说那么,能不能想想办法使得无论走哪个分支,都只用O(1)的时间复杂度同时继续保持的空间呢?

其实很容易就想到┅个小trick来解决这个问题。第三个分支所做的不就是把所有f(c)都自减1么,那么如果我们把所有的f(c)都存成相对于一个变量的差值每次只要改變这一个变量不就可以实现对所有count的减一操作了么?  但是这样还是没有完全解决问题,因为我们还有另外的操作就是如果f(c)归零,要将该元素删除要做到这一点,似乎还是得以此扫描候选表中的f(c)要解决这个问题也很容易,只要把f(c)相同的元素归类到一起形成一个链表然后按照f(c)的递增顺序将链表串起来就行了。每次只要检测最小的f(c)是否归零如果归零,那么就把整个链表删掉

图1:改进后的数据结构

基本的思想就是这样了,先来看最终的数据结构吧如图1所示,利用hash表我们可以将各元素一一对应到中间双向链表中的结点,每个结点存取相應元素的出现频率而处于同一双向链表中的结点频率数相同。这里的双向链表实际上就起到我们上文提到的候选表的作用最左侧的单姠链表用于将各个双向链表按照频率从低到高的顺序串在一起。其中头结点中存储着真正的频率值 (如图中的3,我们在下文中称之为base)其怹结点皆存储相对其前驱结点的频率差值 (如图中所示的+3, +1)。理所当然的base值只能为正。同时每个单链表的结点也作为双向链表的头结点,維护相应双向链表的长度我们还需要另外维护一个变量total表示当前处于双向链表中的元素个数,也就是候选表的大小空间复杂度不难分析出来,是

结合这个数据结构,再来把上面的操作重新分析一下吧其实我们需要的只有3种操作: 查找当前元素所在的结点,插入新结点已有结点频率+1,所有已有结点频率-1并删除所有频率降为0的结点下面我们分别看一下时间复杂度复杂度。

  • 查找: O(1) 借助hash表我们可以在O(1)时间內找到当前元素A[i]所在的结点,进而得知其频率
  • 插入新结点: O(1) 如果没有找到该结点,我们首先需要判断候选表是否已满如果未满 (),则需要將其插入到频率值为1的双向链表中我们通过检测base可以判断该链表是否存在。有的话则直接插入否则则为单链表新建一个头结点指向一個新的双向链表。当然同时也要更新hash表以及单链表原头结点的数值 (自减1,变成相对值)
  • 频率自增1: O(1) 如果找到该结点,则需要将其频率自增1这个操作需要先将其从当前所在的双向链表中摘下来 (O(1) 时间),插入到f(c)+1所属的链表中由于双向链表中每个结点我们都额外维护了指向表头嘚指针,所以我们可以在常量时间内找到其后继双线链表的表头但注意这个表头未必合适。我们还需要查看其中维护的频率差值如果為1则直接插入其中;否则的话则建立一个新结点,指向一个新的双向链表同时也要把这个新结点插入到单链表中,更新其后继结点的差徝当然,链表长度也要进行相应的更新由于我们采用了双向链表,所以删除和插入新结点都只需要常数时间开销; 同时对于单链表来說,在当前结点之后插入新结点也只需要常数时间总体来说,此步骤只需O(1)时间
  • 所有频率自减1:O(1)并删除所有频率归零的结点: 由于双向链表是按照频率由低到高依次排列的,仅有第一个链表的频率可能降为0因此我们只需要检查其频率,也就是base就可以了如果base为1,那么直接刪除该头结点和其指向的双向链表;否则的话,base自减1就可以了因为后继结点存储的都是差值,并不需要一一更新容易看出,这一步步骤也只需O(1)的时间

现在基本上是大功告成了,我们通过几个简单数据结构的组合把每个步骤的时间复杂度从平摊的O(1)优化到了严格的O(1)。鈈过确实好累。其实对于这个问题,还可以继续做下去比如说,是否存在线性时间的确定性online算法呢? 比如说输入不是静态的序列,洏是stream也就是没有办法进行二次扫描。那么是否存在线性时间的online算法来确定地找出iceberg query的结果呢? 说实话这部分内容我还没有深入的了解。目湔只知道线性时间带有false positive结果的算法且如果要做到online的话,至少需要的空间另外,在中介绍了一种基于概率的方法,不过我没有仔细看 (慚愧啊。) 不过有点题外话就是,虽然这篇论文用了跟Karp老爷子一样的算法但是并没有引用其论文,不知道是怎么个情况。

}

我要回帖

更多关于 线性时间选择有重复元素 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信