面试经典150:剩余数组问题💬

312 阅读7分钟

数组

LeetCode中的数组题目是非常常见的题型之一,考察的是对数组的基本操作和一些高级算法的运用。数组是一种线性数据结构,它可以存储同一类型的元素,这些元素可以通过索引来访问。数组的操作包括:插入、删除、查找、遍历等。以下是一些常见的LeetCode数组题目类型:

  1. 数组排序

    这类题目主要考察排序算法的实现和优化。常见的排序算法有冒泡排序、插入排序、选择排序、归并排序、快速排序等。LeetCode中的数组排序题目包括:

    • 88.合并两个有序数组
    • 215.数组中的第K个最大元素
    • 287.寻找重复数
    • 75.颜色分类
  2. 数组查找

    这类题目主要考察查找算法的实现和优化。常见的查找算法有线性查找、二分查找、哈希查找等。LeetCode中的数组查找题目包括:

    • 33.搜索旋转排序数组
    • 153.寻找旋转排序数组中的最小值
    • 240.搜索二维矩阵 II
    • 4.寻找两个正序数组的中位数
  3. 数组操作

    这类题目主要考察对数组的一些基本操作的实现和优化。包括去重、最大子序列和、最长上升子序列、数组旋转等。LeetCode中的数组操作题目包括:

    • 26.删除有序数组中的重复项
    • 53.最大子序和
    • 300.最长上升子序列
    • 189.旋转数组
  4. 数组变换

    这类题目主要考察对数组变换的实现和优化。包括对称变换、交换变换、数值变换等。LeetCode中的数组变换题目包括:

    • 31.下一个排列
    • 48.旋转图像
    • 56.合并区间
    • 289.生命游戏

总的来说,LeetCode中的数组题目难度从简单到困难不一,需要具备扎实的数组操作和算法基础知识,同时也需要具备灵活的思维和创新能力,能够根据问题特点灵活选择算法和数据结构。

169. 多数元素 - 力扣(Leetcode)

给定一个大小为 n **的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

 

示例 1:

输入: nums = [3,2,3]
输出: 3

示例 2:

输入: nums = [2,2,1,1,1,2,2]
输出: 2

 

提示:

  • n == nums.length
  • 1 <= n <= 5 * 104
  • -109 <= nums[i] <= 109

 

进阶: 尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        count = Counter(nums)
        for k,v in count.items():
            if v > len(nums) // 2:
                return k

该算法的思路是先使用 collections.Counter 函数统计每个元素在数组中出现的次数,然后遍历 Counter 的字典对象,找到出现次数超过数组长度一半的元素并返回。

算法的时间复杂度为 O(n),因为需要对整个数组进行遍历统计。空间复杂度为 O(n),因为需要使用 Counter 对象来存储每个元素出现的次数。

上边是一个人人都能想到的思路,所以我们看一下进阶解法,如何只使用O(n)的额外空间

摩尔投票法

摩尔投票法是一种用来寻找数组中出现次数超过一半的元素的算法,其基本思想是不断消除不同元素之间的差异,最终剩余的元素就是出现次数超过一半的元素。

算法的具体过程如下:

  1. 假设数组中的第一个元素为候选元素,计数器初始化为1。

  2. 从第二个元素开始遍历数组,如果当前元素与候选元素相同,则计数器加1,否则计数器减1。

  3. 如果计数器变为了0,则将当前元素作为候选元素,并将计数器重新初始化为1。

  4. 遍历完数组后,候选元素就是出现次数超过一半的元素。

该算法的时间复杂度为 O(n),空间复杂度为 O(1)。

摩尔投票法的正确性可以通过反证法证明。

假设有一个数 x 出现的次数超过了数组长度的一半,我们用 count 表示目前 x 出现的次数,遍历整个数组,当遇到 x 时,count 加 1,否则减 1。如果 count 变成了 0,则说明目前并没有超过一半的数,此时从下一个数开始重新计数。最后如果 x 真的出现的次数超过了数组长度的一半,那么最终遍历完成后 x 的计数一定大于 0。

假设有两个数 xy 都出现的次数超过了数组长度的一半,用 count_xcount_y 分别表示两个数目前出现的次数。同样进行遍历,当遇到 xy 时,对应的计数加 1,否则减 1。当 count_xcount_y 都变成了 0 时,说明目前并没有超过一半的数,此时从下一个数开始重新计数。最终,如果 xy 都真的出现的次数超过了数组长度的一半,那么最终遍历完成后 count_xcount_y 一定都大于 0。此时,如果 xy 不相等,那么我们可以认为数组中存在一些数先后出现,抵消掉了 xy 的计数,导致它们的计数都变成了 0,这是矛盾的。因此,xy 必须相等。

综上所述,摩尔投票法可以正确地找出出现次数超过数组长度一半的数。

所以使用摩尔投票法的代码为:

