Python-Leetcode-剑指offer(四月下旬做题整理)

说明:题目均来自:Leetcode;总结了很多大神们的解题思路,因为链接太多,我就没逐一放引用链接,如有侵权请告知,将删除。

(1)4.20- 03. 数组中重复的数字 

                            

考点:沟通能力,问面试官要时间/空间需求

代码:

(A) 时间复杂度O(N)空间复杂度 O(N) 

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:
        #方法一 ----->enumerate()
        dic = {}
        for key,values in enumerate(nums):
            dic[values] = dic.get(values,0)+1
            if dic[values] >=2:
                return values
        #方法二 ----->collections.Counter()
        dic = collections.Counter(nums)
        for x in dic:
            if dic[x] >1:
                return x

(B) 时间复杂度O(NlogN) 空间复杂度O(1)

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:        
        nums.sort() ##排序
        for i in range(0,len(nums)-1):
            if nums[i] == nums[i+1]:
                return nums[i]
        return -1

空间复杂度为什么是O(NlogN) ?sort()函数的时间复杂度是:O(NlogN),详见链接

(C)原地操作(原地哈希)->鸽舍原理 

鸽舍原理:有N+1个鸽子,有N个笼子,至少有两只鸽子会在一个笼子里面(证明:反证法)

给定的元素值均小于len(nums),将见到的元素 放到索引的位置,如果交换时,发现索引处已存在该元素,则重复 。

class Solution:
    def findRepeatNumber(self, nums: List[int]) -> int:          
        for index in range(0,len(nums)):
            while(nums[index] != index):
                if nums[nums[index]] == nums[index] :return nums[index] 
                #nums[index]已经有元素了
                temp = nums[index]
                nums[index] = nums[temp]
                nums[temp] = temp
        return -1

(2)4-21-顺时针打印矩阵

         考点:边界值的处理问题

(3)4-22-青蛙跳台阶问题

考点:(A)总结规律,找关系。 多少种可能性的题目一般都有递推性质。

          (B)大数的问题。详解->点链接

青蛙跳N台阶前,有两种可能N-1和N-2,再跳1下就能够跳完。

所以有 f(N)=f(N-1)+f(N-2)。这个递推公式。f(N-1)对应x种方案,f(N-2)对应y种方案。

找一下临界点:f(0) = 1 f(1) = 1 f(2) = 2 = f(1)+f(0),故n>=2 时,即可用递推来解决。

方法1:递归形式->超时 从大到小的做法。

class Solution:
    def numWays(self, n: int) -> int:
        
        def t(n):
            if n == 1:return 1
            if n == 0:return 1
            return (t(n-1)+t(n-2))%1000000007
        return t(n)

方法2:将每一次的f(n)存起来,从小到大的做法。 时间复杂度O(N) 空间复杂度O(N)

class Solution:
    def numWays(self, n: int) -> int:
        if n == 1:return 1
        if n == 0:return 1
        t = [0 for _ in range(0,n+1)]
        t[0],t[1] = 1,1
        for i in range(2,n+1):
            t[i] = (t[i-1]+t[i-2])%1000000007
        return t[-1]

 方法3:DP做法,因为每次f(n)只与前两项相关,所以可以只存储前两项就好。空间复杂度O(1)

class Solution:
    def numWays(self, n: int) -> int:
        if n ==1 : return 1
        if n ==0 : return 1
        a,b= 1,1
        sum_ = 0
        for i in range(0,n-1):
            sum_ = a+b
            a,b = b,sum_
        return sum_ % 1000000007

盲点:

【1】 Python 中整形数字的大小限制取决计算机的内存 (可理解为无限大),因此可不考虑大数越界问题。

【2】为什么要模1000000007?

        大数相乘,大数的排列组合等为什么要取模?

1,1000000007是一个质数(素数),对质数取余能最大程度避免结果冲突/重复

2,int32位的最大值为2147483647,所以对于int32位来说1000000007足够大。

3,int64位的最大值为2^63-1,用最大值模1000000007的结果求平方,不会在int64中溢出。
所以在大数相乘问题中,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出。

这道题为什么要取模,取模前后的值不就变了吗?

1,确实:取模前 f(43) = 701408733, f(44) = 1134903170, f(45) = 1836311903, 但是 f(46) > 2147483647结果就溢出了。

2,_____,取模后 f(43) = 701408733, f(44) = 134903163 , f(45) = 836311896, f(46) = 971215059没有溢出。

3,取模之后能够计算更多的情况,如 f(46)

4,这道题的测试答案与取模后的结果一致。

5,总结一下,这道题要模1000000007的根本原因是标准答案模了1000000007。不过大数情况下为了防止溢出,模1000000007是通用做法,原因见第一点。

(4)4-23-链表中倒数第k个节点 

