小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
原文位于 github仓库-正在起步阶段的前端知识库,其中记录了一名前端初学者的学习日记🤔&学习之路点点滴滴的记录(练手demo🧑💻,必会知识点🧐)
欢迎大家来贡献更多“前端er必会知识点”🧑🎓/分享更多有意义的demo❤️!(请给这个年幼的小仓库更丰富的内容吧🥺🥺🥺)
这篇文章汇总一个超极高频的问题:
找出排序数组中只出现一次的数字&延伸题目
另外还有一道小变式,蛮有意思
-
第一题:使用异或找到数组中只出现一次的单身🐕固然可行,但是为了追求效率,应该使用二分查找~
-
第二题:如果出现多个单身🐕 那么应该使用分组异或
-
第三题:每个元素都出现三次,寻找单身🐕,位运算贼复杂(不会问一个小前端这个问题吧555),直接暴力哈希了(你要不要吧,你要不要🍉)
之前听学长说面试被问到了,然后追问了更佳的解法,就让我好奇起来了,一查力扣,嗬,剑指Offer,来做做看~
540. 有序数组中的单一元素
异或快捷解决
540题中英文版有规定 :Your solution must run in
O(log n)time andO(1)space. 所以这个方法仅供了解主要考察二分法!
var singleNonDuplicate = function(nums) {
let res;
for(let i = 0; i < nums.length; i++){
res ^= nums[i];
}
return res;
};
再优化,二分法(面试重点)
这里的第一个关键点是先把四种情况列出来!
参考官方题解的图~
例子 1:中间元素的同一元素在右边,且被 mid 分成两半的数组为偶数。
我们将右子数组的第一个元素移除后,则右子数组元素个数变成奇数,我们应将 lo 设置为 mid + 2。
例子 2:中间元素的同一元素在右边,且被 mid 分成两半的数组为奇数。
我们将右子数组的第一个元素移除后,则右子数组的元素个数变为偶数,我们应将 hi 设置为 mid - 1。
例子 3:中间元素的同一元素在左边,且被 mid 分成两半的数组为偶数。
我们将左子数组的最后一个元素移除后,则左子数组的元素个数变为奇数,我们应将 hi 设置为 mid - 2。
例子 4:中间元素的同一元素在左边,且被 mid 分成两半的数组为奇数。
我们将左子数组的最后一个元素移除后,则左子数组的元素个数变为偶数,我们应将 lo 设置为 mid + 1。
然后就常规二分法做就行了~注意分情况讨论的细则即可!
var singleNonDuplicate = function(nums) {
// 定义双指针
let i = 0, j = nums.length - 1;
while(i < j){
// let mid = (i + j) >> 1;
// 为了防止大数溢出 建议这么写
let mid = i + (j - i >> 1)
// 此方法的关键——判断哪边为奇数的变量 要设置好
let isEven = (j - mid) % 2 == 0;
// 如果j-mid为偶数 则去除中间两个值相同的元素并跳过它们之后,两指针(包括两指针)之间有奇数个元素,
// 也就是单个的元素一定在这之间
if(nums[mid] === nums[mid - 1]){
if(isEven){
// 在左边
j = mid - 2;
}
else{
i = mid + 1;
}
}
else if(nums[mid] === nums[mid + 1]){
if(isEven){
// 在右边
i = mid + 2;
}
else{
console.log("last j",j)
j = mid - 1;
}
}
else{
return nums[mid];
}
}
return nums[i];
};
怎么说呢,双指针的题,多画图就完事了!
时间复杂度 O(logn),相比于暴力循环(包括异或),每次迭代将搜索空间缩减了一半!
进一步优化,仅对偶数索引进行二分搜索
最佳实践
var singleNonDuplicate = function(nums) {
let i = 0, j = nums.length - 1;// 数组长度必为奇数,所以一前一后两个元素下标为偶数
while(i < j){
let mid = i + ((j - i) >> 1);
if(mid % 2 === 1){
// mid为奇数则-1变为偶数 则mid现在必为“边缘” 不必再分四种情况来讨论
// 这就是仅对偶数索引进行二分搜索!
mid--;
}
if(nums[mid + 1] === nums[mid]){
// 去除mid那一对数之后,左侧数必为偶数,右侧数必为奇数,继续去紧挨着那对数的右边1个找
i = mid + 2;
}
else{
// 去除mid那一对数之后,左侧数为奇数,右侧数必为偶数,继续去紧挨着那对数的左边1个找
j = mid;// 此时mid已经在原基础上左移一位了 所以j直接放在mid这个位置即可
}
}
return nums[i];
};
- 时间复杂度:O(log n/2) = O(logn)。我们仅对元素的一半进行二分搜索。
剑指 Offer 56 - I. 数组中数字出现的次数
位运算-分组异或
这个分组的方法就很灵性。
var singleNumbers = function(nums) {
let n = 0;
// 01 n 为 两个单独数a b的乘积
// 接下来(02中)使用与运算
// 与运算特点 二进制中只有6&6 = 6 6&0 = 0&0 =0
for(let num of nums){
n ^= num;
}
// 02 m可以保证这个数组中单身的两个数a b中的一个可以不被它抵消掉
// 也就是 m&a = 0 m&b != 0
let m = 1;
while((n & m) === 0){
// 只要n&m不为0 就一直让m左移,直到m可以抵消掉a与b中的一个
m <<= 1;
}
// 03 接下来使用m把两个单独的数分在两堆 并分组
let x = 0, y = 0;
for(let num of nums){
if((num & m) === 0){
x ^= num;
}
else{
y ^= num;
}
}
return [x, y];
};
看不懂我这个解释(或者觉得太大白话) 可以看看 K神的题解
137. 只出现一次的数字 II
同剑指 Offer 56 - II. 数组中数字出现的次数 II一样
暴力哈希解
var singleNumber = function(nums) {
let map = new Map();
for(let num of nums){
if(map.get(num) > 0){
let count = map.get(num);
count++;
map.set(num, count);
}
else{
map.set(num, 1);
}
}
for(let [num,count] of map.entries()){
if(count === 1){
return num;
}
}
};
其他方法太恶心了 饶了我吧…我真不想做位运算了😢😢😢😢😢😢,数电都出来了
如上。