版权申明:本文为博主窗户(Colin Cai)原创,欢迎转帖。如要转贴,必须注明原文网址 http://www.cnblogs.com/Colin-Cai/p/12664044.html 做者:窗户 QQ/微信:6679072 E-mail:6679072@qq.com
所谓众数,源于这样的一个题目:一个长度为len的数组,其中有个数出现的次数大于len/2,如何找出这个数。html
基于排序算法
排序是第一感受,就是把这个数组排序一下,再遍历一遍获得结果。sql
C语言来写基本以下:数组
int find(int a, int len) { sort(a, len); return traverse(a, len); }
排序有时间复杂度为O(nlogn)的算法,好比快速排序、归并排序、堆排序,而遍历一遍排序后的数组获得结果是时间线性复杂度,也就是O(n)。因此整个算法时间复杂度是O(nlogn)。微信
寻找更好的算法函数
上面的算法实在太简单,不少时候咱们第一感就能够出来的东西未必靠谱。spa
咱们是否是能够找到一种线性时间级别的算法,也就是Θ(n)时间级别的算法?Θ是上下界一致的符号。其实咱们很容易证实,不存在比线性时间级别低的算法,也就是时间o(n),小o不一样于大O,是指低阶无穷大。证实大体以下:code
若是一个算法能够以o(n)的时间复杂度解决上述问题。由于是比n更低阶的无穷大,那么必定存在一个长度为N的数组,在完成这个算法以后,数组内被检测的元素小于N/2。假设算法运算的结果为a,而后,咱们把这个数组在运算该算法时没有检测的元素所有替换为成同一个不是算法所得结果a的数b。而后新的数组,再经过算法去运算,由于没有检测的数不会影响其算法结果,结果天然仍是a,可实际上,数组超过N/2次出现的数是b。从而致使矛盾,因此针对该问题的o(n)时间算法不存在。htm
咱们如今能够开始想点更加深刻点的东西。blog
咱们首先会发现,若是一个数组中有两个不一样的数,将数组去掉这两个数,获得一个新数组,那么这个新数组依然和老数组同样存在相同的众数。这一条很容易证实:
假设数组为a,长度为len,众数为x,出现的次数为t,固然知足t>len/2。假设其中有两个数y和z,y≠z。去掉这两个数,剩下的数组长度为len-2。若是这两个数都不等于众数x,也就是x≠y且x≠z,那么x在新的数组中出现的次数依然是t,t>len/2>(len-2)/2,因此t依然是新的数组里的众数。而若是这两个数中存在x,那么天然只有一个x,则剩下的数组中x出现的次数是t-1,t-1>len/2-1=(len-2)/2,因此x依然是新数组的众数。
有了上述的思路,咱们会去想,如何找到这一对对的不一样的数呢。
咱们能够记录数字num和其重复的次数times,遍历一遍数组,按照如下流程图来。
num/times一直记录着数字和其重复次数,times加1和减1都是随着数组新来的数是否和num相同来决定,减1的状况其实就取决于上面证实的那个命题,找到一对不相同的数字,去掉这两个,剩下的数组的众数不变。
关于在于证实最后的结果是所求的众数。若是后面的结果不是众数,那么众数每出现一次,就得与一个不是众数的数一块儿“抵消”,因此数组中不是众数的数的数量不会少于众数的数量,然而这不是现实。因而上述算法成立,它有着线性时间复杂度O(n),常数空间复杂度O(1)。
C语言代码基本以下:
int find(int *a, int len) {
int i, num = 0, times = 0;for(i=0;i<len;i++) { if(times > 0) { if(num == a[i]) times++; else times--; } else { num = a[i]; times = 1; } } return num; }
若是用Scheme编写,程序能够简洁以下:
(define (find s) (car (fold-right (lambda (n r) (if (zero? (cdr r)) (cons n 1) (cons (car r) ((if (eq? n (car r)) + -) (cdr r) 1)))) '(() . 0) s)))
升级以后的问题
上面的众数是出现次数大于数组长度的1/2的,若是将这里的1/2改为1/3,要找出来怎么作呢?
例如,数组是[1, 1, 2, 3, 4],那么要找出的众数为1。
再升华一下,若是是1/m,这里的m是一个参数,该怎么找出来呢?这个问题要去以前那个问题要复杂一些,另外咱们要意识到,问题升级以后,众数是有可能不止一个的,好比[1, 1, 2, 2, 3]长度为5,1和2都大于5/3。最多有m-1个众数。
思路
若是依然是排序以后再遍历,依然是有效的,但是时间复杂度仍是O(nlogn)级别,咱们仍是期待有着线性时间复杂度的算法。
对于第一个问题,成立的前提是去掉数组里两个不同的数,众数依然不变。那么对于升级以后的问题,是否是依然有相似的结果。不一样于以前,咱们如今来看在众数从1/2以上变成1/m以上,咱们来看去掉长度为len的数组a里m个互不相同的数,会发生什么。证实过程以下:
一样,咱们假设a里有一个众数x,x出现的次数为t,看看去掉m个不同的数以后x仍是不是众数。去掉m个数以后,新的数组长度为len-m。x是众数,因此x的出现次数t > len/m,若是去掉的m个数中没有x,则x在剩余的数组中的出现次数依然是t,t > len/m > (len-m)/m,因此这种状况下x仍是众数;若是去掉的m个数中存在x,由于m个数互不相同,因此其中x只有一个,因此x在剩余的数组中的出现次数是t-1,t > len/m,从而t-1 > len/m-1 = (len-m)/m,因此x在剩余的数组里依然是众数。以上对于数组中全部的众数都成立。同理可证,对于数组中不是众数的数,剩余的数组中依然不是众数,实际上,把上面全部的>替换为≤便可。
有了上面的理解,咱们能够仿照以前的算法,只是这里改为了长度最多为n-1的链表。好比对于数组[1, 2, 1, 3],众数1超过数组长度4的1/3,过程以下
初始时,空链表[]
检索第一个元素1,发现链表中没有记录num=1的表元,链表的长度没有达到2,因此插入到链表,获得[(num=1,times=1)]
检索第二个元素2,发现链表中没有记录num=2的表元,链表的长度没有达到2,插入到链表,获得[(num=1,times=1), (num=2,times=1)]
检索第三个元素1,发现链表中已经存在num=1的表元,则把该表元times加1,获得[(num=1,times=2), (num=2,times=1)]
检索第四个元素3,发现链表中没有num=3的表元,链表长度已经达到最大,等于2,因而执行消去,也就是每一个表元的times减1,并把减为0的表元移出链表,获得[(num=1,times=1)]
以上就是过程,最终获得众数为1。
以上过程最终获得的链表的确包含了全部的众数,这一点很容易证实,由于任何一个众数的times都不可能被彻底抵消掉。可是,以上过程实际并不保证最后获得的链表里全都是众数,好比[1,1,2,3,4]最终获得[(num=1,times=1), (num=4,times=1)],但4并非众数。
因此咱们须要获得这个链表以后,再遍历一遍数组,将重复次数记载于链表之中。
Python下使用map/reduce高阶函数来取代过程式下的循环,上述的算法也须要以下这么多的代码。
from functools import reduce def find(a, m): def find_index(arr, test): for i in range(len(arr)): if test(arr[i]): return i return -1 def check(r, n): index = find_index(r, lambda x : x[0]==n) if index >= 0: r[index][1] += 1 return r if len(r) < m-1: return r+[[n,1]] return reduce(lambda arr,x : arr if x[1]==1 else arr+[[x[0],x[1]-1]], r, []) def count(r, n): index = find_index(r, lambda x : x[0]==n) if index < 0: return r r[index][1] += 1 return r return reduce(lambda r,x : r+[x[0]] if x[1]>len(a)//m else r, \ reduce(count, a, \ list(map(lambda x : [x[0],0], reduce(check, a, [])))), [])
若是用C语言编写代码会更多一些,不过能够不用链表,改用固定长度的数组效率会高不少,times=0的状况表明着元素不被占用。此处就不实现了,交给有兴趣的读者本身来实现吧。