考点:双指针的使用,以及链表的基本操作。放在了 python-Leetcode-链表相关中。

(5)4-24-面试题21. 调整数组顺序使奇数位于偶数前面

考点:双指针,借助快排的思想。 时间复杂度O(N),空间复杂度 O(1)

class Solution:
    def exchange(self, nums: List[int]) -> List[int]:
        ''' # 快排
        n = len(nums)    
        i = 0
        j = n-1
        while(i < j):
            while(i < j and nums[i] %2 == 1): i+=1
            while(i < j and nums[j] %2 ==0 ): j-=1
            temp = nums[i]
            nums[i] = nums[j]
            nums[j] = temp  ##changed
            i += 1 
            j -= 1
        return nums'''
        
        #双指针 
        i ,j = 0,len(nums)-1
        while i<=j:
            if nums[i] % 2 ==1:
                i += 1
            elif nums[j] %2 ==0:
                j -= 1
            else:
                temp = nums[i]
                nums[i] = nums[j]
                nums[j] = temp  ##changed
                i += 1
                j -= 1
        return nums

其他做法:

(1)把基数和偶数挑出来,在组合。空间O(N) 时间O(N)

a = []
b = []
for i in range(0,len(nums)):
    if nums[i] %2 == 1:a.append(nums[i])
    else: b.append(nums[i])
return a+b

扩展: 

如果我们将题目改一下,要求将是3的倍数的数字放到前面,不是3的倍数的数字放到后面;或者是要求将负数放在前面将正数放在后面。思考下这中类似的问题,应该怎么处理呢?

应该不能看出只需要改变下判断条件即可,我们是不是能把判断条件(nums[i] %2 ==1)更换就好,这样做的目的增加了我们函数的可扩展性,在这种模式下很方便地把已有的解决方案扩展到同类的问题上。

(6)4-25-面试题20. 表示数值的字符串

考点:这个题测试用例太多了,也有很多种反人类的情况。

(一) 利用try_except 来做 

class Solution:
    def isNumber(self, s: str) -> bool:
        try:
            a = float(s)
            return True
        except:
            return False

(二)正则表达式

(三)确定有限自动机DFA 编译原理里面的,DFA走的是合法的路径。 存在开始状态和结束状态,以及-1(非法状态)。根据题目要求,画出状态图和矩阵,然后在做就简单了。主要是画DFA。

(四)基础做法

这个题,包括三个部分判断,1,不含小数点和指数 2,含有小数点 3,含有指数形式并且含有小数点

(1)其中 包括空格这种类型,第一步先处理,空格。空格在一个数字串左右都可以,但不可以将两部分分开。提取前面不是空格的字符串。

(2)判断是否含有非法字符, 'A-Z','a-d','f-z'。

(3)判断是否是常数(没有小数点和指数):判断+是否单独出现,判断+是否出现在第二位以后。

(4)判断是否只含有小数点。 

       看小数点分隔后的每个部分是否满足条件: '+.8' , 是满足条件的,'.+'是不满足条件的。

       规律就是:+ 在前面第一位可以【小数点后非空】,但是后面不行。 + 不能在后面。

(5)判断是否含有e。

      看e前后是否满足条件。后面:不能有小数,+单独出现,+不能出现在第二位及以后。

      前面:如果有小数:跳转到小数。如果没有按照常数判断。

(6)将以上结果综合,就是最后结果了。【这是我的思路,对应用例写了半天,才写对】

(五)剑指offer里面的做法

   思路是:从前往后扫描,先去除前面空格。然后进行有符号扫描,来判断‘+’。跳过第一个‘+’,向后扫描无符号。若碰到了符号,则判断符号类型,'.' 或者 ‘e’ 或者其他。如果是'.'则跳过 '.' 进行 无符号扫描。如果是'e'则进行有符号扫描。除了后面的空格之外,能扫描到最后,那么就是对的。

这种想法,借助了 点 和 e 只能出现1次的状态。

class Solution:
    def isNumber(self, s: str) -> bool:        
        def scanSigned(s,start):
            if start<len(s) and (s[start]=="+" or s[start] == "-") :
                start+=1
            return scanUnsinged(s,start)
        def scanUnsinged(s,start):
            i = start
            while(start<len(s) and s[start] <='9' and s[start]>='0'):
                start += 1
            return start > i,start  #
        
        index = 0
        while(index < len(s) and s[index] ==' '):index+=1 #去除空格
        num,index = scanSigned(s,index) #扫描整数部分 
        if index < len(s) and s[index] == '.':
            index += 1
            Flag,index = scanUnsinged(s,index)
            num = Flag or num #扫描小数部分
        if index < len(s) and s[index] =='e':
            index += 1
            Flag,index = scanSigned(s,index)
            num = Flag and num
        while(index < len(s) and s[index]==' ') :index+=1
        return num and index == len(s)