class Solution:
    def majorityElement(self, nums: List[int]) -> int:
        res,count = 0,0
        for n in nums:
            if count == 0:
                res = n
                count += 1
            else:
                if n == res:
                    count += 1
                else:
                    count -= 1
        return res
        

这个实现是摩尔投票法的具体实现,跟之前的实现原理是一样的。代码中 res 存储的是当前的候选众数,count 记录了当前候选众数的出现次数。遍历整个数组,如果 count 为0,则当前数字成为候选众数;否则如果当前数字与候选众数相同,将 count 加1;否则将 count 减1。最后剩下的候选众数就是众数。

189. 轮转数组 - 力扣(Leetcode)

给定一个整数数组 nums,将数组中的元素向右轮转 k **个位置,其中 k **是非负数。

 

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

示例 2:

输入: nums = [-1,-100,3,99], k = 2
输出: [3,99,-1,-100]
解释: 
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]

 

提示:

  • 1 <= nums.length <= 105
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 105

 

进阶:

  • 尽可能想出更多的解决方案,至少有 三种 不同的方法可以解决这个问题。
  • 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?
class Solution:
    def rotate(self, nums, k):
        k %= len(nums)
        reverse(nums, 0, len(nums) - 1)
        reverse(nums, 0, k - 1)
        reverse(nums, k, len(nums) - 1)


def reverse(nums,l,r):
    while l < r:
        nums[l], nums[r] = nums[r], nums[l]
        r -= 1
        l += 1

这段代码实现了将列表 nums 向右旋转 k 步的功能,其中 k 是非负整数。该算法采用了三次翻转数组的方法来实现旋转:

  1. 将整个列表翻转。

  2. 将前 k 个元素翻转。

  3. 将剩下的元素翻转。

最终得到的列表就是向右旋转 k 步后的结果。

该算法的时间复杂度为 O(n)O(n),其中 nn 是列表的长度,空间复杂度为 O(1)O(1),因为只用了常数级别的额外空间来保存中间结果。算法的正确性也可以通过手动模拟旋转过程来验证。

238. 除自身以外数组的乘积 - 力扣(Leetcode)

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。

题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内。

不要使用除法, 且在 O(n) 时间复杂度内完成此题。

 

示例 1:

输入: nums = [1,2,3,4]
输出: [24,12,8,6]

示例 2:

输入: nums = [-1,1,0,-3,3]
输出: [0,0,9,0,0]

 

提示:

  • 2 <= nums.length <= 105
  • -30 <= nums[i] <= 30
  • 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在  32 位 整数范围内

 

进阶: 你可以在 O(1) 的额外空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)

当我是个笨比的时候,我还在考虑math.prod() + 数组切片。但是动脑想想都会超时。

所以我决定看看别人咋做的。直到我发现了前后缀结果乘法。

class Solution:
    def productExceptSelf(self, nums):
        ans = []
        preMul = [1]
        tailMul = [1]
        for i in range(len(nums)-1):
            preMul.append(nums[i]*preMul[-1])
            tailMul.append(nums[len(nums)-1-i]*tailMul[-1])
        tailMul.reverse()
        for i in range(len(nums)):
            ans.append(preMul[i]*tailMul[i])
        return ans

该代码实现了一个函数 productExceptSelf,接受一个数组 nums 作为输入,返回一个新数组 ans,其中 ans[i] 等于原数组 nums 中除了 nums[i] 以外所有元素的乘积。具体实现如下:

  1. 首先定义一个空数组 ans
  2. 然后定义两个数组 preMultailMulpreMul 用来记录 nums[i] 左边所有元素的乘积,tailMul 用来记录 nums[i] 右边所有元素的乘积。preMul[0]tailMul[-1] 都初始化为 1。
  3. 通过循环遍历数组 nums,分别计算出 preMultailMul 的所有值。
  4. 由于 tailMul 的顺序是从右往左的,所以需要将其反转过来。
  5. 再次遍历数组 nums,计算 ans 中每个元素的值,并将其加入到 ans 数组中。
  6. 最后返回 ans 数组。

这种方法的时间复杂度是 O(n)O(n),空间复杂度也是 O(n)O(n)

我们看一下如何实现进阶目标:

class Solution:
    def productExceptSelf(self, nums: [int]) -> [int]:
        res = [1]
        pre = 1
        tail = 1
        for i in range(len(nums) - 1): # bottom triangle
            pre *= nums[i]
            res.append(pre)
        for i in range(len(nums) - 1, 0, -1): # top triangle
            tail *= nums[i]
            res[i - 1] *= tail
        return res

这个题解的思路和前一个类似,但是代码更简洁。其主要思路是先遍历一遍数组,计算每个元素左边所有元素的乘积。然后再遍历一遍数组,计算每个元素右边所有元素的乘积,并将左边的乘积乘上右边的乘积作为最终结果。代码中,res用于记录每个元素左边所有元素的乘积,pre用于计算左边乘积,tail用于计算右边乘积。遍历时,先计算左边乘积,然后在计算右边乘积,最后将左边乘积和右边乘积相乘即可。