牛客 剑指offer 算法 汇总

0. 前言

题目来源:牛客网剑指offer专题
编程语言为 python

1. 搜索算法

1.1 JZ53 数字在升序数组中出现的次数(二分法)

描述
给定一个长度为 n 的非降序数组和一个非负数整数 k ,要求统计 k 在数组中出现的次数

数据范围: 0 ≤ n ≤ 1000 , 0 ≤ k ≤ 100 0 \le n \le 1000 , 0 \le k \le 100 0n1000,0k100,数组中每个元素的值满足 0 ≤ v a l ≤ 100 0 \le val \le 100 0val100
要求:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( l o g n ) O(logn) O(logn)

示例1

输入:[1,2,3,3,3,3,4,5],3
返回值:4

示例2

输入:[1,3,4,5],6
返回值:0

二分法

思路:

因为data是一个非降序数组,它是有序的,这种时候我们可能会想到用二分查找。但是一个数组可能有多个k,而且我们要查找的并非常规二分法中k出现的位置,而是k出现的左界和k出现的右界。要是能刚好找到恰好小于k的数字位置和恰好大于k的数字的位置就好了。

再有因为数组中全是整数,因此我们可以考虑,用二分查找找到 k + 0.5 k+0.5 k+0.5应该出现的位置和 k − 0.5 k−0.5 k0.5应该出现的位置,二者相减就是k出现的次数。

具体做法:

  • step 1:写一个二分查找的函数在数组中找到某个元素出现的位置。每次检查区间中点值,根据与中点的大小比较,确定下一次的区间。
  • step 2:分别使用二分查找,找到 k + 0.5 k+0.5 k+0.5 k − 0.5 k-0.5 k0.5 应该出现的位置,中间的部分就全是 k k k,相减计算次数就可以了。
class Solution:
    def GetNumberOfK(self , data: List[int], k: int) -> int:
        def binary_search(k):
            low = 0
            high = len(data) - 1
            while low <= high:
                mid = (low + high) // 2
                if data[mid] < k:
                    low = mid + 1
                elif data[mid] > k:
                    high = mid - 1
            return low  # 返回第1个大于k的位置
        return binary_search(k + 0.5) - binary_search(k - 0.5)

1.2 JZ4 二维数组中的查找(二分法)

描述
在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]
给定 target = 7,返回 true。

给定 target = 3,返回 false。

数据范围:矩阵的长宽满足 0 ≤ n , m ≤ 500 0 \le n,m \le 500 0n,m500, 矩阵中的值满足 0 ≤ v a l ≤ 1 0 9 0 \le val \le 10^9 0val109

进阶:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n + m ) O(n+m) O(n+m)

示例1

输入:7,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
返回值:true
说明:存在7,返回true    

示例2

输入:1,[[2]]
返回值:false

示例3

输入:3,[[1,2,8,9],[2,4,9,12],[4,7,10,13],[6,8,11,15]]
返回值:false
说明:不存在3,返回false    

暴力搜索

略。

二分法

class Solution:
    def Find(self , target: int, array: List[List[int]]) -> bool:
        def binary_search(arr, k):
            low = 0
            high = len(arr) - 1
            while low <= high:
                mid = (low + high) // 2
                if arr[mid] < k:
                    low = mid + 1
                elif arr[mid] > k:
                    high = mid - 1
                else:
                    return True
            return False
        for subarray in array:
            if binary_search(subarray, target):
                return True
        return False

1.3 JZ11 旋转数组的最小数字(二分法)

描述
有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。

数据范围: 1 ≤ n ≤ 10000 1 \le n \le 10000 1n10000,数组中任意元素的值: 0 ≤ v a l ≤ 10000 0 \le val \le 10000 0val10000
要求:空间复杂度: O ( 1 ) O(1) O(1),时间复杂度: O ( l o g n ) O(logn) O(logn)

示例1

输入:[3,4,5,1,2]
返回值:1

示例2

输入:[3,100,200,3]
返回值:3

暴力搜索

略。

二分法

