算法分析与设计第五次作业(leetcode 中 Majority Element 题解)

本文介绍了一种高效的算法,用于查找数组中出现频率超过1/k的众数,包括1/2和1/3众数的特殊情况。算法利用配对消除的概念,通过遍历和计数实现,具有O(n)的时间复杂度和O(k)的空间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

心得体会

这个题目有两个版本Majority Element,和Majority Element II,解题的方法比较巧妙,有点想不到的感觉,并且证明过程也很有趣,所以就记录下来(具体详情见正文题解)。

题解正文

题目描述在这里插入图片描述在这里插入图片描述
问题分析

题目要求majority number,也就是出现次数最多的数,第一个题目求占比超过1/2的数,第二题要求占比超过1/3的那些数,其实这种题目可以拓展为求占比超过1/k的那些数字。下面会从简单版本I的求解过渡到版本II,最后得出1/n众数的求解。

解题思路

下面的思路中,nums表示输入的数字数组

  1. 最简单的方法当然是对于每一个数字都用一个num记录数值,一个count记录出现次数,最后筛选count大于nums.size()/k的那些数字。这样做的话时间复杂度和空间复杂度都是线性的,达不到题目要求的O(1)空间复杂度。
  2. 所以换一个思路,我们把考虑把数字进行配对,比如求出1/2众数,因为占比超过1/2的数字最多有一个,所以我们可以将1/2众数和其它数字两两配对,如果存在1/2众数,那么配对到最后一定还会有1/2众数剩余,因为1/2众数占比超过1/2,也就比其它数字个数总和还多。配对完成之后,还剩下那些没法配对的数字,这些数字就是1/2众数了。所以换一个思路,我们把考虑把数字进行配对,比如求出1/2众数,因为占比超过1/2的数字最多有一个,所以我们可以将1/2众数和其它数字两两配对,如果存在1/2众数,那么配对到最后一定还会有1/2众数剩余,因为1/2众数占比超过1/2,也就比其它数字个数总和还多。配对完成之后,还剩下那些没法配对的数字,这些数字就是1/2众数了。
    通过上面的分析我们将这个问题转化成了一个配对问题,但是具体怎么配对呢:可以用一个num记录数值,count记录对应数值已经出现的次数(初值为0),然后对nums数组遍历,如果遇到和当前num记录数值相同的数,就将count递增,这样做的含义是加入配对队列,等待其它的数字与之配对相消;反之和num值不同则需要将count递减(要求count>0),这样做对应的含义是将两个不同类别的数字配对相消;如果count值为零就将num值替换为遍历的当前数字nums[i] 并递增count。这样做到最后nums数组中数字只有两个结果:a.通过配对消除;b.加入待配对队列(也就是num&count表示的单个数字组成的队列),根据上面的分析,被存在num中的只能是1/2众数,少数数字和部分1/2众数被配对消除。
    问题解决,但是细节部分还有一些问题:
    • 加入到配对队列的不一定就是1/2众数,也可能是1/2众数之外的数字,但是这不影响算法的正确性:
      首先我们确认,加入到配对队列的数字只有两种,1/2众数和其它数字。
      如果加入配对队列的是1/2众数那么就是上面分析的情况,不会有问题。
      如果加入到配对队列的不是1/2众数,那么后续遍历到的与之不同的数字也有两种可能,一种可能是1/2众数,另一种可能是其它数字,如果是1/2众数,那么和上面分析的情况是一样的,即1/2众数 vs 其它数字配对相消;如果不是1/2众数,那么就是其它数字 vs 其它数字配对相消,这样对于找出1/2众数更加有利,因为其他数字正在以比1/2众数更快的速度被配对消去,又因为其他数字本来就比1/2众数少,那么自然更快被消去。
      所以无论如何,一个1/2众数至少消去一个其他数字,最后留在等待配对队列中的只能是1/2众数(因为其它的数都被消去了)。
    • 万一1/2众数根本不存在,这个可以在上述算法做完之后,遍历一遍nums数组求出num数值出现次数,超过n/2则存在,并且就是num,反正不存在1/2众数,因为1/2众数必然满足前面分析到的情况。
  3. 扩展到求1/k众数,思路和求1/2众数一样,只不过上次是每组两个数字配对,这次是每组k个数字的配对。
    具体做法:将众数与非众数划分为k个一组(其中众数每一个只出现一次,其它数字用非众数填充),就能够在第nums.size()/k次消去之前将所有非众数全部去掉,而余下的数字都是1/k众数。
    为什么是这样,可以如下证明:假设有x(0<=x<1)个1/k众数,首先创建一个大小nums.size()的容器N,我们将N均分为k个小容器,其中x个小容器用来放1/k众数,每一个小容器放入不同的1/k众数,这样这x个小容器必定被装满而且每个1/k众数还有剩余;与此同时,均分剩余的非1/k众数填充剩下的容器,一定装不满(因为所有数字加起来刚好填满所有小容器,现在有一些1/k众数还在外面,所以剩余的k-x个小容器一定装不满)。然后我们每次从每个容器中去掉一个数字,对应的实际含义是将k个不同类别的数字配对相消,这样在第nums.size()/k次消去之前所有的小容器都将清空,留下的x个容器中全都是1/k众数,证毕。
    最后说一下配对怎么做(其实和前面1/2众数配对差不多):申请两个大小为k-1的数组num[k-1]={}和count[k-1]={}用来记录众数值和个数,然后遍历nums,如果在num数组中有某个数字与当前的nums[i]相等,相应的count[i]递增,这样做的含义是将数字加入配对队列,等待其它的数字与之配对相消;反之如果在num数组中不存在这样的数字,则需要将所有count[i]递减(要求count数组中所有count>0),这样做对应的含义是将k个不同类别的数字配对相消;如果count数组中存在某个count[i]值为零,就将对应的num[i]值替换为遍历的当前数字nums[i] 并递增count。这样做以后所有可能的1/k众数都在num数组中了,我们只需再遍历一次nums数组求出num[i]对应count[i],如果count[i]>nums.size()/k那么num[i]就是1/k众数。
    如果你还想要问“加入到配对队列的不一定就是1/k众数,也可能是1/k众数之外的数字怎么办”这样的问题,那我的回答和前面2.1说到的一样,不会影响结果,因为这样的话非众数将以更快的速度被消去,更利于求出答案。

