文章目录

  • 2 实现 Singleton 模式
  • 3 找出数组中重复的数字
  • 3.2 不修改数组找出重复的数字
  • 4 二维数组中的查找
  • 5 替换空格
  • 6 从尾到头打印链表
  • 7 重建二叉树
  • 8 二叉树的下一个节点
  • 9 用两个栈实现队列
  • 9.1 用两个队列实现一个栈
  • 10 斐波那契数列
  • 10.2 青蛙跳台阶
  • 10.3 青蛙变态跳台阶
  • 10.4 矩形覆盖问题
  • 11 旋转数组的最小数字
  • 12 矩阵中的路径
  • 13 机器人运动的范围
  • 14 剪绳子
  • 15 二进制中 1 的个数
  • 16 数值的整数次方
  • 17 打印从 1 到最大的 n 位数
  • 18 在O(1)时间删除链表节点
  • 18.2 删除排序链表中重复的节点
  • 19 正则表达式匹配
  • 20 表示数值的字符串
  • 21 调整数组顺序使奇数位于偶数前
  • 22 链表中倒数第 K 个节点
  • 23 链表中环的入口节点
  • 24 反转链表
  • 25 合并两个有序链表
  • 26 树的子结构
  • 27 二叉树的镜像
  • 28 对称的二叉树
  • 29 顺时针打印矩阵
  • 30 包含min函数的栈
  • 31 栈的压入、弹出序列
  • 32 从上往下打印二叉树
  • 32.1 分行从上到下打印二叉树
  • 32.2 之字形打印二叉树
  • 33 二叉搜索树的后序遍历
  • 34 二叉树中和为某一值的路径
  • 35 复杂链表的复制
  • 36 二叉搜索树与双向链表
  • 37 序列化二叉树
  • 38 字符串的排列
  • 38.2 字符串的所有组合
  • 38.3 八皇后问题
  • 39 数组中出现次数超过一半的数字
  • 40 最小的 k 个数
  • 41 数据流中的中位数
  • 42 连续子数组的最大和
  • 43 1 ~ n 整数中 1 出现的次数
  • 44 数字序列中某一位的数字
  • 45 把数组排成最小的数
  • 46 把数字翻译成字符串
  • 47 礼物的最大价值
  • 48 最长不含重复字符的子字符串
  • 49 丑数
  • 50.1 第一个只出现一次的字符
  • 50.2 字符流中第一个只出现一次的字符
  • 51 数组中的逆序对
  • 52 两个链表的第一个公共节点
  • 53.1 数字在排序数组中出现的次数
  • 53.2 0~n-1 中缺失的数字
  • 53.3 数组中数值与下标相等的元素
  • 54 二叉搜索树的第k小的节点
  • 55.1 二叉树的深度
  • 55.2 平衡二叉树
  • 56.1 数组中只出现一次的两个数字
  • 56.2 数组中唯一只出现一次的数字
  • 57.1 和为 s 的数字
  • 57.2 和为 s 的连续正数序列
  • 58.1 翻转字符串
  • 58.2 左旋转字符串
  • 59 滑动窗口的最大值
  • 60 n个骰子的点数
  • 61 扑克牌中的顺子
  • 62 圆圈中最后剩下的数字
  • 63 股票的最大利润
  • 64 求 1+2+...+n
  • 65 不用加减乘除做加法
  • 66 构建乘积数组
  • 67 字符串转化为整数
  • 68 最低公共祖先

2 实现 Singleton 模式

使用 __new__控制实例创建过程

class Singleton:_instance = Nonedef __init__(self):passdef __new__(cls, *args, **kw):if not cls._instance:cls._instance = super().__new__(cls)return cls._instanceclass MyClass(Singleton):pass

3 找出数组中重复的数字

题目描述:

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,
但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

解析:

长度为n,数字范围0~n-1,如果这个数组不存在重复的数字,那么当数组排序后数字 i 将出现在下标为 i 的位置。

即下方跳出 while 循环。

  1. 让我们重新在排列这个数组,从头到尾依次扫描每个数字。当扫到下标为 i 的数字,首先判断这个数字(m)是否等于 i。如果是,则扫描下一个数字
  2. 若不是,则再拿它和下标为 m 的数字比较,相等则找到一个重复的数字 (该数字在下标为 i 和 m 的位置都出现了),若不等则交换两者位置。使得数字 m 对应下标 m。
  3. 接着继续重复这个过程,直到找到重复数字为止。
class Solution:def duplicate(self, nums, duplication):"""Space: O(1)"""for i, num in enumerate(nums):while i != num:  # 当数字m与下标不相等时if nums[num] == num: # 当数字m与第m个数字相等时,就找到了duplication[0] = numreturn Trueelse:  #否则交换nums[i], nums[num] = nums[num], nums[i]num = nums[i]return Falsedef duplicate_1(self, nums, duplication):"""Space: O(n)另起一个数组存储出现过的字符"""t = []for x in nums:if x in t:duplication[0] = xreturn Trueelse:t.append(x)return False

3.2 不修改数组找出重复的数字

题目:

给定一个长度为 n+1 的数组nums,数组中所有的数均在 1∼n 的范围内,其中 n≥1。
请找出数组中任意一个重复的数,但不能修改输入的数组。
样例 给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。 返回 2 或 3。如果只能使用 O(1) 的额外空间,该怎么做呢?

解析:

这道题目主要应用了抽屉原理和分治的思想。
抽屉原理:n+1 个苹果放在 n 个抽屉里,那么至少有一个抽屉中会放两个苹果。
用在这个题目中就是,一共有 n+1 个数,每个数的取值范围是1到n,所以至少会有一个数出现两次。
然后我们采用分治的思想,将每个数的取值的区间[1, n]划分成[1, n/2]和[n/2+1, n]两个子区间,然后分别统计两个区间中数的个数。
注意这里的区间是指,数的取值范围,而不是数组下标。
划分之后,左右两个区间里一定至少存在一个区间,区间中数的个数大于区间长度。
这个可以用反证法来说明:如果两个区间中数的个数都小于等于区间长度,那么整个区间中数的个数就小于等于n,和有n+1个数矛盾。
因此我们可以把问题划归到左右两个子区间中的一个,而且由于区间中数的个数大于区间长度,根据抽屉原理,在这个子区间中一定存在某个数出现了两次。
依次类推,每次我们可以把区间长度缩小一半,直到区间长度为1时,我们就找到了答案。
时间复杂度:每次会将区间长度缩小一半,一共会缩小 O(logn) 次。每次统计两个子区间中的数时需要遍历整个数组,时间复杂度是 O(n)。所以总时间复杂度是 O(nlogn)。
空间复杂度:代码中没有用到额外的数组,所以额外的空间复杂度是 O(1)。

但是不保证找出所有的重复数字。

若左边区间数字出现的次数小于范围,并不保证一定不存在重复数字。

class Solution:def findDuplicate(self, nums) -> int:"""O(nlogn)不保证找出所有重复数字"""if not nums: returnl, r = 1, len(nums)-1  # 数值的范围不是下标的范围,所以是1~n 题目给出。while l<r:mid = l + r >> 1  # [l, mid], [mid+1, r]s = 0for x in nums:# 计算左边区间数字的个数if l <= x <= mid:s += 1if s > mid - l + 1:  #若左边区间数字出现的次数大于范围,则重复数据一定在此区间r = midelse: l = mid +  1return r

4 二维数组中的查找

题目: leetcode 240

在一个二维数组中,每一行都按照从左到右递增的顺序排序。
每一列都按照从上到下递增的顺序排序。
给定一个整数,查找数组中是否存在该整数。
[ [1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15] ]

解析:

不能选左上或右下,因为侯选区域分两块了,变得复杂。
所以从右上或者坐下开始搜索,每次只需考虑一种情况。

class Solution(object):def searchArray(self, array, target):if not array:return Falserow, col = 0, len(array[0]) - 1while row <= len(array)-1 and col >= 0:if array[row][col] == target:return Trueelif array[row][col] < target:row += 1else:col -= 1return False

5 替换空格

题目:

请实现一个函数,把字符串中的每个空格替换成"%20"。

解析:

  1. 首先遍历一遍原数组,求出最终答案的长度length;
  2. 将原数组resize成length大小;
  3. 使用两个指针,指针i指向原字符串的末尾,指针j指向length的位置;
  4. 两个指针分别从后往前遍历,如果str[i] == ’ ‘,则指针j的位置上依次填充’0’, ‘2’, ‘%’,这样倒着看就是"%20";如果str[i] != ’ ',则指针j的位置上填充该字符即可。
class Solution:def replaceSpace(self, s):"""常规解法O(n)"""if not s: return ''s = list(s)# 求出填充之后的长度length = 0for x in s:if x == ' ':length += 3else:length += 1# 扩充原字符串i, j = len(s) - 1, length - 1s += [0] * (length - len(s))while i >= 0:if s[i] == ' ':s[j] = '0's[j - 1] = '2's[j - 2] = '%'j -= 3else:s[j] = s[i]j -= 1i -= 1return ''.join(s)def replaceSpace(self, s):"""pythonic"""if type(s) != str:return ''return s.replace(' ', '%20')

6 从尾到头打印链表

题目:

输入一个链表的头结点,按照 从尾到头 的顺序返回节点的值。

解析

  1. 遍历+倒序
  2. 递归
class Solution(object):def printListReversingly(self, head: ListNode) -> List[int]:"""遍历+倒序"""if not head: return []res = []while head:res.append(head.val)head = head.nextres.reverse()return resdef printListReversingly_1(self, head):"""递归"""self.res = []self.dfs(head)return self.resdef dfs(self, head):if not head:returnself.dfs(head.next)self.res.append(head.val)

7 重建二叉树

题目: leetcode 105.

输入某二叉树的前序遍历中序遍历的结果,请重建出该二叉树。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

解析:

  1. 找到各个子树的根节点 root
  2. 递归构建该根节点的左子树
  3. 递归构建该根节点的右子树
class Solution:def buildTree(self, preorder, inorder):"""返回根节点"""if not preorder or not inorder:return# 前序遍历的第一个节点为根节点root = TreeNode(preorder[0])# 因为没有重复元素,所以可以直接根据值来查找根节点在中序遍历中的位置mid = inorder.index(preorder[0])# 左子树根节点left = self.buildTree(preorder[1:mid+1], inorder[:mid])# 右子树根节点right = self.buildTree(preorder[mid+1:], inorder[mid+1:])root.left = leftroot.right = rightreturn root

8 二叉树的下一个节点

题目: 牛客网

给定一棵二叉树的其中一个节点,请找出中序遍历 [左,根,右] 序列的下一个节点。

  • 如果给定的节点是中序遍历序列的最后一个,则返回空节点;
  • 二叉树一定不为空,且给定的节点一定不是空节点;

解析:

  1. 当此节点有右子树,下一个节点就是右子树中最左侧的节点。
  2. 当此节点没有右子树时,
    • 若它是它父节点的左节点,那么下一个节点就是它的父节点
    • 若不是,就沿着父指针向上遍历,直到找到一个是它父节点的左节点,这个父节点就是我们要找的下一个节点。
class Solution:def GetNext(self, pNode: TreeLinkNode):"""中序遍历的下一个[left, root, right]"""# pNode 不存在则返回Noneif not pNode: return# 节点有右子树,则下一个节点就是它右子树的最左节点if pNode.right:pRight = pNode.rightwhile pRight.left:pRight = pRight.leftreturn pRight# 节点没有右子树,沿着父节点,直到找到是它父节点的左节点while pNode.next:parent = pNode.nextif parent.left == pNode:return parentpNode = parentreturn # 不存在就返回None

9 用两个栈实现队列

题目: leetcode 232

请用栈实现一个队列,支持如下四种操作:

  • push(x) – 将元素x插到队尾;
  • pop() – 将队首的元素弹出,并返回该元素;
  • peek() – 返回队首元素;
  • empty() – 返回队列是否为空;

解析:

  1. push(x):直接将x插入栈1中,时间复杂度O(1)

  2. pop():队列是先进先出,栈是先进后出,所以将栈1所有的元素放入栈2中,此时最先进入的元素在栈2的顶部,弹出即可。下次若栈2不为空,直接弹出栈顶元素即可。时间复杂度O(n)

这种解法是在出队时保证队先进先出的特性。

class MyQueue:def __init__(self):self.s1 = []self.s2 = []def push(self, x: int) -> None:self.s1.append(x)def pop(self) -> int:if self.s2:return self.s2.pop()while self.s1:self.s2.append(self.s1.pop())return self.s2.pop()def peek(self) -> int:if self.s2:return self.s2[-1]while self.s1:self.s2.append(self.s1.pop())return self.s2[-1]def empty(self) -> bool:if self.s1 or self.s2:return Falseelse:return True

解法二:

进队时即保证队先进先出的特性。

    def push(self) -> int:while self.s1:self.s2.append(self.s1.pop())self.s1.append(x)while self.s2:self.s1.append(self.s2.pop())def pop(self) -> int:return self.s1.

9.1 用两个队列实现一个栈

题目: leetcode 225

使用队列实现栈的下列操作:

push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空

解析:

  1. push的时候保证栈的特性即可,栈是先进候出,队列先进先出,入队时,将队1所有元素放入队2,将元素x入队1,再将队2所有元素入队1,则保证了栈的特性
  2. pop直接返回队1队首元素即可。

class MyStack:def __init__(self):from collections import dequeself.q1 = deque()self.q2 = deque()def push(self, x: int) -> None:while self.q1:self.q2.append(self.q1.popleft())self.q1.append(x)while self.q2:self.q1.append(self.q2.popleft())def pop(self) -> int:return self.q1.popleft()def top(self) -> int:return self.q1[0]def empty(self) -> bool:if self.q1:return Falsereturn True

10 斐波那契数列

题目:leetcode 209

求斐波那契数列的第n项

