主元素、主元素II、主元素III

来源:互联网 发布:组策略禁止软件运行 编辑:程序博客网 时间:2024/05/21 09:16

写在前面

这类题目之前还真没接触过,没什么好的思路。不过看要求又是一道考虑时间复杂度的问题,需要掂量(考虑)一下自己的算法有没有达到要求。根据自己目前的能力写出来的算法还真没做到O(n)的时间复杂度,应该是O(n^2),不过居然通过了lintcode测试,而且是100% 数据通过测试,费解ing。
查阅资料发现主元素问题是《编程之美》上的原题,而且lintcode上还有主元素 II主元素 III问题,以及“引导”本人跳转过来的lintcode第3题:统计数字。
这里就只分析记录主元素问题,其他问题另行记录。

正文

题目

给定一个整型数组,找出主元素,它在数组中的出现次数严格大于数组元素个数的二分之一。

样例
给出数组[1,1,1,1,2,2,2],返回 1

挑战
要求时间复杂度为O(n),空间复杂度为O(1)

题目分析

作为lintcode上的难度为容易的题目,却没有什么好的思路,也是汗颜。不过没关系,有问题自己先努力解决之,再向前辈们请教取经。站在巨人的肩膀上,汲取养分,以便更快地成长。

方法1

方法1分析

这是我想的到的一种时间复杂度为O(n^2)的方法,空间复杂度倒是保持了O(1)。应该来说,是最朴素的方法了。本来是想使用新技能,用完发现不适合用在这里,结果效率还这么差,结果居然还通过了测试!
方法是将数组中某一元素与所有的元素进行比较(当然比较方法有多种),然后做统计,在数组中的出现次数严格大于数组元素个数的二分之一的元素自然就是主元素。但是这样需要进行内外两次循环,时间复杂度O(n^2)。
比较方法可以是相等比较,可以求差值为0。目标不外乎比较两个值是否相等。这里本人采用了异或方式(看书get的新技能,本来觉得蛮好玩的,现在发现好像也没什么突破性进步,无奈ing):因为任何整数和自己本身的异或为0。姑且安慰自己说异或方式效率更好吧=.=。

方法1代码

