“只要不是特别大的内存开销,时间复杂度比较重要。因为改进时间复杂度对算法的要求更高。”
——吴斌(NVidia,Graphics Architect)

同样是查找,如果是顺序查找需要O(n)的时间;如果输入的是排序的数组则只需要O(logn)的时间;如果事先已经构造好了哈希表,那查找在O(1)时间就能完成。我们只有对常见的数据结构和算法都了然于胸,才能在需要的时候选择合适的数据结构和算法来解决问题。

面试题29:数组中出现次数超过一半的数字

题目:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。

看到这道题很多应聘者就会想要是这个数组是排序的数组就好了。如果是排好序的数组,那么我们就能很容易统计出每个数字出现的次数。题目给出的数组没有说是排序的,因此我们需要先给它排序。排序的时间复杂度是O(nlogn)。最直观的算法通常不是面试官满意的算法,接下来我们试着找出更快的算法。

解法一:基于Partition函数的O(n)算法

如果我们考虑到数组的特性:数组中有一个数字出现的次数超过了数组长度的一半。如果把这个数组『排序』,那么排序之后位于数组中间的数字一定就是那个出现次数超过数组长度一半的数字。也就是说,这个数字就是统计学上的『中位数』,即长度为n的数组中第n/2大的数字。我们有成熟的O(n)的算法得到数组中任意第k大的数字。

快速排序的实现

在这一段很迷惑,是因为需要好好考虑下如何实现在 O(n) 的算法复杂度中找到任意第 k 大的数。『快速排序』第一趟排序之后,我们的到了一个中间索引(我们暂且称为 index),那么排在中间 index 前面的值都比 array[index] 小;同样的排序在中间 index 后面的元素都比 array[index] 大。第一趟的时间复杂度为O(n/2)。排序完之后,我们获得了两个子序列,同样根据上述的逻辑进行排序,假设当 k < index 时,那么我们就继续排序第二段比 index 小的部分。这一次的时间复杂度为O(n/4)。以此类推,当我们把 index 定位到 k 的时候,我们就会清楚的知道,在 k 以前的数字都比 array[k] 的值小。这也就快速的找到了任意 k 大的数组,时间复杂度 O(n)≈O(n/2)+O(n/4)+O(n/8)+⋯+O(n/2n)O(n) ≈ O(n/2)+O(n/4)+O(n/8)+ \dots + O(n/2^n)O(n)≈O(n/2)+O(n/4)+O(n/8)+⋯+O(n/2n)。参考的 java 代码可以如下:

 void QuickSort(int[] data, int length, int start, int end) {if (start == end) {return;}int index = Partition(data, length, start, end);if (index > start) {QuickSort(data, length, start, index - 1);}if (index < end) {QuickSort(data, length, index + 1, end);}}