解析:
KaTeX parse error: No such environment: equation at position 16: f(n) = \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ \begin{cases} …

  • 递归 (存在大量重复计算)
  • 递推
class Solution:def fib(self, N: int) -> int:"""O(n), O(1)递归+滚动变量"""if N < 2: return Nf0, f1, fn = 0, 1, 0for _ in range(2, N+1):fn = f0 + f1f0, f1 = f1, fnreturn fndef fib(self, N: int) -> int:"""递归 O(2^n)"""if N <= 0:return 0if N == 1:return 1return self.fib(N-1) + self.fib(N-2)

另外一种解法:矩阵乘法+快速幂

利用矩阵运算的性质将通项公式变成幂次形式,然后用平方倍增(快速幂)的方法求解第 n 项。

先说通式:
[an+1ananan−1]=[1110]n\begin{bmatrix} a_{n+1} &amp; a_{n} \\ a_{n} &amp; a_{n-1} \\ \end{bmatrix}= \begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 0 \\ \end{bmatrix}^n [an+1​an​​an​an−1​​]=[11​10​]n

利用数学归纳法证明:
这里的a0,a1,a2是对应斐波那契的第几项
令A=[1110],则A1=[a2a1a1a0]显然成立令A =\begin{bmatrix} 1 &amp; 1 \\ 1 &amp; 0 \\ \end{bmatrix},则A^1 = \begin{bmatrix} a_{2} &amp; a_{1} \\ a_{1} &amp; a_{0} \\ \end{bmatrix} 显然成立 令A=[11​10​],则A1=[a2​a1​​a1​a0​​]显然成立

An=An−1×A=[anan−1an−1an−2]×[a2a1a1a0]=[an+1ananan−1]A^n = A^{n-1} \times A = \begin{bmatrix} a_{n} &amp; a_{n-1} \\ a_{n-1} &amp; a_{n-2} \\ \end{bmatrix} \times \begin{bmatrix} a_{2} &amp; a_{1} \\ a_{1} &amp; a_{0} \\ \end{bmatrix}= \begin{bmatrix} a_{n+1} &amp; a_{n} \\ a_{n} &amp; a_{n-1} \\ \end{bmatrix} An=An−1×A=[an​an−1​​an−1​an−2​​]×[a2​a1​​a1​a0​​]=[an+1​an​​an​an−1​​]

证毕。

所以我们想要的得到ana_nan​ ,只需要求得AnA^nAn ,然后取第一行第二个元素即可。

如果只是简单的从0开始循环求n次方,时间复杂度仍然是O(n),并不比前面的快。我们可以考虑乘方的如下性质,即快速幂:
an={an/2⋅an/2n 为偶数a(n−1)/2⋅a(n−1)/2⋅an 为奇数a^n= \begin{cases} a^{n/2} \cdot a^{n/2} &amp; \text {n 为偶数} \\ a^{(n-1)/2} \cdot a^{(n-1)/2} \cdot a &amp; \text {n 为奇数} \end{cases} an={an/2⋅an/2a(n−1)/2⋅a(n−1)/2⋅a​n 为偶数n 为奇数​
这样只需要 logn 次运算即可得到结果,时间复杂度为 O(logn)

def mul(a, b):  # 首先定义二阶矩阵乘法运算c = [[0, 0], [0, 0]]  # 定义一个空的二阶矩阵,存储结果for i in range(2):  # rowfor j in range(2):  # colfor k in range(2):  # 新二阶矩阵的值计算c[i][j] += a[i][k] * b[k][j]return cdef fib(n):res = [[1, 0], [0, 1]]  # 单位矩阵,等价于1,作为baseA = [[1, 1], [1, 0]]  # A矩阵while n:# 1. 如果n是奇数,则先提取一个A出来# 2. 停止条件 n == 1if n & 1: res = mul(res, A)A = mul(A, A)  # 快速幂n >>= 1  # 整除2,向下取整return res[0][1]

10.2 青蛙跳台阶

题目: 牛客网

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

解析:

记 n 阶台阶的跳法看成 n 的函数,记为 f(n)

  • 若第一次跳的时候只跳 1 级,那么剩下的 n-1 个台阶的跳法是 f(n - 1)
  • 若第一次跳的时候只跳 1 级,那么剩下的 n-2 个台阶的跳法是 f(n - 2)
  • 可以得出 f(n) = f(n-1) + f(n-2),f(1) = 1, f(2) = 2
class Solution:def jumpFloor(self, number):if number <=2 : return max(0, number)f1, f2, fn = 1, 2, 0for _ in range(3, number+1):fn = f1 + f2f1, f2 = f2, fnreturn fn

10.3 青蛙变态跳台阶

题目:牛客网

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

解析:

每个台阶都有跳与不跳两种情况(除了最后一个台阶),最后一个台阶必须跳。所以共用 2(n−1)2^{(n-1)}2(n−1) 中情况.

class Solution:def jumpFloorII(self, number):return 2**(number-1)

10.4 矩形覆盖问题

题目: 牛客网

我们可以用2x1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2x1的小矩形无重叠地覆盖一个2xn的大矩形,总共有多少种方法?

解析:

小矩形有两种摆法,横着和竖着,记 2xn 的大矩形的摆法为 f(n)

  1. 第一个小矩形竖着放时,剩余的 2x(n-1) 的矩形的摆法是 f(n-1)
  2. 第一个小矩形横着放时,下面必须再横着放一个小矩形,剩余的 2x(n-2) 的矩形的摆法是 f(n-2)
  3. 故 f(n) = f(n-1) + f(n-2),f(1) = 1, f(2) = 2
class Solution:def rectCover(self, n):if n<=2:return nf1, f2, fn = 1, 2, 0for _ in range(3, n + 1):fn = f1 + f2f1, f2 = f2, fnreturn fn

11 旋转数组的最小数字

题目: leetcode 153

假设按照升序排序的数组在预先未知的某个点上进行了旋转

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,|||,0,1,2] )。

请找出其中最小的元素。你可以假设数组中不存在重复元素。

解析:

二分法

看到有序序列,自然想到二分法。

由图可以看到,线段由两段递增序列组成,左边大于等于nums[0],右边小于nums[0]。我们要找到右边第一个小于nums[0] 的点。即为我们整个数组的最小值。

class Solution:def findMin(self, nums: List[int]) -> int:if not nums:return n = len(nums) - 1if n == 1:  # 单元素自然有序return nums[0]# 升序则返回第一个,旋转0个或n个时if nums[0] < nums[-1]:return nums[0]# 去除后面与前面重复的部分while n>0 and nums[n] == nums[0]: n -= 1 # 找到第一个小于nums[0]的数l, r = 0, nwhile l < r:mid = l + r >> 1if nums[mid] >= nums[0]:l = mid + 1else:r = midreturn nums[l]

12 矩阵中的路径

题目: leetcode 79

给定一个二维网格和一个单词,找出该单词是否存在于网格中。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[['A','B','C','E'],['S','F','C','S'],['A','D','E','E']
]给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.

解析:

回溯法

外层:遍历

首先遍历 board 的所有元素,先找到和 word 第一个字母相同的元素,然后进入递归流程。

内层:递归
给进入的节点打标记。递归流程主要做了这么几件事:

  • 从 (i, j) 出发,朝它的上下左右试探,看看它周边的这四个元素是否能匹配 word 的下一个字母

  • 如果匹配到了:带着该元素继续进入下一个递归

  • 如果都匹配不到:返回 False

  • 当 word 的所有字母都完成匹配后,整个流程返回 True

几个注意点

  • 递归时元素的坐标是否超过边界

  • 标记以及 return 的时机

class Solution:def exist(self, m: List[List[str]], word: str) -> bool:def dfs(u, i, j):  # u为当前匹配的多少字符,ij为坐标# 停止条件if u == len(word) - 1:return True# 标记visit,并保存temp, m[i][j] = m[i][j], '*'# 四个方向探索for a, b in ((i, j + 1), (i + 1, j), (i, j - 1), (i - 1, j)):if 0 <= a < len(m) and 0 <= b < len(m[0]) and m[a][b] == word[u + 1] and dfs(u + 1, a, b):return Truem[i][j] = temp  # 回溯return False  # 如果都匹配不到:返回 Falsefor i in range(len(m)):for j in range(len(m[0])):if m[i][j] == word[0] and dfs(0, i, j):return Truereturn False

13 机器人运动的范围

题目: 牛客网

地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。

例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

解析: DFS

没有回溯的步骤,因为一个格子最多进入一次。

  1. 从第一个格子进入递归
  2. 标记已访问,res+=1,在四个方向继续探索, 符合则进入递归
  3. 没有停止条件,搜索完就停止
  4. 是数位之和,不是两数之和
class Solution:def movingCount(self, threshold, rows, cols):def dfs(i, j):  # i, j为坐标visited[i][j] = Trueself.res += 1for a, b in ((i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)):if 0 <= a < rows and 0 <= b < cols and not visited[a][b] and self.get_sum(a, b) <= threshold:dfs(a, b)if not rows or not cols or threshold < 0: return 0self.res = 0visited = [[False for _ in range(cols)] for _ in range(rows)]dfs(0, 0)return self.resdef get_sum(self, a, b):"""两数数位之和"""return sum(map(int, str(a) + str(b)))

14 剪绳子

题目:AcWing

