书籍简介
第一章 字符串
1.0 本章导读
1.1 旋转字符串
1.2 字符串包含
1.3 字符串转换成整数
1.4 回文判断
1.5 最长回文子串
1.6 字符串的全排列
1.10 本章习题
第二章 数组
2.0 本章导读
2.1 寻找最小的 k 个数
2.2 寻找和为定值的两个数
2.3 寻找和为定值的多个数
2.4 最大连续子数组和
2.5 跳台阶
2.6 奇偶排序
2.7 荷兰国旗
2.8 矩阵相乘
2.9 完美洗牌
2.10 K个最小和 (UVA 11997 K Smallest Sums)
2.15 本章习题
第三章 树
3.0 本章导读
3.1 红黑树
3.2 B树
3.3 最近公共祖先LCA
3.5 R树:处理空间存储问题
3.10 本章习题
第四章 查找匹配
4.1 有序数组的查找
4.2 行列递增矩阵的查找
4.3 出现次数超过一半的数字
第五章 动态规划
5.0 本章导读
5.1 最大连续乘积子串
5.2 字符串编辑距离
5.3 格子取数
5.4 交替字符串
5.6 最长递增子序列
5.10 本章习题
第六章 海量数据处理
6.0 本章导读
6.1 关联式容器
6.2 分而治之
6.3 simhash算法
6.4 外排序
6.5 MapReduce
6.6 多层划分
6.7 Bitmap
6.8 Bloom filter
6.9 Trie树
6.10 数据库
6.11 倒排索引
6.15 本章习题
第七章 机器学习
7.1 K 近邻算法
7.2 支持向量机
附录 更多题型
附录A 语言基础
附录B 概率统计
附录C 智力逻辑
附录D 系统设计
附录E 操作系统
附录F 网络协议
4.3 出现次数超过一半的数字 - 《程序员编程艺术:面试和算法心得》 - 光年文档管理系统(Light Year Doc)
网站首页
4.3 出现次数超过一半的数字
## 题目描述 题目:数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。 ## 分析与解法 一个数组中有很多数,现在我们要找出其中那个出现次数超过总数一半的数字,怎么找呢?大凡当我们碰到某一个杂乱无序的东西时,我们人的内心本质期望是希望把它梳理成有序的。所以,我们得分两种情况来讨论,无序和有序。 ### 解法一 如果**无序**,那么我们是不是可以先把数组中所有这些数字**先进行排序**(至于排序方法可选取最常用的快速排序)。排完序后,直接遍历,在遍历整个数组的同时统计每个数字的出现次数,然后把那个出现次数超过一半的数字直接输出,题目便解答完成了。总的时间复杂度为O(nlogn + n)。 但**如果是有序的数组呢**,或者经过排序把无序的数组变成有序后的数组呢?是否在排完序O(nlogn)后,还需要再遍历一次整个数组? 我们知道,既然是数组的话,那么我们可以根据数组索引支持直接定向到某一个数。我们发现,一个数字在数组中的出现次数超过了一半,那么在已排好序的数组索引的N/2处(从零开始编号),就一定是这个数字。自此,我们只需要对整个数组排完序之后,然后直接输出数组中的第N/2处的数字即可,这个数字即是整个数组中出现次数超过一半的数字,总的时间复杂度由于少了最后一次整个数组的遍历,缩小到O(n*logn)。 然时间复杂度并无本质性的改变,我们需要找到一种更为有效的思路或方法。 ### 解法二 既要缩小总的时间复杂度,那么可以用查找时间复杂度为O(1)的**hash表**,即以空间换时间。哈希表的键值(Key)为数组中的数字,值(Value)为该数字对应的次数。然后直接遍历整个**hash表**,找出每一个数字在对应的位置处出现的次数,输出那个出现次数超过一半的数字即可。 ### 解法三 Hash表需要O(n)的空间开销,且要设计hash函数,还有没有更好的办法呢?我们可以试着这么考虑,如果**每次删除两个不同的数**(不管是不是我们要查找的那个出现次数超过一半的数字),那么,在剩下的数中,我们要查找的数(出现次数超过一半)出现的次数仍然超过总数的一半。通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。这个方法,免去了排序,也避免了空间O(n)的开销,总得说来,时间复杂度只有O(n),空间复杂度为O(1),貌似不失为最佳方法。 举个简单的例子,如数组a[5] = {0, 1, 2, 1, 1}; 很显然,若我们要找出数组a中出现次数超过一半的数字,这个数字便是1,若根据上述思路4所述的方法来查找,我们应该怎么做呢?通过一次性遍历整个数组,然后每次删除不相同的两个数字,过程如下简单表示: 0 1 2 1 1 =>2 1 1=>1 最终1即为所找。 但是数组如果是{5, 5, 5, 5, 1},还能运用上述思路么?很明显不能,咱们得另寻良策。 ### 解法四 咱们根据数组的特性考虑到: - 数组中有个数字出现的次数超过了数组长度的一半。换句话说,有个数字出现的次数比其他所有数字出现次数的和还要多。 - 因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数,当我们遍历到下一个数字的时候: - 如果下一个数字和我们之前保存的数字相同,则次数加1; - 如果下一个数字和我们之前保存的数字不同,则次数减1,我们需要保存下一个数字,并把次数重新设为1。 下面,举两个例子: * 第一个例子,假定数组为{5, 5, 5, 5, 1} 不同的相消,相同的累积。遍历到第四个数字时,candidate 是5, nTimes 是4;遍历到第五个数字时,candidate 是5, nTimes 是3;nTimes不为0,那么candidate就是超过半数的。 * 第二个例子,假定数组为{0, 1, 2, 1, 1} 开始时,保存candidate是数字0,nTimes为1;遍历到数字1后,与数字0不同,则nTimes减1变为零;接下来,遍历到数字2,2与1不同,candidate保存数字2,且nTimes重新设为1;继续遍历到第4个数字1时,与2不同,nTimes减1为零,同时candidate保存为1;最终遍历到最后一个数字还是1,与我们之前candidate保存的数字1相同,nTimes加1为1。最后返回的是之前保存的candidate为1。 思路清楚了,完整的代码如下: ```c //a代表数组,length代表数组长度 int FindOneNumber(int* a, int length) { int candidate = a[0]; int nTimes, i; for (i = nTimes = 0; i < length; i++) { if (candidate == a[i]) nTimes++; else nTimes--; if (nTimes == 0) { candidate = a[i]; nTimes = 1; } } return candidate; } ``` 即针对数组{0, 1, 2, 1, 1},套用上述程序可得: i=0,candidate=0,nTimes=1; i=1,a[1] != candidate,nTimes--,=0; i=2,candidate=2,nTimes=1; i=3,a[3] != candidate,nTimes--,=0; i=4,candidate=1,nTimes=1; 如果是0,1,2,1,1,1的话,那么i=5,a[5] == candidate,nTimes++,=2;...... ## 举一反三 加强版水王:找出出现次数刚好是一半的数字 分析:我们知道,水王问题:有N个数,其中有一个数出现超过一半,要求在线性时间求出这个数。那么,我的问题是,加强版水王:有N个数,其中有一个数刚好出现一半次数,要求在线性时间内求出这个数。 因为,很明显,如果是刚好出现一半的话,如此例: 0,1,2,1 : 遍历到0时,candidate为0,times为1 遍历到1时,与candidate不同,times减为0 遍历到2时,times为0,则candidate更新为2,times加1 遍历到1时,与candidate不同,则times减为0;我们需要返回所保存candidate(数字2)的下一个数字,即数字1。
上一篇:
4.2 行列递增矩阵的查找
下一篇:
第五章 动态规划