实现快速排序算法的『关键』在于先在数组中选择一个数字(选中的数字,和返回的数字通常是不一样的),接下来把数组中的数字分为两部分,比选择的数字小的数字移到数组的左边,比选择的数字大的数字移到数组的右边。这个函数可以如下实现:

 int Partition(int[] data, int length, int start, int end) {if (data == null || length <= 0 || start < 0 || end > length) {throw new RuntimeException("错误的参数");}// 从  start 到 end 之间,随机选取一个整数int index = new Random().nextInt(end - start) + start;swap(data, index, end);int small = start - 1;for (index = start; index < end; ++index) {if (data[index] < data[end]) {++small;if (small != index) {swap(data, index, small);}}}++small;swap(data, small, end);return small;}void swap(int[] array, int i, int j) {int tmp = array[i];array[i] = array[j];array[j] = tmp;}

上面代码的实现思路如下:

  1. 使用 Random 函数随机生成一个 index,得到一个随机的『中间参考值』,将该值换到函数的末尾。
  2. 从头到尾遍历数组,将数组中,所有小于中间参考值的元素排在数组的前面,small 记录已经确定小于中间参考的下标
  3. 返回 small 并且,换回末尾的中间参考值。
  4. 一趟排序下来,在 small 索引前面的元素,都比 data[index] 小。

还有一种快速排序的实现是分别的从左往右查找小于『中间参考值』的元素,和右往左查找大于『中间参考值』的元素,进行交换,同样可以达到快速排序的目的。参考的代码如下:

   /*** <b>快速排序</b>* <p>在数组中找一个元素(节点),比它小的放在节点的左边,比它大的放在节点右边。不断执行这个操作….</p>** @param array* @return*/static String quickSort(int[] array) {if (array.length <= 0) {return null;}quickSort(array, 0, array.length - 1);return Arrays.toString(array);}/*** 快速排序使用递归可以很好的实现,代码的逻辑结构如下。* <p>1. 随机的获取一个用于比较的<b>中间值</b>,通常区中间的元素 <br>* 2. 从前往后和从后往前分别查找比<b>中间值</b>大的元素和小的元素,进行交换<br>* 3. 直到确定不存在,交换元素,则<b>中间值</b>元素的两边就能确比</p>** @param array* @param left* @param right*/static void quickSort(int[] array, int left, int right) {int i = left;int j = right;//支点: 可以取任意值(但通常选取位置中间)相对来说交换次数会比较小int pivot = array[(left + right) / 2];//左右两端进行扫描,只要两端还没有交替,就一直扫描while (i <= j) {//寻找直到比支点大的数while (pivot > array[i])i++;//寻找直到比支点小的数while (pivot < array[j])j--;//此时已经分别找到了比支点小的数(右边)、比支点大的数(左边),它们进行交换if (i <= j) {int temp = array[i];array[i] = array[j];array[j] = temp;i++;j--;}}//上面一个while保证了第一趟排序支点的左边比支点小,支点的右边比支点大了。//“左边”再做排序,直到左边剩下一个数(递归出口)if (left < j)quickSort(array, left, j);//“右边”再做排序,直到右边剩下一个数(递归出口)if (i < right)quickSort(array, i, right);}

虽然实现两种实现方式不同,但是总体思路是一致的。

小提示:如果面试题是要求在排序的数组(或者部分排序的数组)中查找一个数字或者统计某个数字出现的次数,我们都可以尝试用『二分查找』算法。

回归到本题的思路

这种算法是受『快速排序』算法的启发。在随机快速排序算法中,我们先在数组中随机选择一个数字,然后调整数组中数字的顺序,使得比选中的数字小数字都排在它的左边,比选中的数字大的数字都排在它的右边。如果这个选中的数字的下标刚好是n/2,那么这个数字就是数组的中位数。如果它的下标大于n/2,那么中位数应该位于它的左边,我们可以接着在它的左边部分的数组中查找。如果它的下标小于n/2,那么中位数应该位于它的右边,我们可以接着在它的右边部分的数组中查找。这是一个典型的递归过程,可以用如下代码实现:

int moreThanHalfNum(int[] array, int length){if(ChechInvalidArray(array, length)){return 0;}int middle = length >> 1;int start = 0;int end = length - 1;int index = Partition(array, length, start, end);// 寻找中位数while(index != middle){// 如果得到 index 大于中间值,则往前半部分查找if(index > middle){end = index - 1;index = Partition(array, length, start, end);}else{start = index + 1;index = Partition(array, length, start, end);}}int result = array[middle];if(!CheckMoreThanHalf(array, length, result)){return 0;}return result;
}

上面的代码中,CheckInvalidArray 用来判断输入的数组是不是无效的。下面代码将,使用一个全局变量来表示输入无效的情况。

// 全局变量判断数组是否无效
boolean arrayInvalid = false;boolean CheckInvalidArray(int[] array, int length){arrayInvalid = false;if(array == null && length <= 0){arrayInvalid = true;}return arrayInvalid;
}

而对于如何验证,中位数是否为次数最多的元素,我们可以对其进行一下次数的统计。 CheckMoreThanHalf 函数的实现,如下:

boolean CheckMoreThanHalf(int[] array, int length, int number){int times = 0;for (int i = 0; i < length; i++) {if(array[i] = number){times++;}}boolean isMoreThanHalf = true;if(times * 2 <= length){arrayInvalid = true;isMoreThanHalf = false;}return isMoreThanHalf;
}

解法二:根据数组特点找出O(n)的算法

接下来我们从另外一个角度来解决这个问题。数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现次数的和还要多。因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是次数。

  • 当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加1;
  • 如果下一个数字和我们之前保存的数字不同,则次数减1。
  • 如果次数为零,我们需要保存下一个数字,并把次数设为1。

由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。基于上面的思路写下的代码:

 public Integer moreThanHalfNum(int[] array) {if (array == null)return null;Integer number = null;int count = 0;Integer resultInteger = null;for (int i = 0; i < array.length; i++) {if (number == null) {number = array[i];count++;} else {if (array[i] != number)if (count == 0) {number = array[i];count = 1;} elsecount--;elsecount++;}if (count == 1)resultInteger = number;}if (checkMoreThanHalf(array, resultInteger))return resultInteger;elsereturn null;
}

其实思路,还有很多,如果不是为了考虑效率,且统计较小的话可以直接的通过 HashMap 进行统计,得出最大的值。

据说帅哥靓女都关注了↓↓↓

快速找出数组中出现次数超过一半的数字相关推荐

  1. 找出数组中出现次数超过一半的数

    算法--找出数组中出现次数超过一半的数      每当我看到经典的算法题,就怀念高中,感觉很多算法题就是高中的题目,谁叫哥只读了个专科,高数基本相当没学.      有空要看看高数啊,想当年数学那是相 ...

  2. 用Python找出数组中出现次数超过一半的数

    数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字. 计数+比较 不考虑效率,采用最简单的办法,遍历数组,使用 List 的 count() 方法统计元素出现的次数: def more_th ...

  3. 《编程题》找出数组中出现次数超过一半的数(时间复杂度O(n),空间复杂度为O(1))

    解析:该题目可以用mapreduce的wordcount思想做,就是存储键.值对,键存数字,值存该数字出现的次数,当然需要用一个变量记住出现次数超过一半的数了,说完了,开始撸起袖子干了. public ...

  4. java数组出现次数最多的数_找出数组中出现次数最多的那个数——主元素问题...

    方法一:以空间换时间,可以定义一个计数数组int count[101],用来对数组中数字出现的次数进行计数(只能针对数组中数字的范围1~100),count数组中最大的元素对应的下标,即为出现次数最多 ...

  5. 利用HashMap找出数组中出现次数最多的元素及其次数

    我在牛客网上刷题时遇到了这道题目,因为初学Java不久,对Java的应用还不熟悉,刚开始用C语言做没做出来(我太菜了...),接着百度发现竟然可以用HashMap做,仔细一想发现这真是一个好办法(为什 ...

  6. 剑指offer:面试题39. 数组中出现次数超过一半的数字

    题目:面试题39. 数组中出现次数超过一半的数字 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字. 你可以假设数组是非空的,并且给定的数组总是存在多数元素. 示例 1: 输入: [1, ...

  7. 《剑指offer》-- 复杂链表的复制、字符串的排列、数组中出现次数超过一半的数字、连续子数组的最大和

    一.复杂链表的复制: 参考牛客网的chancy:https://www.nowcoder.com/questionTerminal/f836b2c43afc4b35ad6adc41ec941dba 1 ...

  8. 数据结构与算法-- 数组中出现次数超过一半的数字(时间复杂度的讨论)

    时间效率 互联网想对时间效率格外的敏感,所以我们总是在需求迭代一定程度后去做优化.而且我们解决问题的时候,时间效率往往是一个考查的重点.因此我们平时编码过程中就必须不断的优化效率,追求完美的态度与能力 ...

  9. 【剑指offer】十九,数组中出现次数超过一半的数字

    题目描述 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字.例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}.由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2. ...

最新文章

  1. oracle测试环境表空间清理
  2. 问题解决:Apache: You don't have permission to access / on this server
  3. UVa661 Blowing Fuses
  4. Spring Boot集成Dubbo多模块项目创建与配置
  5. 专注力 化繁为简的惊人力量
  6. 主成分分析和因子分析区别与联系
  7. HDU4405 期望
  8. 【kafka系列】kafka之生产者发送消息实践
  9. Apache中配置ASP.NET环境
  10. java----EL表达式
  11. SQL Nexus Tool
  12. 西安中学2021文科高考成绩查询,2021年西安重点高中名单及排名,西安高中高考成绩排名榜...
  13. 华为云从入门到实战 | 云速建站服务与企业主机安全服务
  14. 程序员必上的20大网站
  15. Ubuntu16.04系统联网(连接WiFi)设置(亲测有效!!!)
  16. 博图在线升级 gsd_《阴阳师妖怪屋》如何快速提升式神等级 式神快速升级方法大放送|阴阳师妖怪屋|如何-360GAME...
  17. 坦白说自动获取有效好友
  18. 【调研】消费者调研与市场调研的区别
  19. Mysql中数据类型括号中的数字代表的含义
  20. OneDrive两台电脑之间文件同步操作

热门文章

  1. VR技术原理分析,【VR原理入门理论篇】
  2. Hadoop之Hadoop2的改进内容简介(笔记24)
  3. 恶意软件查杀工具分享
  4. 服务器的维护工作有哪些
  5. 那些很厉害的人是怎么构建知识体系的
  6. 设计一个三阶巴特沃斯滤波器_二、三阶巴特沃斯滤波器电路设计—电路精选(47)...
  7. android-高德地图:实时轨迹实现
  8. 80后购房八大注重 教授提示需审时度势
  9. 全国职称计算机考试 word2007,职称计算机考试word2007免费版
  10. set集合、深浅拷贝以及部分知识点补充