算法步骤

因为1/2众数求解、1/3众数求解都可以归入1/k众数求解,下面只给出1/k众数求解的步骤:
下面的nums数组为输入的带查询数组,num,count都是大小为k的数组,用于存储k-1个可能的众数。

  1. 遍历nums数组,对于每个nums[i]
    1. 遍历num数组,对于每个num[j]
      判断nums[i]是否等于num[j],如果是,递增count[j],并跳出当层循环(break),并在出循环后跳过外层循环余下代码(continue);
    2. 遍历count数组,对于每个count[j]
      是否存在某个count[j]等于0,如果是,将num[j]设置为当前遍历到的数字nums[i],并设置count[j]为1,然后跳出当层循环(break),出循环之后跳过外层循环余下代码;
    3. 遍历count数组将所有count[j]递减;
  2. count数组全置为零;
  3. 遍历nums数组,对于每个nums[i]. 遍历nums数组,对于每个nums[i]
    1. 遍历遍历num数组,对于每个num[j]
      如果nums[i]和num[j]相等就将count[j]递增;
  4. 遍历count数组,对每个count[i]
    如果count[j]>nums.size()/k就输出num[i]作为结果之一;

算法复杂度分析

时间复杂度:只需要一次遍历nums数组求出k个可能的1/k众数(这其中每次循环都要遍历两个大小为k的数组num和count,判断num[i]是否等于当前数字,count是否等于0);再一次遍历求出k个数字对应的个数count(这其中每次循环都要遍历num数组判断num[i]是否等于当前数字);最后一次遍历num数组判断数字是否确实是众数;所以总的空间复杂度为O(2kn)+O(kn)+O(k)=O(kn)。如果k是常数级别,复杂度可写为O(n)。
空间复杂度:一共使用两个大小为k的数组num和count,空间复杂度为O(k),如果k为常数级别,可写为O(1)。