(7)4-26- 面试题63. 股票的最大利润

考点:这个题做过了, 主要是[a,b] a<b,b-a的最大值。

可以用单调栈来做,可以用DP来做。

普通做法:O(N^2)把max值存起来。普通做法超时。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        ## O(N^2) 超时
        max_ = -float('inf') 
        n = len(prices)
        for i in range(0,n):
            for j in range(i+1,n):
                if prices[j] > prices[i]:
                    max_ = max(max_, prices[j]-prices[i])
        if max_ < 0:
            return 0
        else:
            return max_

单调栈:栈中元素是从小到大的,每次入栈先判断与栈顶的大小,若比栈顶大,则直接入栈(计算差最大)。若比栈顶小,则与栈中元素作比较,压入适合的位置。 时间 70.96% 空间:100%。 O(N) O(N)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:        
        if len(prices) <= 1 :return 0
        max_ = -float('inf')
        s = []
        s.append(prices[0])
        i = 1
        while(i<len(prices)):
            if prices[i] > s[-1]: #跟栈顶元素比大小
                s.append(prices[i])
                max_ = max(max_,prices[i] - s[0]) # 和栈底作加减法
            
            while(len(s)>0 and prices[i] < s[-1]): s.pop(-1)
            # 找合适位置入栈
            s.append(prices[i])
            i += 1
        if max_ < 0:
            return 0
        else:
            return max_ 

DP方法:在O(N^2) 的时候考虑用DP降低时间复杂度。 时间 O(N) 空间 O(1)。 时间优化 : 97.88%。

DP三步骤:状态定义,转移方程,继续优化。其中我认为状态定义是最难的。

在状态定义的时候,要找一个可以表示从小到大关系的状态。这个题,求解的是最大利润,每天股票价格是按照时间排序的,所以状态就是前X天的最大利润

转移方程,体现的是当前i状态是否改变。x天股票卖与不卖。x天时,状态是否改变。 

dp[i] = max( dp[i-1] 不改变 , prices[i] - min(prices[0:i+1])  改变 )

继续优化:按照转移方程,进行一些优化。一般可以分为时间和空间上的。

空间:只存储当前状态相关的信息。

class Solution:
    def maxProfit(self, prices: List[int]) -> int:          
        if len(prices) <= 1 :return 0
        cost = float('inf')
        profit = -float('inf')
        for i in range(0,len(prices)):
            profit = max(profit,prices[i]- min(cost,prices[i]))
            cost = min(cost,prices[i])
        
        return profit

(8)4-27- 面试题58 - I. 翻转单词顺序

比较简单,python一行就能解决,但是面试的时候最好不这样做

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

方法一:双指针:从后往前数。 time O(N)  space O(N)

class Solution:
    def reverseWords(self, s: str) -> str:        
        n = len(s)
        if n == 0 :return s
        i,j = n-1,n-1
        ans = ''
        while(j >= 0):
            if s[j] != ' ':
                i = j
                while(i>=0 and s[i] != ' '): i -= 1
                ans += s[i+1:j+1] + ' '
                j = i
            else:
                j -= 1
        return ans[:len(ans)-1]

方法二:利用栈先入后出。[不写了]

(9)4-28- 面试题53 - II. 0~n-1中缺失的数字

排序数组中的搜索问题,首先想到 二分法 解决,双指针也是高频选项[摘录]。二分法为对数级别复杂度。

分析一下这个题。

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

这三个点是有效信息。

二分法最原始的就是遍历每个元素,然后查看哪个元素和其位置不对应。【没有利用递增这个信息点】。

利用递增可以这样想,如果查到某个元素,nums[m] = m,则前面的都是满足条件的,因为递增。则不满足条件的应该是后半部分。这样避免了二分法中左右两个部分全部都要遍历。 代码: O(logN) O(1)

class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        i,j = 0,len(nums)-1
        while(i<=j):
            m = (i+j)//2
            if nums[m] == m : i = m+1
            else:j = m-1
        return i   

注意看 m如何推导出来的,看左边或者右面是如何赋值的,如何查找边界

做法二:求和

按照题目要求,因为数组中每个数都在 【0-n】范围内,每个数字都是唯一的。

所以可以计算,【0-n】时,总和是多少。然后看下现在数组总和,做差就是题目要求。

class Solution:
    def missingNumber(self, nums: List[int]) -> int:

        # 求和
        n = len(nums)
        return (n+1)*n//2 - sum(nums)  # [0,1,...,n] (n+1个数)

(10) 4-29-面试题50. 第一个只出现一次的字符

