(面试经典刷题)挑战一周刷完150道-Python版本-第1天(11个题)

一、合并数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

在这里插入图片描述
先合并,再排序。
使用列表切片和排序操作。

class Solution:
    def merge(self, nums1, m, nums2, n):
        nums1[m:] = nums2
        nums1.sort()
        

二、移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
在这里插入图片描述

双指针方法,删除比较费时间。
和删除的相同就不管,继续往后走,
和删除的不同,将这个值复制到前边指针指的里边。
最后只管返回前边的数组。

class Solution:
    def removeElement(self, nums: List[int], val: int) -> int:
        a = 0
        b = 0
        while a < len(nums):
            if nums[a] != val:
                nums[b] = nums[a]
                b += 1
            a += 1
        return b        

三、删除有序数组中的重复项

给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。

在这里插入图片描述
双指针法。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if not nums:
            return 0

        n =len(nums)
        a = b = 1
        while a < n:
            if nums[a] != nums[a-1]:
                nums[b] = nums[a]
                b += 1
            a +=1

        return b

四、删除有序数组中的重复项 II

给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
在这里插入图片描述

笨拙的双指针法,加入一个标记次数的量,但是会加入一个新的循环。

比较好的一个双指针方法,给好判断条件,并且前移双指针。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        i = 0
        for j in range(len(nums)):
            if i < 2 or nums[i-2] != nums[j]:
                nums[i] = nums[j]
                i += 1
        return i

五、多数元素

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

你可以假设数组是非空的,并且给定的数组总是存在多数元素。
在这里插入图片描述
摩尔投票法

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

六、买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
在这里插入图片描述
第一个是暴力解决方法。是超出时间的。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        ans = 0
        for i in range(len(prices)):
            for j in range(i + 1,len(prices)):
                ans = max(ans,prices[j] - prices[i])
        return ans

第二个就是一次遍历方法。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        inf = int(1e9)
        minprice = inf
        maxprofit = 0
        for price in prices:
            maxprofit = max(price - minprice, maxprofit)
            minprice = min(price, minprice)
        return maxprofit


七、买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

在这里插入图片描述
1.暴力搜索会超过时间限制。
2.动态规划,设置一个二维矩阵表示状态。
动态规划算法来解决股票买卖问题,具体是计算在给定股票价格列表中的最大利润。下面我将详细解释其中的动态规划部分。

在这个问题中,定义了两个状态:

0:表示持有现金的状态,也就是没有股票的状态。
1:表示持有股票的状态。
动态规划的思想是通过不断更新这两个状态来计算最终的最大利润。

代码中使用了一个二维数组 dp 来存储状态。dp[i][0] 表示在第 i 天持有现金时的最大利润,dp[i][1] 表示在第 i 天持有股票时的最大利润。

接下来,代码通过迭代股票价格列表中的每一天来计算这两个状态的值。具体的过程如下:

初始化第一天的状态:

dp[0][0] 初始化为 0,因为在第一天没有股票,所以持有现金的利润为 0。
dp[0][1] 初始化为 -prices[0],因为在第一天买入股票,所以持有股票的利润为负的股票价格。
使用循环从第二天开始遍历股票价格列表。在每一天,根据动态规划的状态转移方程来更新状态:

dp[i][0] 表示在第 i 天持有现金时的最大利润。它可以通过两种方式获得:
保持前一天的现金状态,不进行股票交易,即 dp[i - 1][0]。
卖出股票,获得当天的股票价格,即 dp[i - 1][1] + prices[i]。
所以 dp[i][0] 更新为 max(dp[i - 1][0], dp[i - 1][1] + prices[i]),选择两者中较大的值。
dp[i][1] 表示在第 i 天持有股票时的最大利润。它可以通过两种方式获得:
保持前一天的股票状态,不进行股票交易,即 dp[i - 1][1]。
买入股票,扣除当天的股票价格,即 dp[i - 1][0] - prices[i]。
所以 dp[i][1] 更新为 max(dp[i - 1][1], dp[i - 1][0] - prices[i]),选择两者中较大的值。
最终,返回 dp[len - 1][0],表示在最后一天持有现金时的最大利润,这个值即为最大利润的答案。

class Solution:
    def maxProfit(self, prices):
        length = len(prices)
        if length < 2:
            return 0

        # 0:持有现金
        # 1:持有股票
        # 状态转移:0 → 1 → 0 → 1 → 0 → 1 → 0
        dp = [[0, 0] for _ in range(length)]

        dp[0][0] = 0
        dp[0][1] = -prices[0]

        for i in range(1, length):
            # 这两行调换顺序也是可以的
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])

        return dp[length - 1][0]