import java.util.ArrayList;public class Solution {    /**     * @param nums: a list of integers     * @return: find a majority number     */    public int majorityNumber(ArrayList<Integer> nums) {        // write your code        int num = nums.size();        int halfNum = (int) Math.ceil(num / 2.0);        int count = 0;        for (int i = 0; i < num; i++) {            for (int j = 0; j < num; j++) {                if ((nums.get(i) ^ nums.get(j)) == 0) {                    count++;                    if (count >= halfNum) {                        return nums.get(i);                    }                }            }        }        return -1;    }}

此处的问题有一个小问题,是在循环结束的时候,如果没有正确执行返回循环体中的数字,返回最后的-1,并不能表示什么意义,反而会在输入的数组没有主元素且数组中有负值的时候因为返回-1而造成迷惑。

tips

这里涉及一些ArrayList的操作:创建,初始化,添加元素和获取元素,可以参考后面给出的链接。两个链接是同一篇文章,importNew[2]引用的CSDN某位细致入微的作者写的文章[1],不过importNew的其他文章也都很出色,推荐一看。首推原作者的博客,珍惜作者的努力。

总之方案1算是很low的了,期待后面有什么好的思路改掉其中的bug吧。此处仅作记录,不建议使用。

其他方案

这里只做介绍,说一些目前大概有哪些思路,不作具体实现。

先排序,后求解

先对整数数组排序,然后根据排序后的规律定位主元素,并求解。定位方法有两种:一是顺序查找并做统计,超过半数的为主元素;二是直接在排序的基础上定位中位数,此时中位数就是主元素。很明显后者更优。
本方案主要在于前面的排序工作,需要O(nlogN)~O(n^2)的时间复杂度(大小写N是一个意思,只是为了避免logn分辨不清)。
顺序统计自然没什么说的,设置count变量,然后相同的数字+1;不同的数字跳过,然后继续下一个元素。

主元素->中位数

因为已经进行了排序操作,因此数组保持有序状态。并且主元素的特点是该元素在数组中的出现次数严格大于数组元素个数的二分之一,因此数组中位数必然是主元素。
示意图如下:

主元素to中位数

数组总数为奇数,那么中间的数自然就是中位数,也就是主元素;如果为偶数,中间的两个数任取其一都可以(例如如果总数为6,要达到主元素的要求,主元素数目必然>=4)。

分治法

将数组一分为二,然后左右数组与原题目要求相同,之后采用相同的方法。
更详细方法和代码,请自行查阅,此处只提供一种思路。

编程之美算法

思路分析

真的是只有这里才能称得上是算法。
思路也很简单:因为主元素的数量比其他元素的数量和都多,因此数组中任意两个不相等的数字相互抵消,最终剩余的数字必然是主元素,当然剩余的主元素个数可能不止为1。
两两抵消的时候又分为两种情况:一是主元素和非主元素抵消,二是非主元素和非主元素抵消。如果是主元素和非主元素抵消,结果是主元素数量-1,非主元素数量-1,仍然满足主元素在数组中的出现次数严格大于数组元素个数的二分之一这一条件。如果是非主元素和和非主元素抵消,结果是非主元素总数-2,主元素总数不变,主元素数目更是严格大于数组元素个数的二分之一.。
具体方式是先取nums[0]为主元素,同时设置计数变量count=1,然后与num[1]比较:如果相同则count+1;如果不同则count-1。同时判断count是否为0:为0则取下一个元素设置为主元素,继续后续比较;如果不为0则继续比较。
方法相当巧妙,个人认为中间难理解的部分是count=0的时候取下一元素设为主元素,其实理解后还是蛮简单的,而且算法改进的关键点也在这里。
现在举例介绍思路如下:
例1:

2,3,1,1,2,1,1

第一个元素和第二个元素均不是主元素,那么count的变化,就是count=1然后count-1,结果是count=0,需要从第三个元素重新开始,重复上述操作。而这一变化对应的是从数组中消除了两个非主元素。
例2:

1,1,2,3,1,1,2

同样的如果是主元素和非主元素相抵消,如1,1,2,3相抵消,对最终结果也没什么影响,到了3后的1自然还是重新设置count=1,相当于对1,1,2进行主元素查找。
后面在小结部分有进一步思考分析。

个人代码

代码为个人根据上述思路自己编写的第一版,比较粗糙。如有错误还请留言讨论。

import java.util.ArrayList;public class Solution {    /**     * @param nums: a list of integers     * @return: find a majority number     */    public int majorityNumber(ArrayList<Integer> nums) {        // write your code        int majorityNumber = nums.get(0);        int count = 1;        for (int i = 1; i < nums.size(); i++) {            if (majorityNumber == nums.get(i)) {                count++;            } else {                count--;            }            if (count == 0) {                // 主元素被取出,下次比较的值应该是在此主元素的下一个                // 因此此处i++表示主元素是本次比较值的下一个,加上for循环中的i++,可以回归到之前的原始文体                i++;                // 判断i是否越界,方便后面拓展到其他的主元素求解。                // 其实,>1/2的主元素是不需要判断的。后面的>1/k需要判断                if (i >= nums.size()) {                    break;                }                // 重新设置主元素                majorityNumber = nums.get(i);                count = 1;            }        }        return majorityNumber;    }}

很容易看出此算法的时间复杂度为O(n),复杂度O(1)。
此处代码只是简单的实现了上面的思路,并没有优化代码。分析算法可以知道算法的转折之处在于nums.get(i)与备选主元素的判断和count=0时的判断操作。因此优化代码之后,可以先列出新版本的流程图:

主元素改进算法流程图

由流程图可以看出,新算法的整体思路还是类似的,不同之处在于count=0的时候的处理方式。
梳理之后整体思路如下:首先初始化主元素和count(计数、赋初始值与否都无所谓),然后进入流程图操作。数组中元素逐个与备选主元素比较:如果与备选主元素相同则count+1;如果不同且count值不为0,则进行count-1,如果此次运算后count=0也不立即将备选主元素的值重新赋值,而是进入下一次循环;如果比较元素与主元素不同,且主元素count=0,意味着,备选主元素抵消完了,此时再进行备选主元素更新操作。
这样,所有的数组中的元素都可以通过统一模型进行操作,不需要单独的判断进行备选主元素更新操作,代码更加简洁。

然后新版本的代码如下:

import java.util.ArrayList;public class Solution {    /**     * @param nums:     *            a list of integers     * @return: find a majority number     */    public int majorityNumber(ArrayList<Integer> nums) {        // write your code        int majorityNumber = 0;        int count = 0;        for (int i = 0; i < nums.size(); i++) {            if (majorityNumber == nums.get(i)) {                count++;            } else if (count == 0) {                majorityNumber = nums.get(i);                count++;            } else {                count--;            }        }        return majorityNumber;    }}

至此,主元素问题解决。

小结

编程之美提供的思路确实精妙,时间、空间开销都很小,为之赞叹。而排序算法后再定位中位数的方法,时间在排序时花销较大,瓶颈是排序算法的好坏。

其实仔细想想,编程之美的算法可以看做是第一种方法的精简版本。
本来是需要数组中的每一个元素与全部元素比较,并且进行统计。这种方式大而全,适合所有有类似结构或思路的问题,但是没有很好地结合本问题的特点:主元素出现的次数严格大于数组大小的二分之一,因而毫无效率可言。最优解的方法是选择两两抵消,保证最后剩余的元素肯定是主元素。算法本质其实也是每次两个元素进行比较,不过是两个元素同时在前进,然后分为两类,要么是主元素,要么不是主元素,然后主元素和非主元素相互抵消,同时“舍弃”一个元素。

相应的,如果是1/3、…、1/k?对于O(n)的时间复杂度,O(k)的空间复杂度,就很好理解了。类比1/2的主元素求解,同时保存k-1个数值不同的备选主元素,后面的值进行比较的时候,保证某一的备选主元素+1或者同时舍弃k个不同的主元素。

换个思路,如果考虑count为0的时候,重置主元素然后重复操作的话,又有一种重新开始,问题回归到初始状态的感觉。解题思路给人一种可以使用最简单的基础知识相互结合求解复杂问题的感觉,从细微之处着手解决大问题。

天下难事必作于易。

引申

问题拓展成1/3、…、1/k,问题依然类似,但又有些不同。

相似的是,原理基本一致。

同时保存k-1个备选主元素,然后进行比较。不同之处在于1/2的时候,剩余的最后一个必然是主元素。但是此时1/3的时候可能剩余两个数,1/k的时候可能剩余k-1个数。考虑最差的比较情况:如果每次都是主元素与另外k-1个非主元素进行抵消(没有非主元素相互抵消的情况),并且主元素数量刚刚满足主元素的条件(比如1/2的时候,5个元素中主元素有3个,下面的例子中1/3的时候,11个元素中主元素有4个)。举例如下,主元素占1/3的时候,11个元素:

1,1,1,1,2,2,3,3,4,4,4

按照抵消模型,可以作图如下:

这里写图片描述

不同的是最终主元素的获取。

这时候,如果按照最终的count值得到主元素,结果会是4,明显错误,因此要以两个备选主元素为指标进行统计,统计后的count最大值对应的才是主元素。

后面的主元素II和主元素III都可以根据上面的流程图进行扩充。因此算法对应的流程图不再赘述。

主元素II

题目

给定一个整型数组,找到主元素,它在数组中的出现次数严格大于数组元素个数的三分之一。

样例
给出数组[1,2,1,2,1,3,3],返回 1

挑战
要求时间复杂度为O(n),空间复杂度为O(1)

主元素II代码

import java.util.ArrayList;public class Solution {    /**     * @param nums: a list of integers     * @return: The majority number that occurs more than 1/3     */    public int majorityNumber(ArrayList<Integer> nums) {        // write your code        int majorityNumber1 = 0;        int majorityNumber2 = 0;        int count1 = 0;        int count2 = 0;        for (int i = 0; i < nums.size(); i++) {            if (majorityNumber1 == nums.get(i)) {                count1++;            } else if (majorityNumber2 == nums.get(i)) {                majorityNumber2 = nums.get(i);                count2++;            } else if (count1 == 0) {                majorityNumber1 = nums.get(i);                count1++;            } else if (count2 == 0) {                majorityNumber2 = nums.get(i);                count2++;            } else {                count1--;                count2--;            }        }        count1 = count2 = 0;        for (int i = 0; i < nums.size(); i++) {            if (majorityNumber1 == nums.get(i)) {                count1++;            } else if (majorityNumber2 == nums.get(i)) {                count2++;            }        }        return count1 > count2 ? majorityNumber1 : majorityNumber2;    }}

主元素III

题目

给定一个整型数组,找到主元素,它在数组中的出现次数严格大于数组元素个数的1/k。

样例
给出数组[1,2,1,2,1,3,3],返回 1

挑战
要求时间复杂度为O(n),空间复杂度为O(1)

主元素III代码

注意此处代码中breakcontinue的应用,以及标志位的配合。

import java.util.ArrayList;public class Solution {    /**     * @param nums: A list of integers     * @param k: As described     * @return: The majority number     */    public int majorityNumber(ArrayList<Integer> nums, int k) {        // write your code        // 初始化        // java默认int初始化为0,可以不进行下面的赋值操作        int[] majorityNumber = new int[k - 1];        int[] count = new int[k - 1];        boolean hasUsed = false;        for (int i = 0; i < k - 1; i++) {            majorityNumber[i] = 0;            count[i] = 0;        }        // 还选比较操作。一定要有continue        for (int i = 0; i < nums.size(); i++) {            hasUsed = false;            for (int j = 0; j < k - 1; j++) {                if (majorityNumber[j] == nums.get(i)) {                    count[j]++;                    hasUsed = true;                    break;                }            }            if (hasUsed) {                continue;            }            for (int j = 0; j < k - 1; j++) {                if (count[j] == 0) {                    majorityNumber[j] = nums.get(i);                    count[j]++;                    hasUsed = true;                    break;                }            }            if (hasUsed) {                continue;            }            for (int j = 0; j < k - 1; j++) {                if (count[j] != 0) {                    count[j]--;                }            }        }        // 重置count,统计k-1个备选主元素的数量,找出真正的主元素        for (int i = 0; i < k - 1; i++) {            count[i] = 0;        }        for (int i = 0; i < nums.size(); i++) {            for (int j = 0; j < k - 1; j++) {                if (majorityNumber[j] == nums.get(i)) {                    count[j]++;                }            }        }        // 找出count的最大值        int index = 0;        int temp = 0;        for (int i = 0; i < k - 1; i++) {            if (temp < count[i]) {                temp = count[i];                index = i;            }        }        return majorityNumber[index];    }}

参考网址:

  • [1] http://blog.csdn.net/u013256816/article/details/50916648
  • [2] http://www.importnew.com/19233.html
0 0