代码实现&结果分析
  • 1/2众数求解:
    class Solution {
    public:
        int majorityElement(vector<int>& nums) {
            int num = 0;
            int count = 0;
            for (int i = 0; i < nums.size(); ++i) {
                if ( num == nums[i] ) {
                    count++;
                } else if ( count == 0 ) {
                    num = nums[i];
                    count++;
                } else {
                    count--;
                }
            }
            return num;
        }
    };
    
    提交结果:
    在这里插入图片描述
  • 1/3众数求解:
    class Solution {
    public:
        vector<int> majorityElement(vector<int>& nums) {
            int num1 = 0, num2 = 0;
            int count1 = 0, count2 = 0;
            for (int i = 0; i < nums.size(); ++i) {
                if ( num1 == nums[i] ) {
                    count1++;
                } else if ( num2 == nums[i] ) {
                    count2++;
                } else if ( count1 == 0 ) {
                    num1 = nums[i];
                    count1++;
                } else if ( count2 == 0 ) {
                    num2 = nums[i];
                    count2++;
                } else {
                    count1--;
                    count2--;
                }
            }
            count1 = 0;
            count2 = 0;
            for (int i = 0; i < nums.size(); ++i)
            {
                if ( nums[i] == num1 ) count1++;
                else if ( nums[i] == num2 ) count2++;
            }
            vector<int> res;
            if ( count1 > nums.size()/3 ) res.push_back(num1);
            if ( count2 > nums.size()/3 ) res.push_back(num2);
            return res;
        }
    };
    
    提交结果:
    在这里插入图片描述
    beat 98%+,上述解法基本上就是最优解法。
