├── 公子龙-刷题指南.pdf ├── Datawhale组队刷题系列 └── img │ └── 分治思想.png ├── 剑指offer系列 ├── 面试题20.表示数值的字符串.md ├── 面试题58-II.左旋转字符串.md ├── 面试题17.打印从1到最大的n位数.md ├── 面试题58-I.翻转单词顺序.md ├── 面试题64.求1+2+...+n.md ├── 面试题22.链表中倒数第k个节点.md ├── 面试题10-II.青蛙跳台阶问题.md ├── 面试题05.替换空格.md ├── 面试题53-II.0~n-1中缺失的数字.md ├── 面试题57.和为s的两个数字.md ├── 面试题39.数组中出现次数超过一半的数字.md ├── 面试题06.从尾到头打印链表.md ├── 面试题57-II.和为s的连续正数序列.md ├── 面试题31.栈的压入、弹出序列.md ├── 面试题21.调整数组顺序使奇数位于偶数前面.md ├── 面试题32-I.从上到下打印二叉树.md ├── 面试题50.第一个只出现一次的字符.md ├── 面试题46.把数字翻译成字符串.md ├── 面试题66.构建乘积数组.md ├── 面试题49.丑数.md ├── 面试题63.股票的最大利润.md ├── 面试题18.删除链表的节点.md ├── 面试题09.用两个栈实现队列.md ├── 面试题42.连续子数组的最大和.md ├── 面试题54.二叉搜索树的第k大节点.md ├── 面试题47.礼物的最大价值.md ├── 面试题59-I.滑动窗口的最大值.md ├── 面试题59-II.队列的最大值.md ├── 面试题65.不用加减乘除做加法.md ├── 面试题61.扑克牌中的顺子.md ├── 面试题10-I.斐波那契数列.md ├── 面试题15.二进制中1的个数.md ├── 面试题62.圆圈中最后剩下的数字.md ├── 面试题38.字符串的排列.md ├── 面试题30.包含min函数的栈.md ├── 面试题34.二叉树中和为某一值的路径.md ├── 面试题44.数字序列中某一位的数字.md ├── 面试题28.对称二叉树.md ├── 面试题03.数组中重复的数字.md ├── 面试题32-II.从上到下打印二叉树II.md ├── 面试题26.树的子结构.md ├── 面试题11.旋转数组的最小数字.md ├── 面试题53-I.在排序数组中查找数字I.md ├── 面试题60.n个骰子的点数.md ├── 面试题68-II.二叉树的最近公共祖先.md ├── 面试题36.二叉搜索树与双向链表.md ├── 面试题24.反转链表.md ├── 面试题29.顺时针打印矩阵.md ├── 面试题55-I.二叉树的深度.md ├── 面试题25.合并两个排序的链表.md ├── 面试题27.二叉树的镜像.md ├── 面试题32-III.从上到下打印二叉树III.md ├── 面试题52.两个链表的第一个公共节点.md ├── 面试题33.二叉搜索树的后序遍历序列.md ├── 面试题67.把字符串转换成整数.md ├── 面试题56-I.数组中数字出现的次数.md ├── 面试题45.把数组排成最小的数.md ├── 面试题12.矩阵中的路径.md ├── 面试题16.数值的整数次方.md └── 面试题43.1~n整数中1出现的次数.md └── 每日一题系列 ├── 406.根据身高重建队列.md ├── 1221.分割平衡字符串.md ├── 969.煎饼排序.md ├── 面试题17.08.马戏团人塔.md ├── 542.01矩阵.md ├── 面试题47.礼物的最大价值.md ├── 1105.填充书架.md ├── 1276.不浪费原料的汉堡制作方案.md ├── 1007.行相等的最少多米诺旋转.md ├── 1405.最长快乐字符串.md ├── 452.用最少数量的箭引爆气球.md ├── 1403.非递增顺序的最小子序列.md ├── 978.最长湍流子数组.md ├── 559.N叉树的最大深度.md ├── 743.网络延迟时间.md ├── 983.最低票价.md ├── 面试题04.09.二叉搜索树序列.md ├── 1240.铺瓷砖.md ├── 1247.交换字符是的字符串相同.md ├── 874.模拟行走机器人.md ├── 435.无重叠区间.md ├── 1223.掷骰子模拟.md ├── 808.分汤.md ├── 1162.地图分析.md ├── 1235.规划兼职工作.md ├── 1000.合并石头的最低成本.md ├── 1005.K次取反后最大化的数组和.md ├── 934.最短的桥.md ├── 1253.重构2行二进制数组.md ├── 1386.安排电影院座位.md ├── 1284.转化为全零矩阵的最少反转次数.md ├── 690.员工的重要性.md ├── 1326.灌溉花园的最少水龙头数目.md ├── 407.接雨水II.md ├── 1402.做菜顺序.md ├── 面试题16.19.水域大小.md ├── 773.滑动谜题.md ├── 1345.跳跃游戏IV.md ├── 130.被围绕的区域.md ├── 909.蛇梯棋.md └── 1311.获取你好友已观看的视频.md /公子龙-刷题指南.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrippleKing/LeetCode_Python3/HEAD/公子龙-刷题指南.pdf -------------------------------------------------------------------------------- /Datawhale组队刷题系列/img/分治思想.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrippleKing/LeetCode_Python3/HEAD/Datawhale组队刷题系列/img/分治思想.png -------------------------------------------------------------------------------- /剑指offer系列/面试题20.表示数值的字符串.md: -------------------------------------------------------------------------------- 1 | 表示数值的字符串 2 | 3 | # 题目描述 4 | 5 | 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"、"5e2"、"-123"、"3.1416"、"0123"都表示数值,但"12e"、"1a3.14"、"1.2.3"、"+-5"、"-1E-16"及"12e+5.4"都不是。 6 | 7 | # 解题思路 8 | 9 | > 这道题像是在考察编译原理中的有限自动机,也可以通过写正则表达式去匹配。 10 | 11 | 个人感觉这道题没有太多的算法思想在里边,单纯靠逻辑判断。在这里就不多讲解,直接贴一个Leetcode上的[解题分享](https://leetcode-cn.com/problems/biao-shi-shu-zhi-de-zi-fu-chuan-lcof/solution/onpythonjie-ti-wu-fa-luo-ji-pan-duan-regexdfadeng-/)。 -------------------------------------------------------------------------------- /剑指offer系列/面试题58-II.左旋转字符串.md: -------------------------------------------------------------------------------- 1 | 左旋转字符串 2 | 3 | # 题目描述 4 | 5 | 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字`2`,该函数将返回左旋转两位得到的结果"cdefgab"。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: s = "abcdefg", k = 2 11 | 输出: "cdefgab" 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: s = "lrloseumgh", k = 6 18 | 输出: "umghlrlose" 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `1 <= k < s.length <= 10000` 24 | 25 | # 解题思路 26 | 27 | 直接将前`k`个字符放置到尾部即可。 28 | 29 | ## 复杂度分析 30 | 31 | - 时间复杂度:$O(N)$,$N$为字符串`s`的长度,字符串切片函数为线性时间复杂度 32 | - 空间复杂度:$O(N)$,两个字符串切片的总长度为$N$ 33 | 34 | ## 代码 35 | 36 | ```python 37 | class Solution: 38 | def reverseLeftWords(self, s: str, n: int) -> str: 39 | 40 | return s[n:] + s[:n] 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /剑指offer系列/面试题17.打印从1到最大的n位数.md: -------------------------------------------------------------------------------- 1 | 打印从1到最大的n位数 2 | 3 | # 题目描述 4 | 5 | 输入数字`n`,按顺序打印出从1到最大的`n`位十进制数。比如输入3,则打印出1、2、3一直到最大的3位数999。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: n = 1 11 | 输出: [1,2,3,4,5,6,7,8,9] 12 | ``` 13 | 14 | ## 说明 15 | 16 | - 用返回一个整数列表来代替打印。 17 | - `n`为正整数。 18 | 19 | # 解题思路 20 | 21 | > 本题在Python语言面前会变得十分简单,因为Python语言本身通常不需要考虑大数越界的问题,这是语言的特性并不能认为是作弊。 22 | 23 | 我们还是可以稍微看一下这道题的用意——对大数问题的处理。 24 | 25 | 本题原意是在考察`n`为超大值的情况下,int类型不能满足输出,需要采用字符串的方式,输出可以看做是1-9数字字符的排列组合。 26 | 27 | 不过leetcode有要求返回整数列表(这其实又反过来说明可以用int类型表示,不会越界),这题目就显得些许尴尬。 28 | 29 | ## 复杂度分析 30 | 31 | > 设$n$为最大十进制数的位数 32 | 33 | - 时间复杂度:$O(10^n-1)$。 34 | - 空间复杂度:$O(10^n-1)$ 35 | 36 | ## 代码 37 | 38 | ```python 39 | class Solution: 40 | def printNumbers(self, n: int) -> List[int]: 41 | max_num = 10 ** n 42 | return [i for i in range(1, max_num)] 43 | ``` 44 | 45 | -------------------------------------------------------------------------------- /剑指offer系列/面试题58-I.翻转单词顺序.md: -------------------------------------------------------------------------------- 1 | 翻转单词顺序 2 | 3 | # 题目描述 4 | 5 | 输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student.",则输出"student. a am I"。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: "the sky is blue" 11 | 输出: "blue is sky the" 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: "  hello world!  " 18 | 输出: "world! hello" 19 | 解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 20 | ``` 21 | 22 | ## 示例3 23 | 24 | ``` 25 | 输入: "a good   example" 26 | 输出: "example good a" 27 | 解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 28 | ``` 29 | 30 | ## 说明 31 | 32 | - 无空格字符构成一个单词 33 | - 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括 34 | - 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个 35 | 36 | # 解题思路 37 | 38 | 借助自带函数可以快速解题,利用`split()`,按空格拆分字符串,得到以每个单词为元素的列表,利用`" ".join()`,将列表倒序后组成字符串并返回即可。 39 | 40 | ## 复杂度分析 41 | 42 | - 时间复杂度:$O(N)$,总体为线性时间复杂度,`split()`方法为$O(N)$,倒序方法为$O(N)$ 43 | - 空间复杂度:$O(N)$,字符串拆分成列表,需要额外空间 44 | 45 | ## 代码 46 | 47 | ```python 48 | class Solution: 49 | def reverseWords(self, s: str) -> str: 50 | s_lst = s.split() 51 | return " ".join(s_lst[::-1]) 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /剑指offer系列/面试题64.求1+2+...+n.md: -------------------------------------------------------------------------------- 1 | 求1+2+...+n 2 | 3 | # 题目描述 4 | 5 | 求`1 + 2 + ... + n`,要求不能使用乘除法、for、while、if、else、switch、case等关键字及判断语句(A?B:C) 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: n = 3 11 | 输出: 6 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: n = 9 18 | 输出: 45 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `1 <= n <= 10000` 24 | 25 | # 解题思路 26 | 27 | 本题设置了很多限制,基本只剩加减、逻辑运算了。 28 | 29 | **逻辑运算符的短路效应**: 30 | 31 | 常见的逻辑运算符有三种,即"与,&&","或,||","非,!";而其有重要的短路效应,如下所示: 32 | 33 | ```python 34 | if(A && B) # 若 A 为 false ,则 B 的判断不会执行(即短路),直接判定 A && B 为 false 35 | 36 | if(A || B) # 若 A 为 true ,则 B 的判断不会执行(即短路),直接判定 A || B 为 true 37 | ``` 38 | 39 | 本题需要实现"当`n=1`时终止递归"的需求,可通过短路效应实现。 40 | 41 | ```python 42 | n > 1 && sumNums(n - 1) # 当 n = 1 时 n > 1 不成立 ,此时 “短路” ,终止后续递归 43 | ``` 44 | 45 | ## 复杂度分析 46 | 47 | - 时间复杂度:$O(N)$ 48 | - 空间复杂度:$O(N)$ 49 | 50 | ## 代码 51 | 52 | ```python 53 | class Solution: 54 | def __init__(self): 55 | self.res = 0 56 | def sumNums(self, n: int) -> int: 57 | 58 | n > 1 and self.sumNums(n-1) 59 | self.res += n 60 | return self.res 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /每日一题系列/406.根据身高重建队列.md: -------------------------------------------------------------------------------- 1 | 根据身高重建队列 2 | 3 | # 题目描述 4 | 5 | 假设有打乱顺序的一群人站成一个队列。每个人由一个整数对`(h,k)`表示,其中`h`是这个人的身高,`k`是排在这个人前面且身高大于或等于`h`的人数。编写一个算法来重建这个队列。 6 | 7 | ## 注意 8 | 9 | 总人数少于1100人。 10 | 11 | ## 示例 12 | 13 | ``` 14 | 输入: [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]] 15 | 16 | 输出: [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] 17 | ``` 18 | 19 | # 解题思路 20 | 21 | 将题目内容更加形象化一些,可以认为在排队中,身高为`h`的人只能看见身高大于等于`h`的人,且看到的人数即可认为是`k`,换言之,在身高为`h`的人前插入身高小于`h`的人,并不影响他的`k`值,因为他"看不见"。 22 | 23 | ## 算法思路 24 | 25 | - 将原始`people`按`k`值,升序排列; 26 | - 再将上一步排列好的`people`按`h`值降序排列;(这样得到的`people`序列中可以确保`h`值相同的人`k`值小的在前) 27 | - 再对`people`进行遍历,按照`k`值插入输出列表`res`中即可 28 | 29 | ## 复杂度分析 30 | 31 | - 时间复杂度:$O(N^2)$,每次排序使用$O(NlogN)$,每个人插入到输出序列中需要$O(K)$的时间,其中$K$是当前序列中元素的个数,最坏的情况时间复杂度为$O(N^2)$。 32 | - 空间复杂度:$O(N)$。 33 | 34 | ## 代码 35 | 36 | ```python 37 | class Solution: 38 | def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]: 39 | people.sort(key=lambda x: x[1]) 40 | people.sort(key=lambda x: -x[0]) 41 | res = [] 42 | for p in people: 43 | res.insert(p[1],p) 44 | return res 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /剑指offer系列/面试题22.链表中倒数第k个节点.md: -------------------------------------------------------------------------------- 1 | 链表中倒数第k个节点 2 | 3 | # 题目描述 4 | 5 | 输入一个人链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 给定一个链表: 1->2->3->4->5, 和 k = 2. 11 | 12 | 返回链表 4->5. 13 | ``` 14 | 15 | # 解题思路 16 | 17 | > 本题比较简单,很容易想到一种基本解法:先遍历一遍链表,统计出链表的总节点数$n$,从倒数第k个节点开始输出,则再从头走$n-k$步即可。 18 | 19 | 也可以借用双指针来完成,例如设置双指针`slow`和`fast`,初始时都指向`head`,让`fast`先走`k`步,`slow`再出发,当`fast`走到`none`时,输出`cur`即可。 20 | 21 | 本题给的测试用例中没有考虑以下三种情况: 22 | 23 | 1. `head`为空指针,即链表为空; 24 | 2. k大于链表的长度; 25 | 3. k为0; 26 | 27 | 代码可以简单一些,如果要考虑的话,可以加入以下判断: 28 | 29 | 1. 链表为空,则不论k取多大,返回空链表; 30 | 2. k大于链表的长度,返回空链表或者返回整个链表?(这个要看个人理解了,如果真遇到这种情况,题目一般会给出说明) 31 | 3. k为0,返回`none` 32 | 33 | ## 复杂度分析 34 | 35 | - 时间复杂度:$O(N)$ 36 | - 空间复杂度:$O(1)$ 37 | 38 | ## 代码 39 | 40 | ```python 41 | # Definition for singly-linked list. 42 | # class ListNode: 43 | # def __init__(self, x): 44 | # self.val = x 45 | # self.next = None 46 | 47 | class Solution: 48 | def getKthFromEnd(self, head: ListNode, k: int) -> ListNode: 49 | cur = head 50 | while head: 51 | head = head.next 52 | k -= 1 53 | if k < 0: 54 | cur = cur.next 55 | return cur 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /剑指offer系列/面试题10-II.青蛙跳台阶问题.md: -------------------------------------------------------------------------------- 1 | 青蛙跳台阶问题 2 | 3 | # 题目描述 4 | 5 | 一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个`n`级台阶总共有多少种跳法。 6 | 7 | 答案需要取模`1e9+7(1000000007)`。如计算初始结果为:1000000008,请返回1. 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入:n = 2 13 | 输出:2 14 | ``` 15 | 16 | ## 示例2 17 | 18 | ``` 19 | 输入:n = 7 20 | 输出:21 21 | ``` 22 | 23 | ## 提示 24 | 25 | - `0 <= n <= 100` 26 | 27 | # 解题思路 28 | 29 | > 此类求`多少种可能性`的题目一般都有递推性质,即$f(n)$和$f(n-1)...f(1)$之间是有联系的。 30 | 31 | 假设跳上$n$级台阶有$f(n)$中跳法。在所有情况中,青蛙的最后一步只有两种情况:**跳上1级或2级台阶**。 32 | 33 | 1. 当为1级台阶时:即青蛙处在第$n-1$级台阶,此情况有$f(n-1)$种跳法。 34 | 2. 当为2级台阶时:即青蛙处在第$n-2$级台阶,此情况有$f(n-2)$种跳法。 35 | 36 | 那么,$f(n)$就是以上两种情况之和,即$f(n)=f(n-1)+f(n-2)$,这样的递推性质就是[斐波那契数列](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题10-I.斐波那契数列.md),方法也就可以套用[面试题10-I](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题10-I.斐波那契数列.md),唯一不同在于起始数字不同: 37 | 38 | - 青蛙跳台阶问题:$f(0)=1,f(1)=1,f(2)=2$; 39 | - 斐波那契数列问题:$f(0)=0,f(1)=1,f(2)=1$; 40 | 41 | 算法思路及分析完全可以参见[面试题10-I](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题10-I.斐波那契数列.md),此处就不再赘述。 42 | 43 | ## 代码 44 | 45 | ```python 46 | class Solution: 47 | def numWays(self, n: int) -> int: 48 | a = 1 49 | b = 1 50 | for _ in range(n): 51 | a, b = b, a+b 52 | return a % 1000000007 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /剑指offer系列/面试题05.替换空格.md: -------------------------------------------------------------------------------- 1 | 替换空格 2 | 3 | # 题目描述 4 | 5 | 请实现一个函数,把字符串`s`中的每个空格替换成"%20"。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:s = "We are happy." 11 | 输出:"We%20are%20happy." 12 | ``` 13 | 14 | ## 限制 15 | 16 | `0 <= s的长度 <= 10000` 17 | 18 | # 解题思路 19 | 20 | ## 方法一:replace函数 21 | 22 | 直接调用`replace()`函数完成字符替换,就是这么简单! 23 | 24 | ## 代码 25 | 26 | ```python 27 | class Solution: 28 | def replaceSpace(self, s: str) -> str: 29 | 30 | return s.replace(' ','%20') 31 | ``` 32 | 33 | ## 方法二:建立新的字符串 34 | 35 | 创建一个新的空字符串,并对原来的字符串进行遍历。 36 | 37 | ## 代码 38 | 39 | ```python 40 | class Solution: 41 | def replaceSpace(self, s: str) -> str: 42 | s1 = '' 43 | for c in s: 44 | if c == ' ': 45 | s1 += '%20' 46 | else: 47 | s1 += c 48 | return s1 49 | ``` 50 | 51 | ## 方法三:建立列表 52 | 53 | 在方法二的基础上进行改进,因为字符串为不可变类型,每加一个字符就会成为一个新的字符串,方法二比较耗内存。使用列表对新字符串进行存储,最后用join函数将列表转换为字符串。 54 | 55 | ## 代码 56 | 57 | ```python 58 | class Solution: 59 | def replaceSpace(self, s: str) -> str: 60 | s1 = [] 61 | for c in s: 62 | if c == ' ': 63 | s1.append('%20') 64 | else: 65 | s1.append(c) 66 | return ''.join(s1) 67 | ``` 68 | 69 | -------------------------------------------------------------------------------- /剑指offer系列/面试题53-II.0~n-1中缺失的数字.md: -------------------------------------------------------------------------------- 1 | 0~n-1中缺失的数字 2 | 3 | # 题目描述 4 | 5 | 一个长度为`n-1`的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围`0 ~ n-1`内的`n`个数字中有且仅有一个数字不在该数组中,请找出这个数字。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: [0,1,3] 11 | 输出: 2 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: [0,1,2,3,4,5,6,7,9] 18 | 输出: 8 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `1 <= 数组长度 <= 10000` 24 | 25 | # 解题思路 26 | 27 | 本题直接遍历求解,应该比较容易想到,但需要注意一种特殊情况,如对于`[0, 1, 2, 3, 4, 5, 6, 7]`,看似每个位置上都出现了正确的数字,但应返回`8`,表明其缺失了数字`8`。 28 | 29 | 不过,对于排序数组中的搜索问题,考虑**二分法**也应该是容易想到的。 30 | 31 | 根据题意,数组可以按照以下规则划分为两部分: 32 | 33 | - **左子数组**:`nums[i] = i`; 34 | - **右子数组**:`nums[i] != i`。 35 | 36 | 需要求解的缺失的数字等于**右子数组的首个元素**对应的索引;利用**二分查找**查找该元素; 37 | 38 | **算法解析**: 39 | 40 | - **初始化**:左边界`i = 0`,右边界`j = len(nums)-1`,代表闭区间`[i, j]`; 41 | - **循环二分**:当`i > j`时跳出 42 | - 计算中点`mid = (i + j) // 2` 43 | - 若`nums[mid] = mid`,则执行`i = mid + 1` 44 | - 若`nums[mid] != mid`,则执行`j = mid - 1` 45 | - **返回值**:返回`i`即可 46 | 47 | ## 复杂度分析 48 | 49 | - 时间复杂度:$O(\log N)$ 50 | - 空间复杂度:$O(1)$ 51 | 52 | ## 代码 53 | 54 | ```python 55 | class Solution: 56 | def missingNumber(self, nums: List[int]) -> int: 57 | i, j = 0, len(nums)-1 58 | while i <= j: 59 | mid = (i + j) // 2 60 | if nums[mid] == mid: 61 | i = mid + 1 62 | else: 63 | j = mid - 1 64 | return i 65 | ``` 66 | 67 | -------------------------------------------------------------------------------- /剑指offer系列/面试题57.和为s的两个数字.md: -------------------------------------------------------------------------------- 1 | 和为s的两个数字 2 | 3 | # 题目描述 4 | 5 | 输入一个递增排序的数组和一个数字`s`,在数组中查找两个数,使得它们的和正好是`s`。如果有多对数字的和等于`s`,则输出任意一对即可。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:nums = [2,7,11,15], target = 9 11 | 输出:[2,7] 或者 [7,2] 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入:nums = [10,26,30,31,47,60], target = 40 18 | 输出:[10,30] 或者 [30,10] 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `1 <= nums.length <= 10^5` 24 | - `1 <= nums[i] <= 10^6` 25 | 26 | # 解题思路 27 | 28 | 最简单的,暴力穷举显然是最容易想到的,但时间复杂度很高,而且没有利用数组是**递增排序**的这一性质。 29 | 30 | 借助HashMap可以将时间复杂度降至$O(N)$,但是空间复杂度变成了$O(N)$。 31 | 32 | 这里介绍**双指针法**,**算法流程**如下: 33 | 34 | - **初始化**:双指针`i, j`分别指向数组`nums`的左右两端,即`i = 0, j = len(nums) - 1` 35 | - **循环搜索**:当双指针相遇后跳出; 36 | - 计算`s = nums[i] + nums[j]` 37 | - 若`s == target`,返回`[nums[i], nums[j]]` 38 | - 若`s < target`,执行`i += 1` 39 | - 若`s > tarhet`,执行`j -= 1` 40 | - 返回空数组,代表无符合要求的数字组合 41 | 42 | ## 复杂度分析 43 | 44 | - 时间复杂度:$O(N)$ 45 | - 空间复杂度:$O(1)$ 46 | 47 | ## 代码 48 | 49 | ```python 50 | class Solution: 51 | def twoSum(self, nums: List[int], target: int) -> List[int]: 52 | i, j = 0, len(nums) - 1 53 | while i <= j: 54 | s = nums[i] + nums[j] 55 | if s == target: 56 | return [nums[i], nums[j]] 57 | if s < target: 58 | i += 1 59 | if s > target: 60 | j -= 1 61 | return [] 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /剑指offer系列/面试题39.数组中出现次数超过一半的数字.md: -------------------------------------------------------------------------------- 1 | 数组中出现次数超过一半的数字 2 | 3 | # 题目描述 4 | 5 | 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。 6 | 7 | 你可以假设数组是非空的,并且给定的数组总是存在多数元素。 8 | 9 | ## 示例 10 | 11 | ``` 12 | 输入: [1, 2, 3, 2, 2, 2, 5, 4, 2] 13 | 输出: 2 14 | ``` 15 | 16 | ## 限制 17 | 18 | - `1 <= 数组长度 <= 50000` 19 | 20 | # 解题思路 21 | 22 | > 本题常见解法如下: 23 | > 24 | > 1. 哈希表统计法:遍历数组`nums`,用HashMap统计各数字的数量,最终超过数组长度一半的数字则为众数。此方法时间和空间复杂度均为$O(N)$。 25 | > 2. 数组排序法:将数组`nums`排序,由于众数的数量超过数组长度一半,因此数组中点的元素一定为众数。此方法时间复杂度为$O(NlogN)$。 26 | > 3. 摩尔投票法:核心思想是"正负抵消";时间和空间复杂度分别为$O(N)$和$O(1)$,应该是最佳解法。 27 | 28 | **摩尔投票法**: 29 | 30 | - **票数和**:由于众数出现的次数超过数组长度的一半;若记**众数**的票数为`+1`,**非众数**的票数为`-1`,则一定有所有数字的票数和`> 0`。 31 | - **票数正负抵消**:设数组`nums`中的众数为`x`,数组长度为`N`。若`nums`的前`a`个数字的票数和`= 0`,则数组后`(N - a)`个数字的票数和一定仍`> 0`(即后`(N - a)`个数字的众数仍为`x`)。 32 | 33 | ## 算法流程 34 | 35 | - **初始化**:票数统计`votes = 0`,众数`x`; 36 | - **循环抵消**:遍历数组`nums`中的每个数字`num`: 37 | - 当票数`votes == 0`时,则假设当前数字`num`为众数`x`; 38 | - 当`num == x`时,票数`votes += 1`;否则`votes -= 1`。 39 | - **返回值**:返回`x` 40 | 41 | ## 复杂度分析 42 | 43 | - 时间复杂度:$O(N)$ 44 | - 空间复杂度:$O(1)$ 45 | 46 | ## 代码 47 | 48 | ```python 49 | class Solution: 50 | def majorityElement(self, nums: List[int]) -> int: 51 | votes = 0 52 | for num in nums: 53 | if not votes: 54 | x = num 55 | if x == num: 56 | votes += 1 57 | else: 58 | votes -= 1 59 | return x 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /剑指offer系列/面试题06.从尾到头打印链表.md: -------------------------------------------------------------------------------- 1 | 从尾到头打印链表 2 | 3 | # 题目描述 4 | 5 | 输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入:head = [1,3,2] 11 | 输出:[2,3,1] 12 | ``` 13 | 14 | ## 限制 15 | 16 | `0 <= 链表长度 <= 10000` 17 | 18 | # 解题思路 19 | 20 | ## 方法一:栈 21 | 22 | 借助栈的"先进后出"的特点实现倒序输出。 23 | 24 | 先遍历链表,将元素依次放入栈中,再逆序输出。 25 | 26 | ## 复杂度分析 27 | 28 | - 时间复杂度:$O(n)$。 29 | - 空间复杂度:$O(n)$。 30 | 31 | ## 代码 32 | 33 | ```python 34 | # Definition for singly-linked list. 35 | # class ListNode: 36 | # def __init__(self, x): 37 | # self.val = x 38 | # self.next = None 39 | 40 | class Solution: 41 | def reversePrint(self, head: ListNode) -> List[int]: 42 | res = [] 43 | while head: 44 | res.append(head.val) 45 | head = head.next 46 | return res[::-1] 47 | ``` 48 | 49 | ## 方法二:递归 50 | 51 | 利用递归:先走至链表末端,回溯时依次将节点值加入列表中,这样就可以实现链表的倒序输出具体如下: 52 | 53 | - **递归阶段**:每次传入`head.next`,以`head == None`为递归终止条件,此时返回空列表`[]`。 54 | - **回溯阶段**:递归回溯时每次返回`当前list + 当前节点值[head.val]`,即可实现节点的倒序输出。 55 | 56 | ## 复杂度分析 57 | 58 | - 时间复杂度:$O(n)$。 59 | - 空间复杂度:$O(n)$。 60 | 61 | ## 代码 62 | 63 | ```python 64 | # Definition for singly-linked list. 65 | # class ListNode: 66 | # def __init__(self, x): 67 | # self.val = x 68 | # self.next = None 69 | class Solution: 70 | def reversePrint(self, head:ListNode) -> List[int]: 71 | return self.reversePrint(head.next) + [head.val] if head else [] 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /每日一题系列/1221.分割平衡字符串.md: -------------------------------------------------------------------------------- 1 | 分割平衡字符串 2 | 3 | # 题目描述 4 | 5 | 在一个`平衡字符串`中,`L`和`R`字符的数量是相同的。 6 | 7 | 给出一个`平衡字符串s`,请你将它分割成尽可能多的平衡字符串。 8 | 9 | 返回可以通过分割得到的平衡字符串的最大数量。 10 | 11 | ## 示例1 12 | 13 | ``` 14 | 输入:s = "RLRRLLRLRL" 15 | 输出:4 16 | 解释:s 可以分割为 "RL", "RRLL", "RL", "RL", 每个子字符串中都包含相同数量的 'L' 和 'R'。 17 | ``` 18 | 19 | ## 示例2 20 | 21 | ``` 22 | 输入:s = "RLLLLRRRLR" 23 | 输出:3 24 | 解释:s 可以分割为 "RL", "LLLRRR", "LR", 每个子字符串中都包含相同数量的 'L' 和 'R'。 25 | ``` 26 | 27 | ## 示例3 28 | 29 | ``` 30 | 输入:s = "LLLLRRRR" 31 | 输出:1 32 | 解释:s 只能保持原样 "LLLLRRRR". 33 | ``` 34 | 35 | ## 提示 36 | 37 | - `1 <= s.length <= 1000` 38 | - `s[i] = 'L' 或 'R'` 39 | - 分割得到的每个字符串都必须是平衡字符串 40 | 41 | # 解题思路 42 | 43 | 注意审题,首先明确一下几点: 44 | 45 | - 输入的字符串`s`本身就是一个平衡字符串; 46 | - 如能够分割,则需要保证分割得到的每一部分字符串仍然是平衡字符串;否则不符合要求。 47 | 48 | ## 算法思路 49 | 50 | 控制两个变量`balance`和`count`,遍历字符串`s`,遇到`'L'`则`balance += 1`;遇到`'R'`则`balance -= 1`;如果`balance == 0`,则`count += 1`。 51 | 52 | 因为输入字符串`s`本身就是一个平衡字符串,所以无论如何,遍历完字符串`s`后,`balance`都会等于0。 53 | 54 | ## 复杂度分析 55 | 56 | - 时间复杂度:$O(N)$。 57 | - 空间复杂度:$O(1)$。 58 | 59 | ## 代码 60 | 61 | ```python 62 | class Solution: 63 | def balancedStringSplit(self, s: str) -> int: 64 | balance = 0 65 | count = 0 66 | for i in range(len(s)): 67 | if s[i] == 'L': 68 | balance += 1 69 | if s[i] == 'R': 70 | balance -= 1 71 | if balance == 0: 72 | count += 1 73 | return count 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /每日一题系列/969.煎饼排序.md: -------------------------------------------------------------------------------- 1 | 煎饼排序 2 | 3 | # 问题描述 4 | 5 | 给定数组`A`,我们可以对其进行`煎饼排序`翻转:我们选择一些正数`k<=A.length`,然后翻转`A`的前`k`个元素的顺序。我们要执行零次或多次煎饼翻转(按顺序一次接一次地进行)以完成对数组`A`的排序。 6 | 7 | 返回能使`A`排序的煎饼翻转操作所对应的k值序列。任何将数组排序且翻转次数在`10*A.length`范围内的有效答案都将被判断为正确。 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入:[3,2,4,1] 13 | 输出:[4,2,4,3] 14 | 解释: 15 | 我们执行4次煎饼翻转,k值分别为4,2,4和3. 16 | 初始状态A = [3,2,4,1] 17 | 第一次翻转后(k=4):A = [1,4,2,3] 18 | 第二次翻转后(k=2):A = [4,1,2,3] 19 | 第三次翻转后(k=4):A = [3,2,1,4] 20 | 第四次翻转后(k=3):A = [1,2,3,4],此时已完成排序。 21 | ``` 22 | 23 | ## 示例2 24 | 25 | ``` 26 | 输入:[1,2,3] 27 | 输出:[] 28 | 解释: 29 | 输入已经排序,因此不需要翻转任何内容。 30 | 请注意,其他可能的答案,如[3,3],也将被接受。 31 | ``` 32 | 33 | ## 提示 34 | 35 | 1. `1<=A.length<=100` 36 | 2. `A[i]`是`[1,2,...,A.length]`的排列 37 | 38 | # 解题思路 39 | 40 | 1. 找到前`n`个值的最大值索引`k` 41 | 2. 翻转前`k+1`个元素,使得当前最大元素移到最前边 42 | 3. 翻转`0~n`个元素将最大元素移动到后面 43 | 4. `n -= 1`,直到`n=1`(只有1个元素时不用进行操作) 44 | 45 | (若最大值索引`k`为0,则跳过步骤2;若最大值索引`k`为`n-1`,则跳过步骤2、3。可以适当减少时间) 46 | 47 | ## 代码 48 | 49 | ```python 50 | class Solution: 51 | def pancakeSort(self, A: List[int]) -> List[int]: 52 | i, res = len(A), [] 53 | while i > 0: 54 | max_idx = A[:i].index(i) 55 | if max_idx != i-1: 56 | A[:max_idx+1] = A[:max_idx+1][::-1] 57 | A[:i] = A[:i][::-1] 58 | res.extend([max_idx+1, i]) 59 | elif max_idx == 0: 60 | A[:i] = A[:i][::-1] 61 | res.extend([i]) 62 | i +=-1 63 | return res 64 | ``` 65 | 66 | -------------------------------------------------------------------------------- /剑指offer系列/面试题57-II.和为s的连续正数序列.md: -------------------------------------------------------------------------------- 1 | 和为s的连续正数序列 2 | 3 | # 题目描述 4 | 5 | 输入一个正整数`target`,输出所有和为`target`的连续正整数序列(至少含有两个数)。 6 | 7 | 序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入:target = 9 13 | 输出:[[2,3,4],[4,5]] 14 | ``` 15 | 16 | ## 示例2 17 | 18 | ``` 19 | 输入:target = 15 20 | 输出:[[1,2,3,4,5],[4,5,6],[7,8]] 21 | ``` 22 | 23 | ## 限制 24 | 25 | - `1 <= target <= 10^5` 26 | 27 | # 解题思路 28 | 29 | 根据题意,满足要求的必然是一个**连续正整数序列**(本质上是公差为1的等差数列),设首项为$a_1$,有$n$项($n \ge 2$),即$(a_1,a_1+1,a_1+2,...,a_1+n-1)$。根据等差数列求和公式可得: 30 | $$ 31 | target =\frac{(a_1 + a_1 + n - 1)\times n}{2}=a_1\times n + \frac{(n-1)\times n}{2} 32 | $$ 33 | 转换为: 34 | $$ 35 | a_1 = \frac{target-\frac{n(n-1)}{2}}{n} 36 | $$ 37 | 即可以将问题转换为找出所有满足条件的$n,a_1$对即可。 38 | 39 | 核心思路就是:$n$从`2`开始遍历,验证$a_1$是否为正整数。那么$n$要遍历到多少呢?其实不需要精确地去计算$n$的上界,随着$n$的增加,当$a_1\le 0$时就可以不用再寻找了,跳出循环即可。因此,可以设置一个很宽的上界,如$n <= target$。 40 | 41 | ## 复杂度分析 42 | 43 | - 时间复杂度:$O(target)$,$n$的一个粗略上界应该是$\sqrt{2target}$,整理`res`需要遍历$O(\sqrt{2target})$。 44 | - 空间复杂度:$O(target)$ 45 | 46 | ## 代码 47 | 48 | ```python 49 | class Solution: 50 | def findContinuousSequence(self, target: int) -> List[List[int]]: 51 | res = [] 52 | for n in range(2, target+1): 53 | temp = target - n*(n-1)//2 54 | if temp <= 0: 55 | break 56 | if not temp % n: 57 | a_1 = temp // n 58 | if a_1 > 0: 59 | res.append([a_1 + i for i in range(n)]) 60 | return res[::-1] 61 | ``` 62 | 63 | -------------------------------------------------------------------------------- /剑指offer系列/面试题31.栈的压入、弹出序列.md: -------------------------------------------------------------------------------- 1 | 栈的压入、弹出序列 2 | 3 | # 题目描述 4 | 5 | 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列`{1,2,3,4,5}`是某栈的压栈序列,序列`{4,5,3,2,1}`是该压栈序列对应的一个弹出序列,但`{4,3,5,1,2}`就不可能是该压栈序列的弹出序列。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1] 11 | 输出:true 12 | 解释:我们可以按以下顺序执行: 13 | push(1), push(2), push(3), push(4), pop() -> 4, 14 | push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1 15 | ``` 16 | 17 | ## 示例2 18 | 19 | ``` 20 | 输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2] 21 | 输出:false 22 | 解释:1 不能在 2 之前弹出。 23 | ``` 24 | 25 | ## 提示 26 | 27 | - `0 <= pushed.length == popped.length <= 1000` 28 | - `0 <= pushed[i], popped[i] < 1000` 29 | - `pushed`是`popped`的排列 30 | 31 | # 解题思路 32 | 33 | > 本题显然考察栈这个数据结构的相关知识。"栈"唯一的特点就是**先进后出**(通俗点说就是,最先进来的元素,最后出去。) 34 | 35 | 由于数字不重复,则对于序列`popped`而言,其第一个元素必然是最先执行出栈时弹出的元素,我们可以构建一个辅助栈`stack`,来进行模拟。 36 | 37 | 根据序列`pushed`的顺序依次向辅助栈`stack`中添加元素,判断`stack`栈顶元素是否与`popped`首个元素相等,若相等则将该元素从`stack`中弹出,且`popped`的"首个元素"指向下一个,不断循环判断。若最终`stack`为空,说明符合要求,返回`True`;否则,返回`False`。 38 | 39 | ## 复杂度分析 40 | 41 | - 时间复杂度:$O(N)$ 42 | - 空间复杂度:$O(N)$ 43 | 44 | ## 代码 45 | 46 | ```python 47 | class Solution: 48 | def validateStackSequences(self, pushed: List[int], popped: List[int]) -> bool: 49 | stack = [] 50 | idx = 0 51 | for i in range(len(pushed)): 52 | stack.append(pushed[i]) 53 | while stack and stack[-1] == popped[idx]: 54 | stack.pop() 55 | idx += 1 56 | return not stack 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /剑指offer系列/面试题21.调整数组顺序使奇数位于偶数前面.md: -------------------------------------------------------------------------------- 1 | 调整数组顺序使奇数位于偶数前面 2 | 3 | # 题目描述 4 | 5 | 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入:nums = [1,2,3,4] 11 | 输出:[1,3,2,4] 12 | 注:[3,1,2,4] 也是正确的答案之一。 13 | ``` 14 | 15 | ## 提示 16 | 17 | - `1 <= nums.length <= 50000` 18 | - `1 <= nums[i] <=10000` 19 | 20 | # 解题思路 21 | 22 | # 方法一:取出奇数和偶数再合并 23 | 24 | 一种很直观的方法就是先从原数组中取出奇数和偶数,分别放入两个新列表中,然后将两个列表合并输出即可。 25 | 26 | ## 复杂度分析 27 | 28 | - 时间复杂度:$O(N)$ 29 | - 空间复杂度:$O(N)$ 30 | 31 | ## 代码 32 | 33 | ```python 34 | class Solution: 35 | def exchange(self, nums: List[int]) -> List[int]: 36 | odd = [] 37 | even = [] 38 | for num in nums: 39 | if num % 2: 40 | odd.append(num) 41 | else: 42 | even.append(num) 43 | return odd + even 44 | ``` 45 | 46 | ## 方法二:双指针 47 | 48 | 建立两个指针,一个从前往后移动,一个从后往前移动,前指针用于定位奇数,后指针用于定位偶数。当前指针遇到偶数时,暂停移动,当后指针遇到奇数时,也暂停移动,此时发生交换,然后继续移动。直至两指针相遇或越过。 49 | 50 | ## 复杂度分析 51 | 52 | - 时间复杂度:$O(N)$ 53 | - 空间复杂度:$O(1)$ 54 | 55 | ## 代码 56 | 57 | ```python 58 | class Solution: 59 | def exchange(self, nums: List[int]) -> List[int]: 60 | if len(nums) == 1: 61 | return nums 62 | i = 0 63 | j = len(nums)-1 64 | while i < j: 65 | while i < j and nums[i] % 2: 66 | i += 1 67 | while i < j and not nums[j] % 2: 68 | j -= 1 69 | nums[i], nums[j] = nums[j], nums[i] 70 | 71 | return nums 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /剑指offer系列/面试题32-I.从上到下打印二叉树.md: -------------------------------------------------------------------------------- 1 | 从上到下打印二叉树 2 | 3 | # 题目描述 4 | 5 | 从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。 6 | 7 | ## 示例 8 | 9 | 给定二叉树:`[3,9,20,null,null,15,7]` 10 | 11 | ``` 12 | 3 13 | / \ 14 | 9 20 15 | / \ 16 | 15 7 17 | ``` 18 | 19 | 返回: 20 | 21 | ``` 22 | [3,9,20,15,7] 23 | ``` 24 | 25 | ## 提示 26 | 27 | - `节点总数 <= 1000` 28 | 29 | # 解题思路 30 | 31 | > 树的按层次打印就是广度优先搜索(BFS) 32 | 33 | **算法流程**: 34 | 35 | - **特例处理**:当树的根节点为空,则直接返回空列表`[]`。 36 | - **初始化**:初始化结果列表`res=[]`,包含根节点的队列`queue = collections.deque([root])` 37 | - **BFS循环**:(当`queue`为空时跳出) 38 | - **出队**:队首元素出队,记为`tmp` 39 | - **加入列表**:将`tmp.val`加入`res`尾部 40 | - **添加子节点**:若`tmp`的左(右)子节点不为空,则将左(右)子节点加入队列`queue`中。 41 | - **返回值**:返回`res` 42 | 43 | ## 复杂度分析 44 | 45 | - 时间复杂度:$O(N)$,$N$为总节点数 46 | - 空间复杂度:$O(N)$。 47 | 48 | ## 代码 49 | 50 | ```python 51 | # Definition for a binary tree node. 52 | # class TreeNode: 53 | # def __init__(self, x): 54 | # self.val = x 55 | # self.left = None 56 | # self.right = None 57 | 58 | class Solution: 59 | def levelOrder(self, root: TreeNode) -> List[int]: 60 | queue = collections.deque() 61 | queue.append(root) 62 | res = [] 63 | if not root: 64 | return res 65 | while queue: 66 | tmp = queue.popleft() 67 | res.append(tmp.val) 68 | if tmp.left: 69 | queue.append(tmp.left) 70 | if tmp.right: 71 | queue.append(tmp.right) 72 | 73 | return res 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /剑指offer系列/面试题50.第一个只出现一次的字符.md: -------------------------------------------------------------------------------- 1 | 第一个只出现一次的字符 2 | 3 | # 题目描述 4 | 5 | 在字符串`s`中找出第一个只出现一次的字符。如果没有,返回一个单空格。`s`只包含小写字母。 6 | 7 | ## 示例 8 | 9 | ``` 10 | s = "abaccdeff" 11 | 返回 "b" 12 | 13 | s = "" 14 | 返回 " " 15 | ``` 16 | 17 | ## 限制 18 | 19 | - `0 <= s 的长度 <= 50000` 20 | 21 | # 解题思路 22 | 23 | 使用**哈希表**: 24 | 25 | - 遍历字符串`s`,使用哈希表统计"各字符数量是否 > 1"; 26 | - 再遍历字符串`s`,在哈希表中找到**首个**"数量为1的字符",并返回。 27 | 28 | **算法流程**: 29 | 30 | - **初始化**:空字典`letter_dict`。(使用内置字典`collections.defaultdict(list)`,以列表的形式存储字典的value);初始`min_idx = len(s)` 31 | - **遍历字符串s**:获取字符串`s`中每一个字母`letter`及其索引`idx`,加入字典中,`letter_dict[letter].append(idx)`; 32 | - **遍历字典**,寻找首个"数量为1的字符"(即对应列表长度为`1`,且`idx`值最小): 33 | - 若列表长度大于1,当前字符说明存在重复,直接跳过; 34 | - 否则,更新`min_idx`,`min_idx = min(min_idx, val[0])`,确保`min_idx`是最小的符合条件的索引。 35 | - **返回值**:若`min_idx == len(s)`,即不存在符合要求的字符,返回单空格;否则返回`s[min_idx]`。 36 | 37 | ## 复杂度分析 38 | 39 | - 时间复杂度:$O(N)$ 40 | - 空间复杂度:$O(N)$ 41 | 42 | ## 代码 43 | 44 | ```python 45 | class Solution: 46 | def firstUniqChar(self, s: str) -> str: 47 | if not s: 48 | return " " 49 | 50 | letter_dict = collections.defaultdict(list) 51 | for idx, letter in enumerate(s): 52 | letter_dict[letter].append(idx) 53 | 54 | min_idx = len(s) 55 | for key, val in letter_dict.items(): 56 | if len(val) > 1: 57 | continue 58 | min_idx = min(min_idx, val[0]) 59 | 60 | if min_idx == len(s): 61 | return " " 62 | return s[min_idx] 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /每日一题系列/面试题17.08.马戏团人塔.md: -------------------------------------------------------------------------------- 1 | 马戏团人塔 2 | 3 | # 题目描述 4 | 5 | 有个马戏团正在设计叠罗汉的表演节目,一个人要站在另一个人的肩膀上。出于实际和美观的考虑,在上面的人要比下面的人矮一点且轻一点。已知马戏团每个人的身高和体重,请编写代码计算叠罗汉最多能叠几个人。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入:height = [65,70,56,75,60,68] weight = [100,150,90,190,95,110] 11 | 输出:6 12 | 解释:从上往下数,叠罗汉最多能叠 6 层:(56,90), (60,95), (65,100), (68,110), (70,150), (75,190) 13 | ``` 14 | 15 | ## 提示 16 | 17 | - `height.length == weight.length <= 10000` 18 | 19 | # 解题思路 20 | 21 | 根据题意,将数组`height`和`weight`对应元素打包成元组,并依据`height`做升序排列,相同身高的情况下,体重`weight`为降序排列。 22 | 23 | 就可以把原问题转换为求[最长上升子序列问题](https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-dong-tai-gui-hua-2/),解题思路在链接中有非常清晰的阐述。 24 | 25 | 注意:若单纯使用动态规划求解,时间复杂度为$O(N^2)$,在本题中将**超时**;需要使用动态规划+二分查找的方式进行优化,此时状态表示为:`dp[i]`的值代表**长度为**`i+1`的**子序列尾部元素的值**。(这与纯动态规划的状态表示是不同的,要注意理解!!) 26 | 27 | ## 代码 28 | 29 | ```python 30 | class Solution: 31 | def bestSeqAtIndex(self, height: List[int], weight: List[int]) -> int: 32 | person = [(h, w) for h, w in zip(height, weight)] 33 | person = sorted(person, key=lambda x: [x[0], -x[1]]) 34 | dp = [0 for _ in range(len(person))] 35 | res = 0 36 | for i in range(len(person)): 37 | k, j = 0, res 38 | while k < j: 39 | m = (k + j) // 2 40 | if dp[m] < person[i][1]: k = m + 1 # 如果要求非严格递增,将此行 '<' 改为 '<=' 即可。 41 | else: j = m 42 | dp[k] = person[i][1] 43 | if j == res: res += 1 44 | 45 | return res 46 | ``` 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /剑指offer系列/面试题46.把数字翻译成字符串.md: -------------------------------------------------------------------------------- 1 | 把数字翻译成字符串 2 | 3 | # 题目描述 4 | 5 | 给定一个数字,我们按照如下规则把它翻译为字符串:`0`翻译成`"a"`,`1`翻译成`"b"`,......`11`翻译成`"l"`,......`25`翻译成`"z"`。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: 12258 11 | 输出: 5 12 | 解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi" 13 | ``` 14 | 15 | ## 提示 16 | 17 | - `0 <= num < 2^31` 18 | 19 | # 解题思路 20 | 21 | > 这是一道典型的动态规划问题。 22 | 23 | 对于一个数`num[i]`,可能存在至多两种选择: 24 | 25 | - 只翻译自己; 26 | - 和前一个数字`num[i-1]`组合一起翻译,前提是组合成的数在`[10, 25]`之间。 27 | 28 | 由此,可以使用`dp[i]`表示前`i`个数字的翻译方法数量,根据以上两种选择,可以有如下分析: 29 | 30 | - 如果**只翻译自己**,比如数字`12345`,如果`5`单独翻译,那么`12345`的方法数与`1234`的方法数一致,即`dp[i] = dp[i-1]` 31 | - 如果**和前面的数字可以组合翻译**,比如数字`13425`,如果`25`组合翻译,从两方面考虑: 32 | - `25`看成一个整体,虽然加了`5`但是和没加是一样的,状态`dp[i] = dp[i-1]`; 33 | - `25`看成一个整体,意味着它不能再与之前的数进行组合,相当于`25`自己翻译,则有`dp[i-2]`种方法。 34 | - 上述两种方法相加即可。 35 | 36 | 由此,得到状态转移方程: 37 | 38 | - 如果`num[i]`与`num[i-1]`组合成的数在`[10, 25]`之间,则`dp[i] = dp[i-2] + dp[i-1]`; 39 | - 否则,`dp[i] = dp[i-1]`; 40 | 41 | ## 复杂度分析 42 | 43 | - 时间复杂度:$O(N)$,$N$为数字`num`的长度(将数字转化成字符串) 44 | - 空间复杂度:$O(N)$,将数字转换成字符串需要$O(N)$的额外空间 45 | 46 | ## 代码 47 | 48 | ```python 49 | class Solution: 50 | def translateNum(self, num: int) -> int: 51 | num2str = str(num) 52 | dp = [0] * (len(num2str)+1) 53 | dp[0] = 1 54 | dp[1] = 1 55 | for i in range(2, len(num2str)+1): 56 | if "10" <= num2str[i-2:i] <= "25": 57 | dp[i] = dp[i-1] + dp[i-2] 58 | else: 59 | dp[i] = dp[i-1] 60 | 61 | return dp[-1] 62 | ``` 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /剑指offer系列/面试题66.构建乘积数组.md: -------------------------------------------------------------------------------- 1 | 构建乘积数组 2 | 3 | # 题目描述 4 | 5 | 给定一个数组`A[0, 1, ..., n-1]`,请构建一个数组`B[0, 1, , ..., n-1]`,其中`B`中的元素`B[i] = A[0] x A[1] x A[2] x ... x A[i-1] x A[i+1] x ... x A[n-1]`。**不能使用除法**。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入: [1,2,3,4,5] 11 | 输出: [120,60,40,30,24] 12 | ``` 13 | 14 | ## 提示 15 | 16 | - 所有元素乘积之和不会溢出`32`位整数 17 | - `a.length <= 100000` 18 | 19 | # 解题思路 20 | 21 | 本题的难点在于**不能使用除法**,即需要**只用乘法**生成数组`B`。 22 | 23 | 根据题目对`B[i]`的定义,可列出一下表格: 24 | 25 | | `B[0]=` | 1 | `A[1]` | `A[2]` | `...` | `A[n-1]` | 26 | | :----------: | :--------: | :--------: | :--------: | :-------: | :----------: | 27 | | **`B[1]=`** | **`A[0]`** | **1** | **`A[2]`** | **`...`** | **`A[n-1]`** | 28 | | **`B[2]=`** | **`A[0]`** | **`A[1]`** | **1** | **`...`** | **`A[n-1]`** | 29 | | **`...`** | **`...`** | **`...`** | **`...`** | **1** | **`...`** | 30 | | **`B[n-1]`** | **`A[0]`** | **`A[1]`** | **`A[2]`** | **`...`** | **1** | 31 | 32 | **算法流程**: 33 | 34 | - **初始化**:数组`B`,其中`B[0] = 1`, 辅助变量`tmp = 1`。 35 | - 计算`B[i]`的**下三角**各元素的乘积,直接乘入`B[i]`。 36 | - 计算`B[i]`的**上三角**各元素的乘积,记为`tmp`,并乘入`B[i]`。 37 | - 返回`B` 38 | 39 | ## 复杂度分析 40 | 41 | - 时间复杂度:$O(N)$ 42 | - 空间复杂度:$O(1)$,数组`B`作为返回值,不计为额外空间复杂度 43 | 44 | ## 代码 45 | 46 | ```python 47 | class Solution: 48 | def constructArr(self, a: List[int]) -> List[int]: 49 | b = [1] * len(a) 50 | tmp = 1 51 | for i in range(1, len(a)): 52 | b[i] = b[i-1] * a[i-1] 53 | for i in range(len(a)-2, -1, -1): 54 | tmp *= a[i+1] 55 | b[i] *= tmp 56 | return b 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /剑指offer系列/面试题49.丑数.md: -------------------------------------------------------------------------------- 1 | 丑数 2 | 3 | # 题目描述 4 | 5 | 我们把只包含因子`2、3和5`的数称作丑数`Ugly Number`。求按从小到大的顺序的第`n`个丑数。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入: n = 10 11 | 输出: 12 12 | 解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。 13 | ``` 14 | 15 | ## 说明 16 | 17 | - `1`是丑数。 18 | - `n`不超过`1690`。 19 | 20 | # 解题思路 21 | 22 | > **丑数的递推性质**:丑数只包含因子`2、3、5`,因此有"丑数 = 某较小丑数 x 某因子"(例如:$10 = 5 \times 2$)。 23 | 24 | 设已知长度为$n$的丑数序列$x_1,x_2,...,x_n$,求第$n+1$个丑数$x_{n+1}$。根据递推性质,丑数$x_{n+1}$只可能是以下三种情况其中之一(索引`a、b、c`为未知数): 25 | $$ 26 | x_{n+1} = min(x_a\times2, x_b\times3, x_c\times5) 27 | $$ 28 | 且要求$x_{n+1}$是**最接近**$x_n$的丑数,索引`a、b、c`应满足以下条件: 29 | $$ 30 | x_a\times2>x_n\ge x_{a-1}\times2 31 | $$ 32 | 33 | $$ 34 | x_b\times3>x_n\ge x_{b-1}\times3 35 | $$ 36 | 37 | $$ 38 | x_c\times5>x_n\ge x_{c-1}\times5 39 | $$ 40 | 41 | 初始时,索引`a、b、c`指向首个丑数(即1),循环根据递推公式得到下一个丑数,并每轮将对应索引执行`+1`即可。 42 | 43 | **动态规划算法流程**: 44 | 45 | - **状态定义**:设动态规划列表`dp`,`dp[i]`表示第$i+1$个丑数。 46 | - **转移方程**: 47 | - 当索引`a、b、c`满足上述条件时,`dp[i]`为三种情况的最小值; 48 | - 每轮计算`dp[i]`后,需要更新索引`a、b、c`的值,使其始终满足方程条件。 49 | - **初始状态**:`dp[0] = 1`,即第一个丑数为`1`。 50 | - **返回值**:`dp[-1]`。 51 | 52 | ## 复杂度分析 53 | 54 | - 时间复杂度:$O(N)$ 55 | - 空间复杂度:$O(N)$ 56 | 57 | ## 代码 58 | 59 | ```python 60 | class Solution: 61 | def nthUglyNumber(self, n: int) -> int: 62 | dp = [1] * n 63 | a, b, c = 0, 0, 0 64 | for i in range(1, n): 65 | n1 = dp[a] * 2 66 | n2 = dp[b] * 3 67 | n3 = dp[c] * 5 68 | dp[i] = min(n1, n2, n3) 69 | if dp[i] == n1: 70 | a += 1 71 | if dp[i] == n2: 72 | b += 1 73 | if dp[i] == n3: 74 | c += 1 75 | 76 | return dp[-1] 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /剑指offer系列/面试题63.股票的最大利润.md: -------------------------------------------------------------------------------- 1 | 股票的最大利润 2 | 3 | # 题目描述 4 | 5 | 假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少? 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: [7,1,5,3,6,4] 11 | 输出: 5 12 | 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 13 | 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。 14 | ``` 15 | 16 | ## 示例2 17 | 18 | ``` 19 | 输入: [7,6,4,3,1] 20 | 输出: 0 21 | 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 22 | ``` 23 | 24 | ## 限制 25 | 26 | - `0 <= 数组长度 <= 10^5` 27 | 28 | # 解题思路 29 | 30 | 设共有`n`天,第`a`天买入,第`b`天卖出,需保证`a < b`;可推出交易方案总数为: 31 | $$ 32 | (n-1)+(n-2)+...+2+1=\frac{n(n-1)}{2} 33 | $$ 34 | 因此,暴力枚举的时间复杂度为$O(N^2)$。考虑使用动态规划进行优化。 35 | 36 | **动态规划解析**: 37 | 38 | - **状态定义**:设动态规划列表`dp`,`dp[i]`代表以前`i`日的最大利润 39 | - **转移方程**:由于题目限定"买卖该股票一次",对于第`i`日存在两种情况: 40 | 41 | - 选择第`i`日卖出,则最大利润为第`i`日价格 - 前`i`日最低价格 42 | - 第`i`日不卖,则最大利润为前`i-1`日最大利润 43 | - 即`dp[i] = max(dp[i-1], prices[i] - min(prices[:i]))` 44 | - **初始状态**:`dp[0] = 0` 45 | - **返回值**:返回`dp`数组中的最大值 46 | 47 | **效率优化**: 48 | 49 | - **优化时间复杂度**:前`i`日的最低价格`min(prices[:i])`的时间复杂度为$O(i)$。在遍历`prices`时,可以借助一个变量`cost`每日更新最低价格。优化后转移方程为: 50 | 51 | $$ 52 | dp[i] = max(dp[i-1], prices[i] - min(cost,prices[i])) 53 | $$ 54 | 55 | - **优化空间复杂度**:由于`dp[i]`只与`dp[i-1], prices[i], cost`有关,因此可使用一个变量`profit`代替`dp`数组,优化后转移方程为: 56 | 57 | $$ 58 | profit = max(profit, prices[i]-cost) 59 | $$ 60 | 61 | ## 复杂度分析 62 | 63 | - 时间复杂度:$O(N)$ 64 | - 空间复杂度:$O(1)$ 65 | 66 | ## 代码 67 | 68 | ```python 69 | class Solution: 70 | def maxProfit(self, prices: List[int]) -> int: 71 | cost = float("+inf") 72 | profit = 0 73 | for price in prices: 74 | cost = min(cost, price) 75 | profit = max(price - cost, profit) 76 | 77 | return profit 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /剑指offer系列/面试题18.删除链表的节点.md: -------------------------------------------------------------------------------- 1 | 删除链表的节点 2 | 3 | # 题目描述 4 | 5 | 给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。 6 | 7 | 返回删除后的链表的节点头。 8 | 9 | **注意**:此题对比原题有改动 10 | 11 | ## 示例1 12 | 13 | ``` 14 | 输入: head = [4,5,1,9], val = 5 15 | 输出: [4,1,9] 16 | 解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9. 17 | ``` 18 | 19 | ## 示例2 20 | 21 | ``` 22 | 输入: head = [4,5,1,9], val = 1 23 | 输出: [4,5,9] 24 | 解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9. 25 | ``` 26 | 27 | ## 说明 28 | 29 | - 题目保证链表中节点的值互不相同 30 | - 若使用C或C++语言,你不需要`free`或`delete`被删除的节点 31 | 32 | # 解题思路 33 | 34 | > 使用双指针,一个指针用于定位要删除的节点,另一个指针用于修改节点间关系。 35 | 36 | 定义快慢指针`slow`和`fast` 37 | 38 | 1. **定位节点**:遍历链表,直到`fast.val == val`时跳出,即可定位到要删除的节点。 39 | 2. **修改连接关系**:在遍历链表时,总有`slow.next = fast`,当定位到节点时,执行`slow.next = fast.next`即可。 40 | 41 | ## 算法框架 42 | 43 | 1. **特例处理**:当链表头即为要删除的节点时,直接返回`head.next`即可。 44 | 2. **初始化快慢指针**:`slow = head`,`fast = head.next`。 45 | 3. **定位节点**:当`fast`为空或`fast`节点值等于`val`时跳出。 46 | - `slow = fast` 47 | - `fast = fast.next` 48 | 4. **修改连接关系**:执行`slow.next = fast.next` 49 | 5. **返回值**:返回`head`即可 50 | 51 | ## 复杂度分析 52 | 53 | - 时间复杂度:$O(N)$ 54 | - 空间复杂度:$O(1)$ 55 | 56 | ## 代码 57 | 58 | ```python 59 | # Definition for singly-linked list. 60 | # class ListNode: 61 | # def __init__(self, x): 62 | # self.val = x 63 | # self.next = None 64 | 65 | class Solution: 66 | def deleteNode(self, head: ListNode, val: int) -> ListNode: 67 | slow = head 68 | fast = head.next 69 | if head.val == val: 70 | return head.next 71 | 72 | while fast.val != val and fast: 73 | slow = fast 74 | fast = fast.next 75 | slow.next = fast.next 76 | return head 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /剑指offer系列/面试题09.用两个栈实现队列.md: -------------------------------------------------------------------------------- 1 | 用两个栈实现队列 2 | 3 | # 题目描述 4 | 5 | 用两个栈实现一个队列。队列的声明如下,请实现它的两个函数`appendTail`和`deleteHead`,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,`deleteHead`操作返回`-1`) 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: 11 | ["CQueue","appendTail","deleteHead","deleteHead"] 12 | [[],[3],[],[]] 13 | 输出:[null,null,3,-1] 14 | ``` 15 | 16 | ## 示例2 17 | 18 | ``` 19 | 输入: 20 | ["CQueue","deleteHead","appendTail","appendTail","deleteHead","deleteHead"] 21 | [[],[5],[2],[],[]] 22 | 输出:[null,-1,null,null,5,2] 23 | ``` 24 | 25 | ## 限制 26 | 27 | - `1 <= values <= 10000` 28 | - `最多会对appendTail、deleteHead进行10000次调用` 29 | 30 | # 解题思路 31 | 32 | ## 补充知识 33 | 34 | - **队列**:先进先出,即最先进队的元素也必定是最先出队的元素。 35 | - **栈**:先进后出,即最先进栈的元素必定是最后出栈的元素。 36 | 37 | ## 算法思路 38 | 39 | 维护两个栈,一个用于存储元素,一个用于辅助操作,具体如下: 40 | 41 | - 初始化两个列表作为两个栈,`stack1`和`stack2`; 42 | - 执行`appendTail`操作时,将`val`加入`stack1`中; 43 | - 执行`deleteHead`操作时,需要分三种情况判断: 44 | 1. 当`stack2`不为空时,`stack2`已存在倒序的元素,直接返回`stack2`末尾的元素(即栈顶元素); 45 | 2. 当`stack1`和`stack2`都为空时,直接返回`-1`; 46 | 3. 其他情况下,将`stack1`中的元素移至`stack2`中,实现元素的倒序,并返回`stack2`的末尾元素(即栈顶元素)。 47 | 48 | ## 代码 49 | 50 | ```python 51 | class CQueue: 52 | 53 | def __init__(self): 54 | self.stack1 = [] 55 | self.stack2 = [] 56 | def appendTail(self, value: int) -> None: 57 | self.stack1.append(value) 58 | 59 | def deleteHead(self) -> int: 60 | if self.stack2: 61 | return self.stack2.pop() 62 | if not self.stack1: 63 | return -1 64 | while self.stack1: 65 | self.stack2.append(self.stack1.pop()) 66 | return self.stack2.pop() 67 | 68 | # Your CQueue object will be instantiated and called as such: 69 | # obj = CQueue() 70 | # obj.appendTail(value) 71 | # param_2 = obj.deleteHead() 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /剑指offer系列/面试题42.连续子数组的最大和.md: -------------------------------------------------------------------------------- 1 | 连续子数组的最大和 2 | 3 | # 题目描述 4 | 5 | 输入一个整型数组,数组里有整数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。 6 | 7 | 要求时间复杂度为$O(n)$。 8 | 9 | ## 示例 10 | 11 | ``` 12 | 输入: nums = [-2,1,-3,4,-1,2,1,-5,4] 13 | 输出: 6 14 | 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 15 | ``` 16 | 17 | ## 提示 18 | 19 | - `1 <= arr.length <= 10^5` 20 | - `-100 <= arr[i] <= 100` 21 | 22 | # 解题思路 23 | 24 | | 常见解法 | 时间复杂度 | 空间复杂度 | 25 | | :------: | :--------: | :--------: | 26 | | 暴力搜索 | $O(N^2)$ | $O(1)$ | 27 | | 分治思想 | $O(NlogN)$ | $O(logN)$ | 28 | | 动态规划 | $O(N)$ | $O(1)$ | 29 | 30 | 动态规划是符合本题要求的解法。 31 | 32 | **动态规划解析**: 33 | 34 | - **状态定义**:设动态规划列表`dp`,`dp[i]`代表以元素`nums[i]`为结尾的连续子数组最大和。 35 | - **转移方程**:若`dp[i-1] <= 0`,说明`dp[i-1]`对`dp[i]`产生负贡献,即`dp[i-1]+nums[i]`还不如`nums[i]`本身大。 36 | - 当`dp[i-1] > 0`时:执行`dp[i] = dp[i-1] + nums[i]`; 37 | - 当`dp[i-1] <= 0`时:执行`dp[i] = nums[i]`; 38 | - **初始状态**:`dp[0] = nums[0]`,即以`nums[0]`结尾的连续子数组最大和为`nums[0]`。 39 | - **返回值**:返回`dp`列表中的最大值,代表全局最大值。 40 | 41 | **空间复杂度优化**: 42 | 43 | - 由于`dp[i]`只与`dp[i-1]`和`nums[i]`有关系,因此可以将原数组`nums`用作`dp`列表,即直接原地修改。 44 | - 这样就省去`dp`列表使用的空间,空间复杂度从$O(N)$降至$O(1)$。 45 | 46 | ## 复杂度分析 47 | 48 | - 时间复杂度:$O(N)$ 49 | - 空间复杂度:$O(1)$ 50 | 51 | ## 代码 52 | 53 | ```python 54 | class Solution: 55 | def maxSubArray(self, nums: List[int]) -> int: 56 | for i in range(1, len(nums)): 57 | nums[i] += max(nums[i-1], 0) 58 | return max(nums) 59 | 60 | # 用max_num来记录当前的最大值,这样可以省去最后计算的max()操作 61 | # class Solution: 62 | # def maxSubArray(self, nums: List[int]) -> int: 63 | # max_num = nums[0] 64 | # for i in range(1, len(nums)): 65 | # nums[i] += max(nums[i-1], 0) 66 | # if nums[i] > max_num: 67 | # max_num = nums[i] 68 | # return max_num 69 | ``` 70 | 71 | -------------------------------------------------------------------------------- /每日一题系列/542.01矩阵.md: -------------------------------------------------------------------------------- 1 | 01矩阵 2 | 3 | # 题目描述 4 | 5 | 给定一个由0和1组成的矩阵,找到每个元素到最近的0的距离。 6 | 7 | 两个相邻元素间的距离为1. 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入: 13 | 0 0 0 14 | 0 1 0 15 | 0 0 0 16 | 输出: 17 | 0 0 0 18 | 0 1 0 19 | 0 0 0 20 | ``` 21 | 22 | ## 示例2 23 | 24 | ``` 25 | 输入: 26 | 0 0 0 27 | 0 1 0 28 | 1 1 1 29 | 输出: 30 | 0 0 0 31 | 0 1 0 32 | 1 2 1 33 | ``` 34 | 35 | ## 注意 36 | 37 | - 给定矩阵的元素个数不超过10000. 38 | - 给定矩阵中至少有一个元素是0 39 | - 矩阵中的元素只在四个方向上相邻:上、下、左、右 40 | 41 | # 解题思路 42 | 43 | > 仍然是矩阵搜索的问题,且有关最短路径,考虑广度优先搜索。 44 | 45 | 将所有0的位置加入队列,以此作为"源"(即有多个起点)。 46 | 47 | 从各个0开始一圈一圈的向1扩散(每个1都是被离它最近的0扩散到)。要注意对已访问过的位置进行记录,否则会重复访问,导致运行超时。 48 | 49 | ## 复杂度分析 50 | 51 | > 设$N,M$分别为原矩阵的行数和列数。 52 | 53 | - 时间复杂度:$O(N+M)$ 54 | - 空间复杂度:$O(N+M)$ 55 | 56 | ## 代码 57 | 58 | ```python 59 | class Solution: 60 | def updateMatrix(self, matrix: List[List[int]]) -> List[List[int]]: 61 | if not matrix: 62 | return 63 | 64 | row = len(matrix) 65 | col = len(matrix[0]) 66 | 67 | zero = [(i, j) for i in range(row) for j in range(col) if matrix[i][j] == 0] 68 | queue = collections.deque(zero) 69 | visited = set(zero) 70 | moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 71 | 72 | while queue: 73 | cur_row, cur_col = queue.popleft() 74 | for move in moves: 75 | next_row = cur_row + move[0] 76 | next_col = cur_col + move[1] 77 | if 0 <= next_row < row and 0 <= next_col < col and (next_row, next_col) not in visited and matrix[next_row][next_col]: 78 | queue.append((next_row, next_col)) 79 | visited.add((next_row, next_col)) 80 | matrix[next_row][next_col] = matrix[cur_row][cur_col] + 1 81 | 82 | return matrix 83 | ``` 84 | 85 | -------------------------------------------------------------------------------- /剑指offer系列/面试题54.二叉搜索树的第k大节点.md: -------------------------------------------------------------------------------- 1 | 二叉搜索树的第k大节点 2 | 3 | # 题目描述 4 | 5 | 给定一棵二叉搜索树,请找出其中第k大的节点。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: root = [3,1,4,null,2], k = 1 11 | 3 12 | / \ 13 | 1 4 14 | \ 15 | 2 16 | 输出: 4 17 | ``` 18 | 19 | ## 示例2 20 | 21 | ``` 22 | 输入: root = [5,3,6,2,4,null,null,1], k = 3 23 | 5 24 | / \ 25 | 3 6 26 | / \ 27 | 2 4 28 | / 29 | 1 30 | 输出: 4 31 | ``` 32 | 33 | ## 限制 34 | 35 | - `1 <= k <=二叉搜索树元素个数` 36 | 37 | # 解题思路 38 | 39 | > 本题解法基于以下性质:二叉搜索树的中序遍历为**递增序列**。 40 | 41 | 由此,易知二叉搜索树的**中序遍历的倒序**即为**递减序列**。 42 | 43 | 因此,求二叉搜索树第`k`大节点即为求中序遍历的倒序的第`k`个节点。 44 | 45 | **递归解析**: 46 | 47 | - **终止条件**:当节点`root`为空(越过叶子节点),则直接返回; 48 | - **递归右子树**:即`dfs(root.right)`; 49 | - 执行以下**三项工作**: 50 | - 提前返回:若`k = 0`,代表意见找到目标节点,无需继续遍历,因此直接返回; 51 | - 统计序号:执行`k -= 1` 52 | - 记录结果:若`k = 0`,代表当前节点为第`k`打的节点,`res = root.val` 53 | - **递归左子树**:即`dfs(root.left)` 54 | 55 | ## 复杂度分析 56 | 57 | - 时间复杂度:$O(N)$,最坏情况下,树退化为链表(全部为右子节点,无论`k`的大小,递归深度都为$N$,占用$O(N)$时间)。 58 | - 空间复杂度:$O(N)$,树退化为链表(全部为右子节点,无论`k`的大小,递归深度都为$N$,占用$O(N)$的栈空间)。 59 | 60 | ## 代码 61 | 62 | ```python 63 | # class TreeNode: 64 | # def __init__(self, x): 65 | # self.val = x 66 | # self.left = None 67 | # self.right = None 68 | 69 | class Solution: 70 | def kthLargest(self, root: TreeNode, k: int) -> int: 71 | self.k = k 72 | def dfs(root): 73 | if not root: return 74 | dfs(root.right) 75 | if self.k == 0: return # 提前返回,也可以不在这里返回 76 | self.k -= 1 77 | if self.k == 0: self.res = root.val 78 | # 当得到res时,可以立即返回 79 | # if self.k == 0: 80 | # self.res = root.val 81 | # return 82 | dfs(root.left) 83 | dfs(root) 84 | return self.res 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /每日一题系列/面试题47.礼物的最大价值.md: -------------------------------------------------------------------------------- 1 | 礼物的最大价值 2 | 3 | # 题目描述 4 | 5 | 在一个`m x n`的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请你计算你最多能拿到多少价值的礼物? 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: 11 | [ 12 | [1,3,1], 13 | [1,5,1], 14 | [4,2,1] 15 | ] 16 | 输出: 12 17 | 解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物 18 | ``` 19 | 20 | ## 提示 21 | 22 | - `0 < grid.length <= 200` 23 | - `0 < grid[0].length <= 200` 24 | 25 | # 解题思路 26 | 27 | > 这是一道考察动态规划的题。 28 | 29 | 使用一个二维数组`dp[i][j]`表示,从左上角到达`(i, j)`所能拿到礼物的最大价值。根据题意,相应的转移方程也很容易得到,对于位置`(i, j)`而言,它有两种方式可以到达:1. 从位置`(i-1, j)`向下走一步到达`(i, j)`;2. 从位置`(i, j-1)`向右走一步到达`(i, j)`,由此我们只要选取两种方式中的最大值即可。(每一步取最大,也蕴含了贪心的思想) 30 | 31 | ## 复杂度分析 32 | 33 | > 设$M, N$为棋盘的行数和列数 34 | 35 | - 时间复杂度:$O(MN)$ 36 | - 空间复杂度:$O(MN)$,如果直接在数组`grid`原地修改,则空间复杂度为$O(1)$。 37 | 38 | ## 代码 39 | 40 | ```python 41 | # 创建额外的dp数组 42 | class Solution: 43 | def maxValue(self, grid: List[List[int]]) -> int: 44 | row = len(grid) 45 | col = len(grid[0]) 46 | dp = [[0 for _ in range(col+1)] for _ in range(row+1)] 47 | 48 | for i in range(1, row+1): 49 | for j in range(1, col+1): 50 | dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1] 51 | 52 | return dp[-1][-1] 53 | 54 | # 直接在grid上原地修改 55 | class Solution: 56 | def maxValue(self, grid: List[List[int]]) -> int: 57 | row = len(grid) 58 | col = len(grid[0]) 59 | for i in range(row): 60 | for j in range(col): 61 | if i == 0 and j == 0: continue 62 | if i == 0: 63 | grid[i][j] += grid[i][j-1] 64 | elif j == 0: 65 | grid[i][j] += grid[i-1][j] 66 | else: 67 | grid[i][j] += max(grid[i-1][j], grid[i][j-1]) 68 | 69 | return grid[-1][-1] 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /剑指offer系列/面试题47.礼物的最大价值.md: -------------------------------------------------------------------------------- 1 | 礼物的最大价值 2 | 3 | # 题目描述 4 | 5 | 在一个`m x n`的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请你计算你最多能拿到多少价值的礼物? 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: 11 | [ 12 | [1,3,1], 13 | [1,5,1], 14 | [4,2,1] 15 | ] 16 | 输出: 12 17 | 解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物 18 | ``` 19 | 20 | ## 提示 21 | 22 | - `0 < grid.length <= 200` 23 | - `0 < grid[0].length <= 200` 24 | 25 | # 解题思路 26 | 27 | > 这是一道考察动态规划的题。 28 | 29 | 使用一个二维数组`dp[i][j]`表示,从左上角到达`(i, j)`所能拿到礼物的最大价值。根据题意,相应的转移方程也很容易得到,对于位置`(i, j)`而言,它有两种方式可以到达:1. 从位置`(i-1, j)`向下走一步到达`(i, j)`;2. 从位置`(i, j-1)`向右走一步到达`(i, j)`,由此我们只要选取两种方式中的最大值即可。(每一步取最大,也蕴含了贪心的思想) 30 | 31 | ## 复杂度分析 32 | 33 | > 设$M, N$为棋盘的行数和列数 34 | 35 | - 时间复杂度:$O(MN)$ 36 | - 空间复杂度:$O(MN)$,如果直接在数组`grid`原地修改,则空间复杂度为$O(1)$。 37 | 38 | ## 代码 39 | 40 | ```python 41 | # 创建额外的dp数组 42 | class Solution: 43 | def maxValue(self, grid: List[List[int]]) -> int: 44 | row = len(grid) 45 | col = len(grid[0]) 46 | dp = [[0 for _ in range(col+1)] for _ in range(row+1)] 47 | 48 | for i in range(1, row+1): 49 | for j in range(1, col+1): 50 | dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + grid[i-1][j-1] 51 | 52 | return dp[-1][-1] 53 | 54 | # 直接在grid上原地修改 55 | class Solution: 56 | def maxValue(self, grid: List[List[int]]) -> int: 57 | row = len(grid) 58 | col = len(grid[0]) 59 | for i in range(row): 60 | for j in range(col): 61 | if i == 0 and j == 0: continue 62 | if i == 0: 63 | grid[i][j] += grid[i][j-1] 64 | elif j == 0: 65 | grid[i][j] += grid[i-1][j] 66 | else: 67 | grid[i][j] += max(grid[i-1][j], grid[i][j-1]) 68 | 69 | return grid[-1][-1] 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /剑指offer系列/面试题59-I.滑动窗口的最大值.md: -------------------------------------------------------------------------------- 1 | 滑动窗口的最大值 2 | 3 | # 题目描述 4 | 5 | 给定一个数组`nums`和滑动窗口的大小`k`,请找出滑动窗口里的最大值。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3 11 | 输出: [3,3,5,5,6,7] 12 | 解释: 13 | 14 | 滑动窗口的位置 最大值 15 | --------------- ----- 16 | [1 3 -1] -3 5 3 6 7 3 17 | 1 [3 -1 -3] 5 3 6 7 3 18 | 1 3 [-1 -3 5] 3 6 7 5 19 | 1 3 -1 [-3 5 3] 6 7 5 20 | 1 3 -1 -3 [5 3 6] 7 6 21 | 1 3 -1 -3 5 [3 6 7] 7 22 | ``` 23 | 24 | ## 提示 25 | 26 | - 你可以假设`k`总是有效的,在输入数组不为空的情况下,`1 <= k <= 输入数组的大小` 27 | 28 | # 解题思路 29 | 30 | 窗口对应的数据结构为**双端队列**,据题意需要窗口中的最大值,则可以使用**单调双端队列**来解决这个问题。 31 | 32 | **算法流程**: 33 | 34 | - **初始化**:双端队列`queue`,结果列表`res` 35 | - **初始窗口**:前`k`个元素进队,须保证队首元素始终为当前队列中的最大的元素(注意:这里的双端队列中并非始终保存着`k`个元素。例如,前`k`个元素为`[1, 2, 3, 4]`,则经过初始入队操作,队列中仅剩下`[4]`,即队首元素代表着`k`个元素中的最大值) 36 | - **滑动窗口**:从第`k + 1`个元素开始遍历,若此时从窗口滑出的元素为队首元素,则将队首元素出队;否则无需出队。向双端队列中加入元素时依然需要像**初始窗口**一样保持队首元素为当前窗口中的最大值。并将最大值加入`res`即可 37 | - **返回值**:返回`res` 38 | 39 | ## 复杂度分析 40 | 41 | - 时间复杂度:$O(N)$ 42 | - 空间复杂度:$O(K)$,队列中最多存在$K$个元素 43 | 44 | ## 代码 45 | 46 | ```python 47 | class Solution: 48 | def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: 49 | if not nums or not k: 50 | return [] 51 | 52 | queue = collections.deque() 53 | for i in range(k): 54 | while queue and queue[-1] < nums[i]: 55 | queue.pop() 56 | queue.append(nums[i]) 57 | 58 | res = [queue[0]] 59 | for i in range(k, len(nums)): 60 | if queue[0] == nums[i - k]: 61 | queue.popleft() 62 | while queue and queue[-1] < nums[i]: 63 | queue.pop() 64 | queue.append(nums[i]) 65 | res.append(queue[0]) 66 | return res 67 | ``` 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /剑指offer系列/面试题59-II.队列的最大值.md: -------------------------------------------------------------------------------- 1 | 队列的最大值 2 | 3 | # 题目描述 4 | 5 | 请定义一个队列并实现函数`max_value`得到队列里的最大值,要求函数`max_value`、`push_back`和`pop_front`的**均摊时间复杂度**都是$O(1)$。 6 | 7 | 若队列为空,`pop_front`和`max_value`需要返回`-1`。 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入: 13 | ["MaxQueue","push_back","push_back","max_value","pop_front","max_value"] 14 | [[],[1],[2],[],[],[]] 15 | 输出: [null,null,null,2,1,2] 16 | ``` 17 | 18 | ## 示例2 19 | 20 | ``` 21 | 输入: 22 | ["MaxQueue","pop_front","max_value"] 23 | [[],[],[]] 24 | 输出: [null,-1,-1] 25 | ``` 26 | 27 | ## 限制 28 | 29 | - `1 <= push_back, pop_front, max_value的总操作数 <= 10000` 30 | - `1 <= value <= 10^5` 31 | 32 | # 解题思路 33 | 34 | 根据题意,使用一个**双端队列**可以使`push_back`和`pop_front`轻松满足要求,而对于`max_value`而言,借鉴[面试题59-I.滑动窗口的最大值](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题59-I.滑动窗口的最大值.md),再利用一个**单调队列**来解决(即在元素入队时,始终使得单调队列队首的元素为当前另一个**双端队列**中所有元素的最大值) 35 | 36 | ## 复杂度分析 37 | 38 | - 时间复杂度:$O(1)$ 39 | - 空间复杂度:$O(N)$ 40 | 41 | ## 代码 42 | 43 | ```python 44 | class MaxQueue: 45 | 46 | def __init__(self): 47 | from collections import deque 48 | self.deque = deque() 49 | self.queue = deque() 50 | 51 | def max_value(self) -> int: 52 | return self.deque[0] if self.deque else -1 53 | 54 | def push_back(self, value: int) -> None: 55 | while self.deque and self.deque[-1] < value: 56 | self.deque.pop() 57 | self.deque.append(value) 58 | self.queue.append(value) 59 | 60 | def pop_front(self) -> int: 61 | if not self.queue: return -1 62 | res = self.queue.popleft() 63 | if res == self.deque[0]: 64 | self.deque.popleft() 65 | return res 66 | 67 | 68 | 69 | # Your MaxQueue object will be instantiated and called as such: 70 | # obj = MaxQueue() 71 | # param_1 = obj.max_value() 72 | # obj.push_back(value) 73 | # param_3 = obj.pop_front() 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /剑指offer系列/面试题65.不用加减乘除做加法.md: -------------------------------------------------------------------------------- 1 | 不用加减乘除做加法 2 | 3 | # 题目描述 4 | 5 | 写一个函数,求两个整数之和,要求在函数体内不得使用`"+", "-", "*", "/"`四则运算符号。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入: a = 1, b = 1 11 | 输出: 2 12 | ``` 13 | 14 | ## 提示 15 | 16 | - `a, b`均可能是负数或`0` 17 | - 结果不会溢出32位整数 18 | 19 | # 解题思路 20 | 21 | 本题考察对位运算的灵活使用,即使用位运算实现加法。 22 | 23 | 设两数字的二进制形式`a, b`,其求和` s = a + b`,`a(i)`代表`a`的二进制第`i`位,则分以下四种情况: 24 | 25 | | `a(i)` | `b(i)` | 无进位和`n(i)` | 进位`c(i+1)` | 26 | | :----: | :----: | :------------: | :----------: | 27 | | 0 | 0 | 0 | 0 | 28 | | 0 | 1 | 1 | 0 | 29 | | 1 | 0 | 1 | 0 | 30 | | 1 | 1 | 0 | 1 | 31 | 32 | 观察发现,**无进位和**与**异或运算**规律相同,**进位**和**与运算**规律相同(并需左移一位)。因此,无进位和`n`与进位`c`的计算如下: 33 | $$ 34 | n = a\oplus b 35 | $$ 36 | 37 | $$ 38 | c = a\& b << 1 39 | $$ 40 | 41 | 和`s`等于无进位和`n`加上进位`c`,即`s = a + b --> s = n + c` 42 | 43 | 循环求`n`和`c`,直至`c = 0`,此时`s = n`,返回`n`即可 44 | 45 | >Q:若数字`a`和`b`中存在负数,则变成了减法,如何处理? 46 | > 47 | >A:在计算机系统中,数值一律用**补码**来表示和存储。**补码的优势**:加法、减法可以统一处理(CPU只有加法器)。因此,以上方法**同时适用于正数和负数的加法**。 48 | 49 | ## 复杂度分析 50 | 51 | - 时间复杂度:$O(1)$,最坏情况需循环31次 52 | - 空间复杂度:$O(1)$ 53 | 54 | ## 补充:Python负数的存储 55 | 56 | > 由于Python的数字存储特点,需要做一些特殊处理,以下详细介绍。 57 | 58 | Python / Java中的数字都是以**补码**形式存储的。但Python没有`int`,`long`等不同长度变量,即没有变量位数的概念。 59 | 60 | **获取负数的补码**:需要将数字与十六进制数`0xffffffff`相与。可以理解为舍去数字`32`位以上的数字,从无线长度变为一个`32`位整数。 61 | 62 | **返回前数字还原**:若补码`a`为负数(`0x7fffffff`是最大的正数的补码),需执行`~(a^x)`操作,将补码还原至Python的存储格式。`a^x`运算将`1`至`32`位按位取反;`~`运算将整个数字取反;因此,`~(a^x)`是将`32`位以上的位取反,即由`0`变为`1`,`1`至`32`位不变。 63 | 64 | ## 代码 65 | 66 | ```python 67 | class Solution: 68 | def add(self, a: int, b: int) -> int: 69 | x = 0xffffffff 70 | a, b = a & x, b & x 71 | while b != 0: 72 | a, b = (a ^ b), (a & b) << 1 & x 73 | return a if a <= 0x7fffffff else ~(a ^ x) 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /剑指offer系列/面试题61.扑克牌中的顺子.md: -------------------------------------------------------------------------------- 1 | 扑克牌中的顺子 2 | 3 | # 题目描述 4 | 5 | 从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为0,可以看成任意数字。A不能视为14。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: [1,2,3,4,5] 11 | 输出: True 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: [0,0,1,2,5] 18 | 输出: True 19 | ``` 20 | 21 | ## 限制 22 | 23 | - 数组长度为`5` 24 | - 数组的数取值为`[0, 13]` 25 | 26 | # 解题思路 27 | 28 | 根据题意,此`5`张牌是顺子的**充分条件**如下: 29 | 30 | - 除大小王外,其余所有牌**无重复**; 31 | - 设此`5`张牌中最大的牌为`max`,最小的牌为`min`(大小王除外),则应满足:`max - min < 5` 32 | 33 | 因此,可以把问题转化为:此`5`张牌是否同时满足以上两个条件。 34 | 35 | ## 方法一:集合 + 遍历 36 | 37 | **算法流程**: 38 | 39 | - 遍历五张牌,遇到大小王(即0)直接跳过。 40 | - **判断是否有重复牌**:利用集合`set`实现遍历判重; 41 | - **获取最大/最小牌**:借助辅助变量`mx, mn`遍历统计即可。 42 | 43 | ## 复杂度分析 44 | 45 | - 时间复杂度:$O(N)=O(5)=O(1)$ 46 | - 空间复杂度:$O(N)=O(5)=O(1)$,`set`集合的额外空间 47 | 48 | ## 代码 49 | 50 | ```python 51 | class Solution: 52 | def isStraight(self, nums: List[int]) -> bool: 53 | repeat = set() 54 | ma, mi = 0, 14 55 | for num in nums: 56 | if num == 0: continue # 跳过大小王 57 | ma = max(ma, num) # 最大牌 58 | mi = min(mi, num) # 最小牌 59 | if num in repeat: return False # 若有重复,提前返回 false 60 | repeat.add(num) # 添加牌至 Set 61 | return ma - mi < 5 # 最大牌 - 最小牌 < 5 则可构成顺子 62 | ``` 63 | 64 | ## 方法二:排序 + 遍历 65 | 66 | **算法流程**: 67 | 68 | - 先对数组进行排序 69 | - 排序后的数组可以通过判断前后两个元素是否相同来判断是否存在重复元素 70 | - 其余与方法一一致 71 | 72 | ## 复杂度分析: 73 | 74 | - 时间复杂度:$O(N\log N)=O(5\log 5)=O(1)$ 75 | - 空间复杂度:$O(1)$ 76 | 77 | ## 代码 78 | 79 | ```python 80 | class Solution: 81 | def isStraight(self, nums: List[int]) -> bool: 82 | joker = 0 83 | nums.sort() # 数组排序 84 | for i in range(4): 85 | if nums[i] == 0: joker += 1 # 统计大小王数量 86 | elif nums[i] == nums[i + 1]: return False # 若有重复,提前返回 false 87 | return nums[4] - nums[joker] < 5 # 最大牌 - 最小牌 < 5 则可构成顺子 88 | ``` 89 | 90 | -------------------------------------------------------------------------------- /剑指offer系列/面试题10-I.斐波那契数列.md: -------------------------------------------------------------------------------- 1 | 斐波那契数列 2 | 3 | # 题目描述 4 | 5 | 写一个函数,输入`n`,求斐波那契数列的第`n`项。斐波那契数列的定义如下: 6 | 7 | ``` 8 | F(0) = 0, F(1) = 1 9 | F(N) = F(N-1)+F(N-2), 其中N > 1. 10 | ``` 11 | 12 | 斐波那契数列由0和1开始,之后的斐波那契数就是有之前的两数相加而得出。 13 | 14 | 答案需要取模`1e9+7(1000000007)`,如计算初始结果为1000000008,请返回 1。 15 | 16 | ## 示例1 17 | 18 | ``` 19 | 输入: n = 2 20 | 输出: 1 21 | ``` 22 | 23 | ## 示例2 24 | 25 | ``` 26 | 输入:n = 5 27 | 输出:5 28 | ``` 29 | 30 | ## 提示 31 | 32 | - `0 <= n <= 100` 33 | 34 | # 解题思路 35 | 36 | 斐波那契数列的定义是$f(n+1)=f(n)+f(n-1)$,生成第$n$项的做法有以下几种: 37 | 38 | 1. **递归法**: 39 | 40 | - **原理**:把$f(n)$问题的计算拆分成$f(n-1)$和$f(n-2)$两个子问题的计算,并递归,以$f(0)$和$f(1)$为终止条件。 41 | - **缺点**:大量重复的递归计算,例如$f(n)$和$f(n-1)$两者向下递归时都需要各自计算$f(n-2)$的值。 42 | 43 | 重复计算,如下图所示(帮助理解): 44 | 45 | ![重复计算.jpg](https://i.loli.net/2020/05/02/6zgG3iEwjN49oJR.jpg) 46 | 47 | 2. **记忆化递归法**: 48 | - **原理**:在递归法的基础上,新建一个长度为$n$的数组,用于在递归时存储$f(0)$至$f(n)$的数值,重复遇到某数字则直接从数组取用,避免了重复的递归计算。 49 | - **缺点**:记忆化存储需要使用$O(N)$的额外空间。 50 | 51 | 3. **动态规划**: 52 | - **原理**:以斐波那数列性质$f(n+1)=f(n)+f(n-1)$为转移方程。 53 | - 从计算效率、空间复杂度上看,动态规划是本题的最佳解法。 54 | 55 | ## 动态规划解析 56 | 57 | - 状态定义:设$dp$为一位数组,其中$dp[i]$的值代表斐波那契数列第$i$个数字。 58 | - 转移方程:$dp[i+1]=dp[i]+dp[i-1]$,即对应数列定义$f(n+1)=f(n)+f(n-1)$。 59 | - 初始状态:$dp[0]=0$,$dp[1]=1$,即初始化前两个数字。 60 | - 返回值:$dp[n]$,即斐波那契数列的第$n$个数字。 61 | 62 | ## 空间复杂度优化 63 | 64 | > 若新建长度为$n$的$dp$列表,则空间复杂度为$O(N)$。 65 | 66 | - 由于$dp$列表第$i$项只与第$i-1$和第$i-2$项有关,因此只需要初始化三个整形变量`sum`,`a`,`b`,利用辅助变量`sum`使`a,b`两个数字交替前进即可。 67 | - 节省了$dp$列表空间,空间复杂度降至$O(1)$。 68 | 69 | ## 复杂度分析 70 | 71 | - 时间复杂度:$O(N)$,计算$f(n)$需循环$n$次,每轮循环内计算操作使用$O(1)$。 72 | - 空间复杂度:$O(1)$。 73 | 74 | ## 代码 75 | 76 | > 由于Python中整形数字的大小限制取决于计算机的内存(可理解为无限大),因此可以不考虑大数越界问题。 77 | 78 | ```python 79 | class Solution: 80 | def fib(self, n: int) -> int: 81 | a = 0 82 | b = 1 83 | for _ in range(n): 84 | a, b = b, b+a 85 | return a %(1000000007) 86 | ``` 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /每日一题系列/1105.填充书架.md: -------------------------------------------------------------------------------- 1 | 填充书架 2 | 3 | # 题目描述 4 | 5 | 附近的家居城促销,你买回了一直心仪的可调节书架,打算把自己的书都整理到新的书架上。你把要摆的书`books`都整理好,叠成 一摞:从上往下,第`i`本书的厚度为`books[i][0]`,高度为`books[i][1]`。**按顺序**将这些书摆放到总宽度为`shelf_width`的书架上。 6 | 7 | 先选几本书放到书架上(它们的厚度之和小于等于书架的宽度`shelf_width`),然后再建一层书架。重复这个过程,直到把所有的书都放在书架上。 8 | 9 | 需要注意的是,在上述过程的每个步骤中,**摆放书的顺序与你整理好的顺序相同**。例如,如果这有`5`本书,那么可能的一种摆放情况是:第一和第二本书放在第一层书架上,第三本书放在第二层书架上,第四本和第五本书放在最后一层书架上。 10 | 11 | 每一层所摆放的书的最大高度就是这一层书架的层高,书架整体的高度为各层高度之和。 12 | 13 | 以这种方式布置书架,返回书架整体可能的最小高度。 14 | 15 | ## 示例 16 | 17 | 示例.jpg 18 | 19 | ``` 20 | 输入:books = [[1,1],[2,3],[2,3],[1,1],[1,1],[1,1],[1,2]], shelf_width = 4 21 | 输出:6 22 | 解释: 23 | 3 层书架的高度和为 1 + 3 + 2 = 6 。 24 | 第 2 本书不必放在第一层书架上。 25 | ``` 26 | 27 | ## 提示 28 | 29 | - `1 <= books.length <= 1000` 30 | - `1 <= books[i][0] <= shelf_width <= 1000` 31 | - `1 <= books[i][1] <= 1000` 32 | 33 | # 解题思路 34 | 35 | 利用动态规划求解,`dp[i]`表示放置前`i`本书所需要的书架最小高度,初始值`dp[0] = 0`,其他设置为最大值`1000 * 1000`(每层一本书)。遍历每一本书,把当前这本书作为书架最后一层的最后一本书(即单独放一层),调整之前的所有书(书的前后顺序保持不变),查看是否可以减少之前的书架高度。状态转移方程为`dp[i] = min(dp[i], dp[j-1][1] + h)`,其中`j`表示最后一层所能容下书籍的索引,`h`表示最后一层最大高度。 36 | 37 | ## 复杂度分析 38 | 39 | - 时间复杂度:$O(N^2)$,最坏情况,`shelf_width`非常大,以致于可以将所有的书放在同一层。 40 | - 空间复杂度:$O(N)$ 41 | 42 | ## 代码 43 | 44 | ```python 45 | class Solution: 46 | def minHeightShelves(self, books: List[List[int]], shelf_width: int) -> int: 47 | n = len(books) 48 | max_num = 1000 * 1000 49 | dp = [max_num] * (n+1) 50 | dp[0] = 0 51 | 52 | for i in range(1, n+1): 53 | tmp_width, j, h = 0, i, 0 54 | while j > 0: 55 | tmp_width += books[j-1][0] 56 | if tmp_width > shelf_width: 57 | break 58 | h = max(h, books[j-1][1]) 59 | dp[i] = min(dp[i], dp[j-1] + h) 60 | j -= 1 61 | return dp[-1] 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /每日一题系列/1276.不浪费原料的汉堡制作方案.md: -------------------------------------------------------------------------------- 1 | 不浪费原料的汉堡制作方案 2 | 3 | # 题目描述 4 | 5 | 圣诞节活动预热开始啦,汉堡店推出了全新的汉堡套餐。为了避免浪费原料,请你帮他们制定合适的制作计划。 6 | 7 | 给你两个整数`tomatoSlices`和`cheeseSlices`,分别表示番茄片和奶酪片的数目。不同汉堡的原料搭配如下: 8 | 9 | - **巨无霸汉堡**:4片番茄和1片奶酪 10 | - **小皇堡**:2片番茄和1片奶酪 11 | 12 | 请你以`[total_jumbo, total_small]`([巨无霸汉堡总述,小皇堡总数])的格式返回恰当的制作方案,使得剩下的番茄片`tomatoSlices`和奶酪片`cheeseSlices`的数量都是0。 13 | 14 | 如果无法使剩下的番茄片`tomatoSlices`和奶酪片`cheeseSlices`的数量为0,就请返回`[]`。 15 | 16 | ## 示例1 17 | 18 | ``` 19 | 输入:tomatoSlices = 16, cheeseSlices = 7 20 | 输出:[1,6] 21 | 解释:制作1个巨无霸汉堡和6个小皇堡需要4*1+2*6=16片番茄片和1+6=7片奶酪。不会剩下原料。 22 | ``` 23 | 24 | ## 示例2 25 | 26 | ``` 27 | 输入:tomatoSlices = 17, cheeseSlices = 4 28 | 输出:[] 29 | 解释:至制作小皇堡或巨无霸汉堡无法用光全部原料。 30 | ``` 31 | 32 | ## 示例3 33 | 34 | ``` 35 | 输入:tomatoSlices = 0, cheeseSlices = 0 36 | 输出:[0,0] 37 | ``` 38 | 39 | # 解题思路 40 | 41 | 很显然,这是一个在一定约束条件下求解二元一次方程组的问题。 42 | 43 | 假设$total\_jumbo=x$;$total\_small=y$。根据题意可以列出以下方程组: 44 | $$ 45 | \begin{equation} 46 | \begin{cases} 47 | 4\times x+2\times y=tomatoSlices\\ 48 | x+y=cheeseSlices 49 | \end{cases} 50 | \end{equation} 51 | $$ 52 | 很容易求解得到: 53 | $$ 54 | \begin{equation} 55 | \begin{cases} 56 | x=\frac{(tomatoSlices-cheeseSlices\times 2)}{2}\\ 57 | y=cheeseSlices - x 58 | \end{cases} 59 | \end{equation} 60 | $$ 61 | 约束条件如下: 62 | $$ 63 | \begin{equation} 64 | \begin{cases} 65 | tomatoSlices必须是偶数\\ 66 | x,y均为非负整数 67 | \end{cases} 68 | \end{equation} 69 | $$ 70 | 71 | ## 代码 72 | 73 | ```python 74 | class Solution: 75 | def numOfBurgers(self, tomatoSlices: int, cheeseSlices: int) -> List[int]: 76 | if tomatoSlices % 2 != 0: 77 | return [] 78 | total_jumbo = (tomatoSlices - cheeseSlices*2)//2 79 | total_small = cheeseSlices - total_jumbo 80 | if total_jumbo < 0 or total_small < 0 or total_small != int(total_small) or total_jumbo != int(total_jumbo): 81 | return [] 82 | else: 83 | return [total_jumbo, total_small] 84 | 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /每日一题系列/1007.行相等的最少多米诺旋转.md: -------------------------------------------------------------------------------- 1 | 行相等的最少多米诺旋转 2 | 3 | # 题目描述 4 | 5 | 在一排多米诺骨牌中,`A[i]`和`B[i]`分别代表第`i`个多米诺骨牌的上半部分和下半部分。(一个多米诺是两个从1到6的数字同列平铺形成的——该平铺的每一半上都有一个数字。) 6 | 7 | 我们可以旋转第`i`张多米诺,使得`A[i]`和`B[i]`的值交换。 8 | 9 | 返回能使`A`中所有值或者`B`中所有值都相同的最小旋转次数。 10 | 11 | 如果无法做到,返回`-1`。 12 | 13 | ## 示例1 14 | 15 | 示例.jpg 16 | 17 | ``` 18 | 输入:A = [2,1,2,4,2,2]; B = [5,2,6,2,3,2] 19 | 输出:2 20 | 解释:结合上图,在旋转之前,A和B给出的多米诺牌。如果我们旋转第二个和第四个多米诺牌,可以使上面一行中每个值都等于2 21 | ``` 22 | 23 | ## 示例2 24 | 25 | ``` 26 | 输入:A = [3,5,1,2,3]; B = [3,6,3,3,4] 27 | 输出:-1 28 | 解释:在这种情况下,不可能旋转多米诺牌使任何一行的值相等。 29 | ``` 30 | 31 | ## 限制 32 | 33 | - `1 <= A[i],B[i] <= 6` 34 | - `2 <= A.length == B.length <= 20000` 35 | 36 | # 解题思路 37 | 38 | 对题目中所考虑的一组多米诺牌,会出现三种情况: 39 | 40 | 1. 以数字`A[0]`为基准,将`A`或`B`中的所有值都变为`A[0]`。 41 | 2. 以数字`B[0]`为基准,将`A`或`B`中所有值都变为`B[0]`。 42 | 3. 无论选择`A[0]`还是`B[0]`都没办法将`A`或`B`中的所有值都变为相同。 43 | 44 | 算法思路: 45 | 46 | - 选择第一块多米诺牌,包含两个数字`A[0]`和`B[0]`; 47 | - 检查其余的多米诺骨牌中是否出现过 `A[0]`。如果都出现过,则求出最少的翻转次数,其为将` A[0]` 全部翻到 `A` 和全部翻到 `B`中的较少的次数。 48 | - 检查其余的多米诺骨牌中是否出现过 `B[0]`。如果都出现过,则求出最少的翻转次数,其为将 `B[0]` 全部翻到 `A` 和全部翻到 `B` 中的较少的次数。 49 | - 如果上述两次检查都失败,则返回 `-1`。 50 | 51 | ## 代码 52 | 53 | ```python 54 | class Solution: 55 | def minDominoRotations(self, A: List[int], B: List[int]) -> int: 56 | def check(x): 57 | rotate_A = 0 58 | rotate_B = 0 59 | for i in range(num): 60 | if A[i] != x and B[i] != x: 61 | return -1 62 | elif A[i] != x: 63 | rotate_A += 1 64 | elif B[i] != x: 65 | rotate_B += 1 66 | return min(rotate_A, rotate_B) 67 | num = len(A) 68 | res = check(A[0]) 69 | if res != -1 or A[0] == B[0]: 70 | return res 71 | else: 72 | return check(B[0]) 73 | ``` 74 | 75 | -------------------------------------------------------------------------------- /每日一题系列/1405.最长快乐字符串.md: -------------------------------------------------------------------------------- 1 | 最长快乐字符串 2 | 3 | # 题目描述 4 | 5 | 如果字符串不含有任何`aaa`,`bbb`或`ccc`这样的字符串作为子串,那么该字符串就是一个"快乐字符串"。 6 | 7 | 给你三个整数`a`,`b`,`c`请你返回**任意一个**满足下列全部条件的字符串`s`: 8 | 9 | - `s`是一个尽可能长的快乐字符串。 10 | - `s`中**最多**有`a`个字母`"a"`、`b`个字母`"b"`、`c`个字母`"c"`。 11 | - `s`中只含有`"a"`、`"b"`、`"c"`三种字母。 12 | 13 | 如果不存在这样的字符串`s`,请返回空字符串`""`。 14 | 15 | ## 示例1 16 | 17 | ``` 18 | 输入:a = 1, b = 1, c = 7 19 | 输出:"ccaccbcc" 20 | 解释:"ccbccacc" 也是一种正确答案。 21 | ``` 22 | 23 | ## 示例2 24 | 25 | ``` 26 | 输入:a = 2, b = 2, c = 1 27 | 输出:"aabbc" 28 | ``` 29 | 30 | ## 示例3 31 | 32 | ``` 33 | 输入:a = 7, b = 1, c = 0 34 | 输出:"aabaa" 35 | 解释:这是该测试用例的唯一正确答案。 36 | ``` 37 | 38 | ## 提示 39 | 40 | - `0 <= a,b <= 100` 41 | - `a + b + c > 0` 42 | 43 | # 解题思路 44 | 45 | > 本题也是用贪心思路求解的一道题。 46 | 47 | 算法的思路其实很简单,具体的实现方式也可以多种多样。在往字符串`s`添加字母时,优先添加当前字母库中数量最多的字母,若添加数量最多的字母会导致出现`aaa`、`bbb`或`ccc`的情况,则改为添加次多的字母,依此循环。 48 | 49 | 此外,字母`"a"`、`"b"`、`"c"`的数量存在相互牵制的关系,例如,假设有$a>b>c$,则能够在字符串`s`中出现字母`"a"`的最大数量为$2\times(b+c)$。 50 | 51 | 由此,我们可以在初始化字母数量时,进行一定的修正。 52 | 53 | ## 复杂度分析 54 | 55 | - 时间复杂度:$O(N)$ 56 | - 空间复杂度:$O(N)$ 57 | 58 | ## 代码 59 | 60 | ```python 61 | class Solution: 62 | def longestDiverseString(self, a: int, b: int, c: int) -> str: 63 | # 更新初始存量,根据插空规则修正单个字符最大可能的数量 64 | d = {'a':min(a,2*(b+c+1)),'b':min(b,2*(a+c+1)),'c':min(c,2*(b+a+1))} 65 | # 修正后的数量确保可以全部用在结果中,求和计算字符串总长 66 | n = sum(d.values()) 67 | # 维护结果列表 68 | res = [] 69 | # 单次插入一个字符,根据长度循环 70 | for _ in range(n): 71 | # 候选的字母 72 | cand = set(['a','b','c']) 73 | # 如果列表最后两个字符相同,根据规则不能插入连续三个,故将该字符从候选中删除 74 | if len(res)>1 and res[-1]==res[-2]: 75 | cand.remove(res[-1]) 76 | # 贪心,在候选中选择存量最大的字符 77 | tmp = max(cand,key=lambda x:d[x]) 78 | # 将它加到结果里 79 | res.append(tmp) 80 | # 把它的剩余计数减去1. 开始下一轮 81 | d[tmp] -= 1 82 | return ''.join(res) 83 | 84 | ``` 85 | 86 | -------------------------------------------------------------------------------- /剑指offer系列/面试题15.二进制中1的个数.md: -------------------------------------------------------------------------------- 1 | 二进制中1的个数 2 | 3 | # 题目描述 4 | 5 | 请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如,把9表示成二进制是1001,有2位是1。因此,如果输入9,则该函数输出2。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:00000000000000000000000000001011 11 | 输出:3 12 | 解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。 13 | ``` 14 | 15 | ## 示例2 16 | 17 | ``` 18 | 输入:00000000000000000000000010000000 19 | 输出:1 20 | 解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。 21 | ``` 22 | 23 | ## 示例3 24 | 25 | ``` 26 | 输入:11111111111111111111111111111101 27 | 输出:31 28 | 解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。 29 | ``` 30 | 31 | # 解题思路 32 | 33 | > 本题显然是与位运算有关。 34 | 35 | ## 补充知识 36 | 37 | - **与运算**:&,设有二进制数字$n$,则有: 38 | - 若`n & 1 = 0`,则$n$二进制**最右一位**为0; 39 | - 若`n & 1 = 1`,则$n$二进制**最右一位**为1。 40 | 41 | ## 方法一:逐位判断 42 | 43 | 根据与运算的特点,考虑一下**循环判断**: 44 | 45 | 1. 判断$n$最右一位是否为1,根据结果计数。 46 | 2. 将$n$右移一位(本题需要把数字$n$看作无符号数,因此使用**无符号右移**操作)。 47 | 48 | ## 复杂度分析 49 | 50 | - 时间复杂度:$O(log(N))$,此算法循环内部仅有**移位**、**与运算**、**加法**等基本运算,占用$O(1)$;逐位判断需循环$logN$次,其中$logN$代表数字$n$最高位1的所在位数。 51 | - 空间复杂度:$O(1)$ 52 | 53 | ## 代码 54 | 55 | ```python 56 | class Solution: 57 | def hammingWeight(self, n: int) -> int: 58 | res = 0 59 | while n: 60 | res += n & 1 61 | n >>= 1 62 | return res 63 | ``` 64 | 65 | ## 方法二:巧用`n & (n-1)` 66 | 67 | - $(n-1)$解析:二进制数字$n$最右边的1变成0,此1右边的0都变成1. 68 | - `n & (n-1)`​解析:二进制数字$n$最右边的1变成0,其余不变。 69 | 70 | 附图解释: 71 | 72 | 位运算.jpg 73 | 74 | 这样就可以变相得用`n & (n-1)`代替移位操作 75 | 76 | ## 复杂度分析 77 | 78 | - 时间复杂度:$O(M)$,`n & (n-1)`操作仅有减法和与运算,占用$O(1)$;设$M$为二进制数字$n$中1的个数,则需循环$M$次(每轮消去一个1),占用$O(M)$。 79 | - 空间复杂度:$O(1)$。 80 | 81 | ## 代码 82 | 83 | ```python 84 | class Solution: 85 | def hammingWeight(self, n: int) -> int: 86 | res = 0 87 | while n: 88 | res += n & 1 89 | n &= n-1 90 | return res 91 | ``` 92 | 93 | -------------------------------------------------------------------------------- /剑指offer系列/面试题62.圆圈中最后剩下的数字.md: -------------------------------------------------------------------------------- 1 | 圆圈中最后剩下的数字 2 | 3 | # 题目描述 4 | 5 | `0, 1, ..., n-1`这`n`个数字排成一个圆圈,从数字`0`开始,每次从这个圆圈里删除第`m`个数字。求出这个圆圈里剩下的最后一个数字。 6 | 7 | 例如,`0, 1, 2, 3, 4`这`5`个数字组成一个圆圈,从数字`0`开始每次删除第`3`个数字,则删除的前`4`个数字依次是`2, 0, 4, 1`,因此最后剩下的数字是`3`。 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入: n = 5, m = 3 13 | 输出: 3 14 | ``` 15 | 16 | ## 示例2 17 | 18 | ``` 19 | 输入: n = 10, m = 17 20 | 输出: 2 21 | ``` 22 | 23 | ## 限制 24 | 25 | - `1 <= n <= 10^5` 26 | - `1 <= m <= 10^6` 27 | 28 | # 解题思路 29 | 30 | **重点提示**:仅关注最终留下的数字的**索引号**的变化情况。 31 | 32 | 这个问题实际上是约瑟夫问题,这个为的描述是: 33 | 34 | > N个人围成一圈,第一个人从1开始报数,报M的将被杀掉,下一个人接着从1开始报。如此反复,最后剩下一个,求最后的胜利者。 35 | 36 | **问题转换** 37 | 38 | 由于索引号是数字,然后每个元素也是数字,可能在解释的时候会引起混淆,我们使用**字母**来解释,本质当然是一样的。 39 | 40 | 下面这个例子是`N = 8, M = 3`的例子。 41 | 42 | 我们定义`f(n, m)`表示最后剩下那个**字母**的`索引号`,我们只关心最后剩下的字母的索引号的变化情况。 43 | 44 | ![约瑟夫环1](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/约瑟夫环1.png) 45 | 46 | 从8个字母开始,每次删去一个字母,然后把删去字母的后一个字母作为开头重新编号 47 | 48 | - 第一次`C`被删去,总数变为`7`,`D`作为开头(最终剩下的`G`的索引号从`6`变成`3`) 49 | - 第二次`F`被删去,总数变为`6`,`G`作为开头(最终剩下的`G`的索引号从`3`变成`0`) 50 | - 第三次`A`被删去,总数变为`5`,`B`作为开头(最终剩下的`G`的索引号从`0`变成`3`) 51 | - 依次类推,当只剩下`G`时,它的索引号必定是`0` 52 | 53 | **从结果反推** 54 | 55 | 根据上图我们知道了`G`的索引号变化过程,我们来反推一下从`N = 7`到`N = 8`的过程。 56 | 57 | 我们先把`C`补回来,然后右移`m`个字母,发现溢出了,再把溢出的补充在最前面,这样就能反推。如下图所示。 58 | 59 | ![约瑟夫环2](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/约瑟夫环2.png) 60 | 61 | 因此,我们可以得到递推公式`f(8, 3) = (f(7, 3) + 3) % 8` 62 | 63 | 推广到一般式,即`f(n, m) = (f(n-1, m) + m) % n` 64 | 65 | 边界情况,`n = 1`时,`f(n, m) = 0` 66 | 67 | ## 复杂度分析 68 | 69 | - 时间复杂度:$O(N)$ 70 | - 空间复杂度:$O(1)$,若使用递归方法求解,则空间复杂度为$O(N)$;若使用迭代方法求解则为$O(1)$ 71 | 72 | ## 代码 73 | 74 | ```python 75 | class Solution: 76 | def lastRemaining(self, n: int, m: int) -> int: 77 | # 共n个数字,最终只剩1个 78 | # 每一次删除1个数字 79 | # 即要删除(n-1)次 80 | # 每一次的跨度为m 81 | # 仅关注最后剩下的元素的索引号变化情况 82 | f = 0 83 | for i in range(2, n+1): 84 | f = (m + f) % i 85 | 86 | return f 87 | ``` 88 | 89 | -------------------------------------------------------------------------------- /剑指offer系列/面试题38.字符串的排列.md: -------------------------------------------------------------------------------- 1 | 字符串的排列 2 | 3 | # 题目描述 4 | 5 | 输入一个字符串,打印出该字符串中字符的所有排列。 6 | 7 | 你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。 8 | 9 | ## 示例 10 | 11 | ``` 12 | 输入:s = "abc" 13 | 输出:["abc","acb","bac","bca","cab","cba"] 14 | ``` 15 | 16 | ## 限制 17 | 18 | - `1 <= s的长度 <= 8` 19 | 20 | # 解题思路 21 | 22 | **排列方案数量**:对于一个长度为`n`的字符串(假设字符互不重复),其排列共有$n\times(n-1)\times(n-2)...\times 2\times 1$种方案。 23 | 24 | **排列方案的生成方法**:根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过**字符交换**,先固定第`1`位字符($n$种情况)、再固定第`2`位字符($n-1$种情况)、... 、最后固定第$n$为字符($1$种情况)。 25 | 26 | **重复方案与剪枝**:当字符串存在重复字符时,排列方案中也存在重复方案。为排除重复方案,需在固定某字符时,保证"每种字符只在此位置固定一次",即遇到重复字符时不交换,直接跳过。从DFS角度看,可以称为"剪枝"。 27 | 28 | ## 算法流程 29 | 30 | - **终止条件**:当`x=len(s)-1​`时,代表所有位置已固定(最后一位只有$1$种情况),则将当前组合`s`转化为字符串并加入`res`,并返回。 31 | 32 | - **递推参数**:当前固定位`x`; 33 | - **递推工作**:初始化一个`set`,用于排除重复的字符;将第`x`位字符与`i in [x, len(s)]`字符分别交换,并进入下层递归; 34 | - **剪枝**:若`s[i]`在`set`中,代表是重复字符,因此"剪枝"; 35 | - 将`s[i]`加入`set`,以便之后遇到重复字符时剪枝; 36 | - **固定字符**:将字符`s[i]`和`s[x]`交换,即固定`s[i]`为当前字符; 37 | - **开启下层递归**:调用`dfs(x+1)`,即开始固定第`x+1`个位置; 38 | - **还原交换**:将字符`s[i]`和`s[x]`交换(还原之前的交换) 39 | 40 | ## 复杂度分析 41 | 42 | - 时间复杂度:$O(N!)$,$N$为字符串`s`的长度;时间复杂度与字符串排列的方案数成线性关系。 43 | - 空间复杂度:$O(N^2)$,全排列的递归深度为$N$,系统累计使用栈空间大小为$O(N)$;递归中辅助`set`累计存储的字符数量最多为$N+(N-1)+...+2+1=\frac{N(N+1)}{2}$,即占用$O(N^2)$的额外空间。 44 | 45 | ## 代码 46 | 47 | ```python 48 | class Solution: 49 | def permutation(self, s: str) -> List[str]: 50 | s_list = list(s) 51 | res = [] 52 | 53 | def dfs(idx): 54 | if idx == len(s_list) - 1: 55 | res.append("".join(s_list)) 56 | return 57 | visited = set() 58 | for i in range(idx, len(s_list)): 59 | if s_list[i] in visited: continue 60 | visited.add(s_list[i]) 61 | s_list[i], s_list[idx] = s_list[idx], s_list[i] 62 | dfs(idx+1) 63 | s_list[i], s_list[idx] = s_list[idx], s_list[i] 64 | 65 | dfs(0) 66 | return res 67 | ``` 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /剑指offer系列/面试题30.包含min函数的栈.md: -------------------------------------------------------------------------------- 1 | 包含min函数的栈 2 | 3 | 定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的min函数在该栈中,调用min、push及pop的时间复杂度都是$O(1)$。 4 | 5 | ## 示例 6 | 7 | ``` 8 | MinStack minStack = new MinStack(); 9 | minStack.push(-2); 10 | minStack.push(0); 11 | minStack.push(-3); 12 | minStack.min(); --> 返回 -3. 13 | minStack.pop(); 14 | minStack.top(); --> 返回 0. 15 | minStack.min(); --> 返回 -2. 16 | ``` 17 | 18 | ## 提示 19 | 20 | - 各函数的调用总次数不超过20000次 21 | 22 | # 解题思路 23 | 24 | 对于单个栈本身而言,执行`pop(),push()`的时间复杂度就为$O(1)$,但是需要找到栈中的`min`值,需要$O(N)$来遍历。 25 | 26 | 那么很自然地,我们可以维护两个栈,其中一个栈用于维护原始数据,另一个栈用来维护当前栈中最小值。 27 | 28 | ## 算法流程 29 | 30 | - 维护两个栈`stack`和`min_stack`,其中`stack`用于记录原始数据,`min_stack`用于维护当前栈中最小元素。 31 | - 执行`push()`时,`stack.append()`加入新元素`x`,`min_stack[-1]`与新元素比较,若`min_stack[-1] >= x`,则将`x`加入`min_stack`,否则不加入。 32 | - 执行`pop()`时,`stack.pop()`,同时需比较弹出元素是否为当前栈中最小元素,`stack.pop() == min_stack[-1]`,若相等,则`min_stack`也需要弹出(即及时更新当前最小元素);否则不操作。 33 | - 执行`top()`,返回`stack[-1]`即可 34 | - 执行`min()`,返回`min_stack[-1]`即可 35 | 36 | ## 复杂度分析 37 | 38 | - 时间复杂度:均为$O(1)$。 39 | - 空间复杂度:$O(N)$。 40 | 41 | ## 代码 42 | 43 | ```python 44 | class MinStack: 45 | 46 | def __init__(self): 47 | """ 48 | initialize your data structure here. 49 | """ 50 | self.stack = [] 51 | self.min_stack = [] 52 | 53 | def push(self, x: int) -> None: 54 | self.stack.append(x) 55 | if not self.min_stack or self.min_stack[-1] >= x: 56 | self.min_stack.append(x) 57 | 58 | def pop(self) -> None: 59 | tmp = self.stack.pop() 60 | if self.min_stack and tmp == self.min_stack[-1]: 61 | self.min_stack.pop() 62 | 63 | def top(self) -> int: 64 | return self.stack[-1] 65 | 66 | def min(self) -> int: 67 | return self.min_stack[-1] if self.min_stack else None 68 | 69 | 70 | # Your MinStack object will be instantiated and called as such: 71 | # obj = MinStack() 72 | # obj.push(x) 73 | # obj.pop() 74 | # param_3 = obj.top() 75 | # param_4 = obj.min() 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /剑指offer系列/面试题34.二叉树中和为某一值的路径.md: -------------------------------------------------------------------------------- 1 | 二叉树中和为某一值的路径 2 | 3 | # 题目描述 4 | 5 | 输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。 6 | 7 | ## 示例 8 | 9 | 给定如下二叉树,以及目标和`sum = 22` 10 | 11 | ``` 12 | 5 13 | / \ 14 | 4 8 15 | / / \ 16 | 11 13 4 17 | / \ / \ 18 | 7 2 5 1 19 | ``` 20 | 21 | 返回 22 | 23 | ``` 24 | [ 25 | [5,4,11,2], 26 | [5,8,4,5] 27 | ] 28 | ``` 29 | 30 | ## 提示 31 | 32 | - `节点总数 <= 10000` 33 | 34 | # 解题思路 35 | 36 | > 很明显是树的深度搜索。 37 | 38 | **算法流程**: 39 | 40 | `dfs(node, target)` 41 | 42 | - **递推参数**:当前节点`node`,当前目标值`target`。 43 | - **终止条件**:若节点`node`为空,则直接返回 44 | - **递推工作**: 45 | - 路径更新:将当前节点值`node.val`加入路径`path`; 46 | - 目标值更新:`target -= node.val`; 47 | - 路径记录:当`root`为叶子节点且`target == 0`,将此路径`path`加入`res` 48 | - 若当前节点`node`存在左(右)子节点,对左(右)子节点开启深度递归 49 | - 路径回复:向上回溯前,需要将当前节点从路径`path`中删除,即`path.pop()` 50 | - **返回值**:返回`res` 51 | 52 | > 注意:值得注意的是,记录路径时若直接执行 `res.append(path)` ,则是将 `path` 对象加入了 `res` ;后续 `path` 改变时, `res` 中的 `path` 对象也会随之改变。 53 | > 54 | > 正确做法:`res.append(list(path))` ,相当于复制了一个 `path` 并加入到 `res` 。 55 | > 56 | 57 | ## 复杂度分析 58 | 59 | - 时间复杂度:$O(N)$,$N$为总节点数 60 | - 空间复杂度:$O(N)$,最坏情况(树退化为链表),`path`存储所有节点 61 | 62 | ## 代码 63 | 64 | ```python 65 | # Definition for a binary tree node. 66 | # class TreeNode: 67 | # def __init__(self, x): 68 | # self.val = x 69 | # self.left = None 70 | # self.right = None 71 | 72 | class Solution: 73 | def pathSum(self, root: TreeNode, sum: int) -> List[List[int]]: 74 | path = [] 75 | res = [] 76 | def dfs(node, target): 77 | if not node: return 78 | path.append(node.val) 79 | target -= node.val 80 | if target == 0 and not node.left and not node.right: 81 | res.append(list(path)) 82 | dfs(node.left, target) 83 | dfs(node.right, target) 84 | path.pop() 85 | dfs(root, sum) 86 | return res 87 | ``` 88 | 89 | -------------------------------------------------------------------------------- /剑指offer系列/面试题44.数字序列中某一位的数字.md: -------------------------------------------------------------------------------- 1 | 数字序列中某一位的数字 2 | 3 | # 题目描述 4 | 5 | 数字以`0123456789101112131415...`的格式序列化到一个字符序列中,第`5`位(从下标`0`)开始计数是`5`,第`13`位是`1`,第`19`位是`4`,等等。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:n = 3 11 | 输出:3 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入:n = 11 18 | 输出:0 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `0 <= n < 2^31` 24 | 25 | # 解题思路 26 | 27 | > 这是一个找数学规律的题。 28 | 29 | 通过观察下表,可以看出一定的规律: 30 | 31 | | 数字范围 | 数量 | 位数 | 占多少位 | 32 | | :-------: | :--: | :--: | :------: | 33 | | 1-9 | 9 | 1 | 9 | 34 | | 10-99 | 90 | 2 | 180 | 35 | | 100-999 | 900 | 3 | 2700 | 36 | | 1000-9999 | 9000 | 4 | 36000 | 37 | | ... | ... | ... | ... | 38 | 39 | 我们可以根据给定的第`n`位,反推出其对应的数`num`: 40 | 41 | 例如:给定`n = 2901`,得到`2901 = 9 + 180 + 2700 + 12`,则说明其对应的数字一定是个`4`位数,`num = 1000 + (12 - 1)//4 = 1000 + 2 = 1002`,即对应的数为`1002`,具体定位在哪个数字可以由`定位1002中的位置 = (12 - 1)% 4 = 3`,可以得到位于`"1002"`的第`3`位,即`str(1002)[3] = "2"`。 42 | 43 | 根据上面的例子,可能大家已经明白如何解题了。对于`n`,我们不断循环减少每个位数最多能占几位,如`n - 9 > 0`,说明`n`对应的数字肯定不是一位数,以此类推。 44 | 45 | ## 复杂度分析 46 | 47 | - 时间复杂度:$O(\log n)$,所求数位$n$对应数字$num$的位数`i`最大为$O(\log n)$;第一步最多循环$O(\log n)$次;第三步中将$num$转化为字符串使用$O(\log n)$时间; 48 | - 空间复杂度:$O(\log n)$,将数字$num$转化为字符串`str(num)`,占用$O(\log n)$的额外空间。 49 | 50 | ## 代码 51 | 52 | ```python 53 | class Solution: 54 | def findNthDigit(self, n: int) -> int: 55 | """ 56 | 找规律 57 | 数字范围 数量 位数 占多少位 58 | 1-9 9 1 9 59 | 10-99 90 2 180 60 | 100-999 900 3 2700 61 | 1000-9999 9000 4 36000 62 | ... ... ... ... 63 | """ 64 | i = 1 65 | digit = 1 66 | count = 9 * i * digit 67 | while n - count > 0: # 1 68 | n -= count 69 | digit = 10**i 70 | i += 1 71 | count = 9 * i * digit 72 | 73 | num = digit + (n - 1) // i # 2 74 | return int(str(num)[(n - 1) % i]) # 3 75 | ``` 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /剑指offer系列/面试题28.对称二叉树.md: -------------------------------------------------------------------------------- 1 | 对称二叉树 2 | 3 | # 题目描述 4 | 5 | 请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。 6 | 7 | 例如,二叉树`[1,2,2,3,4,4,3]`是对称的。 8 | 9 | ``` 10 | 1 11 | / \ 12 | 2 2 13 | / \ / \ 14 | 3 4 4 3 15 | ``` 16 | 17 | 但是下面这个`[1,2,2,null,3,null,3]`则不是镜像对称的: 18 | 19 | ``` 20 | 1 21 | / \ 22 | 2 2 23 | \ \ 24 | 3 3 25 | ``` 26 | 27 | ## 示例1 28 | 29 | ``` 30 | 输入:root = [1,2,2,3,4,4,3] 31 | 输出:true 32 | ``` 33 | 34 | ## 示例2 35 | 36 | ``` 37 | 输入:root = [1,2,2,null,3,null,3] 38 | 输出:false 39 | ``` 40 | 41 | ## 限制 42 | 43 | - `0 <= 节点个数 <= 1000` 44 | 45 | # 解题思路 46 | 47 | 根据题意,**对称二叉树**对于树中任意两个对称节点`L`和`R`一定有如下性质: 48 | 49 | - `L.val = R.val`:即此两对称节点值相等。 50 | - `L.left.val = R.right.val`:即`L`的左节点与`R`的右节点对称 51 | - `L.right.val = R.left.val`:即`L`的右节点与`R`的左节点对称。 52 | 53 | ## 算法流程 54 | 55 | `isSymmetric(root)`: 56 | 57 | - **特例处理**:若根节点`root`为空,则直接返回`True` 58 | - **返回值**:即`recur(root.left, root.right)` 59 | 60 | `recur(L,R)`: 61 | 62 | - **终止条件**: 63 | - 当`L`和`R`同时越过叶子节点:此树从顶至底的节点都对称,返回`True`。 64 | - 当`L`或`R`中只有一个越过叶子节点:此树不对称,返回`False`。 65 | - 当节点`L`值不等于节点`R`的值:此树不对称,返回`False`。 66 | - **递推工作**: 67 | - 判断两节点`L.left`和`R.right`是否对称,即:`recur(L.left, R.right)`; 68 | - 判断两节点`L.right`和`R.left`是否对称,即:`recur(L.right, R.left)`; 69 | - **返回值**:两对节点都对称时,才是对称树,因此用与逻辑符`&&`连接 70 | 71 | ## 复杂度分析 72 | 73 | - 时间复杂度:$O(N)$,每次执行`recur()`可以判断一对节点是否对称,最多调用$N/2$次。 74 | - 空间复杂度:$O(N)$,最坏情况(二叉树的两个子树退化成链表),递归调用栈使用$O(N)$空间。 75 | 76 | ## 代码 77 | 78 | ```python 79 | # Definition for a binary tree node. 80 | # class TreeNode: 81 | # def __init__(self, x): 82 | # self.val = x 83 | # self.left = None 84 | # self.right = None 85 | 86 | class Solution: 87 | def isSymmetric(self, root: TreeNode) -> bool: 88 | if not root: return True 89 | def recur(L,R): 90 | if not L and not R: return True 91 | if not L or not R or L.val != R.val: return False 92 | return recur(L.left, R.right) and recur(L.right, R.left) 93 | return recur(root.left, root.right) 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /每日一题系列/452.用最少数量的箭引爆气球.md: -------------------------------------------------------------------------------- 1 | 用最少数量的箭引爆气球 2 | 3 | # 题目描述 4 | 5 | 在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在$10^4$个气球。 6 | 7 | 一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为$x_{start},x_{end}$,且满足$x_{start}\le x\le x_{end}$,则该气球会被引爆。可以射出的弓箭的数量没有限制。弓箭一旦被射出后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。 8 | 9 | ## 示例 10 | 11 | ``` 12 | 输入: 13 | [[10,16],[2,8],[1,6],[7,12]] 14 | 输出:2 15 | 解释:对于该样例,我们可以在x=6(射爆[2,8],[1,6]两个气球)和x=11(射爆另外两个气球)。 16 | ``` 17 | 18 | # 解题思路 19 | 20 | 一般在看到"找到要做某事的最小数量"或"找到在某些情况下适合的最大物品数量"的问题,可以首先考虑贪心算法。 21 | 22 | 贪心算法的思想就是每一步选择最佳解决方案,最终获得最佳的解决方案。 23 | 24 | ## 算法思路 25 | 26 | - 根据$x_{end}$将气球进行排序。(也可以根据$x_{start}$进行排序) 27 | - 初始化`end=points[0][1]​`。 28 | - 初始化箭的数量`arrows=1`。 29 | - 遍历所有气球: 30 | - 如果气球的$x_{start}>end$: 31 | - 则增加箭的数量。 32 | - 将$end$设置为当前气球的$x_{end}$。 33 | - 返回`arrows`。 34 | 35 | ## 复杂度分析 36 | 37 | - 时间复杂度:$O(NlogN)$(排序花费$O(NlogN)$;遍历花费$O(N)$)。 38 | - 空间复杂度:$O(N)$。 39 | 40 | ## 代码 41 | 42 | ```python 43 | # 根据x_start进行排序 44 | # class Solution: 45 | # def findMinArrowShots(self, points: List[List[int]]) -> int: 46 | # num = len(points) 47 | # if num < 2: 48 | # return num 49 | # points = sorted(points, key=lambda x: x[0]) 50 | # res = 1 51 | # end = points[0][1] 52 | # for i in range(len(points)): 53 | # if points[i][0] > end: 54 | # res += 1 55 | # end = points[i][1] 56 | # else: 57 | # end = min(end, points[i][1]) 58 | # return res 59 | 60 | 61 | # 根据x_end进行排序 62 | class Solution: 63 | def findMinArrowShots(self, points: List[List[int]]) -> int: 64 | num = len(points) 65 | if num < 2: 66 | return num 67 | points = sorted(points, key=lambda x: x[1]) 68 | res = 1 69 | end = points[0][1] 70 | for i in range(len(points)): 71 | if points[i][0] > end: 72 | res += 1 73 | end = points[i][1] 74 | return res 75 | 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /每日一题系列/1403.非递增顺序的最小子序列.md: -------------------------------------------------------------------------------- 1 | 非递增顺序的最小子序列 2 | 3 | # 题目描述 4 | 5 | 给你一个数组`nums`,请你从中抽取一个子序列,满足该子序列的元素之和**严格**大于未包含在该子序列中的各元素之和。 6 | 7 | 如果存在多个解决方案,只需返回**长度最小**的子序列。如果仍然有多个解决方案,则返回**元素之和最大**的子序列。 8 | 9 | 与子数组不同的地方在于,`数组的子序列`**不强调**元素在原数组中的连续性,也就是说,它可以通过从数组中分离一些(也可能不分离)元素得到。 10 | 11 | **注意**,题目数据保证满足所有约束条件的解决方案是**唯一**的,返回的答案应当按**非递增顺序**排列。 12 | 13 | ## 示例1 14 | 15 | ``` 16 | 输入:nums = [4,3,10,9,8] 17 | 输出:[10,9] 18 | 解释:子序列[10,9]和[10,8]是长度最小的,满足元素之和大于其他各元素之和。但是[10,9]的元素之和比[10,8]大。 19 | ``` 20 | 21 | ## 示例2 22 | 23 | ``` 24 | 输入:nums = [4,4,7,6,7] 25 | 输出:[7,7,6] 26 | 解释:子序列[7,7]的和为14,不严格大于剩下的其他元素之和(14=4+4+6)。因此,[7,7,6]是满足题意的最小子序列。注意,元素按非递增顺序返回。 27 | ``` 28 | 29 | ## 示例3 30 | 31 | ``` 32 | 输入:nums = [6] 33 | 输出:[6] 34 | ``` 35 | 36 | ## 提示 37 | 38 | - `1 <= nums.length <= 500` 39 | - `1 <= nums[i] <= 100` 40 | 41 | # 解题思路 42 | 43 | ## 优先过滤 44 | 45 | 根据题目内容以及提示,可以对输入数据进行适当过滤。 46 | 47 | - 当`nums`长度为1时,可以直接返回`nums`,无需其他任何操作。 48 | 49 | - 当然还可以考虑一下,当`nums`长度为2时,直接返回`max(nums)`。 50 | 51 | 要不再顺手考虑一下`nums`长度为3的时候?开个玩笑,毕竟上述情况是少数,我们的算法是要解决更一般的情况。 52 | 53 | `优先过滤`可以作为一种锦上添花的手段,有时候从特殊情况中或许就发现了一般现象。 54 | 55 | ## 算法思路 56 | 57 | 根据题意,我们最终应该从数组`nums`中得到两部分序列:一部分是最终输出结果`result`;另一部分是剩余部分`rest`。要求`sum(result)>sum(rest)`,转换一下其实就相当于要求`sum(result)>(sum(nums)//2)`。 58 | 59 | 那么我们的思路就出来的: 60 | 61 | 1. 先将`nums`进行升序排序(sorted一下或者自己写,都行)。 62 | 2. 然后从排序后的`nums`中取出末尾元素,放入`result`中,判断`sum(result)>(sum(nums)//2)`是否成立。 63 | 3. 如果判断成立,则返回`result`;否则重复第二步 64 | 65 | 因为事先已经对`nums`进行排序,所以每一次取出的元素一定是当前`nums`中的最大值,而放入`result`中也必然是非递增顺序。(完美契合题目要求) 66 | 67 | ## 复杂度分析 68 | 69 | - 时间复杂度:$O(nlogn)$,主要是对`nums`排序比较花费时间。 70 | - 空间复杂度:$O(n)$。 71 | 72 | ## 代码 73 | 74 | ```python 75 | class Solution: 76 | def minSubsequence(self, nums: List[int]) -> List[int]: 77 | if len(nums) == 1: 78 | return nums 79 | nums = sorted(nums) 80 | res = [] 81 | half_num = sum(nums) // 2 82 | for _ in range(len(nums)): 83 | max_num = nums.pop() 84 | res.append(max_num) 85 | if sum(res) > half_num: 86 | return res 87 | ``` 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /剑指offer系列/面试题03.数组中重复的数字.md: -------------------------------------------------------------------------------- 1 | 数组中重复的数字 2 | 3 | # 题目描述 4 | 5 | 找出数组中重复的数字。在一个长度为n的数组nums里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入: 11 | [2,3,1,0,2,5,3] 12 | 输出:2或3 13 | ``` 14 | 15 | ## 限制 16 | 17 | ``` 18 | 2<=n<=100000 19 | ``` 20 | 21 | # 解题思路 22 | 23 | 本题方法很多,每个方法都有优缺点,题目也没有限制是否可以使用额外空间,是否可以修改原始数组。如果在面试的时候,需要询问面试官是否有这些限制,并给出对应的解决方案。 24 | 25 | 在实际的工作中,我们遇到的问题很多时候也不会直接告诉我们有哪些限制,需要我们根据实际情况,具体分析应该使用哪种方法。所以有些面试官可能出的一些题目就是没有说的很清楚,他希望你向他提问,就是为了考察你是不是有考虑得很全面(面试就这样,是和活生生的人对话,不是做题 Accept 就完事,大家要稳,习惯就好,有的时候不能 Accept ,但是面试官看到了你其他方面的素质,其实也会给通过的)。 26 | 27 | ## 方法一:哈希表 28 | 29 | 应该是最容易想到,但显然需要额外空间。 30 | 31 | 特别地,在数组的长度不超过32的时候,使用位运算的技巧可以实现$O(1)$空间复杂度判重,但是这道题不是回溯算法的问题,题目给出的测试用例的长度肯定不止32位,因此,不建议使用位运算的技巧。 32 | 33 | 分析:这种方法不修改原始数组,但是使用了$O(N)$空间,使用空间换时间,是最正常的思路,时间复杂度是$O(N)$。 34 | 35 | ## 方法二:排序 36 | 37 | 排序后,再遍历一遍就知道哪个重复了。 38 | 39 | 分析:这个方法也比较容易想到,但是排序本身也需要时间,同时修改了原始数组,时间复杂度是$O(NlogN)$。Python语言中排序所用蒂姆排序算法空间复杂度为$O(N)$。 40 | 41 | ## 方法三:把数组视为哈希表 42 | 43 | 题目指出`在一个长度为n的数组nums里所有数字都在0~n-1的范围内`。因此,可遍历数组并通过交换操作使元素的**索引**与**值**一一对应(即`nums[i]=i`)。 44 | 45 | 在遍历中,当第二次遇到数字`x`时,一定有`nums[x]=x`(因为第一次遇到`x`时已经将其交换至`nums[x]`处了)。利用以上方法,即可得到一组重复数字。 46 | 47 | ### 算法流程 48 | 49 | - 遍历数组`nums`,设索引初始值为`i=0`: 50 | 1. 若`nums[i]==i`:说明此数字已在对应索引位置,无需交换,因此执行`i += 1`与`continue`; 51 | 2. 若`nums[nums[i]] == nums[i]`:说明索引`nums[i]`处的元素值也为`nums[i]`,即找到一组相同值,返回此值`nums[i]`; 52 | 3. 否则:当前数字是第一次遇到,因此交换索引为`i`和`nums[i]`的元素值,将此数字交换至对应索引位置。 53 | - 若遍历完毕尚未返回,则返回-1,代表数组中无相同值。 54 | 55 | 分析:这个思路利用到了数组的元素值的范围恰好和数组的索引是一样的,因此数组本身可以当做哈希表来使用。遍历一遍就可以找到重复值,但是修改了原始数组。时间复杂度$O(N)$:遍历数组使用$O(N)$,每轮遍历的判断和交换操作使用$O(1)$。空间复杂度$O(1)$:使用常数复杂度的额外空间。 56 | 57 | ### 代码: 58 | 59 | ```python 60 | class Solution: 61 | def findRepeatNumber(self, nums: List[int]) -> int: 62 | for i in range(len(nums)): 63 | if nums[i] == i: 64 | i += 1 65 | continue 66 | elif nums[nums[i]] == nums[i]: 67 | return nums[i] 68 | else: 69 | nums[nums[i]], nums[i] = nums[i], nums[nums[i]] 70 | return -1 71 | ``` 72 | 73 | -------------------------------------------------------------------------------- /剑指offer系列/面试题32-II.从上到下打印二叉树II.md: -------------------------------------------------------------------------------- 1 | 从上到下打印二叉树 II 2 | 3 | # 题目描述 4 | 5 | 从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。 6 | 7 | ## 示例 8 | 9 | 给定二叉树`[3,9,20,null,null,15,7]` 10 | 11 | ``` 12 | 3 13 | / \ 14 | 9 20 15 | / \ 16 | 15 7 17 | ``` 18 | 19 | 返回其层次遍历结果: 20 | 21 | ``` 22 | [ 23 | [3], 24 | [9,20], 25 | [15,7] 26 | ] 27 | ``` 28 | 29 | ## 提示 30 | 31 | - `节点总数 <= 1000` 32 | 33 | # 解题思路 34 | 35 | > 这道题算是[从上到下打印二叉树](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题32-I.从上到下打印二叉树.md)的一个衍生,核心依然是用广度优先搜索。 36 | 37 | 根据题意,在广度优先搜素中需要知道哪些节点是同一层的,这样才能同一层打印在同一行。 38 | 39 | ## 算法流程 40 | 41 | - **特例处理**:当根节点为空,则返回空列表`[]` 42 | - **初始化**:打印结果列表`res = []`,包含根节点的队列`queue = [root]`,代码中使用`collections.deque()`来实现,这样每次出队操作的时间复杂度都是$O(1)$(直接使用`list.pop(0)`的时间复杂度是$O(N)$) 43 | - **BFS**循环:(当队列`queue`为空时跳出) 44 | - 初始化一个临时列表`tmp = []`,用于存储当前层打印结果; 45 | - 当前层循环打印:循环次数为当前层节点数(即队列长度): 46 | - **出队**:队首元素出队,记为`node`; 47 | - **打印**:将`node.val`加入`tmp`; 48 | - **添加子节点**:若`node`的左(右)子节点不为空,则将左(右)子节点加入队列中; 49 | - 将当前层的结果`tmp`加入`res` 50 | - **返回值**:返回`res` 51 | 52 | > 在Python中,`range()`函数的工作机制是在开启循环时建立一个列表,然后循环按照这个列表进行,因此只会"在第一次进入循环前执行一次`len(queue)`计算",所以即使在`for`循环中仍会添加元素,其循环此处也不受影响。 53 | 54 | ## 复杂度分析 55 | 56 | - 时间复杂度:$O(N)$,$N$为树的节点数 57 | - 空间复杂度:$O(N)$ 58 | 59 | ## 代码 60 | 61 | ```python 62 | # Definition for a binary tree node. 63 | # class TreeNode: 64 | # def __init__(self, x): 65 | # self.val = x 66 | # self.left = None 67 | # self.right = None 68 | 69 | class Solution: 70 | def levelOrder(self, root: TreeNode) -> List[List[int]]: 71 | if not root: return [] 72 | 73 | queue = collections.deque() 74 | queue.append(root) 75 | res = [] 76 | while queue: 77 | tmp = [] 78 | for _ in range(len(queue)): 79 | node = queue.popleft() 80 | tmp.append(node.val) 81 | if node.left: queue.append(node.left) 82 | if node.right: queue.append(node.right) 83 | res.append(tmp) 84 | return res 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /每日一题系列/978.最长湍流子数组.md: -------------------------------------------------------------------------------- 1 | 最长湍流子数组 2 | 3 | # 题目描述 4 | 5 | 当`A`的子数组`A[i], A[i+1], ..., A[j]`满足下列条件时,我们称其为湍流子数组: 6 | 7 | - 若`i <= k < j`,当`k`为奇数时,`A[k] > A[k+1]`,且当`k`为偶数时,`A[k] < A[k+1]`; 8 | - **或**若`i <= k < j`,当`k`为偶数时,`A[k] > A[k+1]`,且当`k`为奇数时,`A[k] < A[k+1]`; 9 | 10 | 也就是说,如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是湍流子数组。 11 | 12 | 返回`A`的最大湍流子数组的**长度**。 13 | 14 | ## 示例1 15 | 16 | ``` 17 | 输入:[9,4,2,10,7,8,8,1,9] 18 | 输出:5 19 | 解释:(A[1] > A[2] < A[3] > A[4] < A[5]) 20 | ``` 21 | 22 | ## 示例2 23 | 24 | ``` 25 | 输入:[4,8,12,16] 26 | 输出:2 27 | ``` 28 | 29 | ## 示例3 30 | 31 | ``` 32 | 输入:[100] 33 | 输出:1 34 | ``` 35 | 36 | ## 提示 37 | 38 | - `1 <= A.length <= 40000` 39 | - `0 <= [i] <= 10^9` 40 | 41 | # 解题思路 42 | 43 | 首先来理解一下题目的意思,我们把数组中`A[i+1] > A[i]`称为**上升流**,把`A[i+1] < A[i]`称为**下降流**,`A[i+1] = A[i]`称为**水平流**。那么我们要找的是一段交替上升下降的子数组。例如示例1输入`[9,4,2,10,7,8,8,1,9]`,就可以表示成如下图所示的意思: 44 | 45 | ![示例.jpg](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/85bd024ca1de1c4a6953b552b3868085c9243a8ba8fddab344d4064ce905f368.jpg) 46 | 47 | **算法流程**: 48 | 49 | - **特例处理**:若`A`的长度为`1`,则直接返回`1`;若`max(A) == min(A)`,表示`A`中全部数字都相同,也直接返回`1`。 50 | - **初始化**:初始化`dp`数组,长度为`A`的长度,初始值都为`0` 51 | - **状态表示**:`dp[i]`表示以`A[i]`为结尾的最长湍流子数组的长度 52 | - **状态转移**:`dp[0] = 1`,以`A[0]`为结尾自然就`1`个数字,很好理解;观察上图所示的示例,可以发现对于第`i`个数字,如果它比两边数字都大**或者**比两边数字都小,那么第`i`个数字就可以加入到湍流子数组中,即有`dp[i] = dp[i-1] + 1`;否则`dp[i] = 1` 53 | - **返回值**:`max(dp) + 1`。这里为什么要`+1`呢?解释如下: 54 | - 因为有对比存在某个**中间数字**不满足条件,但是中间数字的前一个条件满足,那么`+1`就表示把中间数字加上去。 55 | - 如果全部都满足条件,但是最后一个数字是没有对比的,所以如果最后一个数字的前面一个数字满足,同样要`+1`,才能得到正确答案。 56 | 57 | ## 复杂度分析 58 | 59 | - 时间复杂度:$O(N)$ 60 | - 空间复杂度:$O(N)$ 61 | 62 | ## 代码 63 | 64 | ```python 65 | class Solution: 66 | def maxTurbulenceSize(self, A: List[int]) -> int: 67 | if len(A) == 1: return 1 68 | 69 | if max(A) == min(A): return 1 70 | 71 | dp = [0 for _ in range(len(A))] 72 | dp[0] = 1 73 | for i in range(1, len(A) - 1): 74 | if A[i-1] < A[i] and A[i] > A[i+1]: 75 | dp[i] = dp[i-1] + 1 76 | elif A[i-1] > A[i] and A[i] < A[i+1]: 77 | dp[i] = dp[i-1] + 1 78 | else: 79 | dp[i] = 1 80 | return max(dp) + 1 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /每日一题系列/559.N叉树的最大深度.md: -------------------------------------------------------------------------------- 1 | N叉树的最大深度 2 | 3 | # 题目描述 4 | 5 | 给定一个N叉树,找到其最大深度。 6 | 7 | 最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。 8 | 9 | ## 示例 10 | 11 | 给定一个`3叉树` 12 | 13 | 示例.jpg 14 | 15 | 我们应返回其最大深度`3`。 16 | 17 | ## 说明 18 | 19 | - 树的深度不会超过`1000` 20 | - 树的节点总数不会超过`5000` 21 | 22 | # 解题思路 23 | 24 | ## 方法一:层序遍历(BFS) 25 | 26 | **算法流程**: 27 | 28 | - **特例处理**:若树为空,则返回`0`; 29 | - **初始化**:队列`queue`,包含`(root, 1)`,`1`表示`root`深度为`1`。 30 | - **层序遍历**:不断出队,直至队列为空 31 | - 队首元素弹出,记为`cur_node, depth`; 32 | - 遍历其孩子节点`child`: 33 | - 若孩子节点为空,则`continue`; 34 | - 若孩子节点不为空,则将`(child, depth+1)`加入队列 35 | 36 | **返回值**:返回`depth` 37 | 38 | ## 复杂度分析 39 | 40 | > 设$N$为节点个数 41 | 42 | - 时间复杂度:$O(N)$ 43 | - 空间复杂度:$O(N)$ 44 | 45 | ## 代码 46 | 47 | ```python 48 | """ 49 | # Definition for a Node. 50 | class Node: 51 | def __init__(self, val=None, children=None): 52 | self.val = val 53 | self.children = children 54 | """ 55 | 56 | class Solution: 57 | def maxDepth(self, root: 'Node') -> int: 58 | if not root: return 0 59 | queue = collections.deque() 60 | queue.append((root, 1)) 61 | 62 | while queue: 63 | cur_node, depth = queue.popleft() 64 | 65 | for child in cur_node.children: 66 | if not child: 67 | continue 68 | queue.append((child, depth+1)) 69 | return depth 70 | 71 | ``` 72 | 73 | ## 方法二:递归法 74 | 75 | **算法流程**: 76 | 77 | - **终止条件**:当前节点为空时,返回`0`;当前节点无孩子节点时,返回`1`; 78 | - **递推工作**:对节点的孩子节点进行递归 79 | - **返回值**:取`max()+1` 80 | 81 | ## 复杂度分析 82 | 83 | - 时间复杂度:$O(N)$,最坏情况,退化为链表 84 | - 空间复杂度:$O(N)$,最坏情况,退化为链表 85 | 86 | ## 代码 87 | 88 | ```python 89 | class Solution(object): 90 | def maxDepth(self, root): 91 | """ 92 | :type root: Node 93 | :rtype: int 94 | """ 95 | if not root: 96 | return 0 97 | if not root.children: 98 | return 1 99 | return max([self.maxDepth(child) for child in root.children])+1 100 | ``` 101 | 102 | -------------------------------------------------------------------------------- /每日一题系列/743.网络延迟时间.md: -------------------------------------------------------------------------------- 1 | 网络延迟时间 2 | 3 | # 题目描述 4 | 5 | 有`N`个网络节点,标记为`1`到`N`。 6 | 7 | 给定一个列表`times`,表示信号经过**有向边**的传递时间。`times = (u, v, w)`,其中`u`是源节点,`v`是目标节点,`w`是一个信号从源节点传递到目标节点的时间。 8 | 9 | 现在,我们从某个节点`K`发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回`-1`。 10 | 11 | ## 示例 12 | 13 | ![示例.jpg](https://assets.leetcode.com/uploads/2019/05/23/931_example_1.png) 14 | 15 | ``` 16 | 输入:times = [[2,1,1],[2,3,1],[3,4,1]], N = 4, K = 2 17 | 输出:2 18 | ``` 19 | 20 | ## 注意 21 | 22 | - `N`的范围在`[1, 100]`之间 23 | - `K`的范围在`[1, N]`之间 24 | - `times`的长度在`[1, 6000]`之间 25 | - 所有的边`times[i] = (u, v, w)`都有`1 <= u, v <= N`且`0 <= w <= 100` 26 | 27 | # 解题思路 28 | 29 | > 本题可以切实地构想一下信号传播,信号并不是每次仅传到一个节点,传播类似同心圆,同心圆的半径与传播时间成正比,凡在同心圆范围内的节点,都会接收到信号。(当然,题目中节点之间是**有向边**,所以需要考虑节点之间是否可以传输) 30 | 31 | 我们可以使用`Dijkstra's`算法找到从源节点到所有节点的最短路径。 32 | 33 | `Dijkstra's`算法是每一次扩展一个距离最短的点,更新与其相邻点的距离。 34 | 35 | 使用优先队列(堆的方式来维护),可以得到$O(NlogN)$的之间复杂度。 36 | 37 | **算法流程**: 38 | 39 | - 通过Hashmap的方式构建一个图,方便后续查询。 40 | - 构建一个优先队列(用堆的方式维护),队列中初始元素为`(0, K)`,表示从源节点`K`到节点`K`所需时间为`0`。 41 | - 构建一个距离字典`dist{}`,用于记录从源节点到某节点的所用的最短时间(或者说最短距离)。 42 | - **BFS**:对优先队列进行循环(每次弹出堆顶元素) 43 | - 弹出堆顶元素,记为`weight, node` 44 | - 若`node`在`dist`中,说明已有其他最短距离,`continue` 45 | - 否则,记录到`dist`中,`dist[node] = weight` 46 | - 遍历`node`节点的邻节节点及其所需时间,若邻接节点不在`dist`中,则加入队列中 47 | - 若`dist`中节点个数等于`N`,说明可以使所有节点收到信号,则返回`max(dist.values())`,否则返回`-1` 48 | 49 | ## 复杂度分析 50 | 51 | - 时间复杂度: $O(E \log E)$,$E$是`times`的长度。因为每个边都可能添加到堆中。 52 | - 空间复杂度:$O(N + E)$,图的大小是 $O(E)$ 加上其他对象的大小 $O(N)$。 53 | 54 | ## 代码 55 | 56 | ```python 57 | class Solution: 58 | def networkDelayTime(self, times: List[List[int]], N: int, K: int) -> int: 59 | u_dict = collections.defaultdict(list) 60 | for u, v, w in times: 61 | u_dict[u].append((v, w)) 62 | 63 | if K not in u_dict: 64 | return -1 65 | 66 | queue = [(0, K)] 67 | dist = {} 68 | 69 | while queue: 70 | weight, node = heapq.heappop(queue) 71 | if node in dist: continue 72 | dist[node] = weight 73 | for next_node, next_weight in u_dict[node]: 74 | if next_node not in dist: 75 | heapq.heappush(queue, (weight + next_weight, next_node)) 76 | return max(dist.values()) if len(dist) == N else -1 77 | ``` 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /每日一题系列/983.最低票价.md: -------------------------------------------------------------------------------- 1 | 最低票价 2 | 3 | # 题目描述 4 | 5 | 在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为`days`的数组给出。每一项是一个从`1`到`365`的整数。 6 | 7 | 火车票有三种不同的销售方式: 8 | 9 | - 一张为期一天的通行证售价为`costs[0]`美元; 10 | - 一张为期七天的通行证售价为`costs[1]`美元; 11 | - 一张为期三十天的通行证售价为`costs[2]`美元。 12 | 13 | 通行证允许数天无限制的旅行。例如,如果我们在第2天获得一张为期7天的通行证,那么我们可以连着旅行7天:第2天、第3天、第4天、第5天、第6天、第7天和第8天。 14 | 15 | 返回你想要完成在给定的列表`days`中列出的每一天的旅行所需要的最低消费。 16 | 17 | ## 示例1 18 | 19 | ``` 20 | 输入:days = [1,4,6,7,8,20], costs = [2,7,15] 21 | 输出:11 22 | 解释: 23 | 例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划: 24 | 在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。 25 | 在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, ..., 9 天生效。 26 | 在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。 27 | 你总共花了 $11,并完成了你计划的每一天旅行。 28 | ``` 29 | 30 | ## 示例2 31 | 32 | ``` 33 | 输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15] 34 | 输出:17 35 | 解释: 36 | 例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划: 37 | 在第 1 天,你花了 costs[2] = $15 买了一张为期 30 天的通行证,它将在第 1, 2, ..., 30 天生效。 38 | 在第 31 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 31 天生效。 39 | 你总共花了 $17,并完成了你计划的每一天旅行。 40 | ``` 41 | 42 | ## 提示 43 | 44 | - `1 <= days.length <= 365` 45 | - `1 <= days[i] <= 365` 46 | 47 | # 解题思路 48 | 49 | 使用动态规划的思想(还蕴含着贪心的思想)进行解题。`dp[i]`表示到当前第`i`天需要花费的最少费用。`dp`数组的长度选择`days`中最后一天多加1个长度(使`dp`数组下标与天数对应)。初始化时,全为`0`,之后开始对`dp`数组进行更新,每到达一个位置`i`,考虑当前天数是否在`days`中(即,这一天是否发生旅行),如果不需要发生旅行,(基于贪心的思想)自然不花钱是最省钱的,即`dp[i] = dp[i-1]`,也就是到第`i`天的花费与到第`i-1`天是一样的(因为第`i`天不花钱);如果需要发生旅行,需要从三种购买方式中选择花费最少的一种,即你想第`i`天旅行,需要从`i`的前`1`或`7`或`30`天的**后一位置**花费对应`costs[0]、costs[1]、costs[2]`的钱才可以,找到三种方式的最小值即可。 50 | 51 | ## 复杂度分析 52 | 53 | - 时间复杂度:$O(N)$ 54 | - 空间复杂度:$O(N)$ 55 | 56 | ## 代码 57 | 58 | ```python 59 | class Solution: 60 | def mincostTickets(self, days: List[int], costs: List[int]) -> int: 61 | dp = [0 for _ in range(days[-1] + 1)] 62 | # 使用day_idx来标识当前应处理的最近应发生旅行的一天 63 | # 后续方便直接判断i == days[day_idx],而不需要使用 i in days 64 | day_idx = 0 65 | for i in range(1, days[-1]+1): 66 | if i == days[day_idx]: 67 | dp[i] = min(dp[max(0, i-1)] + costs[0], 68 | dp[max(0, i-7)] + costs[1], 69 | dp[max(0, i-30)] + costs[2]) 70 | day_idx += 1 # 换到下一个应发生旅行的一天 71 | else: 72 | dp[i] = dp[i-1] 73 | 74 | return dp[-1] 75 | ``` 76 | 77 | -------------------------------------------------------------------------------- /每日一题系列/面试题04.09.二叉搜索树序列.md: -------------------------------------------------------------------------------- 1 | 二叉搜索树序列 2 | 3 | # 题目描述 4 | 5 | 从左向右遍历一个数组,通过不断将其中的元素插入树中可以逐步地生成一棵二叉搜索树。给定一个由不同节点组成的二叉树,输出所有可能生成此树的数组。 6 | 7 | ## 示例 8 | 9 | 给定如下二叉树 10 | 11 | ``` 12 | 2 13 | / \ 14 | 1 3 15 | ``` 16 | 17 | 返回: 18 | 19 | ``` 20 | [ 21 | [2,1,3], 22 | [2,3,1] 23 | ] 24 | ``` 25 | 26 | # 解题思路 27 | 28 | ## 补充知识 29 | 30 | 先明确什么是二叉搜索树? 31 | 32 | **二叉搜索树**(又称:二叉查找树、二叉排序树):它或者是一棵空树,或者是一棵具有以下性质的二叉树:若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;它的左右子树也均为一棵二叉搜索树。 33 | 34 | ## 题目分析 35 | 36 | 首先从最简单的示例看看: 37 | 38 | ``` 39 | 2 40 | / \ 41 | 1 3 42 | ``` 43 | 44 | 返回: 45 | 46 | ``` 47 | [ 48 | [2,1,3], 49 | [2,3,1] 50 | ] 51 | ``` 52 | 53 | 由于是从数组中从左到右插入元素到二叉搜索树中,因此每个元素在已存在的二叉搜索树中的插入位置都是确定的。 54 | 55 | 可以得出以下结论: 56 | 57 | - **数组的第一个元素,必定对应于二叉搜索树的根节点**; 58 | - **一个节点对应的子节点在序列上必定在父节点元素之后**; 59 | - **输出序列中可以先出现右节点再出现左节点**。 60 | 61 | 看一个复杂一点的例子: 62 | 63 | ``` 64 | 2 65 | / \ 66 | 1 4 67 | / 68 | 3 69 | ``` 70 | 71 | 返回: 72 | 73 | ``` 74 | [ 75 | [2,1,4,3], 76 | [2,4,1,3], 77 | [2,4,3,1] 78 | ] 79 | ``` 80 | 81 | 输出序列不仅仅在于子树的插入顺序不同,如上述结果中,`4 -> 1`右子树`4`后面可以先跟左子树`1`,再接`3`。但是可以确定的是,节点`3`无论如何不会直接出现在根节点`2`后面。 82 | 83 | 即,**序列中子树节点的相对顺序是可以确定的**,我们将子树中的节点看作单个序列,如上述例子中左边序列为`{1}`,右边序列为`{4, 3}`,将两个序列按照相对顺序进行混合: 84 | 85 | ``` 86 | [ 87 | [1,4,3], 88 | [4,1,3], 89 | [4,3,1] 90 | ] 91 | ``` 92 | 93 | 再在各序列中加入根节点`2`,恰好对应了答案。 94 | 95 | ## 代码 96 | 97 | ```python 98 | class Solution: 99 | def BSTSequences(self, root: TreeNode) -> List[List[int]]: 100 | if not root: 101 | return [[]] 102 | 103 | res = [] 104 | 105 | def recur(root, q, path): 106 | if root.left: 107 | q.append(root.left) 108 | if root.right: 109 | q.append(root.right) 110 | if not q: 111 | res.append(path) 112 | return 113 | 114 | for i, next_node in enumerate(q): 115 | new_queue = q[:i] + q[i+1:] 116 | recur(next_node, new_queue, path+[next_node.val]) 117 | 118 | recur(root, [], [root.val]) 119 | return res 120 | ``` 121 | 122 | -------------------------------------------------------------------------------- /剑指offer系列/面试题26.树的子结构.md: -------------------------------------------------------------------------------- 1 | 树的子结构 2 | 3 | # 题目描述 4 | 5 | 输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构) 6 | 7 | B是A的子结构,即A中有出现和B相同的结构和节点值。 8 | 9 | 例如: 10 | 11 | 给定的树A: 12 | 13 | ``` 14 | 3 15 | / \ 16 | 4 5 17 | / \ 18 | 1 2 19 | ``` 20 | 21 | 给定的树B: 22 | 23 | ``` 24 | 4 25 | / 26 | 1 27 | ``` 28 | 29 | 返回true,因为B与A的一个子树拥有相同的结构和节点值。 30 | 31 | ## 示例1 32 | 33 | ``` 34 | 输入:A = [1,2,3], B = [3,1] 35 | 输出:false 36 | ``` 37 | 38 | ## 示例2 39 | 40 | ``` 41 | 输入:A = [3,4,5,1,2], B = [4,1] 42 | 输出:true 43 | ``` 44 | 45 | ## 限制 46 | 47 | - `0 <= 节点个数 <= 10000` 48 | 49 | # 解题思路 50 | 51 | > 关于树的题目,往往是考察递归。 52 | 53 | 若树B是树A的子结构,则子结构的根节点必然是树A中的某一节点,且由该节点长出的树结构应与树B相同。 54 | 55 | 因此,判断树B是否为树A的子结构需要完成以下两步工作: 56 | 57 | 1. 先序遍历树A中的每个节点,记为`node`; 58 | 2. 判断树A中以`node`为根节点的子树是否包含树B。 59 | 60 | ## 算法流程 61 | 62 | 定义递归函数`recur(A, B)`: 63 | 64 | - **终止条件**: 65 | 1. 当节点B为空:说明树B已匹配完成,因此返回`true` 66 | 2. 当节点A为空:说明已经越过树A的叶子节点,匹配失败,返回`false` 67 | 3. 当节点A和节点B的值不同:说明匹配失败,返回`false` 68 | - **返回值**: 69 | 1. 判断A和B的左子节点是否相等,即调用`recur(A.left, B.left)` 70 | 2. 判断A和B的左子节点是否相等,即调用`recur(A.left, B.left)` 71 | 3. 判断A和B的右子节点是否相等,即调用`recur(A.right, B.right)` 72 | 73 | `isSubStructure(A, B)`函数: 74 | 75 | - **特例处理**:当树A为空或树B为空时,直接返回`false` 76 | - **返回值**:当树B是树A的子结构,则必须满足以下三种情况之一: 77 | 1. 以节点A为根节点的子树包含树B; 78 | 2. 树B是树A左子树的子结构; 79 | 3. 树B是树A右子树的子结构; 80 | 81 | ## 复杂度分析 82 | 83 | > 设$N,M$分别为树A和树B的节点数。 84 | 85 | - 时间复杂度:$O(NM)$,先序遍历A占用$O(N)$,调用`recur()`占用$O(M)$ 86 | - 空间复杂度:$O(M)$,当树A和B都退化为链表时,递归深度最大。 87 | 88 | ## 代码 89 | 90 | ```python 91 | # Definition for a binary tree node. 92 | # class TreeNode: 93 | # def __init__(self, x): 94 | # self.val = x 95 | # self.left = None 96 | # self.right = None 97 | 98 | class Solution: 99 | def isSubStructure(self, A: TreeNode, B: TreeNode) -> bool: 100 | if not B or not A: 101 | return False 102 | def recur(A, B): 103 | if not B: 104 | return True 105 | if not A or A.val != B.val: 106 | return False 107 | return recur(A.left, B.left) and recur(A.right, B.right) 108 | 109 | return recur(A, B) or self.isSubStructure(A.left, B) or self.isSubStructure(A.right, B) 110 | ``` 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /剑指offer系列/面试题11.旋转数组的最小数字.md: -------------------------------------------------------------------------------- 1 | 旋转数组的最小数字 2 | 3 | # 题目描述 4 | 5 | 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出数组的最小元素。例如,数组`[3,4,5,1,2]`为`[1,2,3,4,5]`的一个旋转,该数组的最小值为`1`。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:[3,4,5,1,2] 11 | 输出:1 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入:[2,2,2,0,1] 18 | 输出:0 19 | ``` 20 | 21 | # 解题思路 22 | 23 | 题目的本质其实是找出给定数组中的最小元素,可以直接使用`min()`函数,则时间复杂度为$O(N)$。 24 | 25 | 再深入思考一下,对于数组的查找问题是否可以使用**二分查找**解决(注意:使用二分查找需要原数组为有序数组),原数组显然不是有序数组,但仍存在一定的特殊性,例如可以划分为两个有序数组,如下图所示: 26 | 27 | ![二分查找.jpg](http://q9qozit0b.bkt.clouddn.com/%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.JPG) 28 | 29 | ## 算法流程 30 | 31 | 1. 循环二分:设置$i,j$指针分别指向`numbers`数组左右两端,$m=(i+j)//2$为每次二分的中点(`//`代表向下取整法,因此恒有$i\le mnumbers[j]`时:$m$一定在左排序数组中,即旋转点$x$一定在$[m+1,j]$闭区间内,因此执行$i=m+1$; 34 | 35 | (2)当`numbers[m]= 右排序数组任一元素`,因此可以推出旋转点元素值`numbers[x]<=numbers[j]=numbers[m]`,则有: 54 | 55 | - 若`numbers[x] int: 70 | i = 0 71 | j = len(numbers) - 1 72 | while i < j: 73 | m = (i+j)//2 74 | if numbers[m] > numbers[j]: 75 | i = m+1 76 | elif numbers[m] < numbers[j]: 77 | j = m 78 | else: 79 | j = j-1 80 | return numbers[i] 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /每日一题系列/1240.铺瓷砖.md: -------------------------------------------------------------------------------- 1 | 铺瓷砖 2 | 3 | # 题目描述 4 | 5 | 你是一位施工队的工长,根据设计师的要求准备为一套设计风格独特的房子进行市内装修。房子的客厅大小为`n x m`,为保持极简的风格,需要使用尽可能少的**正方形**瓷砖来铺地面。 6 | 7 | 假设正方形瓷砖的规格不限,边长都是整数。 8 | 9 | 请你帮设计师计算一下,最少需要用到多少块正方形瓷砖。 10 | 11 | ## 示例1 12 | 13 | ![示例1.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/10/25/sample_11_1592.png) 14 | 15 | ``` 16 | 输入:n = 2, m = 3 17 | 输出:3 18 | 解释:3 块地砖就可以铺满卧室。 19 | 2 块 1x1 地砖 20 | 1 块 2x2 地砖 21 | ``` 22 | 23 | ## 示例2 24 | 25 | ![示例2.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/10/25/sample_22_1592.png) 26 | 27 | ``` 28 | 输入:n = 5, m = 8 29 | 输出:5 30 | ``` 31 | 32 | ## 示例3 33 | 34 | 示例3.jpg 35 | 36 | ``` 37 | 输入:n = 11, m = 13 38 | 输出:6 39 | ``` 40 | 41 | ## 提示 42 | 43 | - `1 <= n <= 13` 44 | - `1 <= m <= 13` 45 | 46 | # 解题思路 47 | 48 | > 本题是一道搜索题,有涉及到最少方案,参考**示例1**和**示例2**,发现**示例2**中包含了**示例1**的子问题,由此考虑动态规划,而且在划分的时候,直接能想到每次选取最大长度的正方形进行分割(**贪心思想**),似乎很对,但是看到**示例3**发现不满足动态规划条件(最后发现是个特例)。 49 | 50 | 其实,动态规划不算是本题的正确解法,因为特例的存在。当然,你也可以把特例单独处理,剩余部分用动态规划求解。 51 | 52 | **算法流程**: 53 | 54 | - **状态说明**:设`dp[i][j]`表示当房间面积为`i x j`时所需的**最少**瓷砖数。 55 | 56 | - 进一步思考,有一点很明确,`dp[i][j] = dp[j][i]`,且当`i = j`时`dp[i][j] = 1`;此外,当`i = 1 or j = 1`时,`dp[1][j] = j or dp[i][1] = i`。 57 | 58 | - **状态转移方程**: 59 | $$ 60 | dp(i,j)=min(min(dp(i,j-k)+dp(i,k)), min(dp(i-k,j)+dp(k,j))),i\ne j\ne 1 61 | $$ 62 | 63 | - **特例处理**:`dp[11][13] = 6, dp[13][11] = 6` 64 | - **返回**:`dp[n][m]` 65 | 66 | ## 复杂度分析 67 | 68 | > 假设`n = m = N` 69 | 70 | - 时间复杂度:$O(N^3)$。(这其实是一个N-P hard问题) 71 | - 空间复杂度:$O(N^2)$ 72 | 73 | ## 代码 74 | 75 | ```python 76 | class Solution: 77 | def tilingRectangle(self, n: int, m: int) -> int: 78 | S = 13+1 79 | dp = [[0 for _ in range(S)] for _ in range(S)] 80 | max_num = n * m 81 | for i in range(1, S): 82 | for j in range(1, S): 83 | dp[i][j] = max_num 84 | if i == j: 85 | dp[i][j] = 1 86 | continue 87 | for r in range(1, i): 88 | dp[i][j] = min(dp[i][j], dp[r][j]+dp[i-r][j]) 89 | for c in range(1, j): 90 | dp[i][j] = min(dp[i][j], dp[i][c]+dp[i][j-c]) 91 | dp[11][13] = 6 92 | dp[13][11] = 6 93 | 94 | return dp[n][m] 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /每日一题系列/1247.交换字符是的字符串相同.md: -------------------------------------------------------------------------------- 1 | 交换字符使得字符串相同 2 | 3 | # 题目描述 4 | 5 | 有两个长度相同的字符串`s1`和`s2`,且它们其中**只含有**字符`"x"`和`"y"`,你需要通过"交换字符"的方式使这两个字符串相同。 6 | 7 | 每次"交换字符"的时候,你都可以在两个字符串中各选一个字符进行交换。 8 | 9 | 交换只能发生在两个不同的字符串之间,绝对不能发生在同一个字符串内部。也就是说,我们可以交换`s1[i]`和`s2[j]`,但不能交换`s1[i]`和`s1[j]`。 10 | 11 | 最后,请你返回使`s1`和`s2`相同的最小交换次数,如果没有方法能够使得这两个字符串相同,则返回`-1`。 12 | 13 | ## 示例1 14 | 15 | ``` 16 | 输入:s1 = "xx", s2 = "yy" 17 | 输出:1 18 | 解释:交换 s1[0] 和 s2[1],得到 s1 = "yx",s2 = "yx"。 19 | ``` 20 | 21 | ## 示例2 22 | 23 | ``` 24 | 输入:s1 = "xy", s2 = "yx" 25 | 输出:2 26 | 解释:交换 s1[0] 和 s2[0],得到 s1 = "yy",s2 = "xx" 。 27 | 交换 s1[0] 和 s2[1],得到 s1 = "xy",s2 = "xy" 。 28 | 注意,你不能交换 s1[0] 和 s1[1] 使得 s1 变成 "yx",因为我们只能交换属于两个不同字符串的字符。 29 | ``` 30 | 31 | ## 示例3 32 | 33 | ``` 34 | 输入:s1 = "xx", s2 = "xy" 35 | 输出:-1 36 | ``` 37 | 38 | ## 示例4 39 | 40 | ``` 41 | 输入:s1 = "xxyyxyxyxx", s2 = "xyyxyxxxyx" 42 | 输出:4 43 | ``` 44 | 45 | ## 提示 46 | 47 | - `1 <= s1.length,s2.length <= 1000` 48 | - `s1, s2`只包含`x`或`y`。 49 | 50 | # 解题思路 51 | 52 | 根据题意,可以分析出以下情况: 53 | 54 | - `s1`与`s2`必须长度相同; 55 | - 若`s1[i]==s2[i]`,即对应位置上字符相同,该位置显然无需交换,不影响最终结果。(因为我们需要的是最小交换次数,能不交换当然收益最大,贪心算法思想); 56 | - 若`s1[i]!=s2[i]`,则必然是`x-->y`或者`y-->x`这两种对应关系中的一种。 57 | 58 | 进一步分析,将`s1[i]==s2[i]`的情况隐藏掉,剩余字符的情况必然是: 59 | 60 | - `s1`中剩余$n$个`x`和$m$个`y`,对应`s2`中剩余$n$个`y`和$m$个`x`。 61 | - 其中,$n\ge0$,$m\ge0$。 62 | 63 | 结合示例1、示例2、示例3,可以得到以下分析: 64 | 65 | - `xx-->yy`只需一次交换;`xy-->yx`只需两次交换;仅`x-->y`无法实现; 66 | - 对于`s1`中的$n$个`x`与$m$个`y`,满足以下关系即可: 67 | 1. 若`abs(n-m)%2 != 0`,则无法实现,返回-1 68 | 2. 否则,返回`n//2+n%2+m//2+m%2`。 69 | 70 | ## 复杂度分析 71 | 72 | - 时间复杂度:$O(N)$,仅对`s1`遍历一次即可。 73 | - 空间复杂度:$O(1)$。 74 | 75 | ## 代码 76 | 77 | ```python 78 | class Solution: 79 | def minimumSwap(self, s1: str, s2: str) -> int: 80 | num_s1 = len(s1) 81 | num_s2 = len(s2) 82 | num_x = 0 83 | num_y = 0 84 | if num_s1 != num_s2: 85 | return -1 86 | for i in range(num_s1): 87 | if s1[i] == s2[i]: 88 | continue 89 | else: 90 | if s1[i] == 'x': 91 | num_x += 1 92 | else: 93 | num_y += 1 94 | tmp = abs(num_x - num_y) 95 | if tmp % 2 != 0: 96 | return -1 97 | 98 | return (num_x//2)+(num_x % 2) + (num_y//2)+(num_y % 2) 99 | 100 | 101 | ``` 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /剑指offer系列/面试题53-I.在排序数组中查找数字I.md: -------------------------------------------------------------------------------- 1 | 在排序数组中查找数字 I 2 | 3 | # 题目描述 4 | 5 | 统计一个数字在排序数组中出现的次数。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: nums = [5,7,7,8,8,10], target = 8 11 | 输出: 2 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: nums = [5,7,7,8,8,10], target = 6 18 | 输出: 0 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `0 <= 数组长度 <= 50000` 24 | 25 | # 解题思路 26 | 27 | 看完题目应该很容易想到**二分查找**吧?直接顺序遍历当然也是可以的,就是性能低了点。 28 | 29 | 直接使用**二分查找**去寻找目标数字`target`,若能找到则计数`cnt += 1`,这是**二分查找**的直接应用,但是要注意对于`nums[mid]`的判断,当`nums[mid] == target`时,仍需要再查找两侧的数组。(例如:`nums = [5, 7, 8, 8, 8, 10, 12], target = 8`,第一次二分`mid = 3`,`nums[mid] = target`成立,剩余的两个`8`被划分到**两侧数组**中,需要对两侧数组**均使用二分查找**),这样每次都去寻找`target`效率也不见得会高,因为最坏的情况仍然是$O(N)$(`nums`全是`target`)。 30 | 31 | 我们进行一定的优化,因为数组是有序的,如果存在`target`,那么`target`一定连续排列在一起,形成一个窗口,我们只要找到窗口的**左边界**和**右边界**,就可以算出窗口长度,也即`target`的数量。 32 | 33 | 由此,问题就可以转化为使用二分法找到**左边界**`left`和**右边界**`right`,则`target`的数量为`right - left - 1`。(注意:这里说的**左边界**和**右边界**是指窗口左边和右边的首个元素索引) 34 | 35 | **算法解析**: 36 | 37 | - **初始化**:左边界`left = 0`,右边界`right = len(nums) - 1`; 38 | - **循环二分**:当闭区间`[left, right]`无元素或者找到边界时跳出 39 | - 计算中点索引`mid = (left + right) // 2`; 40 | - 若`nums[mid] < target`,则`target`在区间`[mid + 1, right]`中,则令`left = mid + 1`; 41 | - 若`nums[mid] > target`,则`target`在区间`[left, mid - 1]`中,则令`right = mid - 1`; 42 | - 若`nums[mid] = target`,则**左边界**在区间`[left, mid - 1]`中,**右边界**在区间`[mid + 1, right]`中。分以下两种情况: 43 | - 若查找**右边界**,则执行`right = mid + 1` 44 | - 若查找**左边界**,则执行`left = mid - 1` 45 | - **返回值**:找到**左边界**`left`和**右边界**`right`,返回`right - left - 1`。 46 | 47 | **效率优化**: 48 | 49 | > 以下优化基于:查找完右边界,则`nums[j]`指向最右边的`target`(若存在)。 50 | 51 | - 查找完右边界后,可用`nums[j] = target`判断数组中是否包含`target`,若不包含直接返回`0`,无需再查找左边界。 52 | - 查找完右边界后,左边界一定再区间`[0, j]`中,继续二分查找即可。 53 | 54 | ## 复杂度分析 55 | 56 | - 时间复杂度:$O(\log N)$ 57 | - 空间复杂度:$O(1)$ 58 | 59 | ## 代码 60 | 61 | ```python 62 | class Solution: 63 | def search(self, nums: [int], target: int) -> int: 64 | # 搜索右边界 right 65 | i, j = 0, len(nums) - 1 66 | while i <= j: 67 | m = (i + j) // 2 68 | if nums[m] <= target: i = m + 1 69 | else: j = m - 1 70 | right = i 71 | # 若数组中无 target ,则提前返回 72 | if j >= 0 and nums[j] != target: return 0 73 | # 搜索左边界 left 74 | i = 0 75 | while i <= j: 76 | m = (i + j) // 2 77 | if nums[m] < target: i = m + 1 78 | else: j = m - 1 79 | left = j 80 | return right - left - 1 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /剑指offer系列/面试题60.n个骰子的点数.md: -------------------------------------------------------------------------------- 1 | n个骰子的点数 2 | 3 | # 题目描述 4 | 5 | 把`n`个骰子仍在地上,所有骰子朝上一面的点数之和为`s`。输入`n`,打印出`s`的所有可能的值出现的概率。 6 | 7 | 你需要用一个浮点数数组返回答案,其中第`i`个元素代表这`n`个骰子所能掷出的点数集合中第`i`小的那个概率。 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入: 1 13 | 输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667] 14 | ``` 15 | 16 | ## 示例2 17 | 18 | ``` 19 | 输入: 2 20 | 输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778] 21 | ``` 22 | 23 | ## 限制 24 | 25 | - `1 <= n <= 11` 26 | 27 | # 解题思路 28 | 29 | 根据概率公式,点数之和`s`出现概率计算公式为: 30 | $$ 31 | P(k) = \frac{s出现的次数}{总次数} 32 | $$ 33 | 投掷`n`个骰子,所有点数出现的总次数是$6^n$,因为一共有`n`个骰子,每个骰子的点数都有`6`中可能出现的情况。 34 | 35 | 由此,问题就转化为求解`s`出现的次数的问题。 36 | 37 | 我们利用**动态规划**进行求解: 38 | 39 | - **表示状态**:利用二维数组`dp[i][j]`表示当投完第`i`个骰子后,点数之和为`j`的**次数**。 40 | - **转移方程**:单看第`i`个骰子,它的点数可能是`1, 2, 3, 4, 5, 6`六种情况,因此,投掷完第`i`个骰子后点数之和为`j`的次数,可以由投掷完第`i - 1`个骰子后对应点数和`j-1, j-2, j-3, j-4, j-5, j-6`出现的次数之和转移过来。 41 | - **边界处理**:`dp[1][j]`若仅有`1`个骰子,那么点数和的情况为`1, 2, 3, 4, 5, 6`且每种情况出现的次数都是`1`。 42 | 43 | ## 复杂度分析 44 | 45 | > 其实题目中限制了`n`不会取到太大,否则即使用动态规划也很容易超时。 46 | 47 | - 时间复杂度:$O(N^2)$ 48 | - 空间复杂度:$O(N^2)$ 49 | 50 | ## 代码 51 | 52 | ```python 53 | class Solution: 54 | def twoSum(self, n: int) -> List[float]: 55 | # 1 <= n <= 11 56 | dp =[[0 for _ in range(6*11+1)]for _ in range(n+1)] 57 | for i in range(1, 7): 58 | dp[1][i] = 1 59 | 60 | for i in range(2, n+1): 61 | for j in range(i, 6*i+1): 62 | for k in range(1, 7): 63 | if j-k > 0: 64 | dp[i][j] += dp[i-1][j-k] 65 | res = [] 66 | for i in range(n, 6*n+1): 67 | res.append(dp[n][i]/(6**n)) 68 | 69 | return res 70 | ``` 71 | 72 | ## 空间优化 73 | 74 | 通过**转移方程**可以看出在计算`dp[i][j]`时仅与`i - 1`维有关,我们可以借助滚动数组的思想将二维数组压缩为一维数组。 75 | 76 | ## 代码 77 | 78 | ```python 79 | class Solution: 80 | def twoSum(self, n: int) -> List[float]: 81 | # 1 <= n <= 11 82 | dp = [0 for _ in range(6*11 + 1)] 83 | for i in range(1, 7): 84 | dp[i] = 1 85 | 86 | for i in range(2, n+1): 87 | for j in range(6*i, i-1, -1): 88 | for k in range(1, 7): 89 | if j-k > i-1: # 注意这里的判别条件与上面的代码不一样 90 | dp[j] += dp[j-k] 91 | res = [] 92 | all_num = 6**n 93 | for i in range(n, 6*n+1): 94 | res.append(dp[i]/all_num) 95 | 96 | return res 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /每日一题系列/874.模拟行走机器人.md: -------------------------------------------------------------------------------- 1 | 模拟行走机器人 2 | 3 | # 题目描述 4 | 5 | 机器人在一个无限大小的网格上行走,从点`(0,0)`处开始出发,面向北方。该机器人可以接收以下三种类型的命令: 6 | 7 | - `-2`:向左转90度 8 | - `-1`:向右转90度 9 | - `1 <= x <= 9`:向前移动`x`个单位长度 10 | 11 | 在网格上有一些格子被视为障碍物。 12 | 13 | 第`i`个障碍物位于网格点`(obstacles[i][0],obstacles[i][1])`机器人无法走到障碍物上,它将停留在障碍物的前一个网格点上,但仍然可以继续改路线的其余部分。 14 | 15 | 返回从原点到机器人的最大欧氏距离的**平方**。 16 | 17 | ## 示例1 18 | 19 | ``` 20 | 输入: commands = [4,-1,3], obstacles = [] 21 | 输出: 25 22 | 解释: 机器人将会到达 (3, 4) 23 | ``` 24 | 25 | ## 示例2 26 | 27 | ``` 28 | 输入: commands = [4,-1,4,-2,4], obstacles = [[2,4]] 29 | 输出: 65 30 | 解释: 机器人在左转走到 (1, 8) 之前将被困在 (1, 4) 处 31 | ``` 32 | 33 | ## 提示 34 | 35 | - `0 <= commands.length <= 10000` 36 | - `0 <= obstacles.length <= 10000` 37 | - `-30000 <= obstacle[i][0] <= 30000` 38 | - `-30000 <= obstacle[i][1] <= 30000` 39 | 40 | # 解题思路 41 | 42 | > 这道题,看完题目可能还没明白题目的意思。可以在白纸上画一个`x-y`直角坐标系,原点即为机器人初始点,`面向北方`可以理解为机器人面向`y轴正方向`,指令`-1、-2`仅使机器人发生转向,移动时仅朝机器人面向的方向进行直线运动,若会遇到障碍物,则在障碍物前一格停止,可以继续接收指令。 43 | 44 | ## 算法思路 45 | 46 | 设置`dx,dy`表示方向变量,当接收到转向指令时发生改变: 47 | 48 | - 初始时,面向北方,则`dx=0,dy=1`; 49 | - 接收指令`-2`,左转90度,则`dx=-dy,dy=dx`; 50 | - 接收指令`-1`,右转90度,则`dx=dy,dy=-dx`。 51 | 52 | 注意:每次转向时,是以当前面向为基准区分左/右方向。 53 | 54 | 由于每次移动时,都是沿着当前面向的方向做直线运动,则可以设置`x,y`变量来记录真实坐标位置(每次只可能沿一个方向移动)。 55 | 56 | 由于障碍物的存在,我们在移动机器人时,需要判断是否会遇到障碍物,如果遇到,只能在障碍物前停下。可以将障碍物的坐标信息加入集合,这样判断坐标是否冲突时可以以$O(1)$时间内完成(列表需要$O(N)$)。 57 | 58 | 最后,题目所要求的最大距离并不一定是机器人最终所在的位置,每执行一次移动指令,需要对最大距离进行更新。(需要明白,最大距离一定是某一次移动指令的起点距离或者终点距离,移动过程中并不会产生最大距离) 59 | 60 | ## 复杂度分析 61 | 62 | > $N$表示移动指令的数量;$M$表示移动的步数;$K$表示障碍物个数 63 | 64 | - 时间复杂度:$O(NM)$ 65 | - 空间复杂度:$O(K)$,用于存放障碍物坐标的集合 66 | 67 | ## 代码 68 | 69 | ```python 70 | class Solution: 71 | def robotSim(self, commands: List[int], obstacles: List[List[int]]) -> int: 72 | dx, dy = 0, 1 73 | x, y = 0, 0 74 | obs = set() 75 | distance = 0 76 | for obstacle in obstacles: 77 | obs.add(tuple(obstacle)) 78 | for command in commands: 79 | if command == -2: 80 | dx, dy = -dy, dx 81 | elif command == -1: 82 | dx, dy = dy, -dx 83 | else: 84 | for _ in range(command): 85 | next_x = x + dx 86 | next_y = y + dy 87 | if (next_x, next_y) in obs: 88 | break 89 | x, y = next_x, next_y 90 | distance = max(distance, int(math.pow(x, 2)+math.pow(y, 2))) 91 | 92 | return distance 93 | ``` 94 | 95 | -------------------------------------------------------------------------------- /每日一题系列/435.无重叠区间.md: -------------------------------------------------------------------------------- 1 | 无重叠区间 2 | 3 | # 题目描述 4 | 5 | 给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 6 | 7 | ## 注意 8 | 9 | 1. 可以认为区间的终点始终大于它的起点。 10 | 2. 区间`[1,2]`和`[2,3]`的边界相互"接触",但没有相互重叠。 11 | 12 | ## 示例1 13 | 14 | ``` 15 | 输入: [ [1,2], [2,3], [3,4], [1,3] ] 16 | 输出: 1 17 | 解释: 移除 [1,3] 后,剩下的区间没有重叠。 18 | ``` 19 | 20 | ## 示例2 21 | 22 | ``` 23 | 输入: [ [1,2], [1,2], [1,2] ] 24 | 输出: 2 25 | 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 26 | ``` 27 | 28 | ## 示例3 29 | 30 | ``` 31 | 输入: [ [1,2], [2,3] ] 32 | 输出: 0 33 | 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 34 | ``` 35 | 36 | # 解题思路 37 | 38 | > 这道题还是求最值问题,而且与[452.用最少数量的箭引爆气球](https://github.com/TrippleKing/LeetCode_Python3/blob/master/每日一题系列/452.用最少数量的箭引爆气球.md)非常相似,仍然是贪心算法的求解思想。 39 | 40 | 结合下图,进行讲解: 41 | 42 | ![452.jpg](https://pic.leetcode-cn.com/964afe81a1f6023a4f5a337c143bf0b0ad4df9de9089d35af26c2a5974504336-file_1566313617197) 43 | 44 | 我们可以先将原区间集合,按区间的`end`数值进行升序排列(也可以按区间的`start`数值进行升序排序),然后进行遍历: 45 | 46 | - 初始化`end`,赋值为排序后第一个区间的`end`值,此值必然是所有区间中最小的`end`值。 47 | - 访问其他区间`intervals[i]`,若`intervals[i][0]`小于`end`,说明`intervals[i]`必然发生重叠;若`intervals[i][0]`大于等于`end`,说明`intervals[i]`可以保留,同时更新`end=intervals[i][1]` 48 | - 可以选择统计保留下的数量`rest`,则最后结果返回`len(intervals)-rest`;也可以选择直接统计删除的数量。 49 | 50 | ## 复杂度分析 51 | 52 | - 时间复杂度:$O(NlogN)$,排序需要花费$O(NlogN)$;遍历需要花费$O(N)$。 53 | - 空间复杂度:$O(N)$,排序时需要花费$O(N)$。 54 | 55 | ## 代码 56 | 57 | ```python 58 | # 以区间的start数值进行升序排序 59 | # class Solution: 60 | # def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: 61 | # if len(intervals) <= 1: 62 | # return 0 63 | 64 | # intervals = sorted(intervals, key=lambda x: x[0]) 65 | # remove = 0 66 | # end = intervals[0][1] 67 | # for i in range(1, len(intervals)): 68 | # if intervals[i][0] < end: 69 | # remove += 1 70 | # end = min(end, intervals[i][1]) 71 | # continue 72 | # else: 73 | # end = intervals[i][1] 74 | # return remove 75 | 76 | 77 | # 以区间的end数值进行升序排序 78 | class Solution: 79 | def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: 80 | if len(intervals) <= 1: 81 | return 0 82 | intervals = sorted(intervals, key=lambda x: x[1]) 83 | rest = 1 # 统计剩余有效区间 84 | end = intervals[0][1] 85 | for i in range(1, len(intervals)): 86 | if intervals[i][0] >= end: 87 | rest += 1 88 | end = intervals[i][1] 89 | 90 | return len(intervals) - rest 91 | ``` 92 | 93 | -------------------------------------------------------------------------------- /剑指offer系列/面试题68-II.二叉树的最近公共祖先.md: -------------------------------------------------------------------------------- 1 | 二叉树的最近公共祖先 2 | 3 | # 题目描述 4 | 5 | 给定一棵二叉树,找到该树中两个指定节点的最近公共祖先。 6 | 7 | 最近公共祖先的定义为:"对于有根树T的两个节点p、q,最近公共祖先表示为一个节点x,满足x是p、q的祖先且x的深度尽可能大(**一个节点也可以是它自己的祖先**)。" 8 | 9 | 例如,给定如下二叉树:`root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4]` 10 | 11 | ![示例.jpg](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/binarytree.png) 12 | 13 | ## 示例1 14 | 15 | ``` 16 | 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 17 | 输出: 3 18 | 解释: 节点 5 和节点 1 的最近公共祖先是节点 3。 19 | ``` 20 | 21 | ## 示例2 22 | 23 | ``` 24 | 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 25 | 输出: 5 26 | 解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 27 | ``` 28 | 29 | ## 说明 30 | 31 | - 所有节点的值都是唯一的。 32 | - p、q为不同节点且均存在于给定的二叉树中。 33 | 34 | # 解题思路 35 | 36 | 本题与[面试题68-I.二叉搜索树的最近公共祖先](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题68-I.二叉搜索树的最近公共祖先.md)非常相似,唯一区别就是从**二叉搜索树**变成了**二叉树**,其他条件完全相同。 37 | 38 | 从解题核心思想上说,本题也是与[面试题68-I](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题68-I.二叉搜索树的最近公共祖先.md)保持一致,没有了**二叉搜索树**这一条件,意味着不能通过根据节点`p, q`与根`root`数值大小关系去判断节点`p, q`处于左子树还是右子树。 39 | 40 | 所以考虑通过递归对二叉树进行后序遍历,当遇到节点`p`或`q`时返回。从底至顶回溯,当节点`p, q`在节点`root`的两侧,则`root`即为最近公共祖先,向上返回`root`。 41 | 42 | **递归解析** 43 | 44 | - **终止条件**: 45 | - 当越过叶子节点,则直接返回`null`; 46 | - 当`root`等于`p`或`q`,则直接返回`root`; 47 | - **递推工作**: 48 | - 开启递归左子节点,返回值记为`left`; 49 | - 开启递归右子节点,返回值记为`right`; 50 | - **返回值**: 51 | - 当`left`和`right`**同时为空**:说明`root`的左/右子树中都不包含`p, q`,返回`null`; 52 | - 当`left`和`right`**同时不为空**:说明`p, q`分布在`root`的两侧,返回`root`; 53 | - 当`left`**为空**,`right`**不为空**:`p, q`都不在`root`的左子树中,直接返回`right`。具体可分为两种情况: 54 | - `p, q`其中一个在`root`的**右子树**中,此时`right`指向`p`(假设为`p`) 55 | - `p, q`两节点都在`root`的**右子树**中,此时`right`指向**最近公共祖先节点** 56 | - 当`left`**不为空**,`right`**为空**,与上一中情况同理。 57 | 58 | ## 复杂度分析 59 | 60 | - 时间复杂度:$O(N)$,$N$为二叉树节点数 61 | - 空间复杂度:$O(N)$,最坏情况下,递归深度达到$N$。 62 | 63 | ## 代码 64 | 65 | ```python 66 | # Definition for a binary tree node. 67 | # class TreeNode: 68 | # def __init__(self, x): 69 | # self.val = x 70 | # self.left = None 71 | # self.right = None 72 | 73 | class Solution: 74 | def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: 75 | if not root or root == p or root == q: return root 76 | left = self.lowestCommonAncestor(root.left, p, q) 77 | right = self.lowestCommonAncestor(root.right, p, q) 78 | if not left: return right 79 | if not right: return left 80 | return root 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /剑指offer系列/面试题36.二叉搜索树与双向链表.md: -------------------------------------------------------------------------------- 1 | 二叉搜索树与双向链表 2 | 3 | # 题目描述 4 | 5 | 输入一棵二叉搜索树,将该二叉搜索树转换成一个**排序的循环双向链表**。要求不能创建任何新的节点,只能调整数中节点指针的指向。 6 | 7 | 为了让您更好地理解问题,以下面的二叉搜索树为例: 8 | 9 | 例.jpg 10 | 11 | 我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。 12 | 13 | 下图展示了上面的二叉搜索树转化成的链表。`head`表示指向链表中有最小元素的节点。 14 | 15 | 示例.jpg 16 | 17 | 特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。 18 | 19 | # 解题思路 20 | 21 | > 本题解法基于性质:二叉搜索树的中序遍历为**递增序列**。 22 | 23 | 将二叉搜索树转换成一个**排序的循环双向链表**,包含三个部分: 24 | 25 | - **排序链表**:节点应从小到大排序,因此应使用**中序遍历**访问树节点。 26 | - **双向链表**:在构建相邻节点(设前驱节点`pre`,当前节点`cur`)关系时,不仅应`pre.right = cur`,也应`cur.left = pre`。 27 | - **循环链表**:设链表头节点`head`和尾节点`tail`,则应构建`head.left = tail`和`tail.left = head`。 28 | 29 | ## 算法流程 30 | 31 | `dfs(cur_node)`递归法中序遍历: 32 | 33 | - **终止条件**:当节点`cur_node`为空时,直接返回 34 | - 递归左子树,即`dfs(cur_node.left)` 35 | - **构建链表**: 36 | - 当`pre`为空时:代表正在访问链表头节点,记为`head` 37 | - 当`pre`不为空时:修改双向节点引用,`pre.right = cur_node`;`cur_node.left = pre` 38 | - 保存`cur_node`:更新`pre = cur_node`,即节点`cur_node`是`pre`的后继节点 39 | - 递归右子树,即`dfs(cur_node.right)` 40 | 41 | `treeToDoublyList(root)`: 42 | 43 | - **特例处理**:若节点`root`为空,直接返回。 44 | - **初始化**:空节点`pre` 45 | - **转化为双向链表**:调用`dfs(root)` 46 | - **构建循环链表**:中序遍历完成后,`head`指向头节点,`pre`指向尾节点,因此修改`head`和`pre`的双向节点引用即可 47 | - **返回值**:返回`head` 48 | 49 | ## 复杂度分析 50 | 51 | - 时间复杂度:$O(N)$ 52 | - 空间复杂度:$O(N)$,最坏情况,树退化为链表,递归深度达到$N$。 53 | 54 | ## 代码 55 | 56 | ```python 57 | """ 58 | # Definition for a Node. 59 | class Node: 60 | def __init__(self, val, left=None, right=None): 61 | self.val = val 62 | self.left = left 63 | self.right = right 64 | """ 65 | class Solution: 66 | def treeToDoublyList(self, root: 'Node') -> 'Node': 67 | # 中序遍历 -> 即得到排序 68 | def dfs(cur_node): 69 | if not cur_node: return 70 | dfs(cur_node.left) 71 | if self.pre: 72 | self.pre.right, cur_node.left = cur_node, self.pre 73 | else: 74 | self.head = cur_node 75 | self.pre = cur_node 76 | dfs(cur_node.right) 77 | 78 | if not root: return 79 | self.pre = None 80 | dfs(root) 81 | self.head.left, self.pre.right = self.pre, self.head 82 | 83 | return self.head 84 | ``` 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /每日一题系列/1223.掷骰子模拟.md: -------------------------------------------------------------------------------- 1 | 掷骰子模拟 2 | 3 | # 题目描述 4 | 5 | 有一个骰子模拟器会每次投掷的时候生成一个`1`到`6`的随机数。不过我们在使用它时有个约束,就是使得投掷骰子时,**连续**掷出数字`i`的次数不能超过`rolMax[i]`(`i`从`1`开始编号,实际数组中索引从`0`开始)。 6 | 7 | 现在,给你一个整数数组`rolMax`和一个整数`n`,请你来计算掷`n`次骰子可得到的不同点数序列的数量。 8 | 9 | 假如两个序列中至少存在一个元素不同,就认为这两个序列是不同的。由于答案可能很大,所以请返回**模**`10^9 + 7`之后的结果。 10 | 11 | ## 示例1 12 | 13 | ``` 14 | 输入:n = 2, rollMax = [1,1,2,2,2,3] 15 | 输出:34 16 | 解释:我们掷 2 次骰子,如果没有约束的话,共有 6 * 6 = 36 种可能的组合。但是根据 rollMax 数组,数字 1 和 2 最多连续出现一次,所以不会出现序列 (1,1) 和 (2,2)。因此,最终答案是 36-2 = 34。 17 | ``` 18 | 19 | ## 示例2 20 | 21 | ``` 22 | 输入:n = 2, rollMax = [1,1,1,1,1,1] 23 | 输出:30 24 | ``` 25 | 26 | ## 示例3 27 | 28 | ``` 29 | 输入:n = 3, rollMax = [1,1,1,2,2,3] 30 | 输出:181 31 | ``` 32 | 33 | ## 提示 34 | 35 | - `1 <= n <= 5000` 36 | - `rollMax.length == 6` 37 | - `1 <= rollMax[i] <= 15` 38 | 39 | # 解题思路 40 | 41 | 这是一道高维度的动态规划问题。 42 | 43 | **状态表示**:用数组`dp[i][j][k]`表示第`i`轮掷骰子掷出数字`j`时,`j`连续出现`k`次的组合数量。 44 | 45 | **状态转移**: 46 | 47 | - 当`j`并非连续出现时(即`k == 1`时):`j`出现`1`次的组合数等于上一轮投掷出非数字`j`的所有情况的总和,即`dp[i][j][1] = sum(dp[i-1][other != j][:])` 48 | - 当`j`连续出现`k, (k > 1)`次时:本轮投出连续`k`次数字`j`的情况数量等于上一轮投掷出`k-1`次数字`j`的情况数量,即`dp[i][j][k] = dp[i-1][j][k-1]`,且需要保证`k <= rollMax[j]`。 49 | 50 | ## 复杂度分析 51 | 52 | - 时间复杂度:$O(N)$,看似最高三重循环,但除了投掷次数`n`次以外,其他循环次数都是常数次的。 53 | - 空间复杂度:$O(N)$,同理,看似三维空间数组,但只有第一维与投掷次数`n`有关,其他两维都是常数尺度大小。 54 | 55 | ## 代码 56 | 57 | ```python 58 | class Solution: 59 | def dieSimulator(self, n: int, rollMax: List[int]) -> int: 60 | dp = [[[0 for _ in range(16)] for _ in range(7)] for _ in range(n+1)] 61 | mod = 10**9 + 7 62 | 63 | # 投掷的次数 64 | for i in range(1, n+1): 65 | # 第i次投掷 66 | for j in range(1, 7): 67 | # 如果是第1次投掷 68 | if i == 1: 69 | dp[i][j][1] = 1 70 | continue 71 | 72 | # 数字j连续出现k次 73 | for k in range(2, rollMax[j-1] + 1): 74 | dp[i][j][k] = dp[i-1][j][k-1] 75 | 76 | # 前一次投出的数字不是j 77 | cnt = 0 78 | for l in range(1, 7): 79 | if l == j: 80 | continue 81 | for k in range(1, 16): 82 | cnt += dp[i-1][l][k] 83 | cnt %= mod 84 | dp[i][j][1] = cnt 85 | 86 | res = 0 87 | for j in range(1, 7): 88 | for k in range(1, 16): 89 | res += dp[n][j][k] 90 | res %= mod 91 | return res 92 | ``` 93 | 94 | -------------------------------------------------------------------------------- /剑指offer系列/面试题24.反转链表.md: -------------------------------------------------------------------------------- 1 | 反转链表 2 | 3 | # 题目描述 4 | 5 | 定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入: 1->2->3->4->5->NULL 11 | 输出: 5->4->3->2->1->NULL 12 | ``` 13 | 14 | ## 限制 15 | 16 | - `0 <= 节点个数 <= 5000` 17 | 18 | # 解题思路 19 | 20 | > 涉及到链表的题目,一定要在纸上画一画过程,再写程序! 21 | 22 | 这道题,一种最简单直接的方法就是先找个容器,把链表中的节点依次取出并存放,然后逆序构建一个链表即可。但显然这不是最优的解决方案。 23 | 24 | ## 方法一:双指针 25 | 26 | 我们考虑双指针方案,初始化快慢指针`slow`和`fast`: 27 | 28 | - `slow`指向`None`,`fast`指向`head`; 29 | - 不断循环`fast`: 30 | - 临时变量`tmp`记录`fast`的下一个节点; 31 | - `fast`的指向`slow`,即`fast.next = slow`; 32 | - `slow`前进一位到`fast`, 即`slow = fast`; 33 | - `fast`前进一位到`tmp`,即`fast = tmp`; 34 | - 直至`fast = None`,退出循环; 35 | - 返回`slow`即可 36 | 37 | ## 复杂度分析 38 | 39 | - 时间复杂度:$O(N)$ 40 | - 空间复杂度:$O(1)$ 41 | 42 | ## 代码 43 | 44 | ```python 45 | # Definition for singly-linked list. 46 | # class ListNode: 47 | # def __init__(self, x): 48 | # self.val = x 49 | # self.next = None 50 | 51 | class Solution: 52 | def reverseList(self, head: ListNode) -> ListNode: 53 | if not head or not head.next: 54 | return head 55 | slow = None 56 | fast = head 57 | while fast: 58 | tmp = fast.next 59 | fast.next = slow 60 | slow = fast 61 | fast = tmp 62 | return slow 63 | ``` 64 | 65 | ## 方法二:递归 66 | 67 | 递归解法不是很好理解,文字较难描述,如有需要可以[访问链接](https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof/solution/dong-hua-yan-shi-duo-chong-jie-fa-206-fan-zhuan-li/)看一下大佬的动画题解。 68 | 69 | 递归的两个条件: 70 | 71 | - **终止条件**:当前节点或者下一个节点为`None`; 72 | - **回溯阶段**:当前节点的`next`指针指向上一节点;(`head.next.next = head`) 73 | 74 | ## 复杂度分析 75 | 76 | - 时间复杂度:$O(N)$ 77 | - 空间复杂度:$O(N)$ 78 | 79 | ## 代码 80 | 81 | ```python 82 | # Definition for singly-linked list. 83 | # class ListNode: 84 | # def __init__(self, x): 85 | # self.val = x 86 | # self.next = None 87 | 88 | class Solution(object): 89 | def reverseList(self, head): 90 | """ 91 | :type head: ListNode 92 | :rtype: ListNode 93 | """ 94 | # 递归终止条件是当前为空,或者下一个节点为空 95 | if(head == None or head.next == None): 96 | return head 97 | # 这里的cur就是最后一个节点 98 | cur = self.reverseList(head.next) 99 | # 这里请配合动画演示理解 100 | # 如果链表是 1->2->3->4->5,那么此时的cur就是5 101 | # 而head是4,head的下一个是5,下下一个是空 102 | # 所以head.next.next 就是5->4 103 | head.next.next = head 104 | # 防止链表循环,需要将head.next设置为空 105 | head.next = None 106 | # 每层递归函数都返回cur,也就是最后一个节点 107 | return cur 108 | ``` 109 | 110 | -------------------------------------------------------------------------------- /剑指offer系列/面试题29.顺时针打印矩阵.md: -------------------------------------------------------------------------------- 1 | 顺时针打印矩阵 2 | 3 | # 题目描述 4 | 5 | 输入一个矩阵,按照从外向里顺时针的顺序依次打印出每一个数字。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 11 | 输出:[1,2,3,6,9,8,7,4,5] 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] 18 | 输出:[1,2,3,4,8,12,11,10,9,5,6,7] 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `0 <= matrix.length <= 100` 24 | - `0 <= matrix[i].length <= 100` 25 | 26 | # 解题思路 27 | 28 | 根据题意,顺时针打印可拆分为四个方向依次进行: 29 | 30 | 1. 从左到右打印一次; 31 | 2. 从上到下打印一次; 32 | 3. 从右到左打印一次; 33 | 4. 从下到上打印一次。 34 | 35 | 由此,我们为矩阵初始化四个边界:(假设矩阵的行数为`row`,列数为`col`) 36 | 37 | 1. **左边界**:初始值为`0`。 38 | 2. **右边界**:初始值为`col - 1`。 39 | 3. **上边界**:初始值为`0`。 40 | 4. **下边界**:初始值为`row - 1`。 41 | 42 | ## 算法流程 43 | 44 | - 从左边界至右边界打印一次,此时上边界需要更新` + 1` 45 | - 从上边界至下边界打印一次,此时右边界需要更新` - 1` 46 | - 从右边界至左边界打印一次,此时下边界需要更新` - 1` 47 | - 从下边界至上边界打印一次,此时左边界需要更新` + 1` 48 | 49 | 不断循环,直至上下边界交错或者左右边界交错则跳出循环。 50 | 51 | ## 复杂度分析 52 | 53 | - 时间复杂度:$O(N)$ 54 | - 空间复杂度:$O(N)$ 55 | 56 | ## 代码 57 | 58 | ```python 59 | class Solution: 60 | def spiralOrder(self, matrix: List[List[int]]) -> List[int]: 61 | if not matrix or not matrix[0]: 62 | return matrix 63 | row = len(matrix) 64 | col = len(matrix[0]) 65 | bound_left = 0 66 | bound_right = col - 1 67 | bound_top = 0 68 | bound_bot = row - 1 69 | res = [] * (row*col) 70 | while True: 71 | # from left to right 72 | for i in range(bound_left, bound_right + 1): 73 | res.append(matrix[bound_top][i]) 74 | bound_top += 1 75 | if bound_top > bound_bot: 76 | break 77 | # from top to bottom 78 | for i in range(bound_top, bound_bot + 1): 79 | res.append(matrix[i][bound_right]) 80 | bound_right -= 1 81 | if bound_right < bound_left: 82 | break 83 | # from right to left 84 | for i in range(bound_right, bound_left - 1, -1): 85 | res.append(matrix[bound_bot][i]) 86 | bound_bot -= 1 87 | if bound_bot < bound_top: 88 | break 89 | # from bottom to top 90 | for i in range(bound_bot, bound_top - 1, -1): 91 | res.append(matrix[i][bound_left]) 92 | bound_left += 1 93 | if bound_left > bound_right: 94 | break 95 | return res 96 | ``` 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /每日一题系列/808.分汤.md: -------------------------------------------------------------------------------- 1 | 分汤 2 | 3 | # 题目描述 4 | 5 | 有`A`和`B`两种类型的汤。一开始每种类型的汤有`N`毫升。有四种分配操作: 6 | 7 | - 提供`100ml`的汤`A`和`0ml`的汤`B`; 8 | - 提供`75ml`的汤`A`和`25ml`的汤`B`; 9 | - 提供`50ml`的汤`A`和`50ml`的汤`B`; 10 | - 提供`25ml`的汤`A`和`75ml`的汤`B`; 11 | 12 | 当我们把汤分配给某人之后,汤就没有了。每个回合,我们将从四种概率同为`0.25`的操作中进行分配选择。如果汤的剩余量不足以完成某次操作,我们将尽可能分配。当每种类型的汤都分配完时,停止操作。 13 | 14 | 注意不存在先分配`100ml`汤`B`的操作。 15 | 16 | 需要返回的值:汤`A`先分配完的概率 + 汤`A`和汤`B`同时分配完的概率 / 2 17 | 18 | ## 示例 19 | 20 | ``` 21 | 示例: 22 | 输入: N = 50 23 | 输出: 0.625 24 | 解释: 25 | 如果我们选择前两个操作,A将首先变为空。对于第三个操作,A和B会同时变为空。对于第四个操作,B将首先变为空。 26 | 所以A变为空的总概率加上A和B同时变为空的概率的一半是 0.25 *(1 + 1 + 0.5 + 0)= 0.625。 27 | ``` 28 | 29 | ## 提示 30 | 31 | - `0 <= N <= 10^9` 32 | - 返回值在`10^-6`的范围内被认为是正确的。 33 | 34 | # 解题思路 35 | 36 | **动态规划思想**: 37 | 38 | 首先,由于四种分配操作都是`25`的倍数,我们可以将`N`除以`25`(如果有余数,则补`1`),相应的分配操作将变为`(4, 0), (3, 1), (2, 2), (1, 3)`。 39 | 40 | 当`N`较小时,我们可以用动态规划来解决这个问题,设`dp[i][j]`表示汤`A`和汤`B`分别剩下`i`和`j`份时,所求的概率。状态转移方程为: 41 | 42 | `dp[i][j] = 0.25 * (dp[i-4][j] + dp[i-3][j-1] + dp[i-2][j-2] + dp[i-1][j-3])` 43 | 44 | 边界条件为: 45 | 46 | ```python 47 | if i <= 0 and j <= 0: dp[i][j] = 0.5 # 同时分配完 48 | if i <= 0 and j > 0: dp[i][j] = 1.0 # A先分配完 49 | if i > 0 and j <= 0: dp[i][j] = 0.0 # B先分配完 50 | ``` 51 | 52 | 这个动态规划的时间复杂度为$O(N^2)$,即使将$N$除以`25`,可以降低$N$的大小,但当$N$很大时,仍然无法在短时间内得到答案,因此我们需要进一步分析。可以发现,分配的操作有`(4, 0), (3, 1), (2, 2), (1, 3)`四种,在每一回合,分配汤`A`的期望为`(4 + 3 + 2 + 1)/4 = 2.5`份,分配汤`B`的期望为`(0 + 1 + 2 + 3)/4 = 1.5`份,初始时均为`N`,则当$N$很大时,汤`A`会有很大的概率比汤`B`先分配完,所以概率应该非常接近`1`。事实上,当`N >= 500 * 25`时,所求概率已经大于`0.999999`了(可以通过上面的动态规划方法求出),它和`1`的误差(无论是绝对误差还是相对误差)都小于`10^-6`。因此在`N >= 500 * 25`时,直接返回`1`即可。 53 | 54 | ## 复杂度分析 55 | 56 | - 时间复杂度:$O(N^2)$,$N < 500\times 25$;当$N >= 500\times 25$时,为$O(1)$。 57 | - 空间复杂度:$O(N^2)$,$N < 500\times 25$;当$N >= 500\times 25$时,为$O(1)$。 58 | 59 | ## 代码 60 | 61 | ```python 62 | class Solution: 63 | def soupServings(self, N: int) -> float: 64 | if not N: 65 | return 0.5 66 | 67 | if not N % 25: 68 | N = N // 25 69 | else: 70 | N = N // 25 + 1 71 | 72 | if N >= 500: 73 | return 1 74 | 75 | dp = [[0.0 for _ in range(N+1)] for _ in range(N+1)] 76 | for i in range(1, N+1): 77 | for j in range(1, N+1): 78 | for ii, jj in [(i-4, j), (i-3, j-1), (i-2, j-2), (i-1, j-3)]: 79 | if ii > 0 and jj > 0: 80 | dp[i][j] += dp[ii][jj] 81 | elif ii <= 0: 82 | dp[i][j] += 0.5 if jj <= 0 else 1.0 83 | dp[i][j] *= 0.25 84 | return dp[-1][-1] 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /剑指offer系列/面试题55-I.二叉树的深度.md: -------------------------------------------------------------------------------- 1 | 二叉树的深度 2 | 3 | # 题目描述 4 | 5 | 输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。 6 | 7 | ## 示例 8 | 9 | 给定二叉树`[3, 9, 20, null, null, 15, 7]` 10 | 11 | ``` 12 | 3 13 | / \ 14 | 9 20 15 | / \ 16 | 15 7 17 | ``` 18 | 19 | 返回它的最大深度`3` 20 | 21 | ## 提示 22 | 23 | - `节点总数 <= 10000` 24 | 25 | # 解题思路 26 | 27 | > 树的遍历方式总体分两种:深度优先搜索(DFS)、广度优先搜索(BFS) 28 | > 29 | > - 常见的DFS:先序遍历、中序遍历、后序遍历 30 | > - 常见的BFS:层序遍历 31 | 32 | 本题求树的深度,需要遍历树的所有节点,基于DFS或BFS均可求解。 33 | 34 | ## 方法一:DFS 35 | 36 | **核心思想**:树的深度等于**左子树的深度**与**右子树的深度**中的**较大值** + 1。 37 | 38 | **算法流程**: 39 | 40 | - **终止条件**:当`root`为空,说明已越过叶子几点,此时返回深度`0`; 41 | - **递推工作**: 42 | - 计算节点`root`的**左子树的深度**,调用`maxDepth(root.left)`; 43 | - 计算节点`root`的**右子树的深度**,调用`maxDepth(root.right)`; 44 | - **返回值**:返回此数的深度,即`max(maxDepth(root.left), maxDepth(root.right)) + 1`。 45 | 46 | ## 复杂度分析 47 | 48 | - 时间复杂度:$O(N)$,$N$为树的节点数量(需遍历所有节点) 49 | - 空间复杂度:$O(N)$,最差情况下(树退化为链表),递归深度到达$N$ 50 | 51 | ## 代码 52 | 53 | ```python 54 | class Solution: 55 | def maxDepth(self, root: TreeNode) -> int: 56 | if not root: return 0 57 | return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1 58 | ``` 59 | 60 | ## 方法二:BFS 61 | 62 | **核心思想**:每遍历一层,计数器`+ 1`,直到遍历完成,则得到树的深度。 63 | 64 | **算法解析**: 65 | 66 | - **特例处理**:当`root`为空,直接返回深度`0`; 67 | - **初始化**:队列`queue`(加入根节点`root`),深度计数`depth = 0`; 68 | - **循环遍历**:当队列`queue`为空时跳出 69 | - 遍历当前队列`queue`中所有节点(此时的节点均处于同一层)`cur_node` 70 | - 若节点`cur_node`节点存在左节点或右节点,则将左节点或右节点加入队列,`queue.append(cur_node.left)、queue.append(cur_node.right)` 71 | - 同一层节点遍历完后,执行`depth += 1` 72 | - **返回值**:返回`depth` 73 | 74 | ## 复杂度分析 75 | 76 | - 时间复杂度:$O(N)$,$N$为树的节点数量,需遍历所有节点 77 | - 空间复杂度:$O(N)$,最坏情况下(当树平衡时),队列`queue`同时存储$N/2$个节点 78 | 79 | ## 代码 80 | 81 | ```python 82 | # Definition for a binary tree node. 83 | # class TreeNode: 84 | # def __init__(self, x): 85 | # self.val = x 86 | # self.left = None 87 | # self.right = None 88 | 89 | class Solution: 90 | def maxDepth(self, root: TreeNode) -> int: 91 | if not root: return 0 92 | queue = collections.deque() 93 | queue.append(root) 94 | depth = 0 95 | 96 | while queue: 97 | for _ in range(len(queue)): 98 | cur_node = queue.popleft() 99 | if cur_node.left: queue.append(cur_node.left) 100 | if cur_node.right: queue.append(cur_node.right) 101 | depth += 1 102 | return depth 103 | ``` 104 | 105 | -------------------------------------------------------------------------------- /每日一题系列/1162.地图分析.md: -------------------------------------------------------------------------------- 1 | 地图分析 2 | 3 | # 题目描述 4 | 5 | 你现在手里有一份大小为`N x N`的"地图"(网格)`grid`,上面的每个"区域"(单元格)都用`0`和`1`标记号了。其中`0`代表海洋,`1`代表陆地,请你找出一个海洋区域,这个海洋区域到离它最近的陆地区域的距离是最大的。 6 | 7 | 我们这里说的距离是"曼哈顿距离"(Manhattan Distance):`(x0, y0)`和`(x1, y1)`这两个区域之间的距离是`|x0 - x1| + |y0 - y1|`。 8 | 9 | 如果我们的地图上只有陆地或者海洋,请返回`-1`。 10 | 11 | ## 示例1 12 | 13 | ![示例1.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/08/17/1336_ex1.jpeg) 14 | 15 | ``` 16 | 输入:[[1,0,1],[0,0,0],[1,0,1]] 17 | 输出:2 18 | 解释: 19 | 海洋区域 (1, 1) 和所有陆地区域之间的距离都达到最大,最大距离为 2。 20 | ``` 21 | 22 | ## 示例2 23 | 24 | ![示例2.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/08/17/1336_ex2.jpeg) 25 | 26 | ``` 27 | 输入:[[1,0,0],[0,0,0],[0,0,0]] 28 | 输出:4 29 | 解释: 30 | 海洋区域 (2, 2) 和所有陆地区域之间的距离都达到最大,最大距离为 4。 31 | ``` 32 | 33 | ## 提示 34 | 35 | - `1 <= grid.length == grid[0].length <= 100` 36 | - `grid[i][j]`不是`0`就是`1`。 37 | 38 | # 解题思路 39 | 40 | > 对二维网格的搜索,且要找最短距离中的最大值,考虑广度优先搜索。 41 | 42 | 注意审题,题目要求的是海洋区域到离它**最近**的陆地区域的**最大距离**。 43 | 44 | 我们考虑从陆地出发(如果有多个陆地区域,就是多源问题)。 45 | 46 | ## 算法流程 47 | 48 | - **初始化**:队列`queue`;遍历网格,将元素为`1`的位置`(i, j, 0)`加入队列中(即加入陆地区域),`0`代表距离(初始时陆地上距离为`0`) 49 | - **特例判断**:若队列`queue`为空或者队列`queue`长度等于`N x N`,则说明网格中全为海洋或全为陆地,则返回`-1` 50 | - **BFS**:循环队列,直至为空 51 | - 弹出队首元素,记为`cur_row, cur_col, cur_dis`; 52 | - 遍历当前位置的上、下、左、右四个位置: 53 | - 若发生越界或位置上的元素为`1`,则`continue`; 54 | - 否则,将`(next_row, next_col, cur_dis+1)`加入队列,并将`grid[next_row][next_col]`标记为`1`。 55 | - **返回**:返回`cur_dis` 56 | 57 | ## 复杂度分析 58 | 59 | > 网格大小为$N\times N$ 60 | 61 | - 时间复杂度:$O(N^2)$ 62 | - 空间复杂度:$O(N^2)$ 63 | 64 | ## 代码 65 | 66 | ```python 67 | class Solution: 68 | def maxDistance(self, grid: List[List[int]]) -> int: 69 | row = len(grid) 70 | col = len(grid[0]) 71 | moves = [(-1, 0), (0, -1), (1, 0), (0, 1)] 72 | queue = collections.deque() 73 | 74 | for i in range(row): 75 | for j in range(col): 76 | if grid[i][j]: 77 | queue.append((i, j, 0)) 78 | if not queue or len(queue) == row*col: return -1 79 | 80 | while queue: 81 | cur_row, cur_col, cur_dis = queue.popleft() 82 | for move in moves: 83 | next_row = cur_row + move[0] 84 | next_col = cur_col + move[1] 85 | if next_row < 0 or next_col < 0 or next_row >= row or next_col >= col or grid[next_row][next_col]: 86 | continue 87 | queue.append((next_row, next_col, cur_dis + 1)) 88 | grid[next_row][next_col] = 1 89 | return cur_dis 90 | 91 | ``` 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /剑指offer系列/面试题25.合并两个排序的链表.md: -------------------------------------------------------------------------------- 1 | 合并两个排序的链表 2 | 3 | # 题目描述 4 | 5 | 输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入:1->2->4, 1->3->4 11 | 输出:1->1->2->3->4->4 12 | ``` 13 | 14 | ## 提示 15 | 16 | - `0 <= 链表长度 <= 1000` 17 | 18 | # 解题思路 19 | 20 | > 一种最简单基本的方法是,创建一个新链表,遍历两个链表`l1、l2`,比较当前节点大小,较小的节点加入新链表中。 21 | 22 | ## 方法一:新建链表合并 23 | 24 | 1. **初始化**:创建一个新链表,表头为`newLink`,指针`cur`指向`newLink`; 25 | 2. **循环合并**:当`l1`或`l2`为空时跳出循环: 26 | - 当`l1.val <= l2.val`时:`newLink`的`next`指针指向`l1`,且`l1`向下一个节点移动一步; 27 | - 当`l1.val > l2.val`时:`newLink`的`next`指针指向`l2`,且`l2`向下一个节点移动一步; 28 | - `newLink`向下一个节点移动一步; 29 | 3. **合并剩余尾部**:跳出循环时,`l1`或`l2`为空,则将非空链表的剩余部分直接添加至`newLink`后。 30 | 4. **返回值**:因为新链表的头节点为空(可以看做伪头节点),返回`cur.next`即可。 31 | 32 | ## 复杂度分析 33 | 34 | > 设$N,M$分别为链表`l1, l2`的长度。 35 | 36 | - 时间复杂度:$O(N+M)$,最坏情况需要完整遍历两个链表 37 | - 空间复杂度:$O(N+M)$ 38 | 39 | ## 代码 40 | 41 | ```python 42 | # Definition for singly-linked list. 43 | # class ListNode: 44 | # def __init__(self, x): 45 | # self.val = x 46 | # self.next = None 47 | 48 | class Solution: 49 | def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: 50 | cur = newLink = ListNode() 51 | while l1 and l2: 52 | if l1.val <= l2.val: 53 | newLink.next = ListNode(l1.val) 54 | l1 = l1.next 55 | else: 56 | newLink.next = ListNode(l2.val) 57 | l2 = l2.next 58 | newLink = newLink.next 59 | newLink.next = l1 if l1 else l2 60 | return cur.next 61 | ``` 62 | 63 | ## 方法二:递归 64 | 65 | 核心思想: 66 | 67 | 1. 当`l1`为空时,直接返回`l2`;当`l2`为空时,直接返回`l1`。这两种都是递归的边界条件。 68 | 2. 当`l1,l2`都不为空时,得到两链表头中的较小值,并返回。使用递归的方式不断比较(递归发生时更新表头) 69 | 70 | ## 复杂度分析 71 | 72 | > 设$N,M$分别为链表`l1, l2`的长度。 73 | 74 | - 时间复杂度:$O(N+M)$ 75 | - 空间复杂度:$O(N+M)$,递归时调用栈的深度 76 | 77 | ## 代码 78 | 79 | ```python 80 | # Definition for singly-linked list. 81 | # class ListNode: 82 | # def __init__(self, x): 83 | # self.val = x 84 | # self.next = None 85 | 86 | class Solution: 87 | def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode: 88 | if not l1: 89 | return l2 90 | if not l2: 91 | return l1 92 | 93 | p_head = None 94 | if l1.val < l2.val: 95 | p_head = l1 96 | l1.next = self.mergeTwoLists(l1.next, l2) 97 | else: 98 | p_head = l2 99 | l2.next = self.mergeTwoLists(l1, l2.next) 100 | return p_head 101 | ``` 102 | 103 | -------------------------------------------------------------------------------- /每日一题系列/1235.规划兼职工作.md: -------------------------------------------------------------------------------- 1 | 规划兼职工作 2 | 3 | # 题目描述 4 | 5 | 你打算利用空闲时间来做兼职工作赚些零花钱。 6 | 7 | 这里有`n`份兼职工作,每份工作预计从`startTime[i]`开始到`endTime[i]`结束,报酬为`profit[i]`。 8 | 9 | 给你一份兼职工作表,包含开始时间`startTime`,结束时间`endTime`和预计报酬`profit`三个数组,请你计算并返回可以获得的最大报酬。 10 | 11 | 注意,时间上出现重叠的2份工作不能同时进行。 12 | 13 | 如果你选择的工作在时间`X`结束,那么你可以立刻进行在时间`X`开始的下一份工作。 14 | 15 | ## 示例1 16 | 17 | ![示例1.jpg](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/sample1_1584.png) 18 | 19 | ``` 20 | 输入:startTime = [1,2,3,3], endTime = [3,4,5,6], profit = [50,10,40,70] 21 | 输出:120 22 | 解释: 23 | 我们选出第 1 份和第 4 份工作, 24 | 时间范围是 [1-3]+[3-6],共获得报酬 120 = 50 + 70。 25 | ``` 26 | 27 | ## 示例2 28 | 29 | ![示例2.jpg](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/sample22_1584.png) 30 | 31 | ``` 32 | 输入:startTime = [1,2,3,4,6], endTime = [3,5,10,6,9], profit = [20,20,100,70,60] 33 | 输出:150 34 | 解释: 35 | 我们选择第 1,4,5 份工作。 36 | 共获得报酬 150 = 20 + 70 + 60。 37 | ``` 38 | 39 | ## 示例3 40 | 41 | ![示例3.jpg](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/sample3_1584.png) 42 | 43 | ``` 44 | 输入:startTime = [1,1,1], endTime = [2,3,4], profit = [5,6,4] 45 | 输出:6 46 | ``` 47 | 48 | ## 提示 49 | 50 | - `1 <= startTime.length == endTime.length == profit.length <= 5 * 10^4` 51 | - `1 <= startTime[i] < endTime[i] <= 10^9` 52 | - `1 <= profit[i] <= 10^4` 53 | 54 | # 解题思路 55 | 56 | 利用动态规划进行解题,本质上还是一个背包问题。 57 | 58 | **算法流程**: 59 | 60 | - **初始化**:将`startTime[i], endTime[i], profit[i]`打包成元祖并放入列表`jobs`中,并按`endTime`做升序排列。 61 | - **定义**:对于第`i`份工作,开始时间为`start(i)`,结束时间为`end(i)`,收益为`profit(i)` 62 | - **状态表示**:`dp[i]`表示考虑前`i`份工作时的最大收益 63 | - **状态转移方程**:`dp[i] = max(dp[i-1], x + profit(i))`,其中`x`为第`i`份工作开始时间之前所能达到的最大收益:如果存在`k`满足`end(k) <= start(i)`,则最大收益`x = dp[max(k)]`;如果`k`不存在,则`x = 0`,即第`i`份工作开始时间之前没有收益。 64 | - **边界条件**:`dp[0] = profit(0)` 65 | 66 | ## 复杂度分析 67 | 68 | - 时间复杂度:$O(N\log N)$,排序花费$O(N\log N)$的时间,计算`dp`数组时,若寻找满足条件的`k`也需要完整遍历一次,则将使时间复杂度达到$O(N^2)$。 69 | - 空间复杂度:$O(N)$ 70 | 71 | ## 代码 72 | 73 | ```python 74 | class Solution: 75 | def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int: 76 | 77 | jobs = [(start, end, pro) for start, end, pro in zip(startTime, endTime, profit)] 78 | # 根据结束时间升序排列 79 | jobs = sorted(jobs, key=lambda x: x[1]) 80 | 81 | dp = [0 for _ in range(len(startTime))] 82 | dp[0] = jobs[0][2] 83 | 84 | for i in range(1, len(dp)): 85 | # 寻找符合条件的k, 程序中用j表示 86 | j = i - 1 87 | while j >= 0 and jobs[j][1] > jobs[i][0]: 88 | j -= 1 89 | 90 | last_pro = 0 if j < 0 else dp[j] 91 | 92 | dp[i] = max(last_pro + jobs[i][2], dp[i-1]) 93 | return dp[-1] 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /剑指offer系列/面试题27.二叉树的镜像.md: -------------------------------------------------------------------------------- 1 | 二叉树的镜像 2 | 3 | # 题目描述 4 | 5 | 请完成一个函数,输入一个二叉树,该函数输出它的镜像。 6 | 7 | 例如输入: 8 | 9 | ``` 10 | 4 11 | / \ 12 | 2 7 13 | / \ / \ 14 | 1 3 6 9 15 | ``` 16 | 17 | 镜像输出: 18 | 19 | ``` 20 | 4 21 | / \ 22 | 7 2 23 | / \ / \ 24 | 9 6 3 1 25 | ``` 26 | 27 | ## 示例1 28 | 29 | ``` 30 | 输入:root = [4,2,7,1,3,6,9] 31 | 输出:[4,7,2,9,6,3,1] 32 | ``` 33 | 34 | ## 限制 35 | 36 | - `0 <= 节点个数 <= 1000` 37 | 38 | # 解题思路 39 | 40 | > 涉及树的题目,通常是考察递归。 41 | 42 | ## 方法一:递归法 43 | 44 | 递归遍历二叉树,交换每个节点的左/右子节点,即可生成二叉树的镜像。 45 | 46 | **递归解析**: 47 | 48 | - **终止条件**:当节点`root`为空时(即越过叶子节点),则返回; 49 | - **递推工作**: 50 | 1. 初始化节点`tmp`,暂存`root`的左子节点; 51 | 2. 开启递归右子节点`mirrorTree(root.right)`,并将返回值作为`root`的左子节点 52 | 3. 开启递归左子节点`mirrorTree(root.left)`,并将返回值作为`root`的右子节点 53 | - **返回值**:返回`root` 54 | 55 | ## 复杂度分析 56 | 57 | - 时间复杂度:$O(N)$,$N$为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点。 58 | - 空间复杂度:$O(N)$,最坏情况(二叉树退化为链表),递归时需要使用$O(N)$大小的栈空间。 59 | 60 | ## 代码 61 | 62 | ```python 63 | # Definition for a binary tree node. 64 | # class TreeNode: 65 | # def __init__(self, x): 66 | # self.val = x 67 | # self.left = None 68 | # self.right = None 69 | 70 | class Solution: 71 | def mirrorTree(self, root: TreeNode) -> TreeNode: 72 | 73 | if not root: return 74 | tmp = root.left 75 | root.left = self.mirrorTree(root.right) 76 | root.right = self.mirrorTree(tmp) 77 | 78 | return root 79 | ``` 80 | 81 | ## 方法二:辅助栈(或队列) 82 | 83 | 利用栈(或队列)遍历树的所有节点`node`,并交换每个`node`的左/右节点。 84 | 85 | **算法流程**: 86 | 87 | - **特例处理**:当`root`为空时,返回 88 | - **初始化**:栈(或队列),本文用栈,并加入根节点`root` 89 | - **循环交换**:当栈`stack`为空时跳出 90 | 1. **出栈**:记为`node` 91 | 2. **添加子节点**:将`node`左和右子节点入栈 92 | 3. **交换**:交换`node`的左/右子节点 93 | - **返回值**:返回`root` 94 | 95 | ## 复杂度分析 96 | 97 | - 时间复杂度:$O(N)$,$N$为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点。 98 | - 空间复杂度:$O(N)$,最坏情况(满二叉树),最多同时存储$N/2$个节点。 99 | 100 | ## 代码 101 | 102 | ```python 103 | # Definition for a binary tree node. 104 | # class TreeNode: 105 | # def __init__(self, x): 106 | # self.val = x 107 | # self.left = None 108 | # self.right = None 109 | class Solution: 110 | def mirrorTree(self, root: TreeNode) -> TreeNode: 111 | if not root: return 112 | stack = [root] 113 | while stack: 114 | node = stack.pop() 115 | if node.left: stack.append(node.left) 116 | if node.right: stack.append(node.right) 117 | node.left, node.right = node.right, node.left 118 | 119 | return root 120 | ``` 121 | 122 | -------------------------------------------------------------------------------- /每日一题系列/1000.合并石头的最低成本.md: -------------------------------------------------------------------------------- 1 | 合并石头的最低成本 2 | 3 | # 题目描述 4 | 5 | 有`N`堆石头排成一排,第`i`堆中有`stones[i]`块石头。 6 | 7 | 每次移动需要将**连续的**`K`堆石头合并为一堆,而这个移动的成本为这`K`堆石头的总数。 8 | 9 | 找出把所有石头合并成一堆的最低成本。如果不可能,返回`-1`。 10 | 11 | ## 示例1 12 | 13 | ``` 14 | 输入:stones = [3,2,4,1], K = 2 15 | 输出:20 16 | 解释: 17 | 从 [3, 2, 4, 1] 开始。 18 | 合并 [3, 2],成本为 5,剩下 [5, 4, 1]。 19 | 合并 [4, 1],成本为 5,剩下 [5, 5]。 20 | 合并 [5, 5],成本为 10,剩下 [10]。 21 | 总成本 20,这是可能的最小值。 22 | ``` 23 | 24 | ## 示例2 25 | 26 | ``` 27 | 输入:stones = [3,2,4,1], K = 3 28 | 输出:-1 29 | 解释:任何合并操作后,都会剩下 2 堆,我们无法再进行合并。所以这项任务是不可能完成的。 30 | ``` 31 | 32 | ## 示例3 33 | 34 | ``` 35 | 输入:stones = [3,5,1,2,6], K = 3 36 | 输出:25 37 | 解释: 38 | 从 [3, 5, 1, 2, 6] 开始。 39 | 合并 [5, 1, 2],成本为 8,剩下 [3, 8, 6]。 40 | 合并 [3, 8, 6],成本为 17,剩下 [17]。 41 | 总成本 25,这是可能的最小值。 42 | ``` 43 | 44 | ## 提示 45 | 46 | - `1 <= stones.length <= 30` 47 | - `2 <= K <= 30` 48 | - `1 <= stones[i] <= 100` 49 | 50 | # 解题思路 51 | 52 | 首先,该题有着较明显的区间关联(相邻的K堆合成一堆),可以初步判定为**区间DP**。 53 | 54 | 对**区间DP**这个概念不清楚的小伙伴,可以上B站找找相关视频,这里推荐一个[西北工业大学ACM2017暑假集训-区间DP](https://www.bilibili.com/video/BV1Qx411q7bz?from=search&seid=17410535217395696530)视频。 55 | 56 | 我们将`dp[i][j]`定义为在区间`[j, i]`上合并到合并不了的时候的最低成本(即使这个区间最终不能合并为1堆石子,也应计算最低成本和)。 57 | 58 | 于是,状态转移方程由以下分析得出: 59 | 60 | - `len1`为区间`[j, i]`最后一次合并时的第一个区间的长度,`len2`为其余区间的长度。由于是区间`[j, i]`上的最后一次合并,所以第一个区间一定合并成1堆;第二个区间合并到不能合并时,最大可能的石堆数为`K-1`。 61 | - 于是有,`[第一个区间合并完剩下的石堆数] + [其余区间合并完剩下的石堆数] <= K`。 62 | - 如果`[第一个区间合并完剩下的石堆数] + [其余区间合并完剩下的石堆数] = K`,即可以继续合并成1堆,则`dp[i][j] = min(dp[i][j], dp[t][j] + dp[i][t+1] + sum(stones[j:i+1])) ` 63 | - 如果不能继续合并了,即`[第一个区间合并完剩下的石堆数] + [其余区间合并完剩下的石堆数] < K`,则`dp[i][j] = min(dp[i][j], dp[t][j] + dp[i][t+1])` 64 | 65 | ## 代码 66 | 67 | ```python 68 | class Solution: 69 | def mergeStones(self, stones: list, K: int) -> int: 70 | N = len(stones) 71 | if N == 1: 72 | return 0 73 | #由于每次石堆合并操作,都会减少K-1堆石头,所以N除以K-1不余1的,一定需要返回-1 74 | if (N-1)%(K-1) != 0: 75 | return -1 76 | 77 | #dp[i][j]是指区间[j, i]上合并到合并不了了时候的成本和 78 | dp = [[float('inf')] * N for _ in range(N)] 79 | 80 | for i in range(N): 81 | dp[i][i] = 0 82 | for i in range(N): 83 | for j in range(i-1, -1, -1): 84 | if i == j+K-1: 85 | dp[i][j] = sum(stones[j:i+1]) 86 | continue 87 | for t in range(j, i, K-1): 88 | tmp = (i-t-1)%(K-1)+2 89 | if tmp == K: 90 | dp[i][j] = min(dp[i][j], dp[t][j] + dp[i][t+1] + sum(stones[j:i+1])) 91 | elif tmp < K: 92 | dp[i][j] = min(dp[i][j], dp[t][j] + dp[i][t+1]) 93 | return dp[-1][0] 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /剑指offer系列/面试题32-III.从上到下打印二叉树III.md: -------------------------------------------------------------------------------- 1 | 从上到下打印二叉树 III 2 | 3 | # 题目描述 4 | 5 | 请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。 6 | 7 | ## 示例 8 | 9 | 给定二叉树:`[3,9,20,null,null,15,7]` 10 | 11 | ``` 12 | 3 13 | / \ 14 | 9 20 15 | / \ 16 | 15 7 17 | ``` 18 | 19 | 返回其层次遍历结果: 20 | 21 | ``` 22 | [ 23 | [3], 24 | [20,9], 25 | [15,7] 26 | ] 27 | ``` 28 | 29 | ## 提示 30 | 31 | - `节点总数 <= 1000` 32 | 33 | # 解题思路 34 | 35 | > 本题是[从上到下打印二叉树](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题32-I.从上到下打印二叉树.md)和[从上到下打印二叉树 II](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题32-II.从上到下打印二叉树II.md)的一个衍生,依然是按层打印,差异是每一层打印顺序发生变化。 36 | 37 | 对比之前两题,本题的关键在于何时进行反向打印。如何判断节点在同一层在[从上到下打印二叉树 II](https://github.com/TrippleKing/LeetCode_Python3/blob/master/剑指offer系列/面试题32-II.从上到下打印二叉树II.md)中已经有明确的描述,我们只要设置一个反向标志符,来提示打印需要反向即可。(每一次打印完一层节点,下一次必然发生反向) 38 | 39 | ## 复杂度分析 40 | 41 | - 时间复杂度:$O(N)$ 42 | - 空间复杂度:$O(N)$ 43 | 44 | ## 代码 45 | 46 | ```python 47 | # Definition for a binary tree node. 48 | # class TreeNode: 49 | # def __init__(self, x): 50 | # self.val = x 51 | # self.left = None 52 | # self.right = None 53 | 54 | class Solution: 55 | def levelOrder(self, root: TreeNode) -> List[List[int]]: 56 | 57 | if not root: return [] 58 | queue = collections.deque() 59 | res = [] 60 | queue.append(root) 61 | flag = -1 62 | while queue: 63 | tmp = [] 64 | for _ in range(len(queue)): 65 | node = queue.popleft() 66 | tmp.append(node.val) 67 | if node.left: queue.append(node.left) 68 | if node.right: queue.append(node.right) 69 | flag = -flag 70 | if flag == 1: 71 | res.append(tmp) 72 | else: 73 | res.append(tmp[::-1]) 74 | return res 75 | 76 | # 补充:也可以使用双端队列来构建tmp,判断换层也可以根据res当前行数是否为奇数/偶数进行判断 77 | # 代码如下:供参考 78 | class Solution: 79 | def levelOrder(self, root: TreeNode) -> List[List[int]]: 80 | 81 | if not root: return [] 82 | queue = collections.deque() 83 | res = [] 84 | queue.append(root) 85 | while queue: 86 | tmp = collections.deque() 87 | for _ in range(len(queue)): 88 | node = queue.popleft() 89 | if len(res) & 1: 90 | tmp.appendleft(node.val) 91 | else: 92 | tmp.append(node.val) 93 | if node.left: queue.append(node.left) 94 | if node.right: queue.append(node.right) 95 | res.append(list(tmp)) 96 | return res 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /剑指offer系列/面试题52.两个链表的第一个公共节点.md: -------------------------------------------------------------------------------- 1 | 两个链表的第一个公共节点 2 | 3 | # 题目描述 4 | 5 | 输入两个链表,找出它们的第一个公共节点。 6 | 7 | 如下面的两个链表: 8 | 9 | 示例.jpg 10 | 11 | 在节点`c1`开始相交。 12 | 13 | ## 示例1 14 | 15 | 示例1.jpg 16 | 17 | ``` 18 | 输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 19 | 输出:Reference of the node with value = 8 20 | 输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 21 | ``` 22 | 23 | ## 示例2 24 | 25 | 示例2.jpg 26 | 27 | ``` 28 | 输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 29 | 输出:Reference of the node with value = 2 30 | 输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。 31 | ``` 32 | 33 | ## 示例3 34 | 35 | 示例3.jpg 36 | 37 | ``` 38 | 输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 39 | 输出:null 40 | 输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 41 | 解释:这两个链表不相交,因此返回 null。 42 | ``` 43 | 44 | ## 注意 45 | 46 | - 如果两个链表没有交点,返回`null`。 47 | - 在返回结果后,两个链表仍须保持原有的结构。 48 | - 可假定整个链表结构中没有循环。 49 | - 程序尽量满足$O(n)$时间复杂度,且仅用$O(1)$内存。 50 | 51 | # 解题思路 52 | 53 | 假设链表`A`由`a`个非公共节点加`c`个公共节点组成,链表`B`由`b`个非公共节点加`c`个公共节点组成。我们使用两个指针`node1`和`node2`,分别指向链表`headA`和链表`headB`的头结点,然后同时分别逐点遍历,当`node1`到达链表`headA`的末尾时,重新定位到链表`headB`的头结点;当`node2`到达链表`headB`的末尾时,重新定位到链表`headA`的头结点。 54 | 55 | 若存在公共节点,即`c > 0`,则当`node1`和`node2`会相遇(因为`node1`此时走了`a + c + b`个节点;`node2`走了`b + c + a`个节点,数量上是相等的); 56 | 57 | 若不存在共节点,即`c = 0`,则`node1`走过`a + b`个节点时,`node2`也走过`b + a`个节点,此时`node1`和`node2`都指向`None`; 58 | 59 | 所以,两个指针相遇时,直接返回即可。 60 | 61 | ## 复杂度分析 62 | 63 | > 设$N,M$分别为链表`A`和链表`B`的长度。 64 | 65 | - 时间复杂度:$O(N + M)$ 66 | - 空间复杂度:$O(1)$ 67 | 68 | ## 代码 69 | 70 | ```python 71 | # Definition for singly-linked list. 72 | # class ListNode: 73 | # def __init__(self, x): 74 | # self.val = x 75 | # self.next = None 76 | 77 | class Solution: 78 | def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: 79 | node1 = headA 80 | node2 = headB 81 | 82 | while node1 != node2: 83 | node1 = node1.next if node1 else headB 84 | node2 = node2.next if node2 else headA 85 | 86 | return node1 87 | ``` 88 | 89 | -------------------------------------------------------------------------------- /每日一题系列/1005.K次取反后最大化的数组和.md: -------------------------------------------------------------------------------- 1 | K次取反后最大化的数组和 2 | 3 | # 题目描述 4 | 5 | 给定一个整数数组A,我们只能用一下方法修改该数组:我们选择某个索引`i`并将`A[i]`替换为`-A[i]`,然后总共重复这个过程`K`次。(我们可以多次选择同一个索引`i`) 6 | 7 | 以这种方式修改数组后,返回数组可能的最大和。 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入:A = [4,2,3], K=1 13 | 输出:5 14 | 解释:选择索引(1,),然后A变成[4,-2,3]。 15 | ``` 16 | 17 | ## 示例2 18 | 19 | ``` 20 | 输入:A = [3,-1,0,2], K=3 21 | 输出:6 22 | 解释:选择索引(1,2,2),然后A变成[3,1,0,2]。 23 | ``` 24 | 25 | ## 示例3 26 | 27 | ``` 28 | 输入:A = [2,-3,-1,5,-4], K=2 29 | 输出:13 30 | 解释:选择索引(1,4),然后A变成[2,3,-1,5,4]。 31 | ``` 32 | 33 | ## 限制 34 | 35 | - `1 <= A.length <= 10000` 36 | - `1 <= K <= 10000` 37 | - `-100 <= A[i] <=100` 38 | 39 | # 解题思路 40 | 41 | 根据题目的意思,我们每次取反的操作只能作用在当前数组的最小值上,这样才能使得最终的数组和最大,所以问题就转换成如何快速得找到当前数组的最小值。 42 | 43 | ## 方法一:排序法 44 | 45 | 最简单也是最直接的方法就是每次取反前,先对当前数组进行排序,再对最小元素进行取反。 46 | 47 | ## 代码 48 | 49 | ```python 50 | class Solution: 51 | def largestSumAfterKNegations(self, A: List[int], K: int) -> int: 52 | for i in range(K): 53 | A.sort() 54 | A[0] = -A[0] 55 | return sum(A) 56 | ``` 57 | 58 | ## 方法二:优化排序法 59 | 60 | 方法一中,每一次都进行排序会浪费很多时间,再对数组稍作分析,可以得到以下规律: 61 | 62 | - 对于一个已经完成排序的数组而言,如果第一个元素本身非负,那么对其取反后,当前位置仍然为最小值,不需要再次排序; 63 | - 如果第一个元素为负数,分两种情况讨论: 64 | 1. 如果元素取反后大于右侧的元素,那么下一次应该考虑对右侧元素取反; 65 | 2. 如果元素取反后不大于右侧的元素,则继续对该位置取反即可。 66 | 67 | ## 代码 68 | 69 | ```python 70 | class Solution: 71 | def largestSumAfterKNegations(self, A: List[int], K: int) -> int: 72 | A.sort() # 仅排序一次 73 | j = 0 74 | for i in range(K): 75 | if A[j] >= 0: 76 | A[j] = -A[j] 77 | else: 78 | A[j] = -A[j] 79 | if j+1 <= len(A)-1 and A[j]>A[j+1]: 80 | j += 1 81 | return sum(A) 82 | ``` 83 | 84 | ## 方法三:无排序法 85 | 86 | 根据限制,可知`-100 <= A[i] <= 100` 87 | 88 | 我们创建一个长度为201的全零数组`newList`,数组的下标等于`A[i]+100`,同时对应元素加1。 89 | 90 | 例如:`A = [2,-3,-1,5,-4]`,则`newList[102] += 1`;`newList[97] += 1`;`newList[99] += 1`;`newList[105] += 1`;`newList[96] += 1`。我们可以发现,原本数组`A`中最小元素`-4`代表着`newList`中首个非零元素的下标。 91 | 92 | 排序所需时间复杂度为$O(nlogn)$,而该方法所需时间复杂度为$O(n)$。 93 | 94 | ## 代码 95 | 96 | ```python 97 | class Solution: 98 | def largestSumAfterKNegations(self, A: List[int], K: int) -> int: 99 | newList = [0]*201 100 | for ele in A: 101 | newList[ele+100] += 1 102 | i = 0 103 | while K > 0: 104 | while newList[i] == 0: 105 | i += 1 106 | newList[i] -= 1 107 | newList[200 - i] += 1 108 | if i > 100: 109 | i = 200 - i 110 | K -= 1 111 | res = 0 112 | for j in range(i,len(newList)): 113 | res += (j-100)*newList[j] 114 | return res 115 | ``` 116 | 117 | -------------------------------------------------------------------------------- /剑指offer系列/面试题33.二叉搜索树的后序遍历序列.md: -------------------------------------------------------------------------------- 1 | 二叉搜索树的后序遍历序列 2 | 3 | # 题目描述 4 | 5 | 输入一个整数数组,判断该数组是不是某**二叉搜索树**的后序遍历结果。如果是则返回`true`,否则返回`false`。假设输入的数组的任意两个数字都互不相同。 6 | 7 | 参考以下这棵二叉搜索树: 8 | 9 | ``` 10 | 5 11 | / \ 12 | 2 6 13 | / \ 14 | 1 3 15 | ``` 16 | 17 | ## 示例1 18 | 19 | ``` 20 | 输入: [1,6,3,2,5] 21 | 输出: false 22 | ``` 23 | 24 | ## 示例2 25 | 26 | ``` 27 | 输入: [1,3,2,6,5] 28 | 输出: true 29 | ``` 30 | 31 | ## 提示 32 | 33 | - `数组长度 <= 1000` 34 | 35 | # 解题思路 36 | 37 | **后序遍历**定义:`[左子树 | 右子树 | 根节点]`,即遍历顺序为"左、右、根"。 38 | 39 | **二叉搜索树**定义:左子树中所有节点的值 < 根节点的值;右子树中所有节点的值 > 根节点的值;其左右子树也分别是二叉搜索树。 40 | 41 | ## 方法一:递归 42 | 43 | > 根据二叉搜索树的定义,通过递归,判断所有子树的**正确性**即可。 44 | 45 | **递归解析**: 46 | 47 | - **终止条件**:当`start >= end`时,说明此子树节点数量`<= 1`,无需判别正确性,因此直接返回`true`; 48 | - **递推工作**: 49 | - **划分左右子树**:遍历数组的`[start, end]`区间元素,找到第一个大于根节点的索引,记为`i`。此时可划分出左子树区间`[start, i - 1]`,右子树区间`[i, end - 1]`。 50 | - **判断是否为二叉搜索树**: 51 | - 确保左子树`[start, i - 1]`区间内部所有元素都应小于`postorder[end]`; 52 | - 确保右子树`[i, end - 1]`区间内部所有元素都应大于`postorder[end]` 53 | - **返回值**:所有子树都正确,才能判断正确 54 | 55 | ## 复杂度分析 56 | 57 | - 时间复杂度:$O(N^2)$,每次调用`recur()`减去一个根节点,占用$O(N)$;最差情况下(树退化为链表),每轮递归需要遍历树的所有节点,占用$O(N)$。 58 | - 空间复杂度:$O(N)$,最差情况下(树退化为链表),递归深度达到$N$。 59 | 60 | ## 代码 61 | 62 | ```python 63 | class Solution: 64 | def verifyPostorder(self, postorder: List[int]) -> bool: 65 | def recur(start, end): 66 | if start >= end: return True 67 | i = start 68 | while postorder[i] < postorder[end]: 69 | i += 1 70 | j = i 71 | while postorder[j] > postorder[end]: 72 | j += 1 73 | 74 | return j == end and recur(start, i - 1) and recur(i, end - 1) 75 | 76 | return recur(0, len(postorder) - 1) 77 | ``` 78 | 79 | ## 方法二:辅助栈法 80 | 81 | **算法流程**: 82 | 83 | - **初始化**:空栈`stack`,父节点值`root = +∞`(初始值为正无穷大,可以把给定的数组看做是正无穷大的左子树) 84 | - **倒序遍历**:记每个节点为`ri` 85 | - **判断**:若`ri > root`,不满足二叉搜索树定义,返回`False`; 86 | - **更新父节点**:当栈不为空且`ri < stack[-1]`时,循环执行出栈,将出栈元素赋给`root`。 87 | - **入栈**:将当前`ri`入栈 88 | - 若遍历完成,则说明后序遍历满足要求,返回`True`。 89 | 90 | ## 复杂度分析 91 | 92 | - 时间复杂度:$O(N)$,遍历所有元素,各元素均入栈/出栈一次,使用$O(N)$时间。 93 | - 空间复杂度:$O(N)$,最坏情况,`stack`存储所有节点。 94 | 95 | ## 代码 96 | 97 | ```python 98 | class Solution: 99 | def verifyPostorder(self, postorder: List[int]) -> bool: 100 | stack = [] 101 | root = float("+inf") 102 | for i in range(len(postorder) - 1, -1, -1): 103 | if postorder[i] >= root: return False 104 | while stack and postorder[i] < stack[-1]: 105 | root = stack.pop() 106 | stack.append(postorder[i]) 107 | return True 108 | ``` 109 | 110 | -------------------------------------------------------------------------------- /剑指offer系列/面试题67.把字符串转换成整数.md: -------------------------------------------------------------------------------- 1 | 把字符串转换成整数 2 | 3 | # 题目描述 4 | 5 | 写一个函数`StrToInt`,实现把字符串转换成整数这个功能。不能使用`atoi`或者其他类似的库函数。 6 | 7 | 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。 8 | 9 | 当我们寻找到第一个非空字符为正或者负号时,则将该负号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。 10 | 11 | 该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。 12 | 13 | 注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或者字符串仅包含空白字符时,则你的函数不需要进行转换。 14 | 15 | 在任何情况下,若函数不能进行有效的转换时,请返回`0`。 16 | 17 | ## 示例1 18 | 19 | ``` 20 | 输入: "42" 21 | 输出: 42 22 | ``` 23 | 24 | ## 示例2 25 | 26 | ``` 27 | 输入: " -42" 28 | 输出: -42 29 | 解释: 第一个非空白字符为 '-', 它是一个负号。 30 |   我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。 31 | ``` 32 | 33 | ## 示例3 34 | 35 | ``` 36 | 输入: "4193 with words" 37 | 输出: 4193 38 | 解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。 39 | ``` 40 | 41 | ## 示例4 42 | 43 | ``` 44 | 输入: "words and 987" 45 | 输出: 0 46 | 解释: 第一个非空字符是 'w', 但它不是数字或正、负号。 47 | 因此无法执行有效的转换。 48 | ``` 49 | 50 | ## 示例5 51 | 52 | ``` 53 | 输入: "-91283472332" 54 | 输出: -2147483648 55 | 解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 56 |   因此返回 INT_MIN (−231) 。 57 | ``` 58 | 59 | ## 说明 60 | 61 | 假设我们的环境只能存储`32`位大小的有符号整数,那么其数值范围为`[-2^31, 2^31 - 1]`。如果数值超过这个返回,请返回`INT_MAX = 2^31 - 1`或`INT_MIN = -2^31`。 62 | 63 | # 解题思路 64 | 65 | 根据题意,有以下四种字符需要考虑: 66 | 67 | - **首部空格**:删除即可 68 | - **符号位**:三种情况,即`"+", "-", "无符号"`;新建一个变量保存符号位,返回前判断正负即可。 69 | - **非数字字符**:遇到首个非数字的字符时,应立即返回。 70 | - **数字字符**: 71 | - **字符转数字**:"此数字的ASCII码"与"0的ASCII码"相减即可; 72 | - **数字拼接**:若从左向右遍历数字,设当前位字符为`s`,当前位数字为`x`,数字结果为`res`,则数字拼接公式为:`res = 10 x res + x`;`x = ascii(c) - ascii(0)` 73 | 74 | **数字越界处理**: 75 | 76 | > 题目要求返回的数值范围应在`[-2^31, 2^31 - 1]`,因此需要考虑数字越界的问题。而由于题目指出`环境只能存储32位大小的有符号整数`,因此判断数字越界时,要始终保持`res`在`int`类型的取值范围内。 77 | 78 | 在每轮数字拼接前,判断`res`**在此轮拼接后是否超过**`2147483647`,若超过则加上符号位直接返回。 79 | 80 | 设数字拼接边界为`boundary = 2147483647 // 10 = 214748364`,则以下两种情况越界: 81 | 82 | - 若`res > boundary`;执行拼接`10 x res >= 2147483650`越界 83 | - 若`res = boundary, x > 7`;拼接后是`2147483648`或`2147483649` 84 | 85 | ## 复杂度分析 86 | 87 | - 时间复杂度:$O(N)$ 88 | - 空间复杂度:$O(N)$,删除首尾空格后需建立新字符串,最差情况下占用$O(N)$额外空间 89 | 90 | ## 代码 91 | 92 | ```python 93 | class Solution: 94 | def strToInt(self, str: str) -> int: 95 | str = str.strip() 96 | if not str: return 0 97 | res = 0 98 | sign = 1 99 | idx = 1 100 | int_max = 2**31 - 1 101 | int_min = -2**31 102 | boundary = 2**31 //10 103 | if str[0] == "-": sign = -1 104 | elif str[0] != "+": idx = 0 105 | for s in str[idx:]: 106 | if not "0" <= s <= "9": break 107 | if res > boundary or res == boundary and s > "7": 108 | return int_max if sign == 1 else int_min 109 | res = 10 * res + ord(s) - ord("0") 110 | 111 | return res * sign 112 | ``` 113 | 114 | -------------------------------------------------------------------------------- /剑指offer系列/面试题56-I.数组中数字出现的次数.md: -------------------------------------------------------------------------------- 1 | 数组中数字出现的次数 2 | 3 | # 题目描述 4 | 5 | 一个整数数组`nums`里除了两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是$O(n)$,空间复杂度是$O(1)$。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入:nums = [4,1,4,6] 11 | 输出:[1,6] 或 [6,1] 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入:nums = [1,2,10,4,1,4,3,3] 18 | 输出:[2,10] 或 [10,2] 19 | ``` 20 | 21 | ## 限制 22 | 23 | - `2 <= nums.length <= 10000` 24 | 25 | # 解题思路 26 | 27 | 假设,我们抛开复杂度的要求,最先想到的解题方法基本上是暴力遍历: 28 | 29 | - 先选出数组`nums`中第一个元素(比如记为a); 30 | - 再遍历剩下的元素,查看是否有相同的元素a; 31 | - 再选出第二个元素,以此类推。 32 | 33 | 这种方法的时间复杂为$O(n^2)$,空间复杂度为$O(1)$。 34 | 35 | 更进一步,借助字典,利用`collections.Counter()`对数组`nums`进行统计,然后进行一次遍历即可得到出现次数为1的元素。该方法的时间复杂度为$O(n)$,但是空间复杂度也为$O(n)$。 36 | 37 | 我们再思考,如果数组`nums`中只有一个数字出现了一次,其他数字都出现了两次,可以怎么做?可以使用异或操作得到结果。(嗯?什么是异或?) 38 | 39 | ## 补充知识 40 | 41 | 我们先补充一点有关异或的知识,具体的与"位运算"相关知识,会在之后专门整理一次。 42 | 43 | 1. 异或的性质 44 | 45 | 两个数字异或的结果`a^b`是将a和b的**二进制**每一位进行运算,得出的数字。 46 | 47 | 运算的逻辑是:如果同一位的数字相同则为0,不同则为1。 48 | 49 | 2. 异或的规律 50 | - 任何数和它本身异或,结果为`0`。即`a^a=0` 51 | - 任何数和0异或,结果为它本身。即`a^0=a` 52 | - 异或满足交换律。即`a^b^c=a^c^b` 53 | 54 | ## 回到解题思路 55 | 56 | 结合上述异或操作的相关知识,如果数组`nums`中只有一个数字出现了一次,其他数字都出现了两次,那么对整个数组进行一次全员异或即可得到结果。(这段话想清楚了再往下看) 57 | 58 | 现在数组中有两个数字出现了一次,其他数字都出现了两次,如果我们能够将该数组`nums`拆分成两个数组`nums1`和`nums2`,这两个数组中有且仅有一个数字出现了一次,其他数字出现了两次,那么再对两个数组分别进行全员异或,即可得到答案。 59 | 60 | 那么问题就转变为如何正确拆分数组`nums`?思路如下: 61 | 62 | 1. 先对原始数组`nums`进行一次全员异或(假设`nums`中两个仅出现一次的数字为a和b),异或结果为`a^b`,记为`xor`; 63 | 2. 那么`xor`(以二进制看)至少在某一位上为1,用`mask`记录这个位置(设置该位置为1,其余位置为0); 64 | 3. 利用`mask`来划分原始数组`nums`,原数组中元素和`mask`进行"与运算,&",如果该位置上为1,则划分进数组`nums1`;否则划分进数组`nums2`; 65 | 4. 最后再对数组`nums1`和`nums2`分别进行一次全员异或,即可得到答案。 66 | 67 | ## 举个例子 68 | 69 | 看了上述思路,可能还是有很多问号,我们举个实例简单说明一下,大家把实例和思路对照起来可以加深理解。 70 | 71 | ``` 72 | 输入:[4,1,4,6] 73 | 二进制形式:[0100,0001,0100,0110] 74 | 第一次全员异或:0100^0001^0100^0110 = 0111 = xor 75 | 找到首个为1的位置:mask = 0001 76 | 以mask拆分nums:0100 & 0001 = 0000;0001 & 0001 = 0001;0100 & 0001 = 0000;0110 & 0001 = 0000 77 | 得到nums1; nums2:nums1=[1];nums2=[4,4,6] 78 | 分别对nums1和nums2进行全员异或,得到结果:[1,6] 79 | ``` 80 | 81 | 如果感觉自己理解起来有些吃力,可能需要补习一下计算机中"位运算"的相关知识。 82 | 83 | ## 代码 84 | 85 | ```python 86 | class Solution: 87 | def singleNumbers(self, nums: List[int]) -> List[int]: 88 | xor = 0 89 | mask = 1 90 | num1, num2 = 0, 0 91 | # 全员异或得到 xor 92 | for num in nums: 93 | xor ^= num 94 | # 寻找xor首个为1的位置 mask 95 | # mask = xor & (-xor) # 可以直接与运算得到mask 96 | while mask & xor == 0: 97 | mask = mask << 1 98 | # 拆分nums,同时进行异或操作 99 | # 这里就没有再把拆分的元素放进列表里(因为没必要) 100 | for num in nums: 101 | if mask & num: 102 | num1 ^= num 103 | else: 104 | num2 ^= num 105 | return [num1, num2] 106 | ``` 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /剑指offer系列/面试题45.把数组排成最小的数.md: -------------------------------------------------------------------------------- 1 | 把数组排成最小的数 2 | 3 | # 题目描述 4 | 5 | 输入一个非负整数数组,把数组里所有数字拼接起来排成一个树,打印能拼接出的所有数字中最小的一个。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: [10,2] 11 | 输出: "102" 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: [3,30,34,5,9] 18 | 输出: "3033459" 19 | ``` 20 | 21 | ## 提示 22 | 23 | - `0 < nums.length <= 100` 24 | 25 | ## 说明 26 | 27 | - 输出结果可能非常大,所以你需要返回一个字符串而不是整数 28 | - 拼接起来的数字可能会有前导0,最后结果不需要去掉前导0 29 | 30 | # 解题思路 31 | 32 | > 本题要求拼接起来的数字最小,本质上是一个排序问题。 33 | > 34 | > **排序判断规则**:设`nums`中任意两个数字的**字符串格式**为`x`和`y`,则有: 35 | > 36 | > - 若**拼接字符串**`x + y > y + x`,则有`int(x)`应排在`int(y)`后面 37 | > - 反之,若**拼接字符串**`x + y < y + x`,则有`int(x)`应排在`int(y)`前面 38 | > 39 | > 基于上述判断规则,从而比较两个数字在数组中的前后排序关系。(即常规的排序比较的是数字本身的大小,而此处的大小判断需按上述要求进行比较),所以套用任何一种排序方法结合上述规则进行排序即可。 40 | 41 | **算法流程**: 42 | 43 | - **初始化**:构建字符串列表`strs`,保存`nums`数组中各数字的字符串格式; 44 | - **列表排序**:应用上述"排序判断规则",对`strs`进行排序; 45 | - **返回值**:拼接`strs`中的所有字符串,并返回。 46 | 47 | ## 复杂度分析 48 | 49 | > 设$N$为`strs`的长度 50 | 51 | - 时间复杂度:使用快排或内置函数的平均时间复杂度度为$O(N\log N)$,最坏为$O(N^2)$。 52 | - 空间复杂度:$O(N)$ 53 | 54 | ## 代码 55 | 56 | > 这里,只列举用**快速排序**思想和**内置函数**实现的两种排序方法,其他方法读者可以自行实现。 57 | 58 | ```python 59 | # 快速排序(先想一下快速排序的主要思想) 60 | class Solution: 61 | def minNumber(self, nums: List[int]) -> str: 62 | strs = [str(num) for num in nums] 63 | def quick_sort(left, right): 64 | if left >= right: return 65 | i, j = left, right 66 | # 基于一个base_element,将数组划分成两部分 67 | while i < j: 68 | # 这里的base_element其实就是strs[left] 69 | #(如果能够理解上面这句话,应该就能get到快排思想) 70 | # 从后往前寻找,那些按规则应排在strs[left]后面的元素 71 | while strs[j] + strs[left] >= strs[left] + strs[j] and i < j: 72 | j -= 1 73 | # 从前往后寻找,那些按规则应排在strs[left]前面的元素 74 | while strs[i] + strs[left] <= strs[left] + strs[i] and i < j: 75 | i += 1 76 | # 交换i, j; 未交换前strs[i]应排在strs[left]后;strs[j]应排在strs[left]前 77 | strs[i], strs[j] = strs[j], strs[i] 78 | # 交换i, left 79 | strs[i], strs[left] = strs[left], strs[i] 80 | quick_sort(left, i - 1) 81 | quick_sort(i + 1, right) 82 | quick_sort(0, len(strs) - 1) 83 | return "".join(strs) 84 | 85 | # 内置函数 86 | class Solution: 87 | def minNumber(self, nums: List[int]) -> str: 88 | # 自定义一个比较函数 89 | # 如果x应排在y的后面,则返回1 90 | # 如果x应排在y的前面,则返回-1 91 | # 如果x与y相等,则返回0 92 | def sort_rule(x, y): 93 | a, b = x + y, y + x 94 | if a > b: return 1 95 | elif a < b: return -1 96 | else: return 0 97 | 98 | strs = [str(num) for num in nums] 99 | strs.sort(key = functools.cmp_to_key(sort_rule)) 100 | return ''.join(strs) 101 | 102 | ``` 103 | 104 | -------------------------------------------------------------------------------- /每日一题系列/934.最短的桥.md: -------------------------------------------------------------------------------- 1 | 最短的桥 2 | 3 | # 题目描述 4 | 5 | 在给定的二维二进制数组`A`中,存在两座岛。(岛是由四面相连的`1`形成的一个最大组。) 6 | 7 | 现在,我们可以将`0`变为`1`,以使两座岛链接起来,变成一座岛。 8 | 9 | 返回必须翻转的`0`的最小数目。(可以保证答案至少是1。) 10 | 11 | ## 示例1 12 | 13 | ``` 14 | 输入:[[0,1],[1,0]] 15 | 输出:1 16 | ``` 17 | 18 | ## 示例2 19 | 20 | ``` 21 | 输入:[[0,1,0],[0,0,0],[0,0,1]] 22 | 输出:2 23 | ``` 24 | 25 | ## 示例3 26 | 27 | ``` 28 | 输入:[[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]] 29 | 输出:1 30 | ``` 31 | 32 | ## 提示 33 | 34 | - `1 <= A.length = A[0].length <= 100` 35 | - `A[i][j] == 0`或`A[i][j] == 1` 36 | 37 | # 解题思路 38 | 39 | > 这道题的核心思路其实比较简单,先通过深度优先搜索先找到一个岛,并将其标记为`2`,这样在数组`A`中就有比较明确的两个岛(一个岛都是`1`,一个岛都是`2`);再通过广度优先搜索(按层"扩散")的方式找到两个岛之间的最短路径。 40 | 41 | 由于最初并不知道数组`A`中元素`1`所在的位置,不得不通过遍历去寻找,只要找到一个`1`,就可以从该位置进行深度优先搜索,要注意只需基于最先找到的`1`进行一次深度优先搜索就可以找到一个完整的岛,此时不能再对另一个岛的`1`进行深度优先搜索(否则会将数组`A`中所有的`1`置换为`2`,就白费时间了)。 42 | 43 | 找完一个岛后,就可以开始广度优先搜索,我们可以直接从岛`2`开始广度优先搜索去寻找岛`1`,这样在深度优先搜索时,就可以将岛`2`的位置加入到队列中,从而节省时间。 44 | 45 | 广度优先搜索的按层"扩散"的方式是指,每一次基于当前层的位置(可以理解为岛`2`的轮廓)同时向外延伸一层(可以理解为"填海造地",一次向外填一圈),步数增加一次,直至延伸至岛`1`,即得到最短步数。 46 | 47 | ## 复杂度分析 48 | 49 | > 设$N,M$为`A`数组的行数和列数。 50 | 51 | - 时间复杂度:$O(NM)$ 52 | - 空间复杂度:$O(NM)$ 53 | 54 | ## 代码 55 | 56 | ```python 57 | class Solution: 58 | def shortestBridge(self, A: List[List[int]]) -> int: 59 | 60 | row = col = len(A) 61 | moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 62 | 63 | queue = collections.deque() 64 | # DFS 65 | def dfs(cur_row, cur_col): 66 | A[cur_row][cur_col] = 2 67 | queue.append((cur_row, cur_col)) 68 | for move in moves: 69 | next_row = cur_row + move[0] 70 | next_col = cur_col + move[1] 71 | if 0 <= next_row < row and 0 <= next_col < col and A[next_row][next_col] == 1: 72 | dfs(next_row, next_col) 73 | return 74 | # BFS 75 | def bfs(): 76 | step = 0 77 | while queue: 78 | for _ in range(len(queue)): 79 | cur_row, cur_col = queue.popleft() 80 | for move in moves: 81 | next_row = cur_row + move[0] 82 | next_col = cur_col + move[1] 83 | if next_row < 0 or next_row >= row or next_col < 0 or next_col >= col or A[next_row][next_col] == 2: 84 | continue 85 | if A[next_row][next_col] == 1: 86 | return step 87 | A[next_row][next_col] = 2 88 | queue.append((next_row, next_col)) 89 | step += 1 90 | 91 | for i in range(row): 92 | for j in range(col): 93 | if A[i][j]: 94 | dfs(i, j) 95 | return bfs() 96 | ``` 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /剑指offer系列/面试题12.矩阵中的路径.md: -------------------------------------------------------------------------------- 1 | 矩阵中的路径 2 | 3 | # 题目描述 4 | 5 | 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格,如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该各自。例如,在下面的$3\times4$的矩阵中包含一条字符串"bfce"的路径。 6 | 7 | ``` 8 | [["a","b","c","e"], 9 | ["s","f","c","s"], 10 | ["a","b","e","e"]] 11 | ``` 12 | 13 | 但矩阵中不包含字符串"abfb"的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。 14 | 15 | ## 示例1 16 | 17 | ``` 18 | 输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED" 19 | 输出:true 20 | ``` 21 | 22 | ## 示例2 23 | 24 | ``` 25 | 输入: board = [["a","b"],["c","d"]], word = "abcd" 26 | 输出:false 27 | ``` 28 | 29 | ## 提示 30 | 31 | - `1 <= board.length <= 200` 32 | - `1 <= board[i].length <= 200` 33 | 34 | # 解题思路 35 | 36 | > 本题是典型的矩阵搜索问题,可使用深度优先搜索(DFS)+剪枝解决。 37 | 38 | ## 算法原理 39 | 40 | - **深度优先搜索**:可以理解为暴力法遍历矩阵中所有字符串可能性。DFS通过递归,先朝一个方向搜索到底,再回溯至上一个节点沿另一个方向搜索,一次类推。 41 | - **剪枝**:在搜索中,遇到`这条路一定不可能和目标字符串匹配成功或不满足题目要求`的情况(如:当前矩阵元素与目标字符不同或此元素已被访问),则应立即返回,称之为`可行性剪枝`。 42 | 43 | ## 算法剖析 44 | 45 | - **递归参数**:当前元素在矩阵`board`中的行列索引`i`和`j`,当前目标字符在`word`中的索引`k`。 46 | - **终止条件**: 47 | 1. **返回False**:(1)行或列索引越界(2)当前矩阵元素与目标字符不同(3)当前矩阵元素已访问过 48 | 2. **返回True**:字符串`word`已全部匹配,即`k=len(word)-1`。 49 | - **递推工作**: 50 | 1. **标记当前矩阵元素**:将`board[i][j]`的值暂存于变量`tmp`,并修改为字符`0`,代表此元素已访问过,防止之后搜索时重复访问。 51 | 2. **搜索下一单元格**:朝当前元素的上、下、右、左四个方向开启下层递归,使用`或`连接(代表只需一条可行路径即可),并记录结果至`res`。 52 | 3. **还原当前矩阵元素**:将`tmp`暂存值还原至`board[i][j]`元素。 53 | - **回溯返回值**:返回`res`,代表是否搜索到目标字符串。 54 | 55 | ## 复杂度分析 56 | 57 | > 设$M,N$分别为矩阵行列大小,$K$为字符串`word`长度。 58 | 59 | - **时间复杂度**:$O(3^KMN)$,最坏情况下,需要遍历矩阵中长度为$K$字符串的所有方案,时间复杂度为$O(3^K)$;矩阵中共有$MN$个起点,时间复杂度为$O(MN)$。 60 | - $3^K$方案数的计算:搜索中每个字符有上、下、左、右四个方向可以选择,舍弃回头的方向,剩下3种选择,因此方案数为$3^K$。 61 | - **空间复杂度**:$O(K)$,搜索过程中的递归深度不超过$K$,因此系统因函数调用累计使用的栈空间占用$O(K)$(因为函数返回后,系统调用的栈空间会释放)。最坏情况下$K=MN$,此时系统栈使用$O(MN)$的额外空间。 62 | 63 | ## 代码 64 | 65 | ```python 66 | class Solution: 67 | def exist(self, board: List[List[str]], word: str) -> bool: 68 | if not word: 69 | return False 70 | self.board = board 71 | self.word = word 72 | self.num_row = len(board) 73 | self.num_col = len(board[0]) 74 | for i in range(self.num_row): 75 | for j in range(self.num_col): 76 | if self.dfs(i, j, 0): 77 | return True 78 | return False 79 | 80 | def dfs(self, i, j, k): 81 | # 在board中寻找到word[0]时,即找到递归的起点,在起点的基础上再发生后续递归 82 | if not 0 <= i < self.num_row or not 0 <= j < self.num_col or self.board[i][j] != self.word[k]: 83 | return False 84 | if k == len(self.word) - 1: 85 | return True 86 | # 暂时将board[i][j]置换为'0'(也可以用其他符号代替),用以描述"已访问"的状态 87 | tmp, self.board[i][j] = self.board[i][j], '0' 88 | res = self.dfs(i+1,j,k+1) or self.dfs(i-1,j,k+1) or self.dfs(i,j+1,k+1) or self.dfs(i,j-1,k+1) 89 | # 递归完成后,不论结果是否为true,均将board[i][j]置换为原来的元素 90 | self.board[i][j] = tmp 91 | return res 92 | 93 | ``` 94 | 95 | -------------------------------------------------------------------------------- /每日一题系列/1253.重构2行二进制数组.md: -------------------------------------------------------------------------------- 1 | 重构2行二进制矩阵 2 | 3 | # 题目描述 4 | 5 | 给你个`2`行`n`列的二进制数组: 6 | 7 | - 矩阵是一个二进制矩阵,这意味着矩阵中的每个元素不是`0`就是`1`。 8 | - 第`0`行的元素之和为`upper`。 9 | - 第`1`行的元素之和为`lower`。 10 | - 第`i`列(从`0`开始编号)的元素之和为`colsum[i]`,`colsum`是一个长度为`n`的整数数组。 11 | 12 | 你需要利用`upper`,`lower`和`colsum`来重构这个矩阵,并以二维整数数组的形式返回它。 13 | 14 | 如果有多个不同的答案,那么任意一个都可以通过本题。 15 | 16 | 如果不存在符合要求的答案,就请返回一个空的二维数组。 17 | 18 | ## 示例1 19 | 20 | ``` 21 | 输入:upper = 2, lower = 1, colsum = [1,1,1] 22 | 输出:[[1,1,0],[0,0,1]] 23 | 解释:[[1,0,1],[0,1,0]]和[[0,1,1],[1,0,0]]也是正确答案。 24 | ``` 25 | 26 | ## 示例2 27 | 28 | ``` 29 | 输入:upper = 2, lower = 3, colsum = [2,2,1,1] 30 | 输出:[] 31 | ``` 32 | 33 | ## 示例3 34 | 35 | ``` 36 | 输入:upper = 5, lower = 5, colsum = [2,1,2,0,1,0,1,2,0,1] 37 | 输出:[[1,1,1,0,1,0,0,1,0,0],[1,0,1,0,0,0,1,1,0,1]] 38 | ``` 39 | 40 | ## 提示 41 | 42 | - `1 <= colsum.length <= 10^5` 43 | - `0 <= upper,lower <= colsum.length` 44 | - `0 <= colsum[i] <= 2` 45 | 46 | # 解题思路 47 | 48 | 由题意可知,若存在可行解,则有$upper+lower=sum(colsum)$,即行求和等于列求和。且提示中已经给出一些限制条件,可以不用考虑其他情况。 49 | 50 | 进一步思考,初始化一个二行`n`列的空列表`res`,对于`colsum`中的每一个元素而言,仅有三种取值: 51 | 52 | 1. 当`colsum[i]=0`:则必有`res[0][i]=0`,`res[1][i]=0`; 53 | 2. 当`colsum[i]=2`:则必有`res[0][i]=1`,`res[1][i]=1`; 54 | 3. 当`colsum[i]=1`:则必有`res[0][i]=1`或`res[1][i]=1`。 55 | 56 | 同时,需要注意每行之和应为`upper`和`lower`,即:`sum(res[0])=upper`;`sum(res[1])=lower`。 57 | 58 | ## 算法思路 59 | 60 | - 遍历`colsum`中的元素,值为`0`或`2`时,直接对`res[0][i]`和`res[1][i]`进行赋值,赋值为`1`时,需对`upper`和`lower`进行减1操作,表明`res[0]`和`res[1]`中剩余可填`1`的次数。值为`1`情况暂不赋值。 61 | - 再遍历`colsum`中的元素,此时仅考虑值为`1`情况即可,若`upper>0`,则对`res[0][i]`赋值为`1`,`upper -= 1`;否则对`res[1][i]`赋值为`1`,`lower -= 1`。 62 | - 最后判断`upper`与`lower`是否为零,若是,则返回`res`,否则返回`[]`。 63 | 64 | ## 复杂度分析 65 | 66 | - 时间复杂度:$O(N)$。 67 | - 空间复杂度:$O(N)$。 68 | 69 | ## 代码 70 | 71 | ```python 72 | class Solution: 73 | def reconstructMatrix(self, upper: int, lower: int, colsum: List[int]) -> List[List[int]]: 74 | num_cols = len(colsum) 75 | res = [[None]*num_cols, [None]*num_cols] 76 | if sum(colsum) == upper + lower: 77 | for i in range(num_cols): 78 | if colsum[i] == 0: 79 | res[0][i] = 0 80 | res[1][i] = 0 81 | elif colsum[i] == 2: 82 | res[0][i] = 1 83 | upper -= 1 84 | res[1][i] = 1 85 | lower -= 1 86 | for i in range(num_cols): 87 | if colsum[i] == 1: 88 | if upper > 0: 89 | res[0][i] = 1 90 | res[1][i] = 0 91 | upper -= 1 92 | else: 93 | res[0][i] = 0 94 | res[1][i] = 1 95 | lower -= 1 96 | if upper == 0 and lower == 0: 97 | return res 98 | else: 99 | return [] 100 | return [] 101 | 102 | ``` 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /每日一题系列/1386.安排电影院座位.md: -------------------------------------------------------------------------------- 1 | 安排电影院座位 2 | 3 | # 题目描述 4 | 5 | 电影院座位.jpg 6 | 7 | 如上图所示,电影院的观影厅有`n`行座位,行编号从`1`到`n`,且每一行内总共有10个座位,列编号从`1`到`10`。 8 | 9 | 给你数组`reservedSeats`,包含所有已经被预约了的座位。比如说,`reservedSeats[i]=[3,8]`,表示第`3`行第`8`个座位被预约了。 10 | 11 | 请你返回**最多能安排多少个4人家庭**。4人家庭要占据**同一行内连续**的4个座位。隔着过道的座位(比方说`[3,3]`和`[3,4]`)不是连续的座位,但是如果你可以将4人家庭拆成过道两边各坐2人,这样子是允许的。 12 | 13 | ## 示例1 14 | 15 | 座位.jpg 16 | 17 | ``` 18 | 输入:n = 3, reservedSeats = [[1,2],[1,3],[1,8],[2,6],[3,1],[3,10]] 19 | 输出:4 20 | 解释:上图所示是最优的安排方案,总共可以安排 4 个家庭。蓝色的叉表示被预约的座位,橙色的连续座位表示一个 4 人家庭。 21 | ``` 22 | 23 | ## 示例2 24 | 25 | ``` 26 | 输入:n = 2, reservedSeats = [[2,1],[1,8],[2,6]] 27 | 输出:2 28 | ``` 29 | 30 | ## 示例3 31 | 32 | ``` 33 | 输入:n = 4, reservedSeats = [[4,3],[1,4],[4,6],[1,7]] 34 | 输出:4 35 | ``` 36 | 37 | ## 提示 38 | 39 | - `1 <= n <= 10^9` 40 | - `1 <= reservedSeats.length <= min(10*n, 10^4)` 41 | - `reservedSeats[i].length == 2` 42 | - `1 <= reservedSeats[i][0] <= n` 43 | - `1 <= reservedSeats[i][1] <= 10` 44 | - 所有`reservedSeats[i]`都是互不相同的 45 | 46 | # 解题思路 47 | 48 | 对于一个家庭而言,只有以下三种给他们安排座位的方法: 49 | 50 | 1. 安排位置`2,3,4,5`; 51 | 2. 安排位置`4,5,6,7`; 52 | 3. 安排位置`6,7,8,9`; 53 | 54 | 我们发现每一排的位置`1`和`10`都是没有意义,不论是否被预约都不影响最终结果,所以可以忽略位置`1`和`10`的情况。 55 | 56 | 同时,我们发现如果某一排在位置`2-9`没有发生预约,则最多安排两个家庭;如果在位置`2-9`中至少有一个被预约,则最多安排一个家庭。 57 | 58 | 由此,可以用8个二进制位来表示一排座位的预约情况,8表示位置`2`到`9`这些座位。如果位置$i$的座位被预约,那么第$9-i$个二进制位为1,否则为0。例如在**示例1**中每一排的预约情况对应的二进制数分别为: 59 | 60 | - **第一排**:座位`2,3,8`被预约,则二进制数表示为$(11000010)_2$; 61 | - **第二排**:座位`6`被预约,则二进制数表示为$(00001000)_2$; 62 | - **第三排**:座位`1,10`被预约(座位`1,10`均不影响,忽略),则二进制数表示为$(00000000)_2$。 63 | 64 | 由此,我们可以通过字典`occupied`的形式来记录每一排根据预约情况形成的二进制数。 65 | 66 | 再通过或运算,基于预约情况判断是否还能安排家庭,只要一下三种安排情况有一种满足要求则能安排,否则不能安排: 67 | 68 | 1. 将家庭安排在位置`2,3,4,5`,则仅需判断预约情况的二进制数`mask`,是否有`(mask | (00001111)) == 00001111`成立。 69 | 2. 将家庭安排在位置`4,5,6,7`,则仅需判断预约情况的二进制数`mask`,是否有`(mask | (11000011)) == 11000011`成立。 70 | 3. 将家庭安排在位置`6,7,8,9`,则仅需判断预约情况的二进制数`mask`,是否有`(mask | (00001111)) == 11110000`成立。 71 | 72 | 总共有$n$行座位,减去发生预约的行数`len(occupied)`,则至少有`(n-len(occupied))*2`个家庭,再结合预约情况遍历计算即可。 73 | 74 | ## 复杂度分析 75 | 76 | - 时间复杂度:$O(N)$,$N$为`len(reservedSeats)`。 77 | - 空间复杂度:$O(N)$,创建字典`occupied`中的键值对数量,也为`len(reservedSeats)`。 78 | 79 | ## 代码 80 | 81 | ```python 82 | class Solution: 83 | def maxNumberOfFamilies(self, n: int, reservedSeats: List[List[int]]) -> int: 84 | left, mid, right = 0b00001111, 0b11000011, 0b11110000 85 | occupied = collections.defaultdict(int) 86 | for seat in reservedSeats: 87 | if 2 <= seat[1] <= 9: 88 | occupied[seat[0]] |= (1 << (9 - seat[1])) 89 | res = (n - len(occupied))*2 90 | 91 | for _, mask in occupied.items(): 92 | if (mask | left) == left or (mask | mid) == mid or (mask | right) == right: 93 | res += 1 94 | return res 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /剑指offer系列/面试题16.数值的整数次方.md: -------------------------------------------------------------------------------- 1 | 数值的整数次方 2 | 3 | # 题目描述 4 | 5 | 实现函数double Power(double base, int exponent),求base的exponent次方。不得使用库函数,同时不需要考虑大数问题。 6 | 7 | ## 示例1 8 | 9 | ``` 10 | 输入: 2.00000, 10 11 | 输出: 1024.00000 12 | ``` 13 | 14 | ## 示例2 15 | 16 | ``` 17 | 输入: 2.10000, 3 18 | 输出: 9.26100 19 | ``` 20 | 21 | ## 示例3 22 | 23 | ``` 24 | 输入: 2.00000, -2 25 | 输出: 0.25000 26 | 解释: 2-2 = 1/22 = 1/4 = 0.25 27 | ``` 28 | 29 | ## 说明 30 | 31 | - `-100.0 < x < 100.0` 32 | - n是32位有符号整数,其数值范围是$[-2^{31},2^{31},-1]$。 33 | 34 | # 解题思路 35 | 36 | > 求$x^n$最简单的方法是通过循环将$n$个$x$乘起来,依次求$x^1,x^2,...,x^{n-1},x^n$,时间复杂度为$O(n)$。 37 | > 38 | > **快速幂法**,可将时间复杂度降低至$O(\log n)$,从"二进制"和"二分法"角度对快速幂法进行解析。 39 | 40 | ## 从二进制角度看快速幂 41 | 42 | > 利用十进制数字$n$的二进制表示,可以对快速幂进行数学化解释。 43 | 44 | - 对于任何十进制正整数$n$,设其二进制为$b_m...b_3b_2b_1$,($b_i$为二进制某个位置,取值为$0$或$1$),则有: 45 | - **二进制转十进制**:$n=2^0b_1+2^1b_2+2^2b_3+...+2^{m-1}b_m$(即二进制转十进制公式) 46 | - **幂的二进制展开**:$x^n=x^{2^0b_1+2^1b_2+2^2b_3+...+2^{m-1}b_m}=x^{2^0b_1}x^{2^1b_2}x^{2^@b_3}...x^{2^{m-1}b_m}$。 47 | - 根据上述推导,可把计算$x^n$转化为解决以下两个问题: 48 | 1. 计算$x^1,x^2,x^4,...,x^{2^{m-1}}$的值:循环执行赋值操作$x=x^2$即可; 49 | 2. 获取二进制各位置$b_1,b_2,...,b_{m}$的值:循环执行以下操作即可: 50 | - `n & 1`(与操作):判断$n$二进制最右一位是否为1; 51 | - `n >> 1`)(移位操作):$n$右移一位(即删除最后一位)。 52 | - 利用上述操作,可以在循环中依次计算$x^{2^0b_1},x^{2^1b_2},x^{2^@b_3},...,x^{2^{m-1}b_m}$的值,并将其累计相乘即可。 53 | - 当$b_i=0$时:$x^{2^{i-1}b_i}=1$; 54 | - 当$b_i=1$时:$x^{2^{i-1}b_i}=x^{2^{i-1}}$。 55 | 56 | 附下图,辅助理解: 57 | 58 | 二进制.jpg 59 | 60 | ## 从二分法角度看快速幂 61 | 62 | > 快速幂实际上是二分思想的一种应用 63 | 64 | - **二分推导**:$x^n=x^{\frac{n}{2}}\times x^{\frac{n}{2}}=(x^2)^{\frac{n}{2}}$,令$\frac{n}{2}$为整数,则需要分为奇数/偶数两种情况("//"记为向下取整除法符号): 65 | - 当$n$为偶数时:$x^n=(x^2)^{n//2}$; 66 | - 当$n$为奇数时:$x^n=x(x^2)^{n//2}$,即会多出一项$x$。 67 | - **幂结果获取**: 68 | - 根据二分推导,可通过循环$x=x^2$操作,每次把幂从$n$降至`n//2`,直至将幂降为0; 69 | - 设`res=1`,则初始状态$x^n=x^n\times res$。在循环二分时,每当$n$为奇数时,将多出一项$x$乘入`res`,则最终可化至$x^n=x^0\times res=res$,返回`res`即可。 70 | 71 | 附下图,辅助理解: 72 | 73 | 二分法.jpg 74 | 75 | - **转化为位运算**: 76 | - 向下整除`n//2`等价于右移一位`n>>1`; 77 | - 取余数`n%2`等价于判断二进制最右一位值`n&1`。 78 | 79 | ## 算法流程 80 | 81 | 1. 当`x=0`时:直接返回0(避免后续`x=1/x`赋值报错)。 82 | 2. 初始化`res=1`; 83 | 3. 当`n<0`时,把问题转换为`n>=0`的范围内,执行`x=1/x;n=-n`; 84 | 4. 循环计算:当`n=0`时跳出循环: 85 | 1. 当`n&1=1`时:将当前`x`乘入`res`(即`res *= x`); 86 | 2. 执行`x=x^2`(即`x *= x`); 87 | 3. 执行右移一位(即`n >>= 1`) 88 | 5. 返回`res`。 89 | 90 | ## 复杂度分析 91 | 92 | - 时间复杂度:$O(logN)$ 93 | - 空间复杂度:$O(1)$ 94 | 95 | ## 代码 96 | 97 | ```python 98 | class Solution: 99 | def myPow(self, x: float, n: int) -> float: 100 | if x == 0: 101 | return 0 102 | res = 1 103 | if n < 0: 104 | x, n = 1/x, -n 105 | while n: 106 | if n & 1: 107 | res *= x 108 | x *= x 109 | n >>= 1 110 | return res 111 | ``` 112 | 113 | -------------------------------------------------------------------------------- /剑指offer系列/面试题43.1~n整数中1出现的次数.md: -------------------------------------------------------------------------------- 1 | 1~n整数中1出现的次数 2 | 3 | # 题目描述 4 | 5 | 输入一个整数`n`,求`1 ~ n`这n个整数的十进制表示中出现1的次数。 6 | 7 | 例如,输入`12`,`1 ~ 12`这些整数中包含`1`的数字有`1,10,11和12`,`1`一共出现了`5`次。 8 | 9 | ## 示例1 10 | 11 | ``` 12 | 输入:n = 12 13 | 输出:5 14 | ``` 15 | 16 | ## 示例2 17 | 18 | ``` 19 | 输入:n = 13 20 | 输出:6 21 | ``` 22 | 23 | ## 限制 24 | 25 | - `1 <= n <2^31` 26 | 27 | # 解题思路 28 | 29 | > **核心思想**:将$1\thicksim n$的个位、十位、百位、......的`1`出现次数相加,即为`1`出现的总次数。 30 | 31 | 设数字$n$是个$x$位数,记$n$的第$i$位为$n_i$,则可将$n$写成:$n_xn_{x-1}...n_2n_1$: 32 | 33 | - 称"$n_i$"为**当前位**,记为`cur`; 34 | - 称"$n_{i-1}n_{i-2}...n_2n_1$"为**低位**,记为`low`; 35 | - 称"$n_xn_{x-1}...n_{i+2}n_{n+1}$"为**高位**,记为`high`; 36 | - 称$10^i$为**位因子**,记为`digit`。 37 | 38 | **某位中1出现次数的计算方法**: 39 | 40 | 根据**当前位**`cur`值的不同,分为以下三种情况: 41 | 42 | 1. 当`cur = 0`时,此位1出现次数只由**高位**`high`决定,计算公式为: 43 | 44 | $$ 45 | high\times dighit 46 | $$ 47 | 48 | > 如下图所示,以$n=2304$为例,求$digit=10$(即十位)的1出现次数。(注意:这不是数学证明,举例只是便于理解,本文不做数学证明) 49 | 50 | 情况1.jpg 51 | 52 | 2. 当`cur = 1`时:此位1的出现次数由**高位**$high$和**低位**$low$决定,计算公式为: 53 | 54 | $$ 55 | high\times digit + low + 1 56 | $$ 57 | 58 | > 如下图所示,以$n=2314$为例,求$digit=10$(即十位)的1出现次数。 59 | 60 | 情况2.jpg 61 | 62 | 3. 当`cur = 2,3,...,9`时:此位1的出现次数只由**高位**$high$决定,计算公式为: 63 | 64 | $$ 65 | (high+1)\times digit 66 | $$ 67 | 68 | > 如下图所示,以$n=2324$为例,求$digit=10$(即十位)的1出现次数。 69 | 70 | 情况3.jpg 71 | 72 | **变量递推公式**: 73 | 74 | 设计按照"个位、十位、......"的顺序计算,则$high/cur/low/digit$应初始化为: 75 | 76 | ```python 77 | high = n // 10 78 | cur = n % 10 79 | low = 0 80 | digit = 1 # 个位 81 | ``` 82 | 83 | 因此,从个位到最高位的变量递推公式为: 84 | 85 | ```python 86 | while high != 0 or cur != 0: # 当 high 和 cur 同时为 0 时,说明已经越过最高位,因此跳出 87 | low += cur * digit # 将 cur 加入 low ,组成下轮 low 88 | cur = high % 10 # 下轮 cur 是本轮 high 的最低位 89 | high //= 10 # 将本轮 high 最低位删除,得到下轮 high 90 | digit *= 10 # 位因子每轮 × 10 91 | ``` 92 | 93 | ## 复杂度分析 94 | 95 | - 时间复杂度:$O(\log_{10}n)$,循环内的计算操作使用$O(1)$时间;循环次数为数字$n$的位数,即$\log_{10}n$。 96 | 97 | - 空间复杂度:$O(1)$ 98 | 99 | ## 代码 100 | 101 | ```python 102 | class Solution: 103 | def countDigitOne(self, n: int) -> int: 104 | high = n // 10 105 | cur = n % 10 106 | low = 0 107 | digit = 1 108 | res = 0 109 | while high != 0 or cur != 0: 110 | if cur == 0: 111 | res += high * digit 112 | elif cur == 1: 113 | res += high * digit +low + 1 114 | else: 115 | res += (high + 1) * digit 116 | low += cur * digit 117 | cur = high % 10 118 | high //= 10 119 | digit *= 10 120 | return res 121 | ``` 122 | 123 | -------------------------------------------------------------------------------- /每日一题系列/1284.转化为全零矩阵的最少反转次数.md: -------------------------------------------------------------------------------- 1 | 转化为全零矩阵的最少反转次数 2 | 3 | # 题目描述 4 | 5 | 给你一个`m x n`的二进制矩阵`mat`。 6 | 7 | 每一步,你可以选择一个单元格并将它反转(反转表示`0`变`1`,`1`变`0`)。如果存在和它相邻的单元格,那么这些相邻的单元格也会被反转。(注:相邻的两个单元格共享同一条边)。 8 | 9 | 请你返回将矩阵`mat`转化为全零矩阵的最少反转次数,如果无法转化为全零矩阵,请返回`-1`。 10 | 11 | 二进制的每一个格子要么是`0`要么是`1`。 12 | 13 | 全零矩阵是所有格子都为`0`的矩阵。 14 | 15 | ## 示例1 16 | 17 | ![示例1.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/12/13/matrix.png) 18 | 19 | ``` 20 | 输入:mat = [[0,0],[0,1]] 21 | 输出:3 22 | 解释:一个可能的解是反转 (1, 0),然后 (0, 1) ,最后是 (1, 1) 。 23 | ``` 24 | 25 | ## 示例2 26 | 27 | ``` 28 | 输入:mat = [[0]] 29 | 输出:0 30 | 解释:给出的矩阵是全零矩阵,所以你不需要改变它。 31 | ``` 32 | 33 | ## 示例3 34 | 35 | ``` 36 | 输入:mat = [[1,1,1],[1,0,1],[0,0,0]] 37 | 输出:6 38 | ``` 39 | 40 | ## 示例4 41 | 42 | ``` 43 | 输入:mat = [[1,0,0],[1,0,0]] 44 | 输出:-1 45 | 解释:该矩阵无法转变成全零矩阵 46 | ``` 47 | 48 | ## 提示 49 | 50 | - `m == mat.length` 51 | - `n == mat[0].length` 52 | - `1 <= m <= 3` 53 | - `1 <= n <= 3` 54 | - `mat[i][j]`是0或1 55 | 56 | # 解题思路 57 | 58 | > 本题的官方题解给出了四种解法,有兴趣的小伙伴可以[访问链接](https://leetcode-cn.com/problems/minimum-number-of-flips-to-convert-binary-matrix-to-zero-matrix/solution/zhuan-hua-wei-quan-ling-ju-zhen-de-zui-shao-fan-2/)进行学习。 59 | 60 | 这里,我简单叙述一下用广度优先搜索进行解题的思路。总体上还是老套的BFS模板,但是每一个新的状态是矩阵形式,如果直接保存矩阵形式会占用较多空间,并且不容易判断是否被搜索过,我们将矩阵转换为元组的形式进行存储。 61 | 62 | 我们就直接看代码就可以。 63 | 64 | ## 代码 65 | 66 | ```python 67 | class Solution: 68 | def minFlips(self, mat: List[List[int]]) -> int: 69 | row = len(mat) 70 | col = len(mat[0]) 71 | moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 72 | 73 | queue = collections.deque() 74 | queue.append([mat, 0]) 75 | visited = set() 76 | 77 | # 定义convert()函数,用于实现矩阵状态的一次改变 78 | def convert(mtx, x, y): 79 | # 转入的是元组形式,转化为列表(如果直接传入列表,属于浅拷贝会改变原值) 80 | mtx = list(map(list, mtx)) 81 | mtx[x][y] ^= 1 # 使用 异或 位运算实现0变1;1变0 82 | for move in moves: 83 | next_x = x + move[0] 84 | next_y = y + move[1] 85 | if 0 <= next_x < row and 0 <= next_y < col: 86 | mtx[next_x][next_y] ^= 1 87 | return tuple(map(tuple, mtx)) # 返回结果仍然转化为元组形式,便于判断是否已存在集合中 88 | 89 | # 定义sum_list()函数,用于计算矩阵所有元素加和 90 | def sum_list(mtx): 91 | ans = 0 92 | for i in mtx: 93 | ans += sum(i) 94 | return ans 95 | 96 | while queue: 97 | matrix, cur_step = queue.popleft() 98 | # 利用元素加和来判断矩阵是否全为零 99 | if sum_list(matrix) == 0: 100 | return cur_step 101 | # 从list形式转换为tuple形式 102 | matrix = tuple(map(tuple, matrix)) 103 | for r in range(row): 104 | for c in range(col): 105 | cur_status = convert(matrix, r, c) 106 | if cur_status not in visited: 107 | visited.add(cur_status) 108 | queue.append((cur_status, cur_step + 1)) 109 | return -1 110 | ``` 111 | 112 | -------------------------------------------------------------------------------- /每日一题系列/690.员工的重要性.md: -------------------------------------------------------------------------------- 1 | 员工的重要性 2 | 3 | # 题目描述 4 | 5 | 给定一个保存员工信息的数据结构,他包含了员工**唯一的id**,**重要度**和**直系下属的id**。 6 | 7 | 比如,员工1是员工2的领导,员工2是员工3的领导。他们相应的重要度为15,10,5。那么员工1的数据结构是`[1, 15, [2]]`,员工2的数据结构是`[2, 10, [3]]`,员工3的数据结构是`[3, 5, []]`。注意虽然员工3也是员工1的一个下属,但是由于并不是**直系下属**,因此没有体现在员工1的数据结构中。 8 | 9 | 现在输入一个公司的所有员工信息,以及单个员工id,返回这个员工和他所有下属的重要度之和。 10 | 11 | ## 示例 12 | 13 | ``` 14 | 输入: [[1, 5, [2, 3]], [2, 3, []], [3, 3, []]], 1 15 | 输出: 11 16 | 解释: 17 | 员工1自身的重要度是5,他有两个直系下属2和3,而且2和3的重要度均为3。因此员工1的总重要度是 5 + 3 + 3 = 11。 18 | ``` 19 | 20 | ## 注意 21 | 22 | - 一个员工最多有一个直系领导,但是可以有多个直系下属 23 | - 员工数量不超过2000。 24 | 25 | # 解题思路 26 | 27 | > 依然是一个搜索问题,考虑广度优先搜索/深度优先搜索。 28 | 29 | 根据题意,一个员工最多有一个直系领导,即不存在重复统计的情况。 30 | 31 | 由于每个员工的`id`是唯一的,我们可以通过Hashmap的方式通过`id`来映射员工,即建立一个字典,`key`值为员工的`id`;`value`值为该员工的数据结构,这样可以快速地通过`id`定位到员工。 32 | 33 | ## 方法一:广度优先搜索BFS 34 | 35 | - 建立员工字典,便于通过`id`定位到员工,即`get_emp = {emp.id: emp for emp in employees}`; 36 | - 初始化空队列`queue`以及记录重要度的变量`importance`,将指定的员工加入队列中,`queue.append(get_emp[id])`,作为BFS的起点。 37 | - **BFS**:(直至队列为空跳出) 38 | - 弹出队首的员工,记为`cur_emp`; 39 | - 累计重要度,`importance += cur_emp.importance`; 40 | - 通过该员工的直系员工`id`,定位到直系员工,并加入队列。(此处不需要判断,该`id`是否已被访问,因为不存在重复的情况) 41 | - 返回`importance` 42 | 43 | ## 复杂度分析 44 | 45 | > 设$N$为员工总数。 46 | 47 | - 时间复杂度:$O(N)$ 48 | - 空间复杂度:$O(N)$ 49 | 50 | ## 代码 51 | 52 | ```python 53 | """ 54 | # Definition for Employee. 55 | class Employee: 56 | def __init__(self, id: int, importance: int, subordinates: List[int]): 57 | self.id = id 58 | self.importance = importance 59 | self.subordinates = subordinates 60 | """ 61 | 62 | class Solution: 63 | def getImportance(self, employees: List['Employee'], id: int) -> int: 64 | get_emp = {emp.id: emp for emp in employees} 65 | queue = collections.deque() 66 | importance = 0 67 | queue.append(get_emp[id]) 68 | while queue: 69 | cur_emp = queue.popleft() 70 | importance += cur_emp.importance 71 | if cur_emp.subordinates: 72 | for next_emp_id in cur_emp.subordinates: 73 | queue.append(get_emp[next_emp_id]) 74 | 75 | return importance 76 | ``` 77 | 78 | ## 方法二:深度优先搜索 79 | 80 | - 建立员工字典,便于通过`id`定位到员工,即`get_emp = {emp.id: emp for emp in employees}`; 81 | - 从指定的`id`开始深度优先搜索: 82 | - 找到该`id`的直系员工id,开启深度递归,回溯时对员工重要性进行累加。 83 | 84 | ## 复杂度分析 85 | 86 | - 时间复杂度:$O(N)$ 87 | - 空间复杂度:$O(N)$ 88 | 89 | ## 代码 90 | 91 | ```python 92 | """ 93 | # Definition for Employee. 94 | class Employee: 95 | def __init__(self, id: int, importance: int, subordinates: List[int]): 96 | self.id = id 97 | self.importance = importance 98 | self.subordinates = subordinates 99 | """ 100 | class Solution: 101 | def getImportance(self, employees: List['Employee'], id: int) -> int: 102 | get_emp = {emp.id: emp for emp in employees} 103 | 104 | def dfs(cur_id): 105 | cur_emp = get_emp[cur_id] 106 | 107 | return cur_emp.importance + sum(dfs(next_emp_id) for next_emp_id in cur_emp.subordinates) 108 | 109 | return dfs(id) 110 | ``` 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /每日一题系列/1326.灌溉花园的最少水龙头数目.md: -------------------------------------------------------------------------------- 1 | 灌溉花园的最少水龙头数目 2 | 3 | # 题目描述 4 | 5 | 在`x`轴上有一个一维的花园。花园长度为`n`,从点`0`开始,到点`n`结束。 6 | 7 | 花园里总共有`n+1`个水龙头,分别位于`[0, 1, ... , n]`。 8 | 9 | 给你一个整数`n`和一个长度为`n+1`的整数数组`ranges`,其中`ranges[i]`(下标从`0`开始)表示:如果打开点`i`处的水龙头,可以灌溉的区域为`[i - ranges[i], i + ranges[i]]`。 10 | 11 | 请你返回可以灌溉整个花园的**最少水龙头数目**。如果花园始终存在无法灌溉到的地方,请返回`-1`。 12 | 13 | ## 示例1 14 | 15 | ![示例1.jpg](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/1685_example_1.png) 16 | 17 | ```python 18 | 输入:n = 5, ranges = [3,4,1,1,0,0] 19 | 输出:1 20 | 解释: 21 | 点 0 处的水龙头可以灌溉区间 [-3,3] 22 | 点 1 处的水龙头可以灌溉区间 [-3,5] 23 | 点 2 处的水龙头可以灌溉区间 [1,3] 24 | 点 3 处的水龙头可以灌溉区间 [2,4] 25 | 点 4 处的水龙头可以灌溉区间 [4,4] 26 | 点 5 处的水龙头可以灌溉区间 [5,5] 27 | 只需要打开点 1 处的水龙头即可灌溉整个花园 [0,5] 。 28 | ``` 29 | 30 | ## 示例2 31 | 32 | ``` 33 | 输入:n = 3, ranges = [0,0,0,0] 34 | 输出:-1 35 | 解释:即使打开所有水龙头,你也无法灌溉整个花园。 36 | ``` 37 | 38 | ## 示例3 39 | 40 | ``` 41 | 输入:n = 7, ranges = [1,2,1,0,2,1,0,1] 42 | 输出:3 43 | ``` 44 | 45 | ## 示例4 46 | 47 | ``` 48 | 输入:n = 8, ranges = [4,0,0,0,0,0,0,0,4] 49 | 输出:2 50 | ``` 51 | 52 | ## 示例5 53 | 54 | ``` 55 | 输入:n = 8, ranges = [4,0,0,0,4,0,0,0,4] 56 | 输出:1 57 | ``` 58 | 59 | ## 提示 60 | 61 | - `1 <= n <= 10^4` 62 | - `ranges.length == n+1` 63 | - `0 <= ranges[i] <= 100` 64 | 65 | # 解题思路 66 | 67 | 本题的解题核心就是贪心思想,这应该比较容易理解。 68 | 69 | 大致思路如下: 70 | 71 | - 先把每个水龙头可以覆盖的范围转化成区间,放到一个数组里。 72 | 73 | > 比如,示例1中就可以转换成:`[[0, 3], [0, 5], [1, 3], [2, 4]]` 74 | > 75 | > ![示例1.jpg](http://xyao-imgs.oss-cn-beijing.aliyuncs.com/img/1685_example_1.png) 76 | > 77 | > 这里做了两个**优化**: 78 | > 79 | > - 长度为零的区间直接忽略(因为不起任何作用) 80 | > - 超出`0`到`n`范围的就截止到`[0, n]`区间内(因为超出的部分同样不起任何作用) 81 | > 82 | > (其实还可以有一点优化,计算区间时,若存在某区间可以直接覆盖`[0, n]`,则直接返回`1`即可) 83 | 84 | - 按区间的左端点升序排列,左端点相同时按右端点降序排列。现在,要明确两点: 85 | 1. 至少需要选择两个区间才能满足题目要求。(一个区间的情况已经在上一步返回) 86 | 2. 数组中第一个区间为包含左端点的覆盖范围最大的区间,必然是会被选中的。 87 | - 如果数组数量为`0`或者第一个区间不能覆盖`0`,则直接返回`-1`;否则开始贪心: 88 | - 第一个区间为必选,记为`[left, right]`,计数从`1`开始。 89 | - 寻找其余区间`[left_i, right_i]`,要求满足`left_i <= right`的条件下,`right_i`尽可能大,并作为下一个扩充的区间,同时更新`right = right_i`,计数`+1`。 90 | - 不断循环上述过程,直至`right = n`结束,并返回计数 91 | 92 | ## 复杂度分析 93 | 94 | - 时间复杂度:$O(N\log N)$ 95 | - 空间复杂度:$O(N)$ 96 | 97 | ## 代码 98 | 99 | ```python 100 | class Solution: 101 | def minTaps(self, n: int, ranges: List[int]) -> int: 102 | 103 | covered = [] 104 | for i in range(n+1): 105 | left = max(i-ranges[i], 0) 106 | right = min(i+ranges[i], n) 107 | if left == right: continue 108 | if left == 0 and right == n: return 1 109 | covered.append((left, right)) 110 | covered = sorted(covered, key=lambda x: (x[0], -x[1])) 111 | if not covered or covered[0][0] > 0: return -1 112 | idx = 0 113 | res = 1 114 | while covered[idx][1] != n: 115 | i = idx 116 | j = idx + 1 117 | while j < len(covered) and covered[j][0] <= covered[idx][1]: 118 | if covered[j][1] > covered[i][1]: 119 | i = j 120 | j += 1 121 | if i == idx: return -1 122 | res += 1 123 | idx = i 124 | return res 125 | ``` 126 | 127 | -------------------------------------------------------------------------------- /每日一题系列/407.接雨水II.md: -------------------------------------------------------------------------------- 1 | 接雨水II 2 | 3 | # 题目描述 4 | 5 | 给你一个`m x n`的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 给出如下 3x6 的高度图: 11 | [ 12 | [1,4,3,1,3,2], 13 | [3,2,1,3,2,4], 14 | [2,3,3,2,3,1] 15 | ] 16 | 17 | 返回 4 。 18 | ``` 19 | 20 | ![示例.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/12/rainwater_empty.png) 21 | 22 | 如上图所示,这是下雨前的高度图`[[1,4,3,1,3,2],[3,2,1,3,2,4],[2,3,3,2,3,1]]`的状态。 23 | 24 | ![示例.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2018/10/12/rainwater_fill.png) 25 | 26 | 下雨后,雨水将会被存储在这些方块中。总的接雨水量是4。 27 | 28 | ## 提示 29 | 30 | - `1 <= m, n <= 110` 31 | - `0 <= heightMap[i][j] <= 20000` 32 | 33 | # 解题思路 34 | 35 | 这是一道很有意思的题目,结合示例应该可以理解题目意思。 36 | 37 | 在分析之前,可以先联想一下水桶装水的短板效应。在矩阵的最外围是肯定不能积水的(可以想象这是一圈"围栏"),所以可以积水的矩阵至少应该是`3x3`。 38 | 39 | 进一步,由于短板效应,我们从最外一圈"围栏"的最低的位置开始搜索,遍历该位置的上下左右四个位置,若四个位置没有越界且未被访问,则比较高度: 40 | 41 | - 若高度低于当前的有效高度:说明该位置可以积水,且积水量为两者的高度差。 42 | - 若高度大于等于当前的有效高度:说明最低位置的"短板"可以拆除,用当前位置进行替换。 43 | 44 | 不断得从当前有效的"最低围栏"位置进行上述搜索,注意搜索过程中需要更新当前的有效高度。 45 | 46 | 可能上述文字还是不能很直观得理解,推荐观看[以下视频](https://www.youtube.com/watch?v=cJayBq38VYw)(需要科学上网)。 47 | 48 | 由于,每一次我们都要找到当前"最低围栏",列表不是一个好的选择,这里我们将使用python自带的heapq来构建一个优先队列,使得每次都能快速找到"最低围栏"。(有关数据结构"堆"的知识,大家可以自行温习一下) 49 | 50 | ## 复杂度分析 51 | 52 | > 设$N,M$分别为队列中的元素和总矩阵大小 53 | 54 | - 时间复杂度:$O(NlogN)$ 55 | - 空间复杂度:$O(M)$ 56 | 57 | ## 代码 58 | 59 | ```python 60 | from heapq import * 61 | # 利用heapq构造一个优先队列(小顶堆) 62 | class Solution: 63 | def trapRainWater(self, heightMap: List[List[int]]) -> int: 64 | if len(heightMap) < 3 or len(heightMap[0]) < 3: 65 | return 0 66 | cur_max = float('-inf') 67 | moves = [(-1, 0), (0, -1), (0, 1), (1, 0)] 68 | res = 0 69 | heap = [] 70 | # 用于存储被访问的位置 71 | visited = set() 72 | row = len(heightMap) 73 | col = len(heightMap[0]) 74 | # 将第一行与最后一行加入优先队列 75 | for i in range(col): 76 | heappush(heap, [heightMap[0][i], 0, i]) 77 | heappush(heap, [heightMap[row-1][i], row-1, i]) 78 | visited.add((0, i)) 79 | visited.add((row-1, i)) 80 | 81 | # 将第一列和最后一列加入优先队列 82 | for j in range(row): 83 | heappush(heap, [heightMap[j][0], j, 0]) 84 | heappush(heap, [heightMap[j][col-1], j, col-1]) 85 | visited.add((j, 0)) 86 | visited.add((j, col-1)) 87 | 88 | # 每次取出优先队列中堆顶的元素(即高度最小) 89 | while heap: 90 | h, r, c = heappop(heap) 91 | cur_max = max(cur_max, h) 92 | # 遍历当前点上下左右四个位置 93 | for move in moves: 94 | next_r = r + move[0] 95 | next_c = c + move[1] 96 | # 位置不能越界且不能已被访问 97 | if 0 <= next_r < row and 0 <= next_c < col and (next_r, next_c) not in visited: 98 | visited.add((next_r, next_c)) 99 | heappush(heap, [heightMap[next_r][next_c], next_r, next_c]) 100 | if heightMap[next_r][next_c] < cur_max: 101 | res += (cur_max - heightMap[next_r][next_c]) 102 | 103 | return res 104 | ``` 105 | 106 | -------------------------------------------------------------------------------- /每日一题系列/1402.做菜顺序.md: -------------------------------------------------------------------------------- 1 | 做菜顺序 2 | 3 | # 题目描述 4 | 5 | 一个厨师收集了他`n`道菜的满意程度`satisfaction`,这个厨师做出每道菜的时间都是`1`单位时间。 6 | 7 | 一道菜的**"喜爱时间"系数**定义为烹饪这道菜以及之前每道菜所花费的时间乘以这道菜的满意程度,也就是`time[i]*satisfaction[i]`。 8 | 9 | 请你返回做完所有菜的"喜爱时间"总和的最大值为多少。 10 | 11 | 你可以按**任意**顺序安排做菜的顺序,你也可以选择放弃做某些菜来获得更大的总和。 12 | 13 | ## 示例1 14 | 15 | ``` 16 | 输入:satisfaction = [-1,-8,0,5,-9] 17 | 输出:14 18 | 解释:去掉第二道和最后一道菜,最大的喜爱时间系数和为 (-1*1 + 0*2 + 5*3 = 14) 。每道菜都需要花费 1 单位时间完成。 19 | ``` 20 | 21 | ## 示例2 22 | 23 | ``` 24 | 输入:satisfaction = [4,3,2] 25 | 输出:20 26 | 解释:按照原来顺序相反的时间做菜 (2*1 + 3*2 + 4*3 = 20) 27 | ``` 28 | 29 | ## 示例3 30 | 31 | ``` 32 | 输入:satisfaction = [-1,-4,-5] 33 | 输出:0 34 | 解释:大家都不喜欢这些菜,所以不做任何菜可以获得最大的喜爱时间系数。 35 | ``` 36 | 37 | ## 示例4 38 | 39 | ``` 40 | 输入:satisfaction = [-2,5,-1,0,3,-3] 41 | 输出:35 42 | ``` 43 | 44 | ## 提示 45 | 46 | - `n == satisfaction.length` 47 | - `1 <= n <= 500` 48 | - `-10^3 <= satisfaction[i] <= 10^3` 49 | 50 | # 解题思路 51 | 52 | ## 方法一:贪心算法 53 | 54 | 我们先考虑一个最简单的情况,假设至多只能选择做一道菜,该如何选择?毫无疑问,选择满意程度最大的那道菜,记为$s_0$,并且需要保证$s_0>0$,若$s_0\le0$,我们将一道菜都不做。 55 | 56 | 接着,我们可以至多选两道菜,那么显然选择最优的$s_0$,同时再选择次优的$s_1$,且先做$s_1$,再做$s_2$。则满意度总和为$s_1+2s_0$,且需要保证$s_1+2s_0>s_0$,即$s_1+s_0>0$,这样我们才能选择$s_1$。 57 | 58 | 同理,如果可以至多选择三道菜,则需要保证$s_2+2s_1+3s_0>s_1+2s_0$,即$s_2+s_1+s_0>0$,这样我们才能选择$s_2$。 59 | 60 | 因此我们就有了一个贪心的大致思路: 61 | 62 | - 将所有菜的满意程度从大到小排序; 63 | - 按照排好序的顺序依次遍历这些菜,对于当前遍历到的菜$s_i$,如果它与之前选择的所有菜的满意程度之和大于`0`,就选择做这道菜,否则可以直接退出遍历的循环。这是因为如果$s_i$与之前选择的所有菜的满意程度之和已经小于等于`0`了,那么后面的菜比$s_i$能贡献的满意程度还要小,就更不可能得到一个大于`0`的和了。 64 | 65 | ## 复杂度分析 66 | 67 | - 时间复杂度:$O(N\log N)$,排序花费$O(N\log N)$的时间复杂度;遍历花费$O(N)$ 68 | - 空间复杂度:$O(\log N)$,使用语言自带的排序,空间复杂度为$O(\log N)$,如果使用堆排序,空间复杂度可以降低至$O(1)$。 69 | 70 | ## 代码 71 | 72 | ```python 73 | class Solution: 74 | def maxSatisfaction(self, satisfaction: List[int]) -> int: 75 | satisfaction = sorted(satisfaction, reverse = True) 76 | 77 | presum = 0 78 | res = 0 79 | for i in satisfaction: 80 | if i + presum > 0: 81 | presum += i 82 | res += presum 83 | 84 | return res 85 | ``` 86 | 87 | ## 方法二:动态规划 88 | 89 | 把该问题看作是一个`0-1`背包问题,利用动态规划的思想进行求解。 90 | 91 | 建立一个二维数组`dp`,`dp[i][j]`表示前`i`个菜中,选中`j`个菜所能达到的最大值。 92 | 93 | 我们可以根据第`i`个菜做与不做,对`dp[i][j]`进行更新: 94 | 95 | - 如果选择做第`i`个菜,则`dp[i][j] = dp[i-1][j-1]+satisfaction[i-1]*j`; 96 | - 如果不选择做第`i`个菜,则`dp[i][j] = dp[i-1][j]`; 97 | 98 | `dp[i][j]`取上述两种更新方式的最大值。 99 | 100 | 特殊地,对于`i = j`的情况,第`i`个菜必须要做;对于`i = 0 or j = 0`,的情况,`dp[i][j]`均为`0`。 101 | 102 | 最终返回`dp`数组中的最大元素即可。 103 | 104 | ## 复杂度分析 105 | 106 | - 时间复杂度:$O(N^2)$ 107 | - 空间复杂度:$O(N^2)$ 108 | 109 | ## 代码 110 | 111 | ```python 112 | class Solution: 113 | def maxSatisfaction(self, satisfaction: List[int]) -> int: 114 | satisfaction = sorted(satisfaction) 115 | n = len(satisfaction) 116 | dp = [[0 for _ in range(n+1)] for _ in range(n+1)] 117 | for i in range(1, n+1): 118 | for j in range(1, n+1): 119 | if i == j: 120 | dp[i][j] = dp[i-1][j-1] + satisfaction[i-1]*j 121 | else: 122 | dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]+satisfaction[i-1]*j) 123 | return max(max(dp)) 124 | ``` 125 | 126 | -------------------------------------------------------------------------------- /每日一题系列/面试题16.19.水域大小.md: -------------------------------------------------------------------------------- 1 | 水域大小 2 | 3 | # 题目描述 4 | 5 | 你有一个用于表示一片土地的整数矩阵`land`,该矩阵中每个点的值代表对应地点的海拔高度。若值为0则表示水域。由垂直、水平或对角链接的水域为池塘。池塘的大小是指相连接的水域的个数。编写一个方法来计算矩阵中所有池塘的大小,返回值需要从小到大排序。 6 | 7 | ## 示例 8 | 9 | ``` 10 | 输入: 11 | [ 12 | [0,2,1,0], 13 | [0,1,0,1], 14 | [1,1,0,1], 15 | [0,1,0,1] 16 | ] 17 | 输出: [1,2,4] 18 | ``` 19 | 20 | ## 提示 21 | 22 | - `0 < len(land) <= 100` 23 | - `0 < len(land[i]) <= 1000` 24 | 25 | # 解题思路 26 | 27 | > 还是熟悉的矩阵搜索题,还是熟悉的DFS/BFS! 28 | 29 | ## 方法一:DFS 30 | 31 | - 遍历矩阵`land`,以数值为`0`的点为起点进行DFS: 32 | - 遍历当前点的周围8个点,若无越界且未访问且值为0,则继续DFS; 33 | - 每次进行DFS,面积`area`需要加1; 34 | - 当周围的8个点都不存在0时,深度递归结束,一片池塘的面积计算完毕,并返回 35 | - 将返回值添加进`list`容器,最终返回时使用`sorted()`排序 36 | 37 | ## 复杂度分析 38 | 39 | > 设$N,M$为矩阵`land`的行数和列数,$A$为池塘数量。 40 | 41 | - 时间复杂度:$O(NM+AlogA)$。 42 | - 空间复杂度:$O(A)$ 43 | 44 | ## 代码 45 | 46 | ```python 47 | class Solution: 48 | def pondSizes(self, land: List[List[int]]) -> List[int]: 49 | def dfs(cur_row, cur_col): 50 | # 把area变量标记为自由变量 51 | nonlocal area 52 | area += 1 53 | land[cur_row][cur_col] = -1 54 | for move in moves: 55 | next_row = move[0] + cur_row 56 | next_col = move[1] + cur_col 57 | if 0 <= next_row < len(land) and \ 58 | 0 <= next_col < len(land[0]) and \ 59 | not land[next_row][next_col]: 60 | 61 | dfs(next_row, next_col) 62 | return 63 | 64 | moves = [(-1, -1), (-1, 0), (-1, 1), 65 | (0, -1), (0, 1), 66 | (1, -1), (1, 0), (1, 1)] 67 | res = [] 68 | 69 | for i in range(len(land)): 70 | for j in range(len(land[0])): 71 | if not land[i][j]: 72 | # 每次开启DFS时,需要先对area清零 73 | area = 0 74 | dfs(i, j) 75 | res.append(area) 76 | return sorted(res) 77 | ``` 78 | 79 | ## 方法二:BFS 80 | 81 | BFS就不做过多的介绍了,直接看代码就可以懂。 82 | 83 | ## 复杂度分析 84 | 85 | > 设$N,M$为矩阵`land`的行数和列数,$A$为池塘数量。 86 | 87 | - 时间复杂度:$O(NM+AlogA)$。 88 | - 空间复杂度:$O(A)$ 89 | 90 | ## 代码 91 | 92 | ```python 93 | class Solution: 94 | def pondSizes(self, land: List[List[int]]) -> List[int]: 95 | areas = [] # 水域面积存储数组 96 | steps = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]] # 八个方向 97 | 98 | def bfs(x, y): 99 | area = 1 # 存在水域才会调用bfs,所以初始水域面积为1 100 | q = [[x, y]] # 广度优先搜索 101 | land[x][y] = -1 102 | while q: 103 | p = q.pop(0) 104 | for i in steps: 105 | dx, dy = p[0] + i[0], p[1] + i[1] 106 | if 0 <= dx < len(land) and 0 <= dy < len(land[0]) and not land[dx][dy]: 107 | q.append([dx, dy]) 108 | area += 1 109 | land[dx][dy] = -1 110 | areas.append(area) 111 | 112 | for i in range(len(land)): 113 | for j in range(len(land[0])): 114 | if not land[i][j]: 115 | bfs(i, j) # 对每个符合条件的水域调用bfs 116 | 117 | return sorted(areas) #返回排序后的结果 118 | ``` 119 | 120 | -------------------------------------------------------------------------------- /每日一题系列/773.滑动谜题.md: -------------------------------------------------------------------------------- 1 | 滑动谜题 2 | 3 | # 题目描述 4 | 5 | 在一个`2 x 3`的板上`(board)`有5块砖瓦,用数字`1~5`来表示,以及一块空缺用`0`来表示。 6 | 7 | 一次移动定义为选择`0`与一个相邻的数字(上下左右)进行交换。 8 | 9 | 最终当板`board`的结果是`[[1,2,3],[4,5,0]]`迷板被解开。 10 | 11 | 给出一个迷板的初始状态,返回最少可以通过多少次移动解开迷板,如果不能解开迷板,则返回`-1`。 12 | 13 | ## 示例1 14 | 15 | ``` 16 | 输入:board = [[1,2,3],[4,0,5]] 17 | 输出:1 18 | 解释:交换 0 和 5 ,1 步完成 19 | ``` 20 | 21 | ## 示例2 22 | 23 | ``` 24 | 输入:board = [[1,2,3],[5,4,0]] 25 | 输出:-1 26 | 解释:没有办法完成谜板 27 | ``` 28 | 29 | ## 示例3 30 | 31 | ``` 32 | 输入:board = [[4,1,2],[5,0,3]] 33 | 输出:5 34 | 解释: 35 | 最少完成谜板的最少移动次数是 5 , 36 | 一种移动路径: 37 | 尚未移动: [[4,1,2],[5,0,3]] 38 | 移动 1 次: [[4,1,2],[0,5,3]] 39 | 移动 2 次: [[0,1,2],[4,5,3]] 40 | 移动 3 次: [[1,0,2],[4,5,3]] 41 | 移动 4 次: [[1,2,0],[4,5,3]] 42 | 移动 5 次: [[1,2,3],[4,5,0]] 43 | ``` 44 | 45 | ## 示例4 46 | 47 | ``` 48 | 输入:board = [[3,2,4],[1,5,0]] 49 | 输出:14 50 | ``` 51 | 52 | ## 提示 53 | 54 | - `board`是一个如上所述的`2 x 3`的数组 55 | - `board[i][j]`是一个`[0, 1, 2, 3, 4, 5]`的排列 56 | 57 | # 解题思路 58 | 59 | > 对于这种计算最小步数的问题,要敏感地想到BFS算法。 60 | 61 | 根据题意,我们可以把`board`从`2`行`3`列的数组转换成`1`行`6`列的列表,方便后续转换成元组并哈希(字符串和元组都可以用来哈希,但元组的哈希速度比字符串要快)。 62 | 63 | 如下图所示: 64 | 65 | 示例.jpg 66 | 67 | 进而我们可以很简单地知道每个`0`位置与其可更换位置的索引,比如: 68 | 69 | - 如上图所示,假设`0`在原数组中位置为`(1, 1)`,在一维数组中索引为`4`,且与其可以发生交换的位置在一维数组中索引为`1, 3, 5`。 70 | 71 | 由此,我们构建一个`moves`列表,用于存储可交换位置的对应关系:`moves = [(1, 3), (0, 2, 4), (1, 5), (0, 4), (1, 3, 5), (2, 4)]`。 72 | 73 | 接下来就是BFS的常规流程: 74 | 75 | - **初始化**:初始化队列`queue`(包含元素`(tuple(board), board.index(0), 0)`);初始化访问集合`visited = {tuple(board)}` 76 | - **BFS**:循环队列 77 | - 弹出队首元素,记为`cur_state, idx, cur_step` 78 | - 若`cur_state == (1,2,3,4,5,0)`,说明解开迷板,返回`cur_step` 79 | - 否则,寻找下一个状态,遍历所有交换的可能性: 80 | - `idx_change in moves[idx]`,找到与当前`0`位置可交换的位置索引 81 | - `next_state[idx_change], next_state[idx] = next_state[idx], next_state[idx_change]`交换位置上的元素,得到新状态`next_state` 82 | - 判断`next_state`是否已访问,若未被访问则加入队列`queue.append((next_state, idx_change, cur_step+1))`;加入已访问集合`visited.add(next_state)` 83 | - 若队列循环完毕都未能找到答案,则返回`-1` 84 | 85 | ## 复杂度分析 86 | 87 | > 设$N,M$分别为`board`的行数和列数 88 | 89 | - 时间复杂度:$O(NM(NM)!)$ 90 | - 空间复杂度:$O(NM(NM)!)$ 91 | 92 | ## 代码 93 | 94 | ```python 95 | class Solution: 96 | def slidingPuzzle(self, board: List[List[int]]) -> int: 97 | # 将二维数组拼接成一维 98 | board = board[0] + board[1] 99 | moves = [(1, 3), (0, 2, 4), (1, 5), (0, 4), (1, 3, 5), (2, 4)] 100 | queue = collections.deque() 101 | queue.append((tuple(board), board.index(0), 0)) 102 | visited = {tuple(board)} 103 | 104 | while queue: 105 | cur_state, idx, cur_step = queue.popleft() 106 | if cur_state == (1, 2, 3, 4, 5, 0): 107 | return cur_step 108 | for idx_change in moves[idx]: 109 | next_state = list(cur_state) 110 | next_state[idx_change], next_state[idx] = next_state[idx], next_state[idx_change] 111 | next_state = tuple(next_state) 112 | if next_state not in visited: 113 | queue.append((next_state, idx_change, cur_step+1)) 114 | visited.add(next_state) 115 | return -1 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /每日一题系列/1345.跳跃游戏IV.md: -------------------------------------------------------------------------------- 1 | 跳跃游戏IV 2 | 3 | # 题目描述 4 | 5 | 给你一个整数数组`arr`, 你一开始在数组的第一个元素处(下标为0)。 6 | 7 | 每一步你可以从下标`1`跳到下标: 8 | 9 | - `i+1`满足:`i+1 <= arr.length` 10 | - `i-1`满足:`i-1 >= 0` 11 | - `j`满足:`arr[i] == arr[j]`且`i != j` 12 | 13 | 请你返回到达数组最后一个元素的下标处所需的**最少操作次数**。 14 | 15 | **注意**:任何时候你都不能跳到数组外面。 16 | 17 | ## 示例1 18 | 19 | ``` 20 | 输入:arr = [100,-23,-23,404,100,23,23,23,3,404] 21 | 输出:3 22 | 解释:那你需要跳跃 3 次,下标依次为 0 --> 4 --> 3 --> 9 。下标 9 为数组的最后一个元素的下标。 23 | ``` 24 | 25 | ## 示例2 26 | 27 | ``` 28 | 输入:arr = [7] 29 | 输出:0 30 | 解释:一开始就在最后一个元素处,所以你不需要跳跃。 31 | ``` 32 | 33 | ## 示例3 34 | 35 | ``` 36 | 输入:arr = [7,6,9,6,9,6,9,7] 37 | 输出:1 38 | 解释:你可以直接从下标 0 处跳到下标 7 处,也就是数组的最后一个元素处。 39 | ``` 40 | 41 | ## 示例4 42 | 43 | ``` 44 | 输入:arr = [6,1,9] 45 | 输出:2 46 | ``` 47 | 48 | ## 示例5 49 | 50 | ``` 51 | 输入:arr = [11,22,7,7,7,7,7,7,7,22,13] 52 | 输出:3 53 | ``` 54 | 55 | ## 提示 56 | 57 | - `1 <= arr.length <= 5*10^4` 58 | - `-10^8 <= arr[i] <= 10^8` 59 | 60 | # 解题思路 61 | 62 | > 本题为最短路径问题,优先考虑广度优先搜索。 63 | 64 | 根据题意,除了可以基于当前位置向前或向后跳一步以外,还可以直接跳到与当前位置的数值相同的其他任意位置。 65 | 66 | 本题的关键在于如何处理相同值的情况,如果每次都遍历寻找相同值的位置,时间开销太大。我们可以用哈希的方式对数组中的数值与对应位置关系进行存储。 67 | 68 | 进一步思考,对于原数组可以进行一定的简化(可以认为在剪枝),如果原数组中存在多个连续相同的值,我们只需保留这一串连续相同值的首尾即可,如`arr = [7,7,7,7,7,7,7,7,7,22,13]`,对于这一连串的`7`,中间重复值其实都是不起作用的,即该数组的最终结果与`arr = [7,7,22,13]`的结果相同,所以我们可以对原数组进行整理。(**注意**:当且仅当元素值相同且连续时可以进行剪枝) 69 | 70 | ## 算法框架 71 | 72 | - 去除连续重复值,简化原数组。 73 | - 对数组中的元素值与索引进行哈希存储,`key`为元素数值,`val`为对应的索引(注意:`val`应为列表,因为相同原数值可存在多个索引) 74 | - 初始化队列,将初始位置加入队列。同时初始化一个集合`visited`用于记录以及访问过的位置。 75 | - BFS流程: 76 | - 弹出队列最前端的元素,记录为`cur_idx`与`cur_step`,表示当前所在位置以及当前的步数。 77 | - 若`cur_idx`已经是数组末尾,则返回`cur_step`。 78 | - 否则,寻找与`arr[cur_idx]`相同值的其他位置: 79 | - 若这些位置没有被访问过,则将该位置加入`visited`,并将下一步加入队列 80 | - 若被访问过,则跳过 81 | - 判断基于当前`cur_idx`向前或向后跳一步`cur_idx + 1`或`cur_idx - 1`是否越界,且是否已被访问过: 82 | - 若没有越界,且没有访问过,则将下一步位置加入`visited`,同时加入队列 83 | - 循环上述过程,直到返回最终结果。 84 | 85 | ## 复杂度分析 86 | 87 | - 时间复杂度:$O(N)$ 88 | - 空间复杂度:$O(N)$ 89 | 90 | ## 代码 91 | 92 | ```python 93 | class Solution: 94 | def minJumps(self, arr: List[int]) -> int: 95 | if len(arr) == 1: 96 | return 0 97 | idx = collections.defaultdict(list) 98 | if len(arr) > 2: # 去除连续重复值,简化原数组 99 | arr = [arr[0]] + [arr[i] for i in range(1, len(arr)-1) if arr[i] != arr[i-1] or arr[i] != arr[i+1]] + [arr[-1]] 100 | 101 | 102 | for i, val in enumerate(arr): 103 | idx[val].append(i) 104 | 105 | queue = collections.deque() 106 | queue.append([0,0]) 107 | visited = {0} 108 | 109 | while queue: 110 | cur_idx, cur_step = queue.popleft() 111 | if cur_idx == len(arr) - 1: 112 | return cur_step 113 | for next_idx in idx[arr[cur_idx]]: 114 | if next_idx not in visited: 115 | visited.add(next_idx) 116 | queue.append([next_idx, cur_step + 1]) 117 | if cur_idx + 1 <= len(arr)-1 and cur_idx + 1 not in visited: 118 | visited.add(cur_idx + 1) 119 | queue.append([cur_idx + 1, cur_step + 1]) 120 | if cur_idx - 1 >= 0 and cur_idx - 1 not in visited: 121 | visited.add(cur_idx - 1) 122 | queue.append([cur_idx - 1, cur_step + 1]) 123 | ``` 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /每日一题系列/130.被围绕的区域.md: -------------------------------------------------------------------------------- 1 | 被围绕的区域 2 | 3 | # 题目描述 4 | 5 | 给定一个二维的矩阵,包含`"X"`和`"O"`(字母O)。 6 | 7 | 找到被所有`"X"`围绕的区域,并将这些区域里所有的`"O"`用`"X"`填充。 8 | 9 | ## 示例 10 | 11 | ``` 12 | X X X X 13 | X O O X 14 | X X O X 15 | X O X X 16 | ``` 17 | 18 | 运行函数后,矩阵变为: 19 | 20 | ``` 21 | X X X X 22 | X X X X 23 | X X X X 24 | X O X X 25 | ``` 26 | 27 | 解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的`'O'`都不会被填充为`'X'`。 任何不在边界上,或不与边界上的`'O'`相连的 `'O'` 最终都会被填充为`'X'`。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 28 | 29 | # 解题思路 30 | 31 | > 本题考查矩阵的元素搜索,可以考虑深度优先搜索(DFS)或广度优先搜索(BFS)。 32 | 33 | 根据题意,我们在搜索时,需要对矩阵边界上的`"O"`点进行DFS/BFS,因为与边界连通的`"O"`点是不可被置换为`"X"`,剩余的`"O"`点需要被置换为`"X"`。因此,我们可以通过DFS/BFS找到与边界`"O"`连通的所有`"O"`点,并将其标记为`"Y"`(也可以标记成其他符号,只要不是`"O"`和`"X"`即可)。 34 | 35 | 最后,将矩阵中仍存在的`"O"`置换为`"X"`;将所有的`"Y"`还原为`"O"`,即可。 36 | 37 | ## 复杂度分析 38 | 39 | > 设$M,N$分别为矩阵的行数和列数。 40 | 41 | - 时间复杂度:$O(MN)$。 42 | - 空间复杂度:$O(MN)$。 43 | 44 | ## 代码 45 | 46 | ```python 47 | # DFS 48 | class Solution: 49 | def solve(self, board: List[List[str]]) -> None: 50 | """ 51 | Do not return anything, modify board in-place instead. 52 | """ 53 | def dfs(i,j): 54 | board[i][j] = "Y" 55 | if (i-1)>=0 and board[i-1][j] is "O": 56 | dfs(i-1, j) 57 | if (i+1)<=(len(board)-1) and board[i+1][j] is "O": 58 | dfs(i+1, j) 59 | if (j-1)>=0 and board[i][j-1] is "O": 60 | dfs(i, j-1) 61 | if (j+1)<=(len(board[0])-1) and board[i][j+1] is "O": 62 | dfs(i, j+1) 63 | 64 | 65 | for i in range(len(board)): 66 | for j in range(len(board[0])): 67 | if (i == 0 or i == (len(board)-1) or j == 0 or j == (len(board[0])-1)) and board[i][j] is "O": 68 | dfs(i,j) 69 | 70 | for i in range(len(board)): 71 | for j in range(len(board[0])): 72 | if board[i][j] is "Y": 73 | board[i][j] = "O" 74 | else: 75 | board[i][j] = "X" 76 | ``` 77 | 78 | ```python 79 | # BFS 80 | class Solution: 81 | def solve(self, board: List[List[str]]) -> None: 82 | """ 83 | Do not return anything, modify board in-place instead. 84 | """ 85 | queue = [] 86 | moves = [(1,0),(-1,0),(0,1),(0,-1)] 87 | 88 | def bfs(board,row,col): 89 | queue = [(row,col)] 90 | while queue: 91 | x,y = queue.pop(0) 92 | for i, j in moves: 93 | newx, newy = x + i, y + j 94 | if 0 <= newx < m and 0<= newy < n and board[newx][newy] == "O": 95 | board[newx][newy] = "#" 96 | queue.append((newx,newy)) 97 | 98 | if not board: return [] 99 | m = len(board) 100 | n = len(board[0]) 101 | for i in range(m): 102 | for j in range(n): 103 | if (i == 0 or j == 0 or i == m-1 or j == n-1) and board[i][j] == 'O': 104 | board[i][j] = '#' 105 | bfs(board,i,j) 106 | 107 | for i in range(m): 108 | for j in range(n): 109 | if board[i][j] == 'O': 110 | board[i][j] = 'X' 111 | if board[i][j] == '#': 112 | board[i][j] = 'O' 113 | ``` 114 | 115 | -------------------------------------------------------------------------------- /每日一题系列/909.蛇梯棋.md: -------------------------------------------------------------------------------- 1 | 蛇梯棋 2 | 3 | # 题目描述 4 | 5 | 在一块`N x N`的棋盘`board`上,**从棋盘的左下角开始**,每一行交替方向,按从`1`到`N*N`的数字给方格编号。例如,对于一块`6x6`大小的棋盘,可以编号如下: 6 | 7 | ![棋盘.jpg](https://assets.leetcode-cn.com/aliyun-lc-upload/uploads/2019/01/31/snakes.png) 8 | 9 | 玩家从棋盘上的方格`1`(总是在最后一行、第一列)开始出发。 10 | 11 | 每一次从方格`x`起始的移动都由以下部分组成: 12 | 13 | - 你选择一个目标方块`s`,它的编号是`x+1`,`x+2`,`x+3`,`x+4`,`x+5`或者`x+6`,只要这个数字`<= N*N`。 14 | - 如果`s`有一个蛇或梯子,你就移动到那个蛇或梯子的目的地。否则,你会移动到`s`。 15 | 16 | 在`r`行`c`列上的方格里有"蛇"或"梯子";如果`board[r][c] != -1`,那个蛇或梯子的目的地将会是`board[r][c]`。 17 | 18 | 注意,你每次移动最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,你都不会继续移动。 19 | 20 | 返回达到方格`N*N`所需的最少移动次数,如果不可能则返回`-1`。 21 | 22 | ## 示例 23 | 24 | ``` 25 | 输入:[ 26 | [-1,-1,-1,-1,-1,-1], 27 | [-1,-1,-1,-1,-1,-1], 28 | [-1,-1,-1,-1,-1,-1], 29 | [-1,35,-1,-1,13,-1], 30 | [-1,-1,-1,-1,-1,-1], 31 | [-1,15,-1,-1,-1,-1]] 32 | 输出:4 33 | 解释: 34 | 首先,从方格 1 [第 5 行,第 0 列] 开始。 35 | 你决定移动到方格 2,并必须爬过梯子移动到到方格 15。 36 | 然后你决定移动到方格 17 [第 3 行,第 5 列],必须爬过蛇到方格 13。 37 | 然后你决定移动到方格 14,且必须通过梯子移动到方格 35。 38 | 然后你决定移动到方格 36, 游戏结束。 39 | 可以证明你需要至少 4 次移动才能到达第 N*N 个方格,所以答案是 4。 40 | ``` 41 | 42 | ## 提示 43 | 44 | - `2 <= board.length = board[0].length <= 20` 45 | - `board[i][j]`介于`1`和`N*N`之间或者等于`-1` 46 | - 编号为`1`的方格上没有蛇或梯子。 47 | - 编号为`N*N`的方格上没有蛇或梯子。 48 | 49 | # 解题思路 50 | 51 | > 题目依然是求解最短路径问题,考虑广度优先搜索(BFS)。 52 | 53 | 这道题目的意思会有些难以理解,建议多读几遍,在明确题意后再做题。 54 | 55 | 首先,根据题意棋盘上的数字编号(记做`score`),可以确定其所在的行`row`和列`col`: 56 | 57 | - **对于行号而言**:每`N`个方格增加一次(即,`1 -> N`为一行;`N+1 -> 2N`为一行,以此类推)。 58 | - **对于列号而言**:对`N`取余即可(但是要注意,方向是交替进行的) 59 | 60 | 明确了如何从数字编号获取对应的行和列,就可以套用BFS方法解题了。 61 | 62 | ## 算法流程 63 | 64 | - 将初始位置的数字`1`加入队列`queue`; 65 | - 初始化记录指定数字位置所需移动次数,`step = {1:0}`(起点即为`1`,移动次数为`0`) 66 | - 执行一下循环:(直至队列为空) 67 | - 弹出队列最前端的元素,记为`cur_score`; 68 | - 判断是否到达指定数字`N*N`: 69 | - `if cur_score == N*N`:返回移动次数`step[cur_score]`; 70 | - 否则,按规则遍历寻找下一个数字`for next_score in range(cur_score + 1, min(cur_score + 6, N*N) + 1)`,根据数字编号获取对应的行`next_row`和列`next_col`。查看`board[next_row][next_col]`是否为`-1`: 71 | - 若等于`-1`:说明当前方格处不存在"蛇"或"梯子"(即不能"传送")。 72 | - 若不等于`-1`:则更新`next_score = board[next_row][next_col]`。 73 | - 如果`next_score`不在`step`中,则将其加入,并更新移动次数`step[next_score] = step[cur_score] + 1`;同时加入队列中`queue.append(next_score)`。 74 | - 若循环完毕,中间无返回,则说明不能完成,返回`-1`。 75 | 76 | ## 复杂度分析 77 | 78 | > 设$N$为棋盘`board`的总方格数。 79 | 80 | - 时间复杂度:$O(N)$。 81 | - 空间复杂度:$O(N)$。 82 | 83 | ## 代码 84 | 85 | ```python 86 | class Solution: 87 | def snakesAndLadders(self, board: List[List[int]]) -> int: 88 | N = len(board) 89 | def get_pos(score): 90 | i, j = divmod((score-1), N) 91 | row = N - 1 - i 92 | col = j if not i%2 else N-1-j 93 | return row, col 94 | queue = collections.deque([1]) 95 | step = {1:0} 96 | while queue: 97 | cur_score = queue.popleft() 98 | if cur_score == N*N: 99 | return step[cur_score] 100 | for next_score in range(cur_score + 1, min(cur_score + 6, N*N) + 1): 101 | next_row, next_col = get_pos(next_score) 102 | if board[next_row][next_col] != -1: 103 | next_score = board[next_row][next_col] 104 | if next_score not in step: 105 | step[next_score] = step[cur_score] + 1 106 | queue.append(next_score) 107 | return -1 108 | ``` 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /每日一题系列/1311.获取你好友已观看的视频.md: -------------------------------------------------------------------------------- 1 | 获取你好友已观看的视频 2 | 3 | # 题目描述 4 | 5 | 有`n`个人,每个人都有一个`0`到`n-1`的唯一id。 6 | 7 | 给你数组`watchVideos`和`friends`,其中`watchedVideo[i]`和`friends[i]`分别表示`id = i`的人观看过的视频列表和他的好友列表。 8 | 9 | **Level 1**的视频包含所有你好友观看的视频,**Level 2**的视频包含所有你好友的好友观看过的视频,以此类推。一般的,`level`为`k`的视频包含所有从你出发,最短距离为`k`的好友观看过的视频。 10 | 11 | 给定你的`id`和一个`level`值,请你找出所有指定`level`的视频,并将它们按观看频率升序返回。如果有频率相同的视频,请将它们按字母顺序从小到大排列。 12 | 13 | ## 示例1 14 | 15 | ![示例1.jpg](http://q9qozit0b.bkt.clouddn.com/%E8%8E%B7%E5%8F%96%E8%A7%86%E9%A2%91%E7%A4%BA%E4%BE%8B1.JPG) 16 | 17 | ``` 18 | 输入:watchedVideos = [["A","B"],["C"],["B","C"],["D"]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 1 19 | 输出:["B","C"] 20 | 解释: 21 | 你的 id 为 0(绿色),你的朋友包括(黄色): 22 | id 为 1 -> watchedVideos = ["C"]  23 | id 为 2 -> watchedVideos = ["B","C"]  24 | 你朋友观看过视频的频率为: 25 | B -> 1  26 | C -> 2 27 | ``` 28 | 29 | ## 示例2 30 | 31 | ![示例2.jpg](http://q9qozit0b.bkt.clouddn.com/%E8%8E%B7%E5%8F%96%E8%A7%86%E9%A2%91%E7%A4%BA%E4%BE%8B2.JPG) 32 | 33 | ``` 34 | 输入:watchedVideos = [["A","B"],["C"],["B","C"],["D"]], friends = [[1,2],[0,3],[0,3],[1,2]], id = 0, level = 2 35 | 输出:["D"] 36 | 解释: 37 | 你的 id 为 0(绿色),你朋友的朋友只有一个人,他的 id 为 3(黄色)。 38 | ``` 39 | 40 | ## 提示 41 | 42 | - `n == watchedVideos.length == friends.length` 43 | - `2 <= n <= 100` 44 | - `1 <= watchedVideos[i].length <= 100` 45 | - `1 <= watchVideos[i][j].length <= 8` 46 | - `0 <= friends[i].length < n` 47 | - `0 <= friends[i][j] < n` 48 | - `0 <= id < n` 49 | - `1 <= level < n` 50 | - 如果`friends[i]`包含`j`,那么`friends[j]`包含`i` 51 | 52 | # 解题思路 53 | 54 | > 本题的核心是图的搜索,使用广度优先搜索或许更合适一些(当然,深度优先搜索也是可以解题的)。 55 | 56 | 这里,以广度优先搜索为例进行思路讲解(有兴趣的小伙伴可以自己使用DFS尝试一下)。 57 | 58 | ## 解题框架 59 | 60 | **步骤一**:找出所有最短路径为**Level k**的好友。 61 | 62 | 从给定的`id`开始,使用广度优先搜索去寻找,如从`id`出发直接能找到的`friend_ids`就是`level = 1`的好友。 63 | 64 | 具体地,使用一个队列帮助我们搜索。队列中初始只有编号为`id`的节点。我们进行`k`轮搜索,第`i`轮搜索开始前,队列中存在的节点是所有`level = i-1`的好友,通过这些节点找到`level = i `的好友。依次取出`level = i-1`的节点(假设记为`x`),遍历`x`对应的所有好友`friends[x]`,如果某个好友未被访问过(如果已经被访问过则说明最短路径不为`i`),那么这就是`level = i`的好友中的一个,将其加入队列。在`k`轮搜索完后,队列中就只剩下`leve = k`的好友了。 65 | 66 | **步骤二**:统计好友观看过的视频 67 | 68 | 上一步得到了`level = k`的好友,就很容易得到其对应观看过的视频了,访问数组`watchedVideos`即可。这里可以使用Python自带的`collections.Counter()`函数来进行统计,也可以自己构建哈希映射进行统计。 69 | 70 | **步骤三**:按要求排序并输出 71 | 72 | 不论使用`Counter()`还是自己构建哈希映射,都可以获得一个字典,`key`为观看过的视频名称,`value`为该视频的频率。 73 | 74 | 利用`sorted()`函数,按照观看次数为第一关键字、视频名称为第二关键字升序排序即可。 75 | 76 | ## 复杂度分析 77 | 78 | > 设$N,M$分别表示总人数和最大好友关系的数量,$V$为符合要求的视频总数。 79 | 80 | - 时间复杂度:$O(NM+VlogV)$ 81 | - 空间复杂度:$O(N+V)$ 82 | 83 | ## 代码 84 | 85 | ```python 86 | class Solution: 87 | def watchedVideosByFriends(self, watchedVideos: List[List[str]], friends: List[List[int]], id: int, level: int) -> List[str]: 88 | queue = collections.deque([id]) 89 | visited = {id} 90 | while level: 91 | num = len(queue) 92 | while num: 93 | tmp = queue.popleft() 94 | for friend in friends[tmp]: 95 | if friend not in visited: 96 | visited.add(friend) 97 | queue.append(friend) 98 | num -= 1 99 | level -= 1 100 | res = [] 101 | while queue: 102 | tmp = queue.popleft() 103 | res += watchedVideos[tmp] 104 | 105 | res = collections.Counter(res) 106 | res = sorted(list(res.items()),key=lambda x: (x[1],x[0]), reverse=False) 107 | return [i[0] for i in res] 108 | ``` 109 | 110 | 111 | 112 | --------------------------------------------------------------------------------