这道题依旧可以用二分法做,不过更新 low 和 high 的条件变了。

  • rotateArray[mid] > rotateArray[high],此时最小值一定在右半区(不包含mid),如[3,4,5,1,2]
  • rotateArray[mid] < rotateArray[high],此时最小值一定在左半区(包括mid),如[4,5,1,2,3]
  • rotateArray[mid] = rotateArray[high],此时最小值无法判断在哪,如[3,1,2,2,2][2,3,1,1,1],此时可以通过high = high - 1缩小判断范围。
class Solution:
    def minNumberInRotateArray(self , rotateArray: List[int]) -> int:
        low = 0
        high = len(rotateArray) - 1
        while low <= high:
            mid = (low + high) // 2
            if rotateArray[mid] > rotateArray[high]: 
                # 此时最小值一定在右半区
                low = mid + 1
            elif rotateArray[mid] < rotateArray[high]:
                # 此时最小值一定在左半区(包括mid)
                high = mid
            else:  # rotateArray[mid] == rotateArray[high]
                # 此时无法判断最小值在哪,只能缩小下范围
                high = high - 1
        return rotateArray[low]

1.4 JZ38 字符串的排列(递归+回溯)

描述
输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。
例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。
在这里插入图片描述

数据范围: n < 10 n < 10 n<10
要求:空间复杂度 O ( n ! ) O(n!) O(n!),时间复杂度 O ( n ! ) O(n!) O(n!)
输入描述:
输入一个字符串,长度不超过10,字符只包括大小写字母。

示例1

输入:"ab"
返回值:["ab","ba"]
说明:返回["ba","ab"]也是正确的       

示例2

输入:"aab"
返回值:["aab","aba","baa"]

示例3

输入:"abc"
返回值:["abc","acb","bac","bca","cab","cba"]

示例4

输入:""
返回值:[]

递归+回溯

都是求元素的全排列,字符串与数组没有区别,一个是数字全排列,一个是字符全排列,因此大致思路与有重复项数字的全排列类似,只是这道题输出顺序没有要求。但是为了便于去掉重复情况,我们还是应该参照数组全排列,优先按照字典序排序,因为排序后重复的字符就会相邻,后续递归找起来也很方便。

使用临时变量去组装一个排列的情况:每当我们选取一个字符以后,就确定了其位置,相当于对字符串中剩下的元素进行全排列添加在该元素后面,给剩余部分进行全排列就是一个子问题,因此可以使用递归。

终止条件: 临时字符串中选取了n个元素,已经形成了一种排列情况了,可以将其加入输出数组中。
返回值: 每一层给上一层返回的就是本层级在临时字符串中添加的元素,递归到末尾的时候就能添加全部元素。
本级任务: 每一级都需要选择一个元素加入到临时字符串末尾(遍历原字符串选择)。

递归过程也需要回溯,比如说对于字符串“abbc”,如果事先在临时字符串中加入了a,后续子问题只能是"bbc"的全排列接在a后面,对于b开头的分支达不到,因此也需要回溯:将临时字符串刚刚加入的字符去掉,同时vis修改为没有加入,这样才能正常进入别的分支。

具体做法:

  • step 1:先对字符串按照字典序排序,获取第一个排列情况。
  • step 2:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
  • step 3:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用,也不需要将其纳入。
  • step 4:进入下一层递归前将vis数组当前位置标记为使用过。
  • step 5:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素,
  • step 6:临时字符串长度到达原串长度就是一种排列情况。

在这里插入图片描述

class Solution:
    def Permutation(self , str: str) -> List[str]:
        res = []
        def recursion(string, tmp_str, vis):
            if len(tmp_str) == len(string):
                res.append(tmp_str)
                return
            for i in range(len(string)):
                if vis[i] == 1:
                    continue
                if i > 0 and string[i-1] == string[i] and vis[i-1] == 1:
                    # 当前str_list[i]与本层str_list[i-1]相同且str_list[i-1]已经用过
                    continue
                vis[i] = 1
                tmp_str += string[i]
                # 递归
                recursion(string, tmp_str, vis)
                # 回溯
                vis[i] = 0
                tmp_str = tmp_str[:-1]
        
        str_sort = sorted(list(str))  # 排序,得到的是list
        vis = [0] * len(str_sort)  # 标记每个位置的字符是否被使用过
        tmp_str = ""
        recursion(str_sort, tmp_str, vis)
        return res