给你一根长度为 n 绳子,请把绳子剪成 m 段(m、n 都是整数,2≤n≤58 并 m≥2。每段的绳子的长度记为k[0]、k[1]、……、k[m]。它们可能的最大乘积是多少?

例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。

解析:

解法一:动态规划

  1. f[n] 为长度为 n 的绳子剪成若干段后乘积的最大值

  2. 剪第一刀时,可选1~n-1,n-1中剪法,我们要选取其中的最大值,
    f[n]=max(f[i]×f[n−i]),1&lt;=i&lt;=n/2f[n] = max(f[i] \times f[n-i]), \qquad 1&lt;=i&lt;=n/2 f[n]=max(f[i]×f[n−i]),1<=i<=n/2

注意边界条件

class Solution():def maxProductAfterCutting(self, length):"""动态规划"""if length < 2: return 0   # 长度小于2拆不了if length == 2: return 1  # 长度为2只能拆成1+1if length == 3: return 2  # 长度为3拆成1+2最大f = [-1] * (length + 1) f[0], f[1], f[2], f[3] = 0, 1, 2, 3for i in range(4, length + 1):maxv = 0for j in range(1, i//2 + 1):maxv = max(maxv, f[j] * f[i - j])f[i] = maxvreturn f[length]

解法二:贪婪算法

当 n >= 5 时,尽可能多地剪长度为 3 的绳子;当剩下长度为 4 时,把绳子剪成两段长度为 2 的绳子。

证明:

首先 n >= 5 时,可证 2(n-2) > n 并且 3(n-3) > n。就是说当绳子剩下长度大于或等于5时,3(n-3) >= 2(n-2) ,因此当 n >= 5 时,尽可能多地剪长度为 3 的绳子。

当长度为 4 时,剪成 2 x 2 最大。所以此时就不用拆 3 了。

    def maxProductAfterCutting0(self, length):"""贪婪算法"""if length < 2: return 0if length == 2: return 1if length == 3: return 3# 尽可能的剪去长度为3的绳子,可能余0,1,2number_3 = length // 3# 若最后余1,说明最后可以剩下4,取出4if length - number_3 * 3 == 1:number_3 -= 1# 算出2的个数number_2 = (length - number_3 * 3) // 2return 2 ** number_2 * 3 ** number_3

15 二进制中 1 的个数

题目: leetcode 191

编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数

解析: 位运算

class Solution(object):def NumberOf1(self,n):res = 0for _ in range(32):  # 防止死循环,负数右移补1res += n&1n>>=1return resdef f2(self,n):"""把一个整数减去1,都是把最右边的1变成0,再和原整数做与运算,会把该整数最右边的1变成01100 - 1 =1011 & = 1000"""res = 0while n:res +=1  # 一个不为0的整数至少含有一个1n &= n-1 # 消灭一个 1 return res

16 数值的整数次方

题目: leetcode 50

实现 pow(x, n) ,即计算 x 的 n 次幂函数。

解析: 快速幂
an={an/2⋅an/2n 为偶数a(n−1)/2⋅a(n−1)/2⋅an 为奇数a^n= \begin{cases} a^{n/2} \cdot a^{n/2} &amp; \text {n 为偶数} \\ a^{(n-1)/2} \cdot a^{(n-1)/2} \cdot a &amp; \text {n 为奇数} \end{cases} an={an/2⋅an/2a(n−1)/2⋅a(n−1)/2⋅a​n 为偶数n 为奇数​

class Solution(object):def myPow(self, x, n):if x == 0:return 1res = 1if n < 0: # n<0时 求倒数x = 1 / xn = -nwhile n:if n & 1:  # n为奇数时,先提取一个出来。以及循环停止时,获取答案res *= xx *= xn >>= 1return resdef myPow_1(self, x, n):"""递归"""if x == 0:return 0if n == 0:return 1if n == 1:return xif n < 0:x = 1 / xn =-nres = self.myPow(x, n>>1)res *= resif n & 1:res *= xreturn res

17 打印从 1 到最大的 n 位数

题目:

输入数字 n ,按顺序打印出 1 到 n 的所有整数。

解析:

解法一:字符串模拟加法

  1. 初始化一个全为 0 的 n 位数组

  2. 模拟加法,每次加 1

    • 注意进位

    • 若溢出,返回变量停止循环

  3. 打印数组

    • 注意要从第一个非零字符开始打印
def Print(n):if n <= 0:returnnums = ['0'] * nwhile not Add(nums):print_arr(nums)def Add(nums):"""字符串加一操作"""stop = False  # 溢出的标记carry = 0  # 进位for i in range(len(nums) - 1, -1, -1):sumv = int(nums[i]) + carryif i == len(nums) - 1:  # 从最低位开始加起sumv += 1if sumv >= 10:  # 若大于10,考虑进位if i == 0:  # 若是最高位大于10,停止外层while循环stop = Trueelse:sumv -= 10  # 去除进位carry += 1  # 进为+1nums[i] = str(sumv)else:nums[i] = str(sumv)  # 直到某一位的和小于10,直接赋值并跳出,没有进位breakreturn stopdef print_arr(nums):"""从第一个非零字符开始打印字符串"""flag = Falsefor x in nums:if x != '0':flag = Trueif flag:print(x, end='')print(end=' ')

解法二:全排列

n 位所有十进制数其实是 n 个从 0 到 9 的全排列。也就是把数字的每一位都从 0 到 9 排列一遍。

def Print2(n):"""解法二:数字全排列,递归"""if n <= 0: returnnum = ['0'] * nfor i in range(10):num[0] = str(i)printRecur(num, n, 0)def printRecur(num, n, idx):if idx == n - 1:printArray(num)returnfor i in range(10):num[idx + 1] = str(i)printRecur(num, n, idx + 1)

18 在O(1)时间删除链表节点

题目: leetcode 237

请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。

解析:

将下一个节点的值赋给自身,删除下一个节点

class Solution(object):def deleteNode(self, node):if not node:returnnode.val = node.next.valnode.next = node.next.next

18.2 删除排序链表中重复的节点

题目: 牛客网 leetcode 82

给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。

输入: 1->2->3->3->4->4->5
输出: 1->2->5

解析: 快慢指针

  1. 首先添加一个头节点,以防删除头节点
  2. 设置 slow ,fast 指针, slow 指向当前不重复的节点,fast 往前搜索重复节点。
class Solution:def deleteDuplication(self, head):if not head or not head.next:  # 若head为None,或者只有一个节点return headdummy = ListNode(-1)dummy.next = head  # 虚拟头节点,防止头节点被删除, 为了方便处理边界情况slow = dummyfast = dummy.nextwhile fast:if fast.next and fast.next.val == fast.val: # 如果是重复的节点while fast.next and fast.val == fast.next.val:fast = fast.next  # 指向最后一个重复的节点slow.next = fast.next  # 删除重复链表fast = fast.next  # 工作指针前移else: # 若不是重复的节点slow, fast = fast, fast.next  # 双指针前移return dummy.next

19 正则表达式匹配

题目: leetcode 10 牛客网

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。

‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。

解析:

  1. 当p为空,s不为空时,肯定匹配失败,返回False。
    当p为空,s为空时, 匹配成功,返回True。
    当s为空,p不为空时还是有可能匹配成功。因为 ‘*’ 可以表示前面的字符出现0次
  2. 开始匹配字符,分两种情况:
    2.1. p下一个字符不为 ‘*’,就直接匹配当前字符。若字符相等或p为 ‘.’ 则成功,还要确保s没有遍历完。
    2.2. p下一个字符为 ‘*’,因为 ‘*’ 可以代表0个或者多个字符,考虑多种情况。
    a. 当 ‘*’ 匹配0个字符时,s不变,p往后移两位,即忽略 ‘a*’ 两个字符
    b. 当 ‘*’ 匹配了至少一个字符时,s往前移动一个字符,p不变。这里匹配一个或多个字符可以看作一个情况,因为:当匹配一个时,由于s移到了下一个字符,而p字符不变,就回到了情况 a。当匹配多于一个字符时,相当于从s的下一个字符继续开始匹配。
class Solution:def isMatch(self, s: str, p: str):"""暴力递归,时间复杂度高"""# 情况1if not p: return not s# s, p对应位置是否相等, bool(s)考虑s是否遍历完,遍历完还是有可能匹配成功,即忽略a*的情况first_match = bool(s) and p[0] in {s[0], '.'}if len(p) > 1 and p[1] == '*':  # 情况2.2return (self.isMatch(s, p[2:]) or  # 情况a(first_match and self.isMatch(s[1:], p)))  # 情况belse:  # 情况2.1return first_match and self.isMatch(s[1:], p[1:])def isMatch_1(self, text, pattern):"""记忆化,自顶向下,动态规划基本根据上面翻译过来,加了字典记录出现过的值,避免了重复计算i,j分别表示text, pattern当前的位置"""def dp(i, j):# 如果存在,直接返回答案if (i, j) in memo: return memo[i, j]# 匹配完毕,直接返回答案if j == len(pattern): return i == len(text)# 匹配当前位置first = i < len(text) and pattern[j] in {text[i], '.'}# 若pattern的下一位是 '*',情况2if j + 1 < len(pattern) and pattern[j + 1] == '*':ans = dp(i, j + 2) or (first and dp(i + 1, j))  # 情况a 和 情况belse:ans = first and dp(i + 1, j + 1)  # 情况2.1# 记录答案memo[i, j] = ansreturn ansmemo = {}return dp(0, 0)def isMatch_2(self, text, pattern):"""自底向上,从后往前,动态规划dp[i][j] 表示 text[i:], pattern[j:]是否匹配"""dp = [[False] * (len(pattern) + 1) for _ in range(len(text) + 1)]dp[-1][-1] = True  # 都为空,返回Truefor i in range(len(text), -1, -1):for j in range(len(pattern) - 1, -1, -1):first_match = i < len(text) and pattern[j] in {text[i], '.'}if j + 1 < len(pattern) and pattern[j + 1] == '*':dp[i][j] = dp[i][j + 2] or (first_match and dp[i + 1][j])else:dp[i][j] = first_match and dp[i + 1][j + 1]return dp[0][0]

20 表示数值的字符串

题目: leetcode 65

实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串’+100’, ‘5e2’, ‘-123’, ‘3.14’, ‘-1E-16’, 都表示
数值,但’12e’,‘1a3.14’, 都不是。

[-|+]A[.[B]][e|E[-|+]C].B[e|E[-|+]C]A是整数部分,B是小数部分,C是指数部分。AC前面可以有正负号。A不是必须的。

解析: 考虑各种情况

  1. 先去除两端空格,判断是否为空字符串
  2. 尽可能多的扫描有符号整数
  3. 判断小数点并扫描无符号整数
  4. 判断e,E并扫描有符号整数
  5. 随时保证不越界
class Solution:def isNumber(self, s: str) -> bool:s = s.strip()  # 去除两端空格if not s: return Falseself.i = 0  # 工作指针,指向字符串的当前位置numeric = self.scanInteger(s)# 如果出现‘.’,则接下来是小数部分if self.i < len(s) and s[self.i] == '.':self.i += 1  # 跳过'.'# 下面用or的原因# 1. 小数可以没有整数部分,如 .123 等于 0.123# 2. 小数点后面可以没有数字,如 233. 等于 233.0# 3. 当然,小数点前和后可以都有数字numeric = self.scanUnsignedInteger(s) or numeric  # 这里一定要先扫描小数点后的整数,再用or判断,不然可能会跳过0.8# 如果出现'e'或'E',则接下来是指数部分if self.i < len(s) and s[self.i] in {'e', 'E'}:self.i += 1  # 跳过'e, E'# 下面用and的原因# 1. e或E前面必须有数字,否则不成立# 2. e或E后面必须有整数numeric = numeric and self.scanInteger(s)return numeric and self.i == len(s)  # 若刚好遍历到字符串的下一个位置说明成功,否则说明被其他字符打断了def scanInteger(self, s):"""扫描符号,并继续扫描整数"""if self.i<len(s) and s[self.i] in {'+', '-'}:self.i += 1return self.scanUnsignedInteger(s)def scanUnsignedInteger(self, s):"""扫描无符号整数"""before = self.iwhile self.i < len(s) and '0' <= s[self.i] <= '9':  # 遇到异常字符则停止self.i += 1# 存在若干个数字时,则返回Truereturn self.i > before

21 调整数组顺序使奇数位于偶数前

题目:牛客网

输入一个整数数组,实现一个函数来调整该数组中数字的顺序。

使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。

解析:

解法一:不考虑相对位置

  1. 从前往后找偶数
  2. 从后往前找奇数
  3. 交换两者位置
  4. 随时保证不越界
def reOrderArray_1(self, arr):if not arr: return []l, r = 0, len(arr) - 1while l < r:while l < r and arr[l] & 1:  # 跳过奇数l += 1while l < r and not arr[r] & 1:  # 跳过偶数r -= 1if l < r:arr[l], arr[r] = arr[r], arr[l]  # 交换奇数偶数return arr

解法二:考虑之前的相对位置

用双向队列

从后往前扫描字符串,找到奇数放到队列前面

从前往后扫描字符串,找到偶数放到队列后面

def reOrderArray(self, arr):"""不改变相对位置的算法, O(n)"""if not arr: returnfrom collections import dequeq = deque()n = len(arr)for i in range(n):if not arr[i] & 1:  # 从前找偶数放到后面q.append(arr[i])if arr[n - 1 - i] & 1:  # 从后找奇数放到前面q.appendleft(arr[n - 1 - i])return q

22 链表中倒数第 K 个节点

题目: 牛客网

输入一个链表,输出该链表中倒数第k个结点。

解析: 快慢指针

快指针先走 k 步,快慢指针再一起走,快指针走到 NULL 时,慢指针走到倒数第 k 个节点

注:要判断 k < 链表长度的情况,返回空

class Solution:def FindKthToTail(self, head, k):if not head or not k: returnfast = slow = headfor _ in range(k):if not fast: return  # 若还没有走到k步就已经空了,则说明链表长度小于kfast = fast.nextwhile fast:  # 直到走到尾节点的下一个节点fast, slow = fast.next, slow.nextreturn slow

23 链表中环的入口节点

题目: leetcode 142

给定一个链表,若其中包含环,则输出环的入口节点。若其中不包含环,则输出null

解析:

  1. 设置快慢指针,假如有环,他们最后一定相遇。
  2. 两个指针分别从链表头和相遇点继续出发,最后一定相遇与环入口,tortoise为慢,hare为块。

class Solution(object):def detectCycle(self, head):if not head: returnfast = slow = head# 检测是否有环while fast and fast.next:slow, fast = slow.next, fast.next.nextif slow == fast:breakelse:  # 当fast为空,或者下一个节点为空时则说明没有环return# 找出入口节点while head != slow:  # 从头节点开始往前走 x 步,必定相遇head, slow = head.next, slow.nextreturn head

24 反转链表

题目: leetcode 206

反转一个单链表。

解析:
解法一:循环

  1. 在遍历列表时,将当前节点的next指针改为指向前一个元素prev。
  2. 由于节点没有引用上一个节点,因此必须事先存储其前一个元素。
  3. 在更改引用之前,还需要另一个指针来存储下一个节点next。
  4. 最后返回新的头节点。
class Solution:def reverseList(self, head):prev = None  # 保存当前节点的上一个节点curr = head  # 当前节点while curr:next = curr.next  # 保存当前节点的下一个节点curr.next = prev  # 将当前节点指向上一个节点,即反转prev, curr = curr, next  # 指向上一个节点的指针和工作指针同时向前移动# 最后curr指向空时,prev刚好指向最后一个节点return prev

解法二:递归

递归版本关键在于反向工作。假设列表的其余部分已经被反转,现在我该如何反转它前面的部分?

假设列表为:

假设 nk+1n_{k+1}nk+1​ 到 nmn_mnm​ 已经反转完,而我们正处于 nkn_{k}nk​ 。

我们希望 nk+1n_{k+1}nk+1​ 的下一个节点指向 nkn_knk​ 。

所以 nkn_knk​.next.next = nkn_knk​

要小心的是 n1 的下一个必须指向空,不然可能会产生循环。

def reverseList(self, head):if not head or not head.next:return headp = self.reverseList(head.next)  # 这返回的是尾节点head.next.next = headhead.next = None # n1 的下一个必须指向空return p  # 返回尾节点

25 合并两个有序链表

题目: leetcode 21

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

解析:

非递归:

  • 新建虚拟头节点
  • 依次加入两链表中小的那个节点
class Solution:def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:dummy = cur = ListNode(-1)while l1 and l2:if l1.val < l2.val:cur.next = l1cur, l1 = l1, l1.nextelse:cur.next = l2cur, l2 = l2, l2.nextcur.next = l1 or l2return dummy.next

递归:

递归的定义在两个链表里的 merge 操作

故,每次都是两个链表头部较小的一个剩下的元素的 merge 操作结果合并。

注意边界条件,确保 l1 和 l2 两链表都不为空,否则返回不为空的那个节点。

    def merge1(self, l1: ListNode, l2: ListNode) -> ListNode:"""递归 优雅"""if not l1 or not l2:return l1 or l2if l1.val < l2.val:# 因为l1小,所以l1与剩下的节点mergel1.next = self.merge1(l1.next, l2)return l1else:# 因为l2小,所以l2与剩下的节点mergel2.next = self.merge1(l1, l2.next)return l2

26 树的子结构

题目: leetcode 572

输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)

解析: 递归

  1. 搜索二叉树 A ,判断每个节点开始的子树是否包含树 B
  2. 若不包含,递归搜索树 A 的左右孩子
class Solution:def isSubtree(self, s: TreeNode, t: TreeNode) -> bool:"""搜索"""if not s or not t: return False  # 只要有一个为空则不匹配# 判断当前两节点是否为相同的树,否则沿着s的左右孩子继续搜索return self.isPart(s, t) or self.isSubtree(s.left, t) or self.isSubtree(s.right, t)def isPart(self, p1, p2) -> bool:"""判断是否相等"""# 完全匹配# 当两棵树同时为空,说明匹配成功if not p2 and not p1: return True# 当一个空一个不为空,或者两个值不相等说明匹配失败if (not p1 or not p2) or p1.val != p2.val: return False# 包含即可,不用完全匹配# 当p2为空,说明匹配完成# if not p2: return True  # 若p2搜索完了,则成功# 若p1先搜索完,或者两者的值不相等,失败# if not p1 or p1.val != p2.val: return False# 递归的匹配左右子树return self.isPart(p1.left, p2.left) and self.isPart(p1.right, p2.right)

27 二叉树的镜像

题目: 牛客网

输入一个二叉树,将它变换为它的镜像。

输入树:8/ \6  10/ \ / \5  7 9 11
输出树:8/ \10  6/ \ / \11 9 7  5

解析:

递归:自顶向下递归交换

非递归:使用栈实现dfs,依次交换

class Solution:def Mirror(self, root):"""自顶向下递归交换"""if not root: returnroot.left, root.right = root.right, root.leftself.Mirror(root.left)self.Mirror(root.right)def fun(self, root):"""非递归 利用栈实现dfs"""stack = root and [root]  # 等价于 stack = None if not root else [root]while stack:n = stack.pop()if n:n.left, n.right = n.right, n.left  # 入栈前进行交换stack += n.right, n.left

28 对称的二叉树

题目: leetcode 101

给定一个二叉树,检查它是否是镜像对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1/ \2   2/ \ / \
3  4 4  3

解析:

如果同时满足下面的条件,两个树互为镜像:

  1. 它们的两个根结点具有相同的值。
  2. 每个树的右子树都与另一个树的左子树镜像对称。
class Solution:def isSymmetric(self, root):return self.dfs(root, root)def dfs(self, p1, p2):"""递归遍历两棵树是否镜像"""# 若两棵树同时遍历完,则成功if not p1 and not p2: return True# 若不是同时遍历完,则失败if not p1 or not p2: return False# 两者相等并且p1的左子树和p2的右子树相等,p1的右子树和p2的左子树相等返回Truereturn p1.val == p2.val and self.dfs(p1.left, p2.right) and self.dfs(p1.right, p2.left)def isSymmetric(self, root: TreeNode) -> bool:"""栈,非递归"""if not root: return Trues = [(root.left, root.right)]while s:p1, p2 = s.pop()        if not p1 and not p2: continue  # 可能提前遇到叶子节点,只能跳过,不能返if not p1 or not p2: return Falseif p1.val != p2.val: return Falses += [(p1.left, p2.right), (p1.right, p2.left)]return Truedef isSymmetric(self, root: TreeNode):"""队列"""if not root: return Truefrom collections import dequeq = deque()q.append((root.left, root.right))while q:p1, p2 = q.popleft()if not p1 and not p2: continueif not p1 or not p2: return Falseif p1.val != p2.val: return Falseq += [(p1.left, p2.right), (p1.right, p2.left)]return True

29 顺时针打印矩阵

题目: leetcode 54

给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。

示例 1:

输入:
[[ 1, 2, 3 ],[ 4, 5, 6 ],[ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]

解析:

解法一:模拟

从00开始走,直到碰壁,或者访问过就换方向。走col*row步。

def spiralOrder1(self, matrix):"""正常解法"""res = []if not matrix: return resrow, col = len(matrix), len(matrix[0])  # 长,宽visited = [[False] * col for _ in range(row)]  # 记录访问过的节点direct = [(1, 0), (0, 1), (-1, 0), (0, -1)]  # 右,下,左,上 方向的集合x, y, d = 0, 0, 0  # 起始点, 和方向的坐标  x代表横坐标,y纵坐标for _ in range(row * col):res.append(matrix[y][x]) # 注意第一个是纵坐标 y,第二个是横坐标 xvisited[y][x] = Truea, b = x + direct[d][0], y + direct[d][1]  # 在方向上移动,a,b为下一个点的坐标# 只要碰壁了,或者访问过了,就换方向if a < 0 or a >= col or b < 0 or b >= row or visited[b][a]:d = (d + 1) % 4a, b = x + direct[d][0], y + direct[d][1]  # 在新的方向上移动x, y = a, breturn res

解法二:

先取第一行,删除,逆时针旋转矩阵,再取第一行,直到矩阵为空

def spiralOrder(self, m):res = []while m:res += m[0]  # 取第一行m = list(zip(*m[1:]))[::-1]  # 顺时针旋转return res

30 包含min函数的栈

题目: leetcode 155

设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。

  • push(x) – 将元素 x 推入栈中。
  • pop() – 删除栈顶的元素。
  • top() – 获取栈顶元素
  • getMin() – 检索栈中的最小元素。

解析:

  1. 数据栈。
  2. 辅助栈,与数据栈同push,pop。每次push插入当前元素后的最小值。
class MinStack:def __init__(self):self.s = []   # 数据栈self.m_s = [] # 最小栈,保存数据栈插入元素后的最小值。最后是和数据栈同步的push popdef push(self, x: int) -> None:self.s.append(x)if len(self.m_s) == 0 or x < self.m_s[-1]:self.m_s.append(x)else:self.m_s.append(self.m_s[-1])  # 这里必须要插入,不然后面pop就没了def pop(self) -> None:self.m_s.pop()return self.s.pop()def top(self) -> int:return self.s[-1]def getMin(self) -> int:return self.m_s[-1]

31 栈的压入、弹出序列

题目:leetcode 946

两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。

假设压入栈的所有数字均不相等。

例如:

压入序列{1, 2, 3, 4, 5},序列{4,5,3,2,1}对应该压栈序列的一个弹出序列。而{4,3,5,1,2}不可能是该压栈序列的弹出序列。

解析:

  1. 用一个辅助栈,模拟pushed序列,依次压栈。

  2. 若辅助栈顶元素 == popped序列首元素,两者同时出栈,并接着循环判断是否相等

  3. 最后popped序列为空则说明匹配成功。

class Solution:def validateStackSequences(self, pushed, popped):s = [] # 辅助栈for num in pushed:s.append(num)while s and s[-1] == popped[0]:popped.pop(0)s.pop()return not popped

32 从上往下打印二叉树

题目: 牛客网

从上往下打印出二叉树的每个节点,同层节点从左至右打印。

解析:

使用队列实现层序遍历

class Solution:def PrintFromTopToBottom(self, root):from collections import dequeq = deque([root])res = []while q:node = q.popleft()if node:res.append(node.val)q += [node.left, node.right]return res

32.1 分行从上到下打印二叉树

题目: leetcode 102

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

解析:

层序遍历,记录每层节点 个数

def levelOrder1(self, root: TreeNode) -> List[List[int]]:if not root:return []from collections import dequeres = []q = deque([root])while q:n = len(q)  # 上层的节点数目cur_level = []  # 存储上层节点的临时数组for _ in range(n):node = q.popleft()  # 出队n次,n为上一层的节点数cur_level.append(node.val)if node.left: q.append(node.left)  # 再依次入队if node.right: q.append(node.right)res.append(cur_level)  # 遍历完一层,加入结果集return res

32.2 之字形打印二叉树

题目: leetcode 103

给定一个二叉树,返回其节点值的 之字形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)

解析:

和上题一样,加一个方向变量

class Solution:def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]:if not root: return []from collections import dequeq = deque([root])res = []i = -1  # 方向while q:n = len(q)  # 每层节点的数目cur = []for _ in range(n):node = q.popleft()cur.append(node.val)if node.left: q.append(node.left)  # 这样确保里面都不为空,方便if node.right: q.append(node.right)i = i * -1  # 换方向啦res.append(cur[::i])return res

33 二叉搜索树的后序遍历

题目: 牛客网

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

解析:

后续遍历:[左,右,根],又因为是二叉搜索树,所以 左<根<右

序列最后一个元素是根,可以把序列分割为两部分,第一部分都小于它,第二部分都大于它。

若不满足则返回False,依次递归判断。

class Solution:def VerifySquenceOfBST(self, seq):def dfs(seq):if not seq: return Trueroot = seq[-1] # 根i = 0while seq[i] < root: i += 1 # 先找到左边区域,这里最多遍历到最后一个元素,也就是根for x in seq[i:-1]: # 右边区域若存在,这里可能为空,不满足条件则返回Falseif x < root: return False# 递归判断左右return dfs(seq[:i]) and dfs(seq[i:-1])if not seq: return Falsereturn dfs(seq)

34 二叉树中和为某一值的路径

题目:

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

解析:

前序遍历二叉树,边遍历边加入沿途节点,若遍历到叶子节点且,路径和相等,则加入结果。否则弹出当前节点,回溯。

class Solution:def pathSum(self, root, sumv):def dfs(root, sumv):sumv -= root.valcur.append(root.val)if not root.left and not root.right and sumv == 0:res.append(cur[:])  # 这里一定要加入复制,否则后面会改变这里的值if root.left:dfs(root.left, sumv)if root.right:dfs(root.right, sumv)cur.pop()if not root: return []res = []cur = []dfs(root, sumv)return res

35 复杂链表的复制

题目: leetcode 138

给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。

要求返回这个链表的深拷贝

解析:

解法一:

  • 用一个哈希表存储所有节点和对应的复制,需要O(n) 的辅助空间

  • 遍历哈希表,链接新链表

class Solution:def copyRandomList(self, head: 'Node') -> 'Node':m = n = headcp = {None:None}  # 1. 不用判断head是否为None,2. 不用管节点的random和next是否为None, 省去很多判断# copywhile m:cp[m] = Node(m.val, None, None)m = m.nextwhile n:cp[n].next = cp[n.next]cp[n].random = cp[n.random]n = n.nextreturn cp[head]

解法二:

原地修改,三步法,O(1) 的空间

  • 根据原始链表的每个节点 N 创建对应的 N‘ ,把 N’ 直接链接到 N 后面
  • 对每个 N‘ ,复制其原始节点的随机指针
  • 将长链表拆成两个链表。依次将每个节点指向下一个的下一个
def copyRandomList_1(self, head):"""O(n) 无需额外空间"""if not head: return headp1 = p2 = p3 = head# while p1:p1.next = Node(p1.val, p1.next, None)p1 = p1.next.nextwhile p2:if p2.random:p2.next.random = p2.random.nextp2 = p2.next.nextnew_head = p3.nextwhile p3.next:  # p3.next存在,也就新链表的节点存在!p3.next, p3 = p3.next.next, p3.next  # 注意这里蛛节点依次变换指针,分别指向下一个的下一个节点return new_head

36 二叉搜索树与双向链表

题目: 牛客网

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。

要求不能创建任何新的结点,只能调整树中结点指针的指向。

注意

  • 需要返回双向链表最左侧的节点。

解析:

解法一:

  • 二叉搜索树,左<中<右,故中序遍历的序列为有序的
  • 根据有序序列建立双向链表
def Convert(self, root):def inOrder(root):if not root: returninOrder(root.left)res.append(root)inOrder(root.right)if not root: returnres = []inOrder(root)for i in range(len(res)- 1):res[i].right = res[i + 1]res[i+1].left = res[i]return res[0]

解法二:

  • 中序遍历,记录一个pre的全局指针,指向上一个节点
  • 递归修改结点指针
def Convert_1(self, root):"""中序遍历,递归"""def dfs(root):if not root: returndfs(root.left)root.left = self.preif self.pre:self.pre.right = rootself.pre = rootdfs(root.right)if not root: returnself.pre = None  # 这里要用全局变量记录pre节点指针,因为pre一直在变。# 因为普通传参递归层数高的不会传给层数低,这个引用可以让递归层数高的改变的pre作用到层数低的函数中。dfs(root)while root.left:root = root.leftreturn root

37 序列化二叉树

题目: leetcode 297

你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

解析:

  • 前序遍历编码字符串
  • 前序遍历解码为二叉树
class Codec:def serialize(self, root):if not root: return '$'return str(root.val) + ',' + self.serialize(root.left) + ',' + self.serialize(root.right)def deserialize(self, data):def dfs(nodes):val = next(nodes)# 不用判断nodes是否迭代完,因为最后肯定是$,会返回的if val == '$': return None root = TreeNode(val)root.left = dfs(nodes)root.right = dfs(nodes)return rootnodes = iter(data.split(','))root = dfs(nodes)return root

38 字符串的排列

题目: leetcode 46

给定一个没有重复数字的序列,返回其所有可能的全排列。

解析: 全排列

回溯法:

是一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认 不是 一个解的话(或者至少不是 最后一个 解),回溯算法会通过在上一步进行一些变化抛弃该解,即 回溯 并且再次尝

  • 数组可以分为第一个字符和后面所有字符两部分。记录一个指针 first 指向当前数组的第一个字符
  • 第一个字符与所有的字符进行交换(包括自己)。
  • 递归的对第二部分进行全排列
  • 回溯,还原数组,才能进行下一次交换。
  • 如果 first 超过了数组元素,说明排列完成,生成一个解
def permute(self, nums):def dfs(first):  # first 记录每次开始的第一个位置if first == n: # 当起始位置超越数组长度了,停止,加入结果集res.append(nums[:])  # 这里要加入其复制,不然传的是引用,会被后面修改for i in range(first, n):  # 第一个数字与后面所有数字依次交换nums[first], nums[i] = nums[i], nums[first]dfs(first + 1)  # 递归替换下一个位置nums[first], nums[i] = nums[i], nums[first]  # 回溯,还原,才能进行下一次交换!n = len(nums)res = []dfs(0)return res

38.2 字符串的所有组合

题目: leetcode 78

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

返回的结果包括空列表。

解析: 子集

解法一:动态规划,递推

dp[i] = dp[i-1] + [each+[nums[i]] for each in dp[i-1]]

  1. 将第一个字符丢到结果集中
  2. 将第二个字符丢到结果中,并将第二个字符与结果中所有元素的组合丢到结果中
  3. 依次类推直到遍历完字符串
def subsets_1(self, nums: List[int]) -> List[List[int]]:res = [[]]for x in nums:res += [each + [x] for each in res] # res中有一个空列表,与其结合就是本身return res

解法二:二进制位 掩码

数组的每个元素,可以有两个状态:

1、不在子数组中(用 0 表示);
2、在子数组中(用 1 表示)。

从 0 到 2 的数组个数次幂(不包括)的整数的二进制表示就能表示所有状态的组合。

def subsets_3(self, nums: List[int]) -> List[List[int]]:"""位运算共有n个字符,对应n位bit,总共有2**n种排列,哪一位为1则将对应位置的字符加入到子集中"""if not nums: return []res = []for i in range(2**len(nums)): sub = []for j in range(len(nums)):if i >> j & 1:sub.append(nums[j])res.append(sub)

解法三:回溯,递归

依次加入,走到底了就回溯。

[1, 2, 3]
1, 12, 123, 13, 2, 23, 3

class Solution: def subsets_3(self, nums):def dfs(first): # first 指向当前第一个元素res.append(sub[:])  # 复制并加入结果集if first == len(nums):  # 若first走完了数组,返回return# 这里与全排列相比,不用交换数字,即数组的顺序是固定的。只需要依次加入元素就可以。for i in range(first, len(nums)):  # 从当前位置到末尾依次遍历sub.append(nums[i])  # 加入当前元素dfs(i + 1)  # 从下一个位置开始递归sub.pop()  # 回溯,去除sub中的元素,为了下一次遍历res, sub = [], []dfs(0)return res

38.3 八皇后问题

题目: leetcode 51

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

即任意两个棋子不在同一行,同一列,同一条对角线。

解析:

解法一:回溯

判断该棋子是否在已有棋子的列上,主对角线,次对角线上

  • 对于所有的主对角线有 行号 + 列号 = 常数,且常数刚好为[0,1,2,3,…,2n-1]
  • 对于所有的次对角线有 行号 - 列号 = 常数. 且常数刚好为[0,1,2,3,…,-2,-1] 也是2n-1个

用一个一维数组queens保存棋子,索引表示行号,值表示列号。

回溯步骤:

  • 从第一行第一列开始遍历。循环列并且试图在每个 column 中放置皇后.
  • 若满足要求则放置棋子。
  • 停止条件:若棋子数目等于n,满足要求加入结果。
  • 回溯,拿掉棋子,恢复状态。

注意 :若把需要回溯的状态放入递归函数的变量可自动回溯,如下面第二个函数

我们如果把 queens, xy_dif, xy_sum 三个状态的变化全部放入递归函数的变量中,则不需要我们手动回溯了,递归时会自动还原上一次的变量。

class Solution_2:"""回溯法"""def solveNQueens(self, n: int):def backtrack():row = len(queens)  # 下标为行的索引if row == n:  # 若已有n个元素则满足要求output.append(queens[:])for col in range(n):  # 从第一行第一列开始遍历if col not in queens and xy_dif[row - col] and xy_sum[row + col]:  # 判断是否能放置棋子queens.append(col)  # 放置棋子xy_dif[row - col], xy_sum[row + col] = False, Falsebacktrack()  # 向下一行探索queens.pop()  # 回溯,挪开棋子xy_dif[row - col], xy_sum[row + col] = True, Truequeens = []xy_dif = [True] * (2 * n - 1)xy_sum = [True] * (2 * n - 1)output = []  # 所有的结果集backtrack()  # 开始回溯return [["." * i + "Q" + "." * (n - i - 1) for i in sol] for sol in output]def solveNQueens_2(self, n):"""dfs,把需要回溯的状态放入递归函数的变量可自动回溯。"""def dfs(queens, xy_dif, xy_sum):row = len(queens)  # 下标为行的索引if row == n:result.append(queens)returnfor col in range(n):  # q为列的索引if not(col in queens or row - col in xy_dif or row + col in xy_sum):dfs(queens + [col], xy_dif + [row - col], xy_sum + [row + col])result = []dfs([], [], [])return [["." * i + "Q" + "." * (n - i - 1) for i in sol] for sol in result]

解法二:暴搜,慢

  1. 全排列出所有的组合。0~n-1
  2. 依次判断每个组合是否符合要求。
class Solution_1:"""全排列出所有顶点组合,[0,1,2,3,4,5,6,7],索引代表行号,值代表列号,所以全排列的结果已满足行列关系。只需判断是否符合对角线的要求即可, 即两个皇后的横坐标和纵坐标的差值的绝对值是否相等。"""def solveNQueens(self, alist):allAns = self.Permutation(alist)res = []for tempList in allAns:if self.Judge(tempList):res.append(tempList)print(tempList)return [["."*i + "Q" + "."*(n-i-1) for i in sol] for sol in res]def Permutation(self, pointArr):def perm(nums, p, q):  # p为一个位置的坐标,q为最后一个位置# 当所有字母用完的时候if p == q:res.append(nums[:])  # 这里要加入其复制,不然传的是引用,会被后面修改return  # 遍历完,结束返回for i in range(p, q):nums[p], nums[i] = nums[i], nums[p]  # 第一个数字与后面所有数字依次交换perm(nums, p + 1, q)  # 第一个数字后面的部分继续做全排列nums[p], nums[i] = nums[i], nums[p]  # 回溯,交换回来res = []perm(pointArr, 0, len(pointArr))return resdef Judge(self, alist): # 判断一个数组是否符合要求length = len(alist)for i in range(length-1):for j in range(i+1, length):if abs(i - j) == abs(alist[i] - alist[j]):  # 绝对值表示正负对角线return Falsereturn True

39 数组中出现次数超过一半的数字

题目: leetocode 168

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

解析:

解法一:摩尔投票法

如果我们把众数记为 +1 ,把其他数记为 −1 ,将它们全部加起来,显然和大于 0 ,从结果本身我们可以看出众数比其他数多。

我们维护一个计数器,如果遇到一个我们目前的侯选众数,就将计数器加一,否则减一。只要计算器等于0,我们就将 nums 中之前访问的数字全部忘记,并把下一个数字当作侯选的众数。

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

解法二:先排序,后取中位数

def majorityElement(self, nums: List[int]) -> int:"""Time: O(nlogn), Space: O(n)将数组排序后,出现次数大于一半的数一定在数组的中间"""nums.sort()return nums[len(nums) // 2]

40 最小的 k 个数

题目: leetcode 215 leetcode是求最大的第K个数,思想一样

在未排序的数组中找到第 k 个最小的元素。

解析:

解法一:堆 Time: O(nlogk), Space: O(k)

我们需要维护容量为k的容器,每次插入新元素,删除最大的元素。

所以我们可以维护一个大根堆,并保持堆的大小小于等于 k.

class Solution:def GetLeastNumbers(self, tinput, k):"""大根堆,O(nlogk) O(k)创建一个大顶堆,将所有数组中的元素加入堆中,并保持堆的大小小于等于 k"""if k > len(tinput) or k < 0: return []import heapq  # python 默认小根堆,所以构建大根堆时进堆和出堆的时候都要加负号heap = []# heapq.heapify(heap)for num in tinput:heapq.heappush(heap, -num)if len(heap) > k:heapq.heappop(heap)return sorted(-x for x in heap)

解法二:快速排序 O(n),O(logn)

因为快速排序每次都可以确定一个元素的最终位置 idx,idx 左边的所有元素都小于它。所以我们判断 idx 和 k 的位置,来选择往左或者往右继续搜索。

而在这里,由于知道要找的第 k 小的元素在哪部分中,我们不需要对两部分都做处理,这样就将平均时间复杂度下降到 O(N)。

class Solution:def GetLeastNumbers_Solution(self, nums, k):def partition(l, r):from random import randinti = randint(l ,r)nums[l], nums[i] = nums[i], nums[l]pivot = nums[l]while l < r:while l < r and nums[r] >= pivot:r -= 1nums[l] = nums[r]while l < r and nums[l] <= pivot:l += 1nums[r] = nums[l]nums[l] = pivotreturn ldef find(l, r):idx = partition(l, r)if idx == k -1 : return nums[idx]elif idx < k -1 : return find(idx + 1, r)else: return find(l, idx - 1)if not nums or k <= 0 or k > len(nums): return []idx = find(0, len(nums) - 1)return sorted(nums[:idx])

解法三:K次冒泡 O(kn),O(1)

    def GetLeastNumbers_Solution(self, nums, k):if k > len(nums) or k <= 0: return []for i in range(k):for j in range(len(nums)-i-1):if nums[j] < nums[j+1]:nums[j], nums[j+1] = nums[j+1], nums[j]return nums[-k:][::-1]

41 数据流中的中位数

题目: leetcode 295

数据流中的数是流动的,如何找到一个数据流中的中位数。

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

解析:

由于数据是从一个数据流中读出来的,所以数据的数目随着时间的变化而增加。如果用一个数据容器来保存从流中读出来的数据,则当有新的数据出来时就需要插入数据容器中。这个数据容器用什么定义最合适呢?

因此我们想找到一种相当快速的方法来插入容器,那么所产生的额外操作可能会减少。

并且我们想找到中位数,并不一定要排序,只需保证前一半的元素都小于这个数,后一半的元素都大于这个数

所以我们需要这样一种数据结构:

  • 快速插入
  • 快速访问最大最小值。

事实证明,有两种数据结构符合:

  1. 堆(或优先级队列)
  2. 自平衡二进制搜索树

堆是这道题的天然原料!向元素添加元素需要对数时间复杂度。它们还可以直接访问组中的最大/最小元素。

我们可以这样维护两个堆:

  1. 用于存储输入数字中较小一半的最大堆
  2. 用于存储输入数字中较大一半的最小堆

其次我们要保证两个堆是平衡的,一个堆的长度最多比另外一个多1。

插入

  • 方法一:

    • 把数字插入下面的最大堆。(这一操作会导致最大堆的长度总大于最小堆)
    • 若此时最大堆的堆顶元素大于最小堆的堆顶,则各自堆顶元素取出插入对方中。这样保证了下面的最大堆的所有元素都小于上面的最小的所有元素。
    • 若最大堆的长度比最小堆+1还大,就取出最大堆的堆顶放入最小堆中,保证两个堆的平衡!
  • 方法二:

    • 先将元素插入最小堆中,取堆顶元素再插入最大堆中。这就可以保证最大堆中的元素总比最小堆的小。
    • 保证两堆的平衡

取中位数:

  • 若两个堆长度相等,则返回两个堆顶元素和的一半
  • 若不相等,此时总长度为奇数,且最大堆最多比最小堆多一个元素,所有此时的中位数就是最大堆的堆顶元素!

注意python自带的只有最小堆。

时间复杂度:O(5*logn) + O(1) = O(logn)

  • 方法二最坏有五次push/pop操作,和一次取值操作

空间复杂度:O(n)

  • 用于在容器中保存输入的线性空间
import heapq as hq
class MedianFinder:def __init__(self):self.min_heap = []  # 存储上半部分数self.max_heap = []  # 存储下半部分数def addNum(self, num: int) -> None:hq.heappush(self.max_heap, -num)  # 因为新元素总是插入到max_heap, 所以max_heap的长度总会大于min_heapif self.min_heap and -self.max_heap[0] > self.min_heap[0]:maxv = -hq.heappop(self.max_heap)minv = hq.heappop(self.min_heap)hq.heappush(self.min_heap, maxv)hq.heappush(self.max_heap, -minv)if len(self.max_heap) > len(self.min_heap) + 1:  # 保证两个堆的平衡,maxheap长度不能超过minheap长度+1hq.heappush(self.min_heap, -hq.heappop(self.max_heap))def addNum_2(self, num):"""1. 先将元素插入最小堆中,取堆顶元素再插入最大堆中。这就可以保证最大堆中的元素总比最小堆的小。3. 保证两个堆的平衡,maxheap长度不能超过minheap长度+1"""hq.heappush(self.min_heap, num)hq.heappush(self.max_heap, -hq.heappop(self.min_heap)) if len(self.max_heap) > len(self.min_heap) + 1:hq.heappush(self.min_heap, -hq.heappop(self.max_heap))def findMedian(self) -> float:if len(self.min_heap) == len(self.max_heap):  # 如果两个堆平衡return (-self.max_heap[0] + self.min_heap[0]) / 2.else:return -self.max_heap[0]

42 连续子数组的最大和

题目: leetcode 53

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

解析: 动态规划

用函数 f(i)f(i)f(i) 表示以第 i 个数字结尾的字数组的最大和,那么我们需要求出 max[f(i)]max[f(i)]max[f(i)] 。

递归公式:

KaTeX parse error: No such environment: equation at position 16: f(i) = \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ \begin{cases} …

  • 当第 i - 1 个数字结尾的字数组中所有数字的和小于0时,如果把这个负数与第 i 个数相加,结果会比第 i 个数本身还要小。所以这种情况下,以第 i 个数字结尾的字数组就是本身。
  • 当第 i - 1 个数字结尾的字数组中所有数字的和大于0时,则与第 i 个数字累加就得到以第 i 个数字结尾的字数组中所有数字的和。

时间复杂度:O(n)

空间复杂度:O(n),因为存储了n个状态 f

优化空间复杂度,用一个变量更新 f(i−1)f(i - 1)f(i−1) ,一个变量记录最大的连续子数组和。

class Solution:def maxSubArray(self, nums: List[int]) -> int:if not nums: return 0f = [nums[0]]for i in range(1, len(nums)):f.append(nums[i]+f[i-1] if f[i-1]>0 else nums[i])return max(f)def maxSubArray_2(self, nums: List[int]) -> int:# 优化空间复杂度,用滚动变量代替状态表示res = f = nums[0]for i in range(1, len(nums)):f = nums[i] if f < 0 else f + nums[i]res = max(res, f)return res

43 1 ~ n 整数中 1 出现的次数

题目: leetcode 233

给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。

示例:

输入: 13
输出: 6
解释: 数字 1 出现在以下数字中: 1, 10, 11, 12, 13 。

解析: 归纳法 牛客网解析

设i为计算1所在数字的位数,i=1表示计算个位数的1的数目,10表示计算十位数的1的个数等。

一个数按位数进行分割,可以分为两部分,高位 n/(i*10),低位 n%(i*10)

当求数字第 i 位1的个数时,

  • 高位部分每个数包含 i 个 1,例如十位上的1,10~19,有十个
  • 低位部分判断是否在[i, i*2-1]范围内,最多i个1,最少0个1。例如十位上的1出现的范围在[10, 19],最多有10个,最少有 0 个。取决于 低位部分 - i + 1的值。
  • 可以直接判断 (低位部分-i+1) 和 i 取最小值,同时保证大于0

所以得到公式:高位 1 的个数 + 低位 1 的个数

i 位上 1 的个数:n // (i * 10) * i + min(max(n % (i * 10) - i + 1, 0), i)

class Solution:def countDigitOne(self, n):cnt, i = 0, 1while i <= n: # i 依次个十百位的算,直到大于 n 为止。cnt += n // (i * 10) * i + min(max(n % (i * 10) - i + 1, 0), i)i *= 10return cnt

44 数字序列中某一位的数字

题目:leetcode 400

在无限的整数序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …中找到第 n 个数字。

注意:此处序列是从1开始的。

解析: 解析法

分析得到:

  • 个位数有9个数,共9位
  • 百位数有90个数,共180位
  • 千位数有900个数,共2700位

所以解题步骤是:

  1. 确定是几位数(个十百位数),1代表个位数,2代表十位数,3代表百位数…
  2. 确定是几位数的第几个数(如百位数的第3个数,即 102)
  3. 确定是这个数的第几位(如102的第2位,即结果为0)

注意:上面的分析是基于数列从1开始的,若序列是从0开始的,可以事先将n+1,但是因为个位从0开始,导致有10个数,与后面不匹配。所以可以将0去掉,n又要-1,即抵消了。

class Solution:def findNthDigit_1(self, n: int) -> int:i = 1  # 位数,进制s = 9  # 表示i位数有多少个数# 确定是几位数(i),以及是几位数的第多少位(n)while n > i * s:n -= i * s  # 总位数减去第i位的位数s *= 10  # 下一位的位数i += 1  # 进制加1# 确定是哪个数, i位数的第一个数为 10**(i-1)number = 10**(i-1) + math.ceil(n/i) - 1# 确定是这个数的第几位idx = n%i if n%i else ireturn int(str(number)[idx - 1])

45 把数组排成最小的数

题目: 牛客网 Acwing

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

例如输入数组[3, 32, 321],则打印出这3个数字能排成的最小数字321323。

解析: 定义排序规则,Time:O(nlogn)

定义 ab < ba 时,a < b
用此规则进行排序,从小到大。拼接出来就是最小的数字。

注意python2和python3自定义排序规则的区别。python3要用cmp_to_key将cmp转化为key。

  1. 写一个cmp函数传入key
  2. 直接写个 lambda a, b: (a+b > b+a) - (a+b < b+a)

解释两者为什么等价

cmp(a, b):

  1. 当a>b, 返回 1
  2. 当a=b, 返回 0
  3. 当a<b, 返回 -1

所以等价于如下:
cmp(a, b) <==> (a>b) - (a<b)

(a>b) - (a<b):

  1. 当a>b, 返回 1 - 0 = 1
  2. 当a=b, 返回 0 - 0 = 0
  3. 当a<b, 返回 0 - 1 = -1 故等价
from functools import cmp_to_keyclass Solution:def PrintMinNumber(self, numbers):# python 2# if not numbers:#     return ""# nums = map(str, numbers)# nums.sort(lambda a, b: cmp(a+b, b+a))# return int(''.join(nums))# python3if not numbers:return ""nums = list(map(str, numbers))nums.sort(key=cmp_to_key(self.cmp))# nums.sort(key=cmp_to_key(lambda a, b: (a+b > b+a) - (a+b < b+a)))return int(''.join(nums))def cmp(self, a, b):s1 = a + bs2 = b + aif s1 > s2: return 1elif s1 < s2: return -1else: return 0

46 把数字翻译成字符串

题目: leetcode 91 Acwing(原书)

给定一个数字,我们按照如下规则把它翻译为字符串:

0翻译成”a”,1翻译成”b”,……,11翻译成”l”,……,25翻译成”z”。

一个数字可能有多个翻译。例如12258有5种不同的翻译,它们分别是”bccfi”、”bwfi”、”bczi”、”mcfi”和”mzi”。

注意:

原书上是从0开始表示。注意leetcode的上是从1开始表示,这会导致0无法单独存在表示为一个字符,前面必须要是1或者2。

解析: 动态规划

原书的题。从0开始表示。范围为‘0’ ~ ‘25’

  1. 状态表示

    f[i] 表示前 i 位数字有多少不同的表示方式,求 f[n]

  2. 状态转移方程

    if i 位和 i - 1 位拼起来可以翻译成一个字母时,此时有两种方案,一、i位翻译成一个字母,方案数为 f[i - 1],二、i 和 i - 1位拼接翻译为一个字母,此时的方案数为 f[i - 2],所以公式为

    f[ i ] = f[i - 1] + f[i - 2]

    else:

    f[ i ] = f[i - 1]

  3. 边界

    f[0] = 1 # 定义一个数字没有的时候方案数是1

    f[1] = 1 # 前一个数字只有1种翻译方式

class Solution:def numDecodings(self, s: str) -> int:n = len(s)f = [0] * (n + 1)  # f[i]表示前i位数字有多少种不同的表示方式,求f[n],故共有n+1个元素f[0], f[1] = 1, 1for i in range(2, n + 1): # 2~n 前n个数字# 此时要得到第 i-1 个和第 i 个数字,对应字符串下标 i-2 和 i-1if '10' <= s[i - 2:i] <= '25':f[i] = f[i - 1] + f[i - 2]else:f[i] = f[i - 1]return f[n]

若是leetcode上的题,从1开始表示字母,这会导致0无法单独存在表示为一个字符,前面必须要是1或者2。

范围为‘1’ ~ ‘26’

  • 如果当前位为0:

    • 如果前一位不是1或2,则这个0无法和任何数组成字母,代表整个串无法构成编码,直接返回0
    • 如果前一位是1或者2,则说明前一位和当前位能组成字母,这时候能构成的编码数目是和 i-2 位相同的,即:dp[i] = dp[i-2]
  • 如果当前位不为0:
    • 如果前一位和当前位能组成字母,则当前位的构成编码数目应该为前一位和前前位之和,即:dp[i] = dp[i-1] + dp[i-2]
    • 如果前一位和当前位不能组成字母,则当前位单独组成字母,即dp[i] = dp[i-1]

还要注意0不能是字符串的开头!

if not s or s[0] == '0':return 0
n = len(s)
f = [0] * (n + 1)
f[0], f[1] = 1, 1
for i in range(2, len(s)+1):if s[i-1] == '0':if s[i-2] in {'1', '2'}:f[i] = f[i - 2]else:return 0else:if '10' <= s[i - 2:i] <= '26':f[i] = f[i - 1] + f[i - 2]else:f[i] = f[i - 1]
return f[n]

47 礼物的最大价值

题目: Acwing 相似题 leetcode 62

在一个m×n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。

你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格直到到达棋盘的右下角。

给定一个棋盘及其上面的礼物,请计算你最多能拿到多少价值的礼物?

m,n>0

样例:

输入:
[[2,3,1],
[1,7,1],
[4,6,1]]

输出:19

解释:沿着路径 2→3→7→6→1 可以得到拿到最大价值礼物。

解析: 动态规划

  1. 状态表示:
    f[i, j] 为走到i, j这个格子的最大价值,所以我们要求的是 f[n, m]
  2. 状态转移方程:
    当前格子的最大价值等于转移过来的两个格子最大的那个的价值加上当前格子自身的礼物价值。
    而之前转移过来的两个格子只能在当前格子的上方和左方。
    f[i, j] = max(f[i-1,j], f[i,j-1]) + gift[i,j]
  3. 边界
    f[i, 0] = f[0, j] = 0,将边界全部置为0即可

注意:下标从1开始算,不用处理边界问题。f[i - 1]不会越界。

class Solution(object):def getMaxValue(self, grid):if not grid: returnrows, cols = len(grid), len(grid[0])f = [[0] * (cols+1) for _ in range(rows+1)]  # 从1开始,所以多一个for i in range(1, rows+1): # 从1开始,不用判断边界for j in range(1, cols+1):f[i][j] = max(f[i - 1][j], f[i][j - 1]) + grid[i-1][j-1]return f[rows][cols]

此方法需要 O(rows*cols) 空间,可以优化到只需要 O(cols) 空间,即一行保存所有状态。
f[j] 之前的值为这一行的状态 f[i, j-1],f[j] 从自身到后面的值为上一行的状态 f[i-1][j],
就相当于状态是一行一行更新的,每次只需保存此行和上一行的状态。

def getMaxValue_2(self, grid):"""优化空间O(n)"""rows, cols = len(grid), len(grid[0])f = [0] * (cols+1)for i in range(1, rows+1):for j in range(1, cols+1):f[j] = max(f[j], f[j - 1]) + grid[i-1][j-1]return f[cols]

48 最长不含重复字符的子字符串

题目: leetcode 3

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

假设字符串中只包含从’a’到’z’的字符。

解析:

双指针算法,滑动窗口
l, r两个指针看作滑动窗口两端,窗口的长度为 r - l + 1
当窗口内出现重复字符时,移除左边元素直到删除重复元素,继续移动窗口右端。

class Solution:def lengthOfLongestSubstring(self, s: str) -> int:"""滑窗法"""if not s: return 0l = 0mem = set()max_len = 0for r in range(len(s)):while s[r] in mem:mem.remove(s[l])l += 1max_len = max(r - l + 1, max_len)mem.add(s[r])return max_len

优化的滑动窗口
上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。
我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。
当我们找到重复的字符时,我们可以立即跳过该窗口, 不需要逐渐增加 l。

def lengthOfLongestSubstring_2(self, s: str) -> int:maxlen, l = 0, 0pos = {}  # 记录每个出现字母的下标for r, ch in enumerate(s):if ch in pos:# 如果重复,则起始点为重复点的下一位开始l = max(pos[ch] + 1, l)  #这里max指l不能后退,有可能此时重复的数字比起始点l还要前pos[ch] = rmaxlen = max(maxlen, r-l+1)return maxlen

49 丑数

题目: leetcode 264

编写一个程序,找出第 n 个丑数。丑数就是只包含质因数 2, 3, 5正整数。习惯上把 1 当作第一个丑数。

解析: 牛客网

一个丑数一定是由另一个丑数乘以2或者乘以3或者乘以5得到。即丑数为 p=2x∗3y∗5zp = 2 ^ x * 3 ^ y * 5 ^ zp=2x∗3y∗5z ,那么我们可以维护三个队列,乘以2的队列,乘以3的队列,乘以5的队列,每次三个队列头中的最小值即为下一个丑数。我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步

class Solution:def nthUglyNumber(self, n: int) -> int:if not n:return# p2,p3,p5分别为三个队列的指针,p2, p3, p5 = 0, 0, 0res = [1]for _ in range(n-1):# min_num 为三个队列头选出来的最小数min_num = min(res[p2]*2, res[p3]*3, res[p5]*5)res.append(min_num)# 这三个if有可能进入一个或者多个,进入多个是三个队列头最小的数有重复的情况if res[p2]*2 == min_num:p2 += 1if res[p3]*3 == min_num:p3 += 1if res[p5]*5 == min_num:p5 += 1return res[-1]

50.1 第一个只出现一次的字符

题目:leetcode 387

给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

解析:

  • 循环一遍字符串,用一个字典记录每个字符出现的次数。
  • 再次循环一遍字符串,一次判断每个字符出现的次数是否为 1
class Solution:def firstUniqChar(self, s: str) -> int:if not s:return -1count = {}for x in s:if x not in count:count[x] = 1else:count[x] += 1for key in count:if count[key] == 1:return s.index(key)return -1

50.2 字符流中第一个只出现一次的字符

题目: 牛客网

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

解析:

  • 利用一个int型数组表示256个字符,这个数组初值置为-1.

  • 每读出一个字符,将该字符的位置存入字符对应数组下标中。

  • 若值为-1标识第一次读入,不为-1且>0表示不是第一次读入,将值改为-2.

  • 之后在数组中找到>0的最小值,该数组下标对应的字符为所求

class Solution_2:def __init__(self):self.count = [-1] * 256  # -1 代表一次都没出现过,索引代表字符的ASCII码值self.index = 0  # 记录当前字符的个数,可以理解为输入的字符串中的下标def FirstAppearingOnce(self):min_value = self.index  # base,当前的最小值ch = '#'# 找出最小的索引for i in range(256):# 在索引大于0的地方寻找,比当前最小值的还小的话,更新当前的最小值索引,更新索引对应的字符if self.count[i] >= 0 and self.count[i] < min_value:min_value = self.count[i]ch = chr(i)return chdef Insert(self, char):# 如果是第一次出现,则将对应元素的值改为indexif self.count[ord(char)] == -1:self.count[ord(char)] = self.index# 如果已经出现过一次,则修改为-2elif self.count[ord(char)] >= 0:self.count[ord(char)] = -2self.index += 1

51 数组中的逆序对

题目: 牛客网 Acwing

在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。

输入一个数组,求出这个数组中的逆序对的总数。

解析: 分治法

其实就是归并排序。

假设分隔到最小的两个数组,p1指向 left 数组中的元素,p2指向 right数组中的元素,此时两数组内部是有序的。

  • 当left[p1] 大于 right[p2] 时,left中p1后面的所有元素(包括p1)都大于right[p2]。所以此时逆序对数 += len(left[p1:])
  • 当left[p1] 小于 right[p2] 时,正常合并即可。
class Solution:def inversePairs(self, nums):"""分治法:O(nlogn) Space: O(n)"""self.count = 0self.merge_sort(nums)return self.countdef merge_sort(self, nums):if len(nums) <= 1:return numsmid = len(nums) >> 1left = self.merge_sort(nums[:mid])right = self.merge_sort(nums[mid:])l, r = 0, 0  # 分别代表左右数组的指针tmp = []  # 存储排序数字的临时数组# 合并两数组到临时数组。while l < len(left) and r < len(right):if left[l] <= right[r]:tmp.append(left[l])l += 1else:tmp.append(right[r])r += 1# 逆序对数self.count += len(left[l:])tmp += left[l:] + right[r:]return tmp

52 两个链表的第一个公共节点

题目: leetcode 160

编写一个程序,找到两个单链表相交的起始节点。

如链表A和链表B。两链表在节点 c1 处相交。

解析:

解法一:哈希表法

用一个哈希表存储链表A的所有节点,然后检查链表 B 中的每一个结点 b_i 是否在哈希表中。若在,则 b_i 为相交结点。

  • 时间复杂度 : O(m+n)
  • 空间复杂度 : O(m)或O(n)
class Solution(object):def getIntersectionNode(self, headA, headB):dic = set()p1 = headAwhile p1:dic.add(p1)p1 = p1.nextwhile headB:if headB in dic:return headBheadB = headB.next

解法二:双指针法

消除长度差: 拼接链表A和链表B。p1,p2分别从两个链表的头部开始遍历,当走到尾节点时,则从另外一个链表的头部继续往前走。这样,若存在相交的点,p1,p2必会在一个点相遇。

class Solution(object):def getIntersectionNode(self, headA, headB):if not headA or not headB:returnq, p = headA, headBwhile q != p:q = q.next if q else headBp = p.next if p else headAreturn q

53.1 数字在排序数组中出现的次数

题目: leetcode 34

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

解析: 二分法

因为有序查找,所以二分。O(logn)

第一次二分找到target的第一个位置,第二次二分找到target的最后一个位置。

class Solution(object):def searchRange(self, nums, target):if not nums: return -1, -1l, r = 0, len(nums) - 1  # 第一次二分查找,找到重复数字最左边的索引while l < r:mid = l + (r - l) // 2if nums[mid] < target: # 说明答案在右边,不包含midl = mid + 1else:r = midif nums[l] != target: return -1, -1  # 如果没有找到这个数,则说明不存在left = l  # 重复数字最左边的索引l, r = 0, len(nums) - 1  # 第二次二分查找,找到重复数字最右边的索引while l < r:mid = l + (r - l + 1) // 2if nums[mid] <= target: # 说明答案在右边,包含mid,故上面加1l = midelse:r = mid - 1return left, rdef searchRange_2(self, nums, target):"""暴力法, 一次循环"""res = []for i, x in enumerate(nums):if x == target:res.append(i)if not res: return -1, -1return res[0], res[-1]

53.2 0~n-1 中缺失的数字

题目: leetcode 268

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0到n-1之内。

在范围0到n-1的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

解析:

因为有序,所以二分。

因为范围为0~n-1,所以缺失值的前半部分值都是等于下标的。从缺失值开始,值与下标就不会一一对应了。所以我们要找到第一个值与下标不相等的元素,那它的下标即我们要找的缺失值。

注意:当所有的数都与下标对应时,缺失值为下一个值。

``python

class Solution(object):def getMissingNumber(self, nums):if not nums: return 0l, r = 0, len(nums) - 1while l < r:mid = l + r >> 1if nums[mid] == mid:l = mid + 1else:r = midif nums[l] == l: l += 1  # 当缺失的为最后一个数时, 返回 l++return l

考虑数组无序的情况

解法一:高斯公式求和

n项和减去数组和即为缺失的值。

    def getMissingNumber_2(self, nums):n = len(nums)sumv = sum(nums)return (0 + n)*(n + 1)//2 - sumv

解法二:异或法

由于异或运算(XOR)满足结合律,并且对一个数进行两次完全相同的异或运算会得到原来的数,因此我们可以通过异或运算找到缺失的数字。

我们知道数组中有 n 个数,并且缺失的数在 [0…n] 中。因此我们可以先得到 [0…n] 的异或值,再将结果对数组中的每一个数进行一次异或运算。未缺失的数在 [0…n]和数组中各出现一次,因此异或后得到 0。而缺失的数字只在 [0…n]中出现了一次,在数组中没有出现,因此最终的异或结果即为这个缺失的数字。

在编写代码时,由于 [0…n]恰好是这个数组的下标加上 n,因此可以用一次循环完成所有的异或运算

class Solution:def missingNumber(self, nums: List[int]) -> int:miss = len(nums)  # 假设缺失值为 nfor i, num in enumerate(nums):miss ^= i ^ numreturn miss

53.3 数组中数值与下标相等的元素

题目:

假设一个单调递增的数组里的每个元素都是整数并且是唯一的。

请编程实现一个函数找出数组中任意一个数值等于其下标的元素。

例如,在数组[-3, -1, 1, 3, 5]中,数字3和它的下标相等

解析: 二分法

因为有序,所以二分。

假设第一个数值与下标相等的元素为m,则m右边的所有元素 小于 其索引,m左边的所有元素 大于 其索引

class Solution(object):def getNumberSameAsIndex(self, nums):l, r = 0, len(nums)while l < r:mid = l + r >> 1if nums[mid] < mid: l = mid + 1else: r = midif nums[l] != l: return -1return nums[l]

54 二叉搜索树的第k小的节点

题目: 牛客网 Acwing

给定一棵二叉搜索树,请找出其中的第k小的结点。

你可以假设树和k都存在,并且1≤k≤树的总结点数。

解析:

中序遍历的同时,没遍历一次,k–,当k = 0时,为答案。

class Solution(object):def kthNode(self, root, k):def dfs(root):if not root: returndfs(root.left)self.k -= 1if self.k == 0: self.res = rootif self.k > 0: dfs(root.right)  # 剪枝self.k = k  # k要为全局变量self.res = Nonedfs(root)return self.res

55.1 二叉树的深度

题目:

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

解析:

递归

class Solution:def maxDepth(self, root: TreeNode) -> int:if not root: return 0left = self.maxDepth(root.left)right = self.maxDepth(root.right)return max(left, right) + 1

55.2 平衡二叉树

题目: leetcdoe 110

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。

解析:

递归的同时记录高度,可以考虑加上剪枝

    def isBalanced_2(self, root):def dfs(root):if not root: return 0left = dfs(root.left)right = dfs(root.right)if abs(left - right) > 1:self.ans = Falsereturn max(left, right) + 1self.ans = Truedfs(root)return self.ans

56.1 数组中只出现一次的两个数字

题目: leetcode

给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。

解析:

解法一:哈希表

两遍循环,第一遍记录每个数字出现的次数。第二遍筛选出只出现一次的数字。

  • 时间复杂度O(2n)
  • 空间复杂度O(n)
class Solution:def singleNumber(self, nums: List[int]) -> List[int]:dic = {}for x in nums:if x not in dic:dic[x] = 1else:dic[x] += 1res = []for x in nums:if dic[x] == 1:res.append(x)return res

解法二:位运算。

  • 先进行一次异或运算,结果为这两个不同的数的异或结果。结果二进制中至少有一位1,记录为n位。

  • 根据第n位是否为1可将数组分为两个子数组。子数组中各存在一个 出现一次的数,且其他的数都出现两次。

  • 在子数组中进行异或运算,可得到一个出现一次的数字。再将这个数字与第一次的异或结果进行异或,即为第二个答案(异或的交换性)。

class Solution(object):def singleNumber(self, nums):mask = 0for num in nums:mask ^= numk = 0  # 找到最后为1的一位while not mask >> k & 1:k += 1first = 0for x in nums:if x >> k & 1:first ^= xreturn first, first ^ mask

56.2 数组中唯一只出现一次的数字

题目: leetcode 137

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。

解析:

如果一个数字出现三次,那么它的二进制表示的每一位(0或者1)也出现三次。如果把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位都能被3整除。

把数组中所有数字的二进制表示的每一位都加起来。如果某一位的和能被 3 整除,那么那个只出现一次的数字二进制对应的那一为是 0,否则就是 1。

    def singleNumber(self, nums):ans = 0for i in range(32):bitCount = 0  # 第i位的 1 出现次数的总和for num in nums:if num >> i & 1:bitCount += 1if bitCount % 3 != 0:  # 如果次数不能被 3 整除,则单独的那个数此位为1ans |= 1 << ireturn self.convert(ans)def convert(self, x):if x >= 2**31:x -= 2**32return x

57.1 和为 s 的数字

题目: Acwing

输入一个数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。

如果有多对数字的和等于s,输出任意一对即可。

你可以认为每组输入中都至少含有一组满足条件的输出。

解析:

因为递增有序,l, r指针分别指向最左和最右,即 l 指向最小的元素,r 指向最大的元素,若两者之和大于目标,r 往左移,若两者之和小于目标,l 往右移,等于则返回

class Solution:def FindNumbersWithSum(self, nums, target):l, r = 0, len(nums) - 1while l < r:if nums[l] + nums[r] == target:return nums[l], nums[r]elif nums[l] + nums[r] < target:l += 1else:r -= 1return []

57.2 和为 s 的连续正数序列

题目: 牛客网 Acwing

输入一个正数s,打印出所有和为s的连续正数序列(至少含有两个数)。

例如输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以结果打印出3个连续序列1~5、4~6和7~8。

解析:

双指针,使用 l,r 分别指向序列的最左端和最右端,l 初始化为 1,r 初始化为 2,记录 l~r 的和为 curSum。

  • 如果curSum小于目标,r向前走,curSum加上r。
  • 如果curSum大于目标,curSum减去 l,l 向前走
  • 等与则把 l~r 的连续序列加入到结果中,r继续往前走,curSum加上r。
class Solution(object):def findContinuousSequence(self, sum):l, r, cur_sum = 1, 2, 3res = []mid = sum + 1 >> 1  # 至少两个数,所以小的那个数不可能大于和的一半。while l <= mid: if cur_sum < sum:  # 当前和小于sum时r += 1  # 右移rcur_sum += r  # 加上新的数elif cur_sum > sum:  # 当前和大于sum时cur_sum -= l  # 减去最小数l += 1  # 右移lelse:  # 等于时加入答案,并且继续右移指针res.append(list(range(l,r+1)))r += 1cur_sum += rreturn res

58.1 翻转字符串

题目: leetcode 151

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:

输入: "the sky is blue"
输出: "blue is sky the"

解析:

用python的话就不多说了。

class Solution:def reverseWords(self, s: str) -> str:return ' '.join(reversed(s.split()))def reverseWords_2(self, s: str):"""两次翻转1. 翻转整个字符串2. 翻转每个单词"""s = s.strip()s = list(s)s.reverse()res = ''for i in range(len(s)):j = iwhile j < len(s) and s[j] != ' ':j += 1res += reversed(s[i:j])i = jreturn ''.join(res)

58.2 左旋转字符串

题目: 牛客网 Acwing

请定义一个函数实现字符串左旋转操作的功能。

比如输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab"

解析:

python 不用多说

class Solution(object):def leftRotateString(self, s, n):return s[n:] + s[:n]

59 滑动窗口的最大值

题目: 牛客网 Acwing

给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。

例如,如果输入数组[2, 3, 4, 2, 6, 2, 5, 1]及滑动窗口的大小3,那么一共存在6个滑动窗口,它们的最大值分别为[4, 4, 6, 6, 6, 5]。

解析:

使用一个双端队列。队列中存入数组元素的下标

  • 准备插入时,判断插入后是否大于size,大则先弹出队首元素,再考虑插入。
  • 准备插入新元素时,先删除队列中比新元素小的所有元素,保留比它大的值(因为当前面大的元素弹出后,新元素有可能成为最大值)。
  • 插入新元素,若size >= k,开始记录最大值,即队首元素。
    def maxInWindows_2(nums, k):"""两端开口的队列"""from collections import dequeif k <= 0: return []  # 异常输入res = []q = deque()for i in range(len(nums)):# 此时准备插入 i, 若i插入后元素大于k,则队首弹出元素,也就是给i留位置,等于事先判断if q and i - q[0] + 1 > k: # 当队列元素个数大于窗口size,从队首弹出元素q.popleft()# 从队尾开始比较,依次弹出比当前num小的元素,同时能保证队列首元素为当前窗口最大值索引,也能保证队首元素为窗口的第一个元素while q and nums[q[-1]] <= nums[i]:q.pop()q.append(i)  # 都处理完后,插入当前元素if i + 1 >= k: # 当形成完整的窗口时,开始写入最大值res.append(nums[q[0]])return res

60 n个骰子的点数

题目: Acwing

将n个骰子投掷1次,获得的总点数为s,s的可能范围为n~6n。

掷出某一点数,可能有多种掷法,例如投掷2次,掷出3点,共有[1,2],[2,1]两种掷法。

请求出投掷n次,掷出n~6n点分别有多少种掷法。

样例1

输入:n=1输出:[1, 1, 1, 1, 1, 1]解释:投掷1次,可能出现的点数为1-6,共计6种。每种点数都只有1种掷法。所以输出[1, 1, 1, 1, 1, 1]。

解析:

解法一:动态规划

状态表示:

  • dp[i][j] 表示 i 个骰子扔出和为 j 的可能数
  • dp[n][j] , j = [n, 6n]
  • 因为一个骰子的范围是 [1, 6],所以我们下标全部从 1 开始。

状态转移:

  • 因为上一个骰子扔出的点数可能为 [1, 6]
  • 所以 dp[i][j]=dp[i-1][j-1]+dp[i-1][j-2]+...+dp[i-1][j-6]

边界:

  • dp[0][0] = 1 ,当一个骰子都没有时,总和为0的情况下的方案数为 1
class Solution(object):def numberOfDice(self, n):# 每个地方索引+1都是因为方便计算。因为是从骰子个数和一个骰子的范围都是从 1 开始的。f = [[0]*(6*n + 1) for _ in range(n + 1)]f[0][0] = 1for i in range(1, n + 1): # 枚举骰子的个数for j in range(i, 6*i + 1): # 枚举骰子为 i 个时的总和 [i, 6*i]for k in range(1, min(j, 6) + 1):  # 上一个骰子的可能出现的点数f[i][j] += f[i - 1][j - k]  # 确保k <= jreturn [f[n][i] for i in range(n, 6*n+1)]

解法二:递归

可能包含大量重复运算。

n 个 骰子的总和范围为 [n, 6n]。对于和的每一个取值,分别算出可能出现方案数。

    def numberOfDice_2(self, n):res = []for i in range(n, n*6+1):  # 总和的范围,依次枚举res.append(self.dfs(n, i))return resdef dfs(self, n, sum): # n为骰子个数,sum为总和# 求解总和为某个n时的可能的投掷的所有方案数if sum < 0: return 0if n == 0: return not sum # n为0时,sum刚好等于0则是一种解决方案,大于0则说明方案不成立ans = 0 # 次数for i in range(1, 7): # 枚举所以的可能ans += self.dfs(n-1, sum - i)return ans

61 扑克牌中的顺子

题目: Acwing 牛客网

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。

2~10为数字本身,A为1,J为11,Q为12,K为13,大小王可以看做任意数字。

为了方便,大小王均以0来表示,并且假设这副牌中大小王均有两张。

解析:

将 5 个数排序,找到第一个非零值的索引,首先判断数组后面是否有重复值,重复则不可能为顺子。

再判断非零的最大值与最小值的差是不是在四以内,在就可以用 0 补。

    def isContinuous_2(self, nums):"""排序后非零的最大值与最小值的差是不是在四以内,在就可以用 0 补"""if not nums: return Falsenums.sort()k = 0while not nums[k]: # 找到第一个不为0的元素的索引k += 1for i in range(k, len(nums)-1):if nums[i] == nums[i + 1]: # 若有重复的则不可能为顺子return Falsereturn nums[-1] - nums[k] <= 4

62 圆圈中最后剩下的数字

题目: Acwing

0, 1, …, n-1这n个数字(n>0)排成一个圆圈,从数字0开始每次从这个圆圈里删除第m个数字。

求出这个圆圈里剩下的最后一个数字。

解析:

总共n个人,从0开始编号,所以人0~n-1,每次从0开始报数,消灭第m个人,即编号m-1的人被消灭.
剩余人的编号从m开始,但是要从0开始重新开始编号并报数。

旧 m, m+1, m+2, ..., m-2
新 0, 1,   2,   ..., n-2  (此时总共n-1个人)
即 旧 = (新 + m) % n 注:这里的n是旧编号对应的n,新编号对应的是n-1了,因为消灭了一个人

我们记 f(n) 为 n个人中报数m最后活下来的人,报数一轮之后就剩n-1个人,活下来的人为f(n-1)
显然f(n) 和 f(n - 1) 为同一个人,只不过编号不一样了,为上面的对应关系。
到最后只剩一个人的时候,那个人就存活了,编号为0. 所以f(1) = 0,这是我们得到最新的编号,所以要往上递推回去。

        -- 0            , n==1
f(n) = | -- (f(n-1)+m)% n, n>1
class Solution(object):def lastRemaining_2(self, n, m):"""递推,最优解"""if n <= 0 or m <= 0:return -1last = 0  # n=1的时候只剩一个人那么0就活了for i in range(2, n + 1):  # 从两个人开始last = (last + m) % i  # 每次模 上一次 的总人数return lastdef lastRemaining_1(self, n, m):"""递归, 可能会爆栈"""if n==1: return 0return (self.lastRemaining(n - 1, m) + m)%n

63 股票的最大利润

题目: leetcode 121

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

解析:

动态规划。

当前位置的最大利润等于当前价格减去之前的最低价格。

状态表示:

  • f[i] 为卖出价为第 i 个数字时可能获得的最大利润
  • max(f[i])

转移方程

  • f[i] = nums[i] - minValueminValuei元素之前的最小价格
  • minValue = min(minValue, nums[i-1])

边界:

  • minValue = nums[0]
  • f[0] = 0 未买入前是不允许卖出的,所以第一个元素不能为卖出价,所以利润是0
class Solution:def maxProfit(self, nums: List[int]) -> int:if not nums: return 0f = 0 # f[0] = 0minPrice = nums[0]for i in range(1, len(nums)): # 枚举每个状态minPrice  = min(nums[i-1], minPrice)f = max(nums[i] - minPrice , f)return f

64 求 1+2+…+n

题目: 牛客网 AcWing

求1+2+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

解析:

递归

class Solution:def getSum(self, n):return n and (n+self.getSum(n - 1))

65 不用加减乘除做加法

题目: leetcode 371

不使用运算符 +- ,计算两整数 ab 之和。

解析:

将加法分为三部分:

  1. 求出无进制加法结果 s1

    我们先来观察下位运算中的两数加法,其实来来回回就只有下面这四种:

    0 + 0 = 0
    0 + 1 = 1
    1 + 0 = 1
    1 + 1 = 0(进位 1)
    

    等价与异或运算。

  2. 求出进位 s2

    在位运算中,我们可以使用与操作获得进位:

    a = 5 = 0101
    b = 4 = 0100
    a & b 如下:
    0 1 0 1
    0 1 0 0
    -------
    0 1 0 0
    

    由计算结果可见,0100 并不是我们想要的进位,1 + 1 所获得的进位应该要放置在它的更高位,即左侧位上,因此我们还要把 0100 左移一位,才是我们所要的进位结果。

  3. s = s1 + s2

因为python正数没有位数限制,可以用numpy来处理。

def getSum(num1, num2):import numpy as npwhile num2:sumv = np.int32(num1 ^ num2)carry = np.int32((num1 & num2) << 1)# 此时应该返回 sumv + carry,但是不能用加法# 所以循环,直到没有进位,也就不需要相加num1, num2 = sumv, carryreturn int(num1)

66 构建乘积数组

题目: AcWing

给定一个数组A[0, 1, …, n-1],请构建一个数组B[0, 1, …, n-1],其中B中的元素B[i]=A[0]×A[1]×… ×A[i-1]×A[i+1]×…×A[n-1]

解析:

class Solution:def multiply(self, A):n = len(A)if not n: return []# 先算第一部分:1, A0, A0A1, ... , A0A1An-2B = [1]for i in range(n-1):B.append(B[-1] * A[i])# 再算第二部分: 从后往前依次乘剩余的部分temp = 1  # 最后一项乘以1,B[n-1]=A0A1An-2for i in range(n-1, -1, -1):B[i] = B[i] * temp temp *= A[i]return B

67 字符串转化为整数

题目: Acwing

请你写一个函数StrToInt,实现把字符串转换成整数这个功能。

当然,不能使用atoi或者其他类似的库函数。

样例

输入:"123"
输出:123

注意:

你的函数应满足下列条件:

  1. 忽略所有行首空格,找到第一个非空格字符,可以是 ‘+/−’ 表示是正数或者负数,紧随其后找到最长的一串连续数字,将其解析成一个整数;
  2. 整数后可能有任意非数字字符,请将其忽略;
  3. 如果整数长度为0,则返回0;
  4. 如果整数大于INT_MAX(2^31 − 1),请返回INT_MAX;如果整数小于INT_MIN(−2^31) ,请返回INT_MIN;

解析:

class Solution:def strToInt(self, s):self.input_valid = Trueif not s:self.input_valid = False  # 标志输如是否合法return 0k = 0while k<len(s) and s[k] == ' ':k += 1 # 去掉行首空格number = 0is_minus = Falseif s[k] == '+':k += 1elif s[k] == '-':k += 1is_minus = Truewhile k < len(s) and '0' <= s[k] <= '9':number = number * 10 + int(s[k])k += 1if is_minus:number *= -1if number > 2 ** 31 - 1: # MAX_INTreturn 2 ** 31 - 1if number < - 2 ** 31:  # MIN_INTreturn -2**31return number

68 最低公共祖先

题目: leetcode 236

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

解析:leetcode

解法一:直接在递归的过程中判断。

解法二:搜出路径,再判断。

class Solution:def lowestCommonAncestor(self, root, p, q):self.ans = Noneself.dfs(root, p, q)return self.ansdef dfs(self, root, p, q):if not root: return Falseleft = self.dfs(root.left, p, q)right = self.dfs(root.right, p, q)# If the current node is one of p or qm = root == p or root == q# 左右子树和自身只要任意两个存在则返回答案if m + right + left >= 2:self.ans = root# 左中右只要有一个存在就返回Truereturn m or right or leftdef lowestCommonAncestor_1(self, root, p, q):"""先找到两条根节点到节点的路径再找到最后一个公共字子节点,则为最近公共祖先"""if not root or not p or not q:returnpath_1 = []path_2 = []res = []self.dfs(root, p, path_1, res)self.dfs(root, q, path_2, res)path_1, path_2 = res[0], res[1]n = min(len(path_1), len(path_2))for i in range(n):if path_1[i] != path_2[i]:return path_1[i - 1]if i == n - 1:return path_1[i]def dfs(self, root, q, path, res):"""递归获取根节点到某一节点的路径"""if not root: returnpath.append(root)if root == q:res.append(path[:])return  # 找到则停止递归self.dfs(root.left, q, path, res)self.dfs(root.right, q, path, res)path.pop()

剑指offer全书题解 (Python)【更新完毕】相关推荐

  1. 剑指offer有python版吗_剑指Offer算法类题目[Python版]

    标签:重复   作用   coding   面试   medium   mba   none   fas   utf-8 面试题012 数值的整数次方 解题思路1 考虑所有情况,循环连乘 代码: de ...

  2. 剑指offer之题解目录(全)

    剑指offer之题解目录(全) 3. 从尾到头打印链表 4. 重建二叉树 5. 用两个栈实现队列 6. 旋转数组的最小数字 7. 斐波那契数列 8. 跳台阶 9. 变态跳台阶 10. 矩阵覆盖 11. ...

  3. 《剑指Offer》题解汇总索引表(leetcode)

    <剑指Offer>题解汇总索引表(leetcode)

  4. JavaScript-牛客网-剑指offer(1-10)题解

    牛客网-剑指Offer题解-js版本 剑指offer第1-10题解答(js) 1.二维数组中查找 2.替换空格 3.从尾到头打印链表 4.重建二叉树 5.用两个栈实现一个队列 6.旋转数组中的最小数字 ...

  5. 剑指Offer面试题解总结21-30

    目录 剑指Offer(21~30) 调整数组顺序使奇数位于偶数前面 链表中倒数第k个节点 反转链表 合并两个有序的链表 树的子结构 二叉树的镜像 对称的二叉树 顺时针打印矩阵 包含min函数的栈 剑指 ...

  6. 剑指Offer面试题解总结11-20

    目录 剑指Offer(11~20) 旋转数组中的最小数字 矩阵中的路径 剪绳子 剪绳子II 二进制中1的个数 数值的整数次方 打印从1到最大的n位数 删除链表的节点 正则表达式匹配 表示数值的字符串 ...

  7. 【剑指offer】——【python中return函数中的and和or表达式的返回值】

    目录 1.# and 结果为真,返回最后一个表达式的结果,若结果为假返回第一个为假的表达式的结果 2.# or 结果为真,返回第一个为真的表达式的结果,若结果为假,返回最后一个表达式的结果 3.应用[ ...

  8. 剑指offer 答案 python_【剑指offer】【python】面试题2~5

    使用python实现<剑指offer>面试题ヾ(◍°∇°◍)ノ゙,以此记录. 2_实现Singleton模式 题目:实现单例模式 单例模式,是一种常用的软件设计模式.在它的核心结构中只包含 ...

  9. JavaScript-牛客网-剑指offer(41-50)题解

    牛客网剑指Offer题解javascript版本 剑指offer第41-50题解答(javascript) 41.和为S的连续正数序列 42.和为S的两个数字 43.左旋转字符串 44.翻转单词顺序列 ...

最新文章

  1. matlab--曲线拟合
  2. Python中的id()函数_怪异现象
  3. 检查电脑是否被安装***的三个小命令
  4. kylin调优,项目中错误总结,知识点总结,kylin jdbc driver + 数据库连接池druid + Mybatis项目中的整合,shell脚本执行kylin restapi 案例
  5. OpenCV图像发现轮廓函数findContours()的使用
  6. JZOJ5776. 【NOIP2008模拟】小x游世界树
  7. java如何阻塞和同步_同步与异步,阻塞与非阻塞
  8. php 自制建议神马收录查询工具
  9. 基础编程题目集 7-1 厘米换算英尺英寸 (15 分)
  10. IT行业里有这么多聪明人,他们之间的区别在哪里?
  11. 数组及对象几种遍历方式对比
  12. 高效管理MacOS中文件的技巧
  13. 全屋WiFi方案:Mesh路由器组网和AC+AP
  14. 90年经典坦克大战(cocos2d-x)
  15. 制作U盘纯DOS启动盘文件
  16. 本地文件怎么传到linux服务器,本地文件传到linux服务器
  17. 宝子,你知道小程序代码大小超限除了分包还能怎么做吗?
  18. python搭建自己的网站_Python+Django搭建自己的blog网站
  19. web页面性能检测工具Lighthouse
  20. sklearn:make_blobs聚类数据生成器

热门文章

  1. zepto为什么不支持animate,报animate is not a function
  2. Infragistics NetAdvantage UltraGrid的使用
  3. Asp.net性能优化-提高ASP.Net应用程序性能的十大方法
  4. MMO游戏数值框架概述(偏模拟方向)
  5. lr 中cookie的解释与用法
  6. 20180929 北京大学 人工智能实践:Tensorflow笔记04
  7. 通过demo搞懂encode_utf8和decode_utf8
  8. int a[5]={1,2,3,4,5}; int *p=(int*)(a+1); printf(%d,*(p-1)); 答案为什么是5?
  9. WPF 界面提示加载出错
  10. 解决FlexPaper分页分段加载问题(转)