剑指Offer刷题日记Day1(3-5)

153 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

剑指 Offer 03. 数组中重复的数字

基本信息

剑指 Offer 03. 数组中重复的数字

image.png

知识点

哈希

思考

第一思路:哈希记录

首先,简单的想法是用哈希表统计所有元素出现的次数,然后返回一个出现次数大于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)
        }
    }
};

可以直观的感觉到:时间上应该是没有什么优化空间了,因为你必须去寻找数组中重复元素,而这个寻找肯定是线性遍历的找的。

image.png 我们现在需要考虑的优化空间。

一个有趣的条件:数组元素大小

然后再次看题的时候,会发现一个有趣的条件:数组长度为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;
};

image.png

剑指 Offer 04. 二维数组中的查找

基本信息

剑指 Offer 04. 二维数组中的查找

image.png

知识点

二分查找,二叉搜索树

思考

第一思路:无序

首先,你可以装作它们都是无序的(,然后简单的去对每个数组进行遍历。这样很明显时间复杂度中,遍历一个数组是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;
};

image.png

进阶:从左到右递增 => 二分

然后,你可以发现每个数组元素是递增的,这时候应该马上想到二分查找,这样很明显时间复杂度会变成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;
}

image.png

再进一步:从上到下递增 => 线性查找

可以注意到:每一列都按照从上到下递增的顺序排序,所以此时我们可以给出一个查找路径:

考虑右顶点的元素,它左边一行的元素必比它小,下面一列的元素必比它大,所以以此为起点我们可以实现线性的查找

在题例中,考虑20的查找

  1. 15 < 20,所以查找下面
  2. 19 < 20,所以查找下面
  3. 22 > 20,所以查找左边
  4. 16 < 20,所以查找下面
  5. 17 < 20,所以查找下面
  6. 26 > 20,所以查找左边
  7. 23 > 20,所以查找左边
  8. 21 > 20,所以查找左边
  9. 18 < 20,所以查找下面,遇到边界没找到,返回false

image.png

此时时间复杂度为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;
};

image.png

为啥想到从右顶点

为什么从右顶点开始找相信大家都能看懂,但是怎么想到这一点呢?

我见过最牛逼的解释还是这个思路:leetcode-cn.com/problems/er…

简单的来说就是把矩阵看成图,图旋转变成树。那么这棵树有什么性质呢?左子节点小,右子节点大,这不就是二叉搜索树!

那么现在的思路就和二叉搜索树查找一样了!多么融会贯通的思路!

我承认我是肯定没有办法想出来这样的解释的。要我解释我就只能从单调的角度解释,查找必须要有个顺序,那么就需要使用上数据本身的顺序...我是绝对想不到这种解释方法的。

希望有一天我也可以融会贯通到这种地步。

image.png

剑指 Offer 05. 替换空格

基本信息

剑指 Offer 05. 替换空格

image.png

知识点

字符串、原地算法

思考

第一想法:简单遍历

由于太过于简单了,我就直接写一下不细说了

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;
};

image.png

第二想法:正则匹配

绝大部分语言都支持正则匹配,我们只需要使用正则表达式将所有的空格替换为%20就可以了

var replaceSpace = function(s) {
    return s.replace(/ /g,'%20')
};

image.png

考虑空间复杂度:有无原地算法

我们可以想到:如果要替换所有空格,必须先遍历一遍全部元素,所以时间复杂度最低是O(n),这点没有什么优化空间了。那么我们自然会想到空间上有什么优化空间呢?

在第一个思路里,我们使用了一个新的字符串来存放结果,所以空间复杂度是O(n);而在第二种思路的正则匹配中,我们可以从MDN文档上找到这样的说明

image.png

可见正则匹配也是返回了新的字符串,也就是说空间复杂度也是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('');
};

\