3.贪心算法,时间更快
贪心算法(Greedy Algorithm)的核心思想是在每一步选择中都采取当前状态下最优的选择,希望通过一系列局部最优的选择达到全局最优的解。贪心算法通常用于解决那些具有最优子结构性质的问题,即问题的最优解可以通过一系列局部最优解的组合得到。

贪心算法的一般步骤如下:

1). 初始化:从问题的所有可选解中,选择一个作为初始解。

2.) 贪心选择:在当前解的基础上,采取局部最优的选择,即在当前状态下做出最好的决策。这一步是贪心算法的核心,也是算法的关键之处。

3). 判断是否满足终止条件:通常情况下,算法会判断是否已经达到问题的终止条件。如果达到了,就返回当前解作为最终解;否则,继续执行步骤2。

4). 更新解:如果当前选择是局部最优的,就将它加入到解集合中,并更新当前状态。

5.) 重复步骤2到步骤4,直到满足终止条件为止。

需要注意的是,贪心算法并不一定能够得到全局最优解,因为它仅仅关注当前步骤的最优选择,而不考虑该选择对后续步骤的影响。因此,在应用贪心算法时,需要确保问题具有贪心选择性质,也就是局部最优解的组合能够得到全局最优解。有些问题适合使用贪心算法求解,而有些则不适合。

贪心算法的优点是简单、高效,通常具有较低的时间复杂度。然而,它不适用于所有类型的问题,特别是那些涉及到问题的先验性质或约束条件的情况。在应用贪心算法时,需要仔细分析问题的性质,确保贪心策略是有效的。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        profit = 0
        for i in range(1, len(prices)):
            tmp = prices[i] - prices[i - 1]
            if tmp > 0: profit += tmp
        return profit


相比之下,动态规划的思想。
动态规划(Dynamic Programming)的核心思想是将原问题分解成一系列子问题,并为每个子问题只求解一次,将其解存储起来,以避免重复计算。动态规划通常用于解决具有最优子结构性质的问题,这意味着问题的最优解可以通过子问题的最优解组合而成。

动态规划的核心步骤包括:

1). 定义状态:首先需要明确定义问题的状态,即问题的子问题是什么,以及如何表示这些子问题的状态。这些状态通常与问题的输入参数有关。

2). 确定状态转移方程:接下来,需要找到问题的状态之间的关系,也就是如何从一个状态转移到下一个状态。这通常通过递归式或迭代式的状态转移方程来表示。

3). 初始化:确定初始状态的值,通常是问题中最简单的情况的解。这些初始状态将用于计算后续状态。

4). 计算最优解:使用状态转移方程和初始状态来计算所有子问题的解,通常使用自底向上(Bottom-Up)或自顶向下(Top-Down)的方法进行计算。

5). 根据问题要求返回结果:根据问题的要求,从已计算的子问题中获取最终问题的解,通常就是问题的最优解。

动态规划的关键特点是它具有重叠子问题和最优子结构性质。重叠子问题意味着问题可以被分解成多个具有相同子问题的子问题,而最优子结构性质意味着问题的最优解可以通过子问题的最优解组合而成。这两个特点使得动态规划能够有效地避免重复计算,提高问题求解的效率。

动态规划广泛应用于各种领域,包括算法设计、优化问题、资源分配等等。它是一种非常强大的问题求解方法,可以用来解决许多复杂的计算问题。

八、最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

在这里插入图片描述
直接全部遍历不经济,所以从最小程度开始。
利用set。set 是一种可变集合数据类型,用于存储无序、唯一的元素集合。每个元素在集合中只能出现一次,而且元素的顺序是不确定的。


class Solution:
    def longestCommonPrefix(self, strs: List[str]) -> str:
        # 先找出字符串数组中最短的那个字符串的长度大小
        min_len = min(len(i) for i in strs)
        res = ""  # 公共前缀结果字符串

        for i in range(min_len):
            # 把每个字符串的第i+1个字符组成一个列表用set去重,如果有不同的即>1,说明这个i+1个字符不是公共前缀
            if len(set([s[i] for s in strs])) > 1:  # s[i]是每个字符串的第i+1个字符
                break
            res += strs[0][i]  # 利用 += 添加进公共前缀结果字符串

        return res


九、反转字符串中的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

在这里插入图片描述
双指针方法

class Solution:
    def reverseWords(self, s: str) -> str:
        s = s.strip()                            # 删除首尾空格
        i = j = len(s) - 1
        res = []
        while i >= 0:
            while i >= 0 and s[i] != ' ': i -= 1 # 搜索首个空格
            res.append(s[i + 1: j + 1])          # 添加单词
            while i >= 0 and s[i] == ' ': i -= 1 # 跳过单词间空格
            j = i                                # j 指向下个单词的尾字符
        return ' '.join(res)                     # 拼接并返回