考点:哈希表(有序哈希表) 

          哈希表 时间复杂度O(2N),空间复杂度O(N)。 N 为字符串 s 的长度;需遍历 s 两轮;

          有序哈希表 建立表时,需要O(N),查询 O(N)。遍历 s 一轮,遍历 dic 一轮

          哈希表是 去重 的,即哈希表中键值对数量 ≤ 字符串 s 的长度。因此,相比于方法一(哈希表),方法二(有序哈希表)减少了第二轮遍历的循环次数。当字符串很长(重复字符很多)时,方法二则效率更高。 以上分析来自:链接

          Python 3.6 后,默认字典就是有序的,因此无需使用 OrderedDict() 。

class Solution:
    def firstUniqChar(self, s: str) -> str:
        ## 自己的
        if s == '':return ' '
        # dic
        l = list(s)
        dic = collections.Counter(l)
        ans = sorted(dic.items(),key = lambda x:x[1])
        if ans[0][1] == 1:
            return ans[0][0]
        else:
            return ' '  

        # 题解中的方法 作者:@ Krahets
        dic = {}
        for c in s:
            dic[c] = not c in dic #第一次True 第二次碰见 False
        for c in dic:
            if dic[c] : #第一次 True,只出现一次
                return c 
        return ' '

题解中,巧妙利用True False 统计出现第几次。只用了一个dic。

我的代码 用了O(3N)的空间。时间复杂度相同。

(11)4-30-面试题47. 礼物的最大价值

考点:DP。

分析:这个题目,第一反应就是DP。奈何我还是没正确写出来,转移方程。

DP题目,首先做的就是定状态,dp表示到X步骤时,获得的最大价值。

然后我就写出了下面的方程:ans = ans + max(grid[i+1][j] , grid[i][j+1]) 未确定往哪里走。

这样一写,其实和DP没什么关系了,就是顺着(0,0)往下走,一直到最后一个点(m-1,n-1)。其实算是一个贪心策略的想法,只考虑当前步骤往下面应该如何走。

但是,定状态的时候,定的是 x状态下,与x-1状态的关系,即x状态是一个确定的状态,而不是下一个状态。因为是一个矩阵问题,所以状态应该是当前走到 i,j 时,最大价值。

    f(i,j) = max(f(i-1,j), f(i,j-1)) +grid[i][j]  当前状态已经确定了,就是i,j。 看的是和上一个状态的关系情况。[写完方程找边界]

正是因为转移方程描述的是与前一个状态,DP才有了从上到下的一个过程(递归从下到上),也才有了仅利用O(1)的空间来解决问题。

非递归从上到下,time O(MN) space O(MN)

建立数组,来存储,每个状态中最大。 【将f多给一行一列 m+1,n+1】 这样就直接写 转移方程就可以了。

class Solution:
    def maxValue(self, grid: List[List[int]]) -> int:
        m,n = len(grid),len(grid[0])
        f = [[ 0 for i in range(n)] for j in range(m)] 
        # 建立数组
        f[0][0]  = grid[0][0]
        # i-1 >= 0 and j-1>= 0:
        # i-1 < 0 and j-1 >=0
        # i-1 >=0 and j-1 < 0
        for i in range(0,m):
            for j in range(0,n):
                if i-1>=0 and j-1>=0:
                    f[i][j] = max(f[i-1][j],f[i][j-1]) + grid[i][j]
                elif i-1 <0 and j-1>=0:
                    f[i][j] = f[i][j-1] + grid[i][j]
                elif i-1 >=0 and j-1<0:
                    f[i][j] = f[i-1][j] + grid[i][j]
            
        return f[m-1][n-1]

从上到下,time O(MN) space O(1) :利用grid原地做。

class Solution:
    def maxValue(self, grid: List[List[int]]) -> int:
        m,n = len(grid),len(grid[0])        
        for i in range(0,m):
            for j in range(0,n):
                if i-1>=0 and j-1>=0:
                    grid[i][j] = max(grid[i-1][j],grid[i][j-1])+grid[i][j]
                elif i-1 <0 and j-1>=0:
                    grid[i][j] = grid[i][j-1] + grid[i][j]
                elif i-1 >=0 and j-1<0:
                    grid[i][j] = grid[i-1][j] + grid[i][j]
        return grid[m-1][n-1]

有一个问题就是grid会被重复刷新吗?不会,因为for 循环保证了每个ij位置,只遍历一遍,只做一次更改。

当 grid矩阵很大时, i=0 或 j=0 的情况仅占极少数,相当循环每轮都冗余了一次判断。因此,可先初始化矩阵第一行和第一列,再开始遍历递推。思想来自:链接

        m, n = len(grid), len(grid[0])
        for j in range(1, n): # 初始化第一行
            grid[0][j] += grid[0][j - 1]
        for i in range(1, m): # 初始化第一列
            grid[i][0] += grid[i - 1][0]
        for i in range(1, m):
            for j in range(1, n):
                grid[i][j] += max(grid[i][j - 1], grid[i - 1][j])
        return grid[-1][-1]

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Foneone

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值