一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
剑指 Offer 03. 数组中重复的数字
基本信息
知识点
哈希
思考
第一思路:哈希记录
首先,简单的想法是用哈希表统计所有元素出现的次数,然后返回一个出现次数大于1的数就可以了。
然后从这个思路出发,发现其实只要第一次发现重复数字就可以直接返回了,不需要遍历完整个数组。
这样做时间复杂度是O(n),因为只有一次遍历(还很可能遍历不完),空间复杂度是O(n),因为需要存储数组中遇到的每个元素(虽然不一定全部遍历)。
var findRepeatNumber = function(nums) {
let map = new Map();
for(let item of nums) {
if(map.has(item)) {
return item
}else {
map.set(item,1)
}
}
};
可以直观的感觉到:时间上应该是没有什么优化空间了,因为你必须去寻找数组中重复元素,而这个寻找肯定是线性遍历的找的。
我们现在需要考虑的优化空间。
一个有趣的条件:数组元素大小
然后再次看题的时候,会发现一个有趣的条件:数组长度为n,数组所有元素都在0~n-1的范围之内。
也就是说:如果没有重复元素,那么数组下标和数组元素值应该是一一对应的,也就是说对于一个i(0 <= i <= n-1),必然在数组中的某个位置出现。
但是现在有重复元素,这会导致什么呢?这会导致元素索引和元素值是一对多的关系,也就是说在不同的元素索引上可以找到相同的值
我们可以这样考虑:既然本身应该一一对应,我们不妨交换元素使其和下标一一对应,如果交换的时候发现那个位置上已经一一对应了,则说明这个元素一定重复了。
比方说题例:[2, 3, 1, 0, 2, 5, 3]。我们这时的做法是这样的:
1.位置0上不是0,所以需要交换,把2换到arr[2]
2.位置1上不是1,所以需要交换,把3换到arr[3]
3.位置2上是2,跳过
4.位置4上不是4,所以需要交换,把0换到arr[0]
5.位置5上不是5,所以需要交换,把2换到arr[2],发现arr[2]已经是2了,证明这个2重复了,返回2
var findRepeatNumber = function(nums) {
let i = 0;
while(i < nums.length) {
if(nums[i] == i) {
i++;
continue;
}
if(nums[nums[i]] == nums[i]) return nums[i];
let tmp = nums[i];
nums[i] = nums[tmp];
nums[tmp] = tmp;
}
return -1;
};
剑指 Offer 04. 二维数组中的查找
基本信息
知识点
二分查找,二叉搜索树
思考
第一思路:无序
首先,你可以装作它们都是无序的(,然后简单的去对每个数组进行遍历。这样很明显时间复杂度中,遍历一个数组是O(m),那么遍历n个数组就是O(nm)。
var findNumberIn2DArray = function(matrix, target) {
for(let arr of matrix) {
for(let item of arr) {
if(item == target) {
return true
}
}
}
return false;
};
进阶:从左到右递增 => 二分
然后,你可以发现每个数组元素是递增的,这时候应该马上想到二分查找,这样很明显时间复杂度会变成O(nlogm)
var findNumberIn2DArray = function(matrix, target) {
for(let arr of matrix) {
if(binarySearch(arr,target)) {
return true;
}
}
return false;
};
var binarySearch = function(arr,target) {
let left = 0, right = arr.length - 1;
while(left <= right) {
let mid = left + Math.floor((right - left) / 2);
if(target == arr[mid]) {
return true;
}
if(target > arr[mid]) {
left = mid + 1;
}else {
right = mid - 1;
}
}
return false;
}
再进一步:从上到下递增 => 线性查找
可以注意到:每一列都按照从上到下递增的顺序排序,所以此时我们可以给出一个查找路径:
考虑右顶点的元素,它左边一行的元素必比它小,下面一列的元素必比它大,所以以此为起点我们可以实现线性的查找。
在题例中,考虑20的查找
- 15 < 20,所以查找下面
- 19 < 20,所以查找下面
- 22 > 20,所以查找左边
- 16 < 20,所以查找下面
- 17 < 20,所以查找下面
- 26 > 20,所以查找左边
- 23 > 20,所以查找左边
- 21 > 20,所以查找左边
- 18 < 20,所以查找下面,遇到边界没找到,返回false
此时时间复杂度为O(n+m)。访问到的下标的行最多增加n最多减少m次,因此循环体最多执行n + m次。
var findNumberIn2DArray = function(matrix, target) {
if(matrix.length == 0 ) {return false}
let rows = matrix.length, columns = matrix[0].length;
let row = 0, column = columns - 1;
while (row < rows && column >= 0) {
let num = matrix[row][column];
if (num == target) {
return true;
} else if (num > target) {
column--;
} else {
row++;
}
}
return false;
};
为啥想到从右顶点
为什么从右顶点开始找相信大家都能看懂,但是怎么想到这一点呢?
我见过最牛逼的解释还是这个思路:leetcode-cn.com/problems/er…
简单的来说就是把矩阵看成图,图旋转变成树。那么这棵树有什么性质呢?左子节点小,右子节点大,这不就是二叉搜索树!
那么现在的思路就和二叉搜索树查找一样了!多么融会贯通的思路!
我承认我是肯定没有办法想出来这样的解释的。要我解释我就只能从单调的角度解释,查找必须要有个顺序,那么就需要使用上数据本身的顺序...我是绝对想不到这种解释方法的。
希望有一天我也可以融会贯通到这种地步。
剑指 Offer 05. 替换空格
基本信息
知识点
字符串、原地算法
思考
第一想法:简单遍历
由于太过于简单了,我就直接写一下不细说了
var replaceSpace = function(s) {
let res = ''
for(let i = 0; i < s.length; i++) {
if(s[i] !== ' ') {
res += s[i]
}else {
res += '%20'
}
}
return res;
};
第二想法:正则匹配
绝大部分语言都支持正则匹配,我们只需要使用正则表达式将所有的空格替换为%20就可以了
var replaceSpace = function(s) {
return s.replace(/ /g,'%20')
};
考虑空间复杂度:有无原地算法
我们可以想到:如果要替换所有空格,必须先遍历一遍全部元素,所以时间复杂度最低是O(n),这点没有什么优化空间了。那么我们自然会想到空间上有什么优化空间呢?
在第一个思路里,我们使用了一个新的字符串来存放结果,所以空间复杂度是O(n);而在第二种思路的正则匹配中,我们可以从MDN文档上找到这样的说明:
可见正则匹配也是返回了新的字符串,也就是说空间复杂度也是O(n)。
那么有没有原地修改的方式呢?
答案是:看语言,如果在那门语言中,字符串本身是可以被修改的,那么就可以用双指针实现原地算法。具体做法是:
1.先记录原来的字符串长度。然后遍历一遍,当遇到空格时,就在字符串末尾增加两个位置
2.第二次从原本字符串的末尾向头遍历,并且设置双指针,p1指向新字符串的末尾,p2指向旧字符串的末尾
2.1 如果p1指向的元素不是空格,就把p2的元素设置为它(可以想到p2最开始都是指向空元素)
2.2 如果p1指向的元素是空格,就把p2和前面两个元素设置为 %20,并且把p2直接向前位移两位
这样做就可以保证p2所指向的元素都是被修改好的,当p1指向开头时p2也指向开头,整个数组被修改好。
js中的字符串并不能原地修改,只要修改字符串,必然创建了一个新的字符串,具体的过程是这样的:
1.创建一个新的字符串变量,它有着足够的空间
2.将原来字符串的值(还有添加的值)填充进来
3.删除原来的字符串
所以js中这题是没有空间复杂度为O(1)的算法的,不过我们还是可以用数组去稍微实现以下这种倒序双指针的思路,这个思路还是挺经常被用到的。
var replaceSpace = function(s) {
s = s.split("");
let oldLen = s.length;
for (let i = 0; i < oldLen; i++) {
if (s[i] === ' ') {
s.length += 2;
}
}
for (let p1 = oldLen - 1, p2 = s.length - 1; p1 >= 0; p1--, p2--) {
if (s[p1] !== ' ') s[p2] = s[p1];
else {
s[p2 - 2] = '%';
s[p2 - 1] = '2';
s[p2] = '0';
p2 -= 2;
}
}
return s.join('');
};
\