引言
以自述式的笔记展示,尽可能用最好理解的方式去叙述我对知识点的理解,方便有需求的小伙伴查看理解,同时锻炼自身的表达能力,共同学习,共同进步,争取“双赢”!
注:本文章根据自身学习的笔记和自身理解编写,难免存在纰漏或不足之处,如有不足,恳请各位在评论区批评指正,谢谢!
一、题目描述
此题链接:面试题 17.04.消失的数字
数组nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
注意:本题相对书上原题稍作改动
示例 1:
输入:[3,0,1] 输出:2
示例 2:
输入:[9,6,4,2,3,5,7,0,1] 输出:8
二、解题思路
1、审题 -> 理方向
阅读题目,我们得出以下信息要求:
1)整型数组中包含0~n
2)0~n中间缺一个数
3)时间要求O(n)内
4)目标找出缺失数
我们看见数组,应该想到可能和循环语句相关,看见寻找缺失数可能相当条件语句判断是否存在,看见时一连串有序数组成,也能想到排序了再去判断等等,这些便是我们一个粗略的方向。
2、思路构建
根据上述思考,接下来我们就进入思路构建阶段。
思路一:暴力循环+判断(不推荐)
首先,我们能马上想到的寻找办法就是直接遍历去判断是否存在,即对数组中每一个数遍历一遍并去与0~n个数一一比较看是否相等,若存在相等情况则记录为1,然后继续用数组中下一个数去依次比较记录,直到找到找到比较完毕后记录为0的数,此数就是缺失的那个数。
功能代码实现如下:
int missingNumber(int* nums, int numsSize){
for(int j=0;j<=numsSize;j++)
{
int count=0;
for(int i=0;i<numsSize;i++)
{
if(nums[i]==j)
{
count=1;
}
}
if(count==0)
{
return j;
}
}
}
时间复杂度分析:外界循环n+1次,且每循环一次内部循环n次,即时间复杂度为O(n^2),不满足时间限制要求,所以下一个思路。
空间复杂度:O(1)
思路二:排序+遍历判断寻找
在思路一的基础上,我思考有没有更快的方式呢?仔细剖析实现细节,我们思考:为什么一开始要遍历n次然后内部再遍历n次呢?是不是因为咱数组与有序0-n不是一一对应的啊。好我们再假设如果让它对应了,那找的时候就只需要一次循环了,虽然排序会占用时间,但相比循环n*(n-1)次的嵌套循环,总时间复杂度还是可以降低一些。
因此整理一下这个思路就是:先把数组里面的数升序排列好,再边遍历边比较是否相等,把不相等的数输出即可。(由于快速排序算法目前还未学习C语言实现,故先使用C++中的algorithm算法中的sort快排,时间复杂度O(nlogn))
C++代码实现如下:
class Solution {
public:
int missingNumber(vector<int>& nums) {
//对数组排序
sort(nums.begin(),nums.end());
//遍历判断
int ret=0;
for(vector<int>::iterator it=nums.begin();it!=nums.end();it++)
{
//如果出现不等则说明缺失ret
if(ret!=*it)
{
break;;
}
ret++;
}
return ret;
}
};
分析时间复杂度:目前咱可以实现的最快的排序算法时间是O(nlogn),然后加上循环O(n),最终该思路的时间复杂度为O(nlogn),遗憾的是,仍大于题目要求时间范围,因此不完全正确。
空间复杂度:O(1)
思路三:运算方式提取
由于前两个思路仍然做不到O(n)内实现,那么我们现在换一个角度去看这个问题,从运算的角度去观察:寻找0-n中缺失的数,如何缺了会怎样?因为是运算,那咱看看最简单的求和运算,相信大家此时突然明白了什么,确实,如果看求和的话,如果缺失一个数,你们对于完整的一连续数来说,他的和恰好就与数组求和以后结果相差那个缺失数的大小,间接就求出了缺失的数。
因此 整理一下思路:分别对完整连续数和数组数求和,然后(完整的和)减去(数组中数的和)即可得到缺失的数。
方法:分别遍历求和+减法运算
代码实现如下:
int missingNumber(int* nums, int numsSize){
//0~n的数求和公式 (1+n)*n/2
int numsSum=0,sum=(1+numsSize)*numsSize/2;
for(int i=0;i<numsSize;i++)
{
numsSum+=nums[i];
}
return sum-numsSum;
}
时间复杂度分析:因为仅单个循环,所以执行次数最多为n,即该方式时间复杂度为O(n) 满足要求√
空间复杂度:O(1)
思路四:异或提取(最优)
前两个思路的时间复杂度总不符合要求,说明我们用最普通的思路还是无法实现,除了运算角度,回过头来,我们再换个角度仔细分析一下,该角度个人认为属于四种思路中的最优解法了。
该题目的是寻找某些数中缺少的一个数,换句话说就是规定的数的范围与数组中的数合起来看,正常情况下应该是0-n的数均会出现两次,但是缺少的数就只有出现一次,那么如果把全部的数通过某种运算能够因为一个数两次出现而消失且只出现一次不会变化的话,那我们是不是可以反复进行而得到缺失数了呢?这里,我们抽象出来就是说1 1 -> 0,且 0 1 或者 1 0 -> 1,这样就有机会了,看看哈,相同变为0、相异变为1,这是不是就是异或运算可能得到的结果呀!沿着这个思路去看。
我们又知道异或^运算有几个性质:
1. 0与任何数异或后结果都是该数本身,
2. 异或运算可以存在结合律交换律a^b^c=(a^b)^c等等
3. 相同的数发生异或以后会变成0。
基于这三条性质,我们可以发现如果用0去与数组中的数发生异或,然后再接着与0-n进行异或,这样不缺的数就被与0异或两次,相当于相同的数发生了异或就会变成0相当于抵消,最后就只剩下0与只出现一次的数发生异或,此时结果就是缺失数本身,恰好。这便是使用异或寻找缺失数的思路。
功能代码实现如下:
int missingNumber(int* nums, int numsSize){
int x=0;
//x与数组每一个数异或一次
for(int i=0;i<numsSize;++i)
{
x^=nums[i];
}
//再与0~numsSize个数分别异或一次
//根据异或的性质,异或两次相同的数就会变成0,且0与任何数异或都为其本身
for(int j=0;j<=numsSize;++j)
{
x^=j;
}
return x;
}
时间复杂度分析:因为是分别循环,所以执行次数最多为n,即该方式时间复杂度为O(n) 满足要求√
空间复杂度:O(1)
三、总结
1、对一个题用不同的角度思考能够获得更加优化的思路和方法,在算法编写过程中,注重发散性思维的培养,多思考,有助于提升自身的编码能力以及优化能力。
2、认识到了异或运算的用处,加深了对异或运算的理解。
最后,欢迎小伙伴们在评论区留言,批评指正,相互学习,共同进步!