1.5 JZ44 数字序列中某一位的数字(位数减法)

描述
数字以 0123456789101112131415… 的格式作为一个字符序列,在这个序列中第 2 位(从下标 0 开始计算)是 2 ,第 10 位是 1 ,第 13 位是 1 ,以此类题,请你输出第 n 位对应的数字。

数据范围: 0 ≤ n ≤ 1 0 9 0 \le n \le 10^9 0n109

示例1

输入:0
返回值:0

示例2

输入:2
返回值:2

示例3

输入:10
返回值:1

示例4

输入:13
返回值:1

位数减法

思路:

我们尝试来找一下规律:
小于10的数字一位数,1~9,共9个数字,9位;
小于100的数字两位数,10~99,共90个数字,180位;
小于1000的数字三位数,100~999,共900个数字,2700位;
……

我们可以用这样的方式,不断减去减去前面位数较少的数字的那些位,锁定第n位所在的区间,即第n位是几位数。这个区间的起点值加上剩余部分除以这个区间的位数就可以定位n在哪个数字上,再通过n对位数取模可以定位是哪一位。(下标从0开始,需要对n减1)

具体做法:

  • step 1:通过对每个区间起点数字的计算,按照上述规律求得该区间的位数,n不断减去它前面区间的位数,定位到属于它的区间。
  • step 2:通过除以位数定位n在哪个数字上,用字符串形式表示。
  • step 3:通过在字符串上位置对几位数取模定位目标数字。
class Solution:
    def findNthDigit(self , n: int) -> int:
        digit = 1  # 位数,如1、2、3
        start = 1  # 当前位数起始数字,如1、10、100
        num_digit = 9  # 该位数对应数字所占的空间,如9*1、90*2、900*3
        while n > num_digit:
            n -= num_digit
            digit += 1
            start *= 10
            num_digit = 9 * start * digit
        num = start + (n - 1) // digit  # 定位是哪个数字
        index = (n - 1) % digit
        return int(str(num)[index])

2. 动态规划

2.1 JZ42 连续子数组的最大和()

描述
输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。

数据范围:
1 < = n < = 2 × 1 0 5 1 <= n <= 2\times10^5 1<=n<=2×105
− 100 < = a [ i ] < = 100 -100 <= a[i] <= 100 100<=a[i]<=100

要求:时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)
进阶:时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)
示例1

输入:[1,-2,3,10,-4,7,2,-5]
返回值:18
说明:经分析可知,输入数组的子数组[3,10,-4,7,2]可以求得最大和为18       

示例2

输入:[2]
返回值:2

示例3

输入:[-10]
返回值:-10

动态规划(时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)

设动态规划列表 max_sum,max_sum[i] 代表以元素 array[i] 为结尾的连续子数组最大和。
状态转移方程: max_sum[i] = max(max_sum[i-1] + array[i], array[i]);

具体思路:

  1. 遍历数组,比较 max_sum[i-1] + array[i] 和 array[i] 的大小;
  2. 为了保证子数组的和最大,每次比较都取两者的最大值;
  3. 用 res 变量记录计算过程中产生的最大的连续和 max_sum[i];
class Solution:
    def FindGreatestSumOfSubArray(self , array: List[int]) -> int:
        max_sum = [array[0]]  # 记录到array[i]为结尾的最大和
        res = array[0]
        for i in range(1, len(array)):
            max_sum.append(max(max_sum[i-1] + array[i], array[i]))
            res = max(max_sum[i], res)
        return res

动态规划(时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)

因为遍历中每次都只用到了 max_sum[i-1],前面的都没用了,因此可以直接用 变量 代替 列表,可以达到同样的效果。

class Solution:
    def FindGreatestSumOfSubArray(self , array: List[int]) -> int:
        max_sum = array[0]  # 记录到array[i]为结尾的最大和
        res = array[0]
        for i in range(1, len(array)):
            max_sum = max(max_sum + array[i], array[i])
            res = max(max_sum, res)
        return res
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

friedrichor

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

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

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

打赏作者

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

抵扣说明:

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

余额充值