LeetCode 每日两题 第四天

142 阅读1分钟

「这是我参与11月更文挑战的第三天,活动详情查看:2021最后一次更文挑战

各位小伙伴大家好啊,今天是第四天了。坚持就是胜利,打卡~~

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

找出数组中重复的数字。在一个长度为 n 的数组 nums里的所有数字都在 0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:23 

限制:

2 <= n <= 100000

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/sh…

解题思路

暴力

暴力解法就利用两次for循环,第一个指针指向一个数字,第二个指针遍历一次数组,是否有和第一个指针指向相同的值。这里提一下,暴力解放的第二个指针可以不用从头开始遍历,因为前面的数字已经判断过了,不可能存在相同的数字。由于暴力法不是很好,这里就不给出实现了。

时间复杂度:O(n^2),空间复杂度:O(1)

哈希表

我们也可以利用哈希表来重新存储这些数字。如果存储过程中有冲突,那么就说明即将存入的那个数字已经重复了。题目中指出,找出任意重复的数字即可,所以就可以将当前数字返回。因为要引入额外的hash数组,所以空间复杂度O(n)。

时间复杂度:O(n),空间复杂度:O(n)

优化哈希表

因为题目明确指出nums里的所有数字都在0~n-1的范围内,如何来利用这个条件呢?其实在hash表的解法中,我已经使用了这个条件,将数组的索引作为哈希的索引,值和索引一一对应。例如2这个数据应放在数组索引2的位置下。这样一来,就将条件用上了。但是用的还不够好,因为哈希表额外使用了空间。所以,我们也可以在原数组修改。将每个值直接放入原数组中对应的位置,如果要存放的数据的索引里已经是这个数据,就说明该元素重复,return 它。

为什么明明用了两次循环实现,该方法的时间复杂度依然是O(n)?我们只是把所有元素放到了对应位置,如果已经放在了对应位置,第二层循环就不会进入,所以每一个元素只会放置最多一次,那么我们的时间复杂度就应该是O(n)。

时间复杂度:O(n),空间复杂度:O(1)

代码实现

// 普通哈希表
var findRepeatNumber = function(nums) {
    let hash = [];
    for(let val of nums) {
        // 如果hash中已经存在该值
        if(hash[val] === val) {
            return val;
        } else {
            // 否则将值存入hash
            hash[val] = val;
        }
    }
};

// 优化后的哈希表
var findRepeatNumber = function(nums) {
    for(let i=0; i<nums.length; i++) {
        // 当前位置值和位置不相同时,需要把正确的值放在当前位置才行。
        while(nums[i] !== i) {
            // nums[i] 当前值
            // nums[nums[i]] 当前值应该放入的位置
            // 如果当前值和对应位置的值一致,说明该值重复
            if(nums[i] === nums[nums[i]]) {
                return nums[i];
            }
            let temp = nums[i];
            nums[i] = nums[temp];
            nums[temp] = temp;
        }
    }
};

第二题 剑指 Offer 53 - I. 在排序数组中查找数字 I

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

示例 2:

输入: nums = [5,7,7,8,8,10], target = 6
输出: 0

限制

  • 0 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9
  • nums 是一个非递减数组
  • -10^9 <= target <= 10^9

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/za…

解题思路:阅读完题目,抓取题目关键信息——排序数组。现在我们还是从最简单的方式说起,最简单的方式肯定是暴力解法。

暴力

设置一个计数器count,循环遍历一次数组,当遇到target值的时候计数器加一,返回count

时间复杂度:O(n),空间复杂度:O(1)

二分查找

题目既然说到了是排序数组,那么我们可以试一试二分法。我们一般都知道二分法是用来查找数组中某个元素的位置。那怎么利用二分法查找该元素的出现次数呢?因为是有序数组,所以我们先找到该元素首次出现的位置,再找到该元素最后出现的位置。用最后出现的位置减去首次出现的位置,得到的结果就是该元素出现的次数。

时间复杂度:O(log n),空间复杂度:O(1)

代码实现

// 二分查找
var search = function(nums, target) {
    if(nums.length === 0) {
        return 0;
    }

    let first = findFirstIndex(nums, target);
    // 如果不存在该元素
    if (first === -1) {
        return 0;
    }

    let last = findLastIndex(nums, target);
    return last - first + 1;
};

function findFirstIndex(nums, target) {
    let left = 0, right = nums.length - 1;
    let mid;

    while (left < right) {
        mid = left + Math.floor((right - left) / 2);
        if(nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return nums[left] === target ? left : -1;
}

function findLastIndex(nums, target) {
    let left = 0, right = nums.length - 1;
    let mid;

    while (left < right) {
        mid = left + Math.floor((right - left + 1) / 2);
        if(nums[mid] <= target) {
            left = mid;
        } else {
            right = mid - 1;
        }
    }

    return nums[left] === target ? left : -1;
}

注意:查找的时候,你会遇见这样的情形,偶数数组。那么此时的问题是,中间的值到底是哪一个?查找首次出现的位置,应该向左边偏移,而查找最后出现的位置,我们应该向右偏移,因为最后出现的位置应该在最右边。

第三题 剑指 Offer 53 - II. 0~n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:

输入: [0,1,3]
输出: 2

示例 2:

输入: [0,1,2,3,4,5,6,7,9]
输出: 8

限制:

1 <= 数组长度 <= 10000

来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/qu…

解题思路:题目又提到了递增排序数组每个数字的范围都在0~n-1,那么有了第一题和第二题的经验,我们可以用二分查找来实现。

二分查找

我们需要找出当前缺失的元素,缺失了元素的特点为后面的值会变大。中间的值等于其索引值,那么说明中间以及之前的值并没有丢失,将left指针指向mid + 1。中间的值大于其索引值,那么说明前面有值丢失,将right指针指向mid,因为有可能刚好是mid的前面一个值丢失了,所以不能置为mid-1。最后,遍历完之后,如果指针指向最后一个元素,并且值也是正确的,那么就说明丢失了n这一个元素,返回left+1,否则返回left

暴力

这题暴力也是可以解决的,在此就不提了,因为有更好的方法就尽量使用更好的方法。

代码实现

// 二分搜索
var missingNumber = function(nums) {
    let left = 0, right = nums.length - 1;
    let mid;

    while(left < right) {
        mid = Math.floor((left + right) / 2);

        if(nums[mid] === mid) {
            left = mid + 1;
        } else if (nums[mid] > mid) {
            right = mid;
        }
    }

    return (left === nums.length - 1 && nums[left] === left) ? left + 1 : left;
};

总结

今天的题主要考查了二分查找这个知识点。一般来说,题目中如果有有序数组这样类似的词语,我们就可以想想是否可以使用二分查找。它的时间复杂度为O(log n),空间复杂度为O(1),所以比一般的暴力查找快。如果有什么不对的地方,或者有更好的方案,欢迎提出,大家一起讨论。