基础篇 1、 算法有哪些特点?它有哪些特征?它和程序的主要区别是什么? 2、 算法的时间复杂度指的是什么?如何表示? 3、 算法的空间复杂度指的是什么?如何表示? 4、 什么是最坏时间复杂性?什么是最好时间复杂性? 5、 什么是递归算法?什么是递归函数? 6、 分治法的设计思想是什么? 7、 动态规划基本步骤是什么? 8、 回溯法分枝限界法之间的相同点是什么?不同之处在哪些方面? 9、 分枝限界法的基本思想是什么? 10、 限界函数的功能是什么? 11、 设某一函数定义如下: 编写一个递归函数计算给定x的M(x)的值。 12、 已知一个顺序表中的元素按元素值非递减有序排列,编写一个函数删除表中多余的值相同的元素。 13、 分别写出求二叉树结点总数及叶子总数的算法。 分治术 14、 有金币15枚,已知其中有一枚是假的,而且它的重量比真币轻。要求用一个天平将假的金币找出来,试设计一种算法(方案),使在最坏情况下用天平的次数最少。 15、 利用分治策略,在n个不同元素中找出第k个最小元素。 16、 设有n个运动员要进行网球循环赛。设计一个满足以下要求的比赛日程表。 (1)每个选手必须其它n-1选手各赛一次; (2)每个选手一天只能赛一次。 17、 已知序列{503,87,512,61,908,170,897,275,652,462},写一个自底向上的归并分类算法对该序列作升序排序,写出算法中每一次归并执行的结果。 贪心法 18、 设有n个文件f1,f2,…,fn要求存放在一个磁盘上,每个文件占磁盘上1个磁道。这n个文件的检索概率分别是p1,p2,…,pn,且 =1。磁头从当前磁道移到被检索信息磁道所需的时间可用这两个磁道之间的径向距离来度量。如果文件fi存放在第i道上,1≤i≤n则检索这n个文件的期望时间是 。其中d(i,j)是第i道第j道之间的径向距离。磁盘文件的最优存储问题要求确定这n个文件在磁盘上的存储位置,使期望检索时间达到最小。试设计一个解此问题的算法,并分析算法的正确性计算复杂性。 19、 设有n个正整数,编写一个算法将他们连接成一排,组成一个最大的多位整数。用贪心法求解本题。 20、 键盘输入一个高精度的正整数N(此整数中没有‘0’),去掉其中任意S个数字后剩下的数字按原左右次序将组成一个新的正整数。编程对给定的N和S,寻找一种方案使得剩下的数字组成的新数最小(输出应包括所去掉的数字的位置和组成的新的正整数,N不超过240位)。 21、 对于下给出的有向网,写出用Dijkstra方法求从顶点A到中其它顶点的最短路径的算法,并写出执行算法过程中顶点的求解次序及从顶点A到各顶点路径的长度。 22、 对于上给出的有向,写出最小成本生成树,给出求解算法。 动态规划 23、 求出上中每对结点间的最短距离的算法,并给出计算结果。 24、 下中给出了一个地,地中每个顶点代表一个城市,两个城市间的连线代表道路,连线上的数值代表道路的长度。现在,想从城市A到达城市E,怎样走路程最短,最短路程的长度是多少? 25、 已知序列a1,a2,…,an,试设计算法,从中找出一子序列 ai1 < ai2 < … E。试用动态规划的最优化原理求出A->E的最省费用。 29、 已知如下,写出用动态规划求最短路径的递推关系式,并写出求从源点A0到终点A3 的最短路径过程。给出求解算法。 6 A1 A2 5 5 2 A0 A3 3 4 4 B1 B2 5 搜索遍历问题 30、 已知有向G=,试设计算法以判断对于任意两点u和v,是否存在一条从u到v的路径,并分析其复杂度。 31、 对于给定的一个二叉树T(如下) a) 设计一个算法,统计二叉树中结点总数; b) 设计一个算法,求二叉树最大宽度及最大宽度所在深度。 32、 判近亲问题。给定一个家族族谱,为简化问题起见,假设家族中的夫妻关系只表示男性成员。设用线性表存储家族成员,用成员的父指针指向其生父。编写一个在此种族谱表示方式下的算法,判断给定的二个家族成员是否是五代内的近亲。(提示:家族成员的表示方式应搜索方式相适应。) 33、 完全二叉树定义为:深度为K,具有N个结点的二叉树的每个结点都深度为K的满二叉树中编号从1至N的结点一一对应。(1)写一个建立二叉树的算法。(2)写一个判别给定的二叉树是否是完全二叉树的算法。 34、 编写计算整个二叉树高度的算法(二叉树的高度也叫二叉树的深度)。 35、 编写计算二叉树最大宽度的算法(二叉树的最大宽度是指二叉树所有层中结点个数的最大值)。 回溯法 36、 (组合问题)求出从自然数1,2,…,n中任取r个数的所有组合。 37、 传教士野人渡河问题。有M个传教士和M个野人准备渡河,船一次最多载2人,任何时刻野人数不能多于传教士数,但允许全部为野人。编写算法给出合理的渡河计划。 38、 某乡有n个村庄,有一个售货员,他要到各个村庄去售货,各村庄之间的路程s是已知的,且A村到B村B村到A村的路大多不同。为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为1。试设计一个算法,帮他选择一条最短的路。 39、 设某一机器由n个部件组成,每一种部件都可以从m个不同的供应商处购得。设wi,j是从供应商j处购得的部件i的重量,ci,j是相应的价格。试设计一个算法,给出总价格不超过c的最小重量机器设计。 40、 设有n件工作分配给n个人。为第i个人分配工作j所需的费用为ci,j 。试设计一个算法,计算最佳工作分配方案,为每一个人都分配1 件不同的工作,并使总费用达到最小。 41、 编写程序求解骑士巡游问题:在n行n列的棋盘上(如n=8),假设一位骑士(按象棋中“马走日”的行走法)从初始坐标位置(x1,y1)出发,要遍访(巡游)棋盘中的每一个位置一次。请编一个程序,为骑士求解巡游“路线”(或告诉骑士,从某位置出发时,无法遍访整个棋盘 — 问题无解骑士巡游)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值