字符串的分割和列表倒序。
s.strip(): 这一步用于删除字符串首尾的空格,确保字符串没有多余的空格。

s.split(): 使用空格字符(包括空格、制表符、换行符等)对字符串进行分割,得到一个包含单词的列表 strs。这一步将字符串分成了单词的部分,去除了单词之间的多余空格。

strs.reverse(): 翻转单词列表 strs,将单词的顺序颠倒过来。这一步实现了反转单词顺序的目标。

’ '.join(strs): 最后,使用空格字符将翻转后的单词列表 strs 中的单词拼接成一个字符串,并返回。这个字符串就是原始字符串中单词顺序反转后的结果。

这个算法的时间复杂度主要取决于字符串的分割和单词列表的翻转,分割的时间复杂度为 O(n),翻转的时间复杂度也为 O(n),因此总的时间复杂度为 O(n),其中 n 是字符串的长度。

class Solution:
    def reverseWords(self, s: str) -> str:
        s = s.strip()         # 删除首尾空格
        strs = s.split()      # 分割字符串
        strs.reverse()        # 翻转单词列表
        return ' '.join(strs) # 拼接为字符串并返回


一行实现,加快速度。
字符串操作:

s.strip(): 这个方法用于删除字符串首尾的空格。它将原始字符串去掉首尾的空格后返回一个新的字符串。
s.split(): 这个方法用于将字符串按照空格字符(包括空格、制表符、换行符等)分割成一个单词列表。默认情况下,它会自动去除单词之间的多余空格。
[::-1]: 这是一个切片操作,用于将列表或字符串反转。在这个代码中,它将单词列表 split() 的结果进行反转,即将单词的顺序颠倒过来。
列表拼接:

’ '.join(…): 这是一个字符串方法,用于将一个可迭代对象(如列表或元组)中的元素用指定的字符(这里是空格 ’ ')连接起来,形成一个新的字符串。在这里,它用于将反转后的单词列表中的单词用空格连接起来,得到一个反转后的字符串。

class Solution:
    def reverseWords(self, s: str) -> str:
        return ' '.join(s.strip().split()[::-1])


十、最后一个单词的长度

给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。

单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。
在这里插入图片描述
一行代码解决。


class Solution:
    def lengthOfLastWord(self, s: str) -> int:
        return len(s.strip().split(' ')[-1])

十一、找出字符串中第一个匹配项的下标

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
在这里插入图片描述
字符串处理的简介快速。


class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        try:
            return haystack.index(needle)
        except:
            return -1
        


相似的查找算法有 KMP,BM,Horspool

这些算法都是字符串查找算法,用于在一个文本串中查找一个模式串是否出现。每个算法都有其独特的特点和适用场景。下面我将简要介绍这三种常见的字符串查找算法:

1). KMP算法(Knuth-Morris-Pratt算法)

  • KMP算法是一种高效的字符串查找算法,特别适用于在长文本串中查找多次出现的模式串。它的主要思想是在匹配失败时,不需要回溯文本串的指针,而是利用已匹配的信息跳过一些不可能匹配的位置,从而提高效率。
  • KMP算法通过构建部分匹配表(也称为失效函数或前缀函数),预先计算模式串中的匹配信息,以便在匹配过程中能够高效地跳过不匹配的部分。
  • KMP算法的时间复杂度为O(n + m),其中n是文本串的长度,m是模式串的长度。

2). Boyer-Moore算法

  • Boyer-Moore算法是一种快速的字符串查找算法,它的特点是在匹配失败时,根据模式串中的字符出现位置进行跳跃式的移动,从而尽可能减少比较的次数。
  • Boyer-Moore算法包括两个主要的启发式规则:坏字符规则(Bad Character Rule)和好后缀规则(Good Suffix Rule),这些规则帮助算法决定如何进行跳跃。
  • Boyer-Moore算法的时间复杂度通常在O(n + m)到O(nm)之间,其中n是文本串的长度,m是模式串的长度。在实际应用中,它通常比KMP算法更快。

3). Horspool算法

  • Horspool算法是一种字符串查找算法,它是Boyer-Moore算法的一种变种。与Boyer-Moore相比,Horspool算法更简单,但在某些情况下性能略逊于Boyer-Moore。
  • Horspool算法主要利用了坏字符规则,根据模式串中最右边的字符在文本串中出现的位置进行跳跃。
  • Horspool算法的时间复杂度通常在O(n + m)到O(nm)之间,取决于文本和模式。

选择哪个算法取决于具体的应用场景和性能需求。KMP算法适用于多次匹配的情况,Boyer-Moore算法通常在单次匹配中表现较好,而Horspool算法则是Boyer-Moore的一种更简单的实现。在实际应用中,通常需要根据问题的特点和性能需求选择合适的算法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值