代码随想录算法训练营第一天 | 704. 二分查找、27. 移除元素

文章目录

  • LeetCode 704.二分查找
    • 题目链接:[LeetCode 704.二分查找](https://leetcode.cn/problems/binary-search/)
    • 遇到的问题
    • 代码实现
    • 总结
  • LeetCode 27.移除元素
    • 题目链接:[LeetCode 27.移除元素](https://leetcode.cn/problems/remove-element/)
    • 遇到的问题
    • 代码实现
    • 总结
  • 今日收获

LeetCode 704.二分查找

题目链接:LeetCode 704.二分查找

遇到的问题

在计算middle时没有考虑到溢出的问题,导致第一次测试失败

代码实现

左闭右闭

var search = function (nums, target) {
	let left, middle = 0, right = nums.length - 1;
	while (left <= right) {
		// 注意溢出问题
		middle = left + ((right - left) >> 1);
		if (nums[middle] > target) {
			right = middle - 1;	// target在左区间[left, middle - 1]
		} else if (nums[middle] < target){
			left = middle + 1; // target在右区间[middle + 1, right]
		} else {
			return middle;
		}
	}
	return - 1;
};

左闭右开

var search = function (nums, target) {
	let left, middle = 0, right = nums.length - 1;
	while (left < right) {
		middle = left + ((right - left) >> 1);
		if (nums[middle] > target) {
			right = middle;// target在左区间[left, middle)
		} else if (nums[middle] < target) {
			left = middle + 1;// target在右区间[middle + 1, right)
		} else {
			return middle;
		}
	}
	return -1;
};

总结

二分查找最重要的是处理好区间的边界,是使用左闭右闭还是左闭右开?一旦确定了边界,题目就变得清晰了。

1.左闭右闭

在这种情况下,如果判断出 target 在左区间(也就是nums[middle] > target时),那么nums[middle] ≠ target,此时左区间应该为[left, middle - 1];

如果判断出 target 在右区间(也就是nums[middle] < target时),那么nums[middle] ≠ target,此时右区间应该为[middle + 1, right]

nums[middle] = target,此时middle就是要找的数组下标,返回 middle 即可。

2.左闭右开

在这种情况下,如果判断出 target 在左区间(也就是nums[middle] > target时),那么nums[middle] ≠ target,而且是左闭右开的区间,实际对比时并不对比右边界,因此此时左区间应该为[left, middle);

如果判断出 target 在右区间(也就是nums[middle] < target时),那么nums[middle] ≠ target,而且是左闭右开的区间,实际对比时并不对比右边界,因此此时右区间应该为[middle + 1, right)

nums[middle] = target,此时middle就是要找的数组下标,返回 middle 即可。

3.浮点数溢出问题

额外总结一下溢出问题。

在JS中,如果一个整型数据经过除运算后的结果是一个浮点数,那么会JS不会默认向下取整,例如:

console.log(10 / 3);
// 输出:3.3333333333333335

因此,在本题中为了取到正确的 middle 值,需要使用更为精确的计算方法,例如使用位运算。
在本题中使用了右移运算符,它的规则是这样:

各二进制位全部右移若干位,正数高位补0,负数高位补1,低位丢弃

举个例子:

// 正数的位运算
12 >> 2 = 3
1100 -> 12
0011 -> 12 >> 2 = 3
因为12是正数,右移过程中高位补上两个0,低位丢弃,结果就是3

// 负数的位运算
-12 >> 2 = -3
0100 -> -12
1101 -> -12 >> 2 = -3
因为-12是负数,右移过程中高位补1,低位丢弃,结果就是-3

明白了右移运算符的规则,回到本题,middle的计算是要计算出一组连续数据最中间的数,也就是传统意义上的(left + right) / 2

我们看几个例子来理解右移 >> 1的特殊含义:

10 >> 1 = 5
1010 -> 10
0101 -> 10 >> 1 = 5

8 >> 1 = 4
1000 -> 8
0100 -> 8 >> 1 = 4

11 >> 1 = 5
1011 -> 11
0101 -> 11 >> 1 = 5

发现了吗,十进制数进行 >> 1的位运算就相当于一个除以2的十进制运算。
也就是说,10 >> 1 等同于10 / 2。

这样我们就能拆分了:

(left + right) / 2 = left / 2 + right / 2 
                   = left + right / 2 - left / 2 
                   = left + ((right - left) / 2)
                   = left + ((right - left) >> 1)

这就是本题中的写法,这种写法避免了浮点数溢出的问题,同时也比直接使用/计算更快一些。

理解了之后,以后在工作中就可以直接使用了。

LeetCode 27.移除元素

题目链接:LeetCode 27.移除元素

遇到的问题

代码实现

暴力解法

var removeElement = function (nums, val) {
    let len = nums.length;
    for (let i = 0; i < len; i++) {
        if(nums[i] == val) { // 找到需要移除的元素,将数组整体左移一位
            for (let j = i + 1; j < len; j++) {
                nums[j - 1] = nums[j];
            }
            i--; // 数组左移一位,因此下标i也要左移
            len--; // 此时已经完成一次删除,数组长度也要减1
        }
    }
    return len;
};

双指针法

var removeElement = function(nums, val) {
	let len = nums.length;
    let i, j = 0; // 定义快慢指针
    for (; i < len; i++) { // 快指针遍历数组,寻找非移除元素
        if (nums[i] != val) { // 找到非移除元素,慢指针指向不含移除元素val的新数组的下标
            nums[j++] = nums[i];
        }
    }
    return j; // 遍历结束后,慢指针就代表新数组的长度
};

总结

暴力解法看似简单粗暴,实则要考虑的细节比较多,远不如双指针好用。

1.暴力解法

在找到要移除的val后,数组整体左移进行删除,此时在进入下一次循环时需要考虑到指针i的位置发生了变化,数组的长度也发生了变化,因此需要更新二者后再进入循环。

2.双指针法

定义快慢指针,快指针遍历数组,寻找val之外的元素,组成不含val元素的新数组;

慢指针指向新数组的元素,一次循环后需要右移一位以便接收下次循环快指针找到的元素,当快指针遍历完nums,快指针的下标实际上就是新数组的长度。

今日收获

  1. 首先是重新巩固了一下位运算,工作中一直在用但不明白原理,这次查资料搞明白了。
  2. 二分查找需要注意区间边界的判断,坚持循环不变量
  3. 复习了一下双指针法。
  4. 今天实际想思路+写代码+查资料的时间也就30分钟,但写博客花了两个小时,准备做一个模板,之后多少能省一些时间。

你可能感兴趣的:(代码随想录算法训练营,算法,leetcode)