使用 __new__
控制实例创建过程
class Singleton:
_instance = None
def __init__(self):
pass
def __new__(cls, *args, **kw):
if not cls._instance:
cls._instance = super().__new__(cls)
return cls._instance
class MyClass(Singleton):
pass
题目描述:
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,
但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解析:
长度为n,数字范围0~n-1,如果这个数组不存在重复的数字,那么当数组排序后数字 i 将出现在下标为 i 的位置。
即下方跳出 while 循环。
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] = num
return True
else: #否则交换
nums[i], nums[num] = nums[num], nums[i]
num = nums[i]
return False
def duplicate_1(self, nums, duplication):
"""Space: O(n)
另起一个数组存储出现过的字符
"""
t = []
for x in nums:
if x in t:
duplication[0] = x
return True
else:
t.append(x)
return False
题目:
给定一个长度为 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: return
l, r = 1, len(nums)-1 # 数值的范围不是下标的范围,所以是1~n 题目给出。
while l<r:
mid = l + r >> 1 # [l, mid], [mid+1, r]
s = 0
for x in nums:
# 计算左边区间数字的个数
if l <= x <= mid:
s += 1
if s > mid - l + 1: #若左边区间数字出现的次数大于范围,则重复数据一定在此区间
r = mid
else: l = mid + 1
return r
题目: 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 False
row, col = 0, len(array[0]) - 1
while row <= len(array)-1 and col >= 0:
if array[row][col] == target:
return True
elif array[row][col] < target:
row += 1
else:
col -= 1
return False
题目:
请实现一个函数,把字符串中的每个空格替换成"%20"。
解析:
class Solution:
def replaceSpace(self, s):
"""常规解法
O(n)
"""
if not s: return ''
s = list(s)
# 求出填充之后的长度
length = 0
for x in s:
if x == ' ':
length += 3
else:
length += 1
# 扩充原字符串
i, j = len(s) - 1, length - 1
s += [0] * (length - len(s))
while i >= 0:
if s[i] == ' ':
s[j] = '0'
s[j - 1] = '2'
s[j - 2] = '%'
j -= 3
else:
s[j] = s[i]
j -= 1
i -= 1
return ''.join(s)
def replaceSpace(self, s):
"""pythonic
"""
if type(s) != str:
return ''
return s.replace(' ', '%20')
题目:
输入一个链表的头结点,按照 从尾到头 的顺序返回节点的值。
解析:
class Solution(object):
def printListReversingly(self, head: ListNode) -> List[int]:
"""遍历+倒序
"""
if not head: return []
res = []
while head:
res.append(head.val)
head = head.next
res.reverse()
return res
def printListReversingly_1(self, head):
"""递归
"""
self.res = []
self.dfs(head)
return self.res
def dfs(self, head):
if not head:
return
self.dfs(head.next)
self.res.append(head.val)
题目: leetcode 105.
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解析:
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 = left
root.right = right
return root
题目: 牛客网
给定一棵二叉树的其中一个节点,请找出中序遍历 [左,根,右] 序列的下一个节点。
解析:
class Solution:
def GetNext(self, pNode: TreeLinkNode):
"""中序遍历的下一个
[left, root, right]
"""
# pNode 不存在则返回None
if not pNode: return
# 节点有右子树,则下一个节点就是它右子树的最左节点
if pNode.right:
pRight = pNode.right
while pRight.left:
pRight = pRight.left
return pRight
# 节点没有右子树,沿着父节点,直到找到是它父节点的左节点
while pNode.next:
parent = pNode.next
if parent.left == pNode:
return parent
pNode = parent
return # 不存在就返回None
题目: leetcode 232
请用栈实现一个队列,支持如下四种操作:
解析:
push(x):直接将x插入栈1中,时间复杂度O(1)
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 False
else:
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.
题目: leetcode 225
使用队列实现栈的下列操作:
push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空
解析:
class MyStack:
def __init__(self):
from collections import deque
self.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 False
return True
题目: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 N
f0, f1, fn = 0, 1, 0
for _ in range(2, N+1):
fn = f0 + f1
f0, f1 = f1, fn
return fn
def fib(self, N: int) -> int:
"""递归 O(2^n)
"""
if N <= 0:
return 0
if N == 1:
return 1
return self.fib(N-1) + self.fib(N-2)
另外一种解法:矩阵乘法+快速幂
利用矩阵运算的性质将通项公式变成幂次形式,然后用平方倍增(快速幂)的方法求解第 n 项。
先说通式:
[ a n + 1 a n a n a n − 1 ] = [ 1 1 1 0 ] n \begin{bmatrix} a_{n+1} & a_{n} \\ a_{n} & a_{n-1} \\ \end{bmatrix}= \begin{bmatrix} 1 & 1 \\ 1 & 0 \\ \end{bmatrix}^n [an+1ananan−1]=[1110]n
利用数学归纳法证明:
这里的a0,a1,a2是对应斐波那契的第几项
令 A = [ 1 1 1 0 ] , 则 A 1 = [ a 2 a 1 a 1 a 0 ] 显 然 成 立 令A =\begin{bmatrix} 1 & 1 \\ 1 & 0 \\ \end{bmatrix},则A^1 = \begin{bmatrix} a_{2} & a_{1} \\ a_{1} & a_{0} \\ \end{bmatrix} 显然成立 令A=[1110],则A1=[a2a1a1a0]显然成立
A n = A n − 1 × A = [ a n a n − 1 a n − 1 a n − 2 ] × [ a 2 a 1 a 1 a 0 ] = [ a n + 1 a n a n a n − 1 ] A^n = A^{n-1} \times A = \begin{bmatrix} a_{n} & a_{n-1} \\ a_{n-1} & a_{n-2} \\ \end{bmatrix} \times \begin{bmatrix} a_{2} & a_{1} \\ a_{1} & a_{0} \\ \end{bmatrix}= \begin{bmatrix} a_{n+1} & a_{n} \\ a_{n} & a_{n-1} \\ \end{bmatrix} An=An−1×A=[anan−1an−1an−2]×[a2a1a1a0]=[an+1ananan−1]
证毕。
所以我们想要的得到 a n a_n an ,只需要求得 A n A^n An ,然后取第一行第二个元素即可。
如果只是简单的从0开始循环求n次方,时间复杂度仍然是O(n),并不比前面的快。我们可以考虑乘方的如下性质,即快速幂:
a n = { a n / 2 ⋅ a n / 2 n 为偶数 a ( n − 1 ) / 2 ⋅ a ( n − 1 ) / 2 ⋅ a n 为奇数 a^n= \begin{cases} a^{n/2} \cdot a^{n/2} & \text {n 为偶数} \\ a^{(n-1)/2} \cdot a^{(n-1)/2} \cdot a & \text {n 为奇数} \end{cases} an={an/2⋅an/2a(n−1)/2⋅a(n−1)/2⋅an 为偶数n 为奇数
这样只需要 logn 次运算即可得到结果,时间复杂度为 O(logn)
def mul(a, b): # 首先定义二阶矩阵乘法运算
c = [[0, 0],
[0, 0]] # 定义一个空的二阶矩阵,存储结果
for i in range(2): # row
for j in range(2): # col
for k in range(2): # 新二阶矩阵的值计算
c[i][j] += a[i][k] * b[k][j]
return c
def fib(n):
res = [[1, 0],
[0, 1]] # 单位矩阵,等价于1,作为base
A = [[1, 1],
[1, 0]] # A矩阵
while n:
# 1. 如果n是奇数,则先提取一个A出来
# 2. 停止条件 n == 1
if n & 1: res = mul(res, A)
A = mul(A, A) # 快速幂
n >>= 1 # 整除2,向下取整
return res[0][1]
题目: 牛客网
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
解析:
记 n 阶台阶的跳法看成 n 的函数,记为 f(n)
class Solution:
def jumpFloor(self, number):
if number <=2 :
return max(0, number)
f1, f2, fn = 1, 2, 0
for _ in range(3, number+1):
fn = f1 + f2
f1, f2 = f2, fn
return fn
题目:牛客网
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
解析:
每个台阶都有跳与不跳两种情况(除了最后一个台阶),最后一个台阶必须跳。所以共用 2 ( n − 1 ) 2^{(n-1)} 2(n−1) 中情况.
class Solution:
def jumpFloorII(self, number):
return 2**(number-1)
题目: 牛客网
我们可以用2x1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2x1的小矩形无重叠地覆盖一个2xn的大矩形,总共有多少种方法?
解析:
小矩形有两种摆法,横着和竖着,记 2xn 的大矩形的摆法为 f(n)
class Solution:
def rectCover(self, n):
if n<=2:
return n
f1, f2, fn = 1, 2, 0
for _ in range(3, n + 1):
fn = f1 + f2
f1, f2 = f2, fn
return fn
题目: 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) - 1
if 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, n
while l < r:
mid = l + r >> 1
if nums[mid] >= nums[0]:
l = mid + 1
else:
r = mid
return nums[l]
题目: 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 True
m[i][j] = temp # 回溯
return False # 如果都匹配不到:返回 False
for i in range(len(m)):
for j in range(len(m[0])):
if m[i][j] == word[0] and dfs(0, i, j):
return True
return False
题目: 牛客网
地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。
例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?
解析: DFS
没有回溯的步骤,因为一个格子最多进入一次。
class Solution:
def movingCount(self, threshold, rows, cols):
def dfs(i, j): # i, j为坐标
visited[i][j] = True
self.res += 1
for 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 0
self.res = 0
visited = [[False for _ in range(cols)] for _ in range(rows)]
dfs(0, 0)
return self.res
def get_sum(self, a, b):
"""两数数位之和
"""
return sum(map(int, str(a) + str(b)))
题目:AcWing
给你一根长度为 n 绳子,请把绳子剪成 m 段(m、n 都是整数,2≤n≤58 并 m≥2。每段的绳子的长度记为k[0]、k[1]、……、k[m]。它们可能的最大乘积是多少?
例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。
解析:
解法一:动态规划
f[n] 为长度为 n 的绳子剪成若干段后乘积的最大值
剪第一刀时,可选1~n-1,n-1中剪法,我们要选取其中的最大值,
f [ n ] = m a x ( f [ i ] × f [ n − i ] ) , 1 < = i < = n / 2 f[n] = max(f[i] \times f[n-i]), \qquad 1<=i<=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+1
if length == 3: return 2 # 长度为3拆成1+2最大
f = [-1] * (length + 1)
f[0], f[1], f[2], f[3] = 0, 1, 2, 3
for i in range(4, length + 1):
maxv = 0
for j in range(1, i//2 + 1):
maxv = max(maxv, f[j] * f[i - j])
f[i] = maxv
return 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 0
if length == 2: return 1
if length == 3: return 3
# 尽可能的剪去长度为3的绳子,可能余0,1,2
number_3 = length // 3
# 若最后余1,说明最后可以剩下4,取出4
if length - number_3 * 3 == 1:
number_3 -= 1
# 算出2的个数
number_2 = (length - number_3 * 3) // 2
return 2 ** number_2 * 3 ** number_3
题目: leetcode 191
编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数
解析: 位运算
class Solution(object):
def NumberOf1(self,n):
res = 0
for _ in range(32): # 防止死循环,负数右移补1
res += n&1
n>>=1
return res
def f2(self,n):
"""把一个整数减去1,都是把最右边的1变成0,
再和原整数做与运算,会把该整数最右边的1变成0
1100 - 1 =
1011 & =
1000
"""
res = 0
while n:
res +=1 # 一个不为0的整数至少含有一个1
n &= n-1 # 消灭一个 1
return res
题目: leetcode 50
实现 pow(x, n) ,即计算 x 的 n 次幂函数。
解析: 快速幂
a n = { a n / 2 ⋅ a n / 2 n 为偶数 a ( n − 1 ) / 2 ⋅ a ( n − 1 ) / 2 ⋅ a n 为奇数 a^n= \begin{cases} a^{n/2} \cdot a^{n/2} & \text {n 为偶数} \\ a^{(n-1)/2} \cdot a^{(n-1)/2} \cdot a & \text {n 为奇数} \end{cases} an={an/2⋅an/2a(n−1)/2⋅a(n−1)/2⋅an 为偶数n 为奇数
class Solution(object):
def myPow(self, x, n):
if x == 0:
return 1
res = 1
if n < 0: # n<0时 求倒数
x = 1 / x
n = -n
while n:
if n & 1: # n为奇数时,先提取一个出来。以及循环停止时,获取答案
res *= x
x *= x
n >>= 1
return res
def myPow_1(self, x, n):
"""递归
"""
if x == 0:
return 0
if n == 0:
return 1
if n == 1:
return x
if n < 0:
x = 1 / x
n =-n
res = self.myPow(x, n>>1)
res *= res
if n & 1:
res *= x
return res
题目:
输入数字 n ,按顺序打印出 1 到 n 的所有整数。
解析:
解法一:字符串模拟加法
初始化一个全为 0 的 n 位数组
模拟加法,每次加 1
注意进位
若溢出,返回变量停止循环
打印数组
def Print(n):
if n <= 0:
return
nums = ['0'] * n
while 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]) + carry
if i == len(nums) - 1: # 从最低位开始加起
sumv += 1
if sumv >= 10: # 若大于10,考虑进位
if i == 0: # 若是最高位大于10,停止外层while循环
stop = True
else:
sumv -= 10 # 去除进位
carry += 1 # 进为+1
nums[i] = str(sumv)
else:
nums[i] = str(sumv) # 直到某一位的和小于10,直接赋值并跳出,没有进位
break
return stop
def print_arr(nums):
"""从第一个非零字符开始打印字符串
"""
flag = False
for x in nums:
if x != '0':
flag = True
if flag:
print(x, end='')
print(end=' ')
解法二:全排列
n 位所有十进制数其实是 n 个从 0 到 9 的全排列。也就是把数字的每一位都从 0 到 9 排列一遍。
def Print2(n):
"""解法二:数字全排列,递归"""
if n <= 0: return
num = ['0'] * n
for i in range(10):
num[0] = str(i)
printRecur(num, n, 0)
def printRecur(num, n, idx):
if idx == n - 1:
printArray(num)
return
for i in range(10):
num[idx + 1] = str(i)
printRecur(num, n, idx + 1)
题目: leetcode 237
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
解析:
将下一个节点的值赋给自身,删除下一个节点
class Solution(object):
def deleteNode(self, node):
if not node:
return
node.val = node.next.val
node.next = node.next.next
题目: 牛客网 leetcode 82
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
输入: 1->2->3->3->4->4->5
输出: 1->2->5
解析: 快慢指针
class Solution:
def deleteDuplication(self, head):
if not head or not head.next: # 若head为None,或者只有一个节点
return head
dummy = ListNode(-1)
dummy.next = head # 虚拟头节点,防止头节点被删除, 为了方便处理边界情况
slow = dummy
fast = dummy.next
while 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
题目: leetcode 10 牛客网
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
解析:
class Solution:
def isMatch(self, s: str, p: str):
"""暴力递归,时间复杂度高
"""
# 情况1
if 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.2
return (self.isMatch(s, p[2:]) or # 情况a
(first_match and self.isMatch(s[1:], p))) # 情况b
else: # 情况2.1
return 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的下一位是 '*',情况2
if j + 1 < len(pattern) and pattern[j + 1] == '*':
ans = dp(i, j + 2) or (first and dp(i + 1, j)) # 情况a 和 情况b
else:
ans = first and dp(i + 1, j + 1) # 情况2.1
# 记录答案
memo[i, j] = ans
return ans
memo = {}
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 # 都为空,返回True
for 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]
题目: leetcode 65
实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串’+100’, ‘5e2’, ‘-123’, ‘3.14’, ‘-1E-16’, 都表示
数值,但’12e’,‘1a3.14’, 都不是。
[-|+]A[.[B]][e|E[-|+]C]
或.B[e|E[-|+]C]
,A
是整数部分,B
是小数部分,C
是指数部分。A
和C
前面可以有正负号。A
不是必须的。
解析: 考虑各种情况
class Solution:
def isNumber(self, s: str) -> bool:
s = s.strip() # 去除两端空格
if not s: return False
self.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 += 1
return self.scanUnsignedInteger(s)
def scanUnsignedInteger(self, s):
"""扫描无符号整数
"""
before = self.i
while self.i < len(s) and '0' <= s[self.i] <= '9': # 遇到异常字符则停止
self.i += 1
# 存在若干个数字时,则返回True
return self.i > before
题目:牛客网
输入一个整数数组,实现一个函数来调整该数组中数字的顺序。
使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分。
解析:
解法一:不考虑相对位置
def reOrderArray_1(self, arr):
if not arr: return []
l, r = 0, len(arr) - 1
while l < r:
while l < r and arr[l] & 1: # 跳过奇数
l += 1
while l < r and not arr[r] & 1: # 跳过偶数
r -= 1
if l < r:
arr[l], arr[r] = arr[r], arr[l] # 交换奇数偶数
return arr
解法二:考虑之前的相对位置
用双向队列
从后往前扫描字符串,找到奇数放到队列前面
从前往后扫描字符串,找到偶数放到队列后面
def reOrderArray(self, arr):
"""不改变相对位置的算法, O(n)
"""
if not arr: return
from collections import deque
q = 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
题目: 牛客网
输入一个链表,输出该链表中倒数第k个结点。
解析: 快慢指针
快指针先走 k 步,快慢指针再一起走,快指针走到 NULL 时,慢指针走到倒数第 k 个节点
注:要判断 k < 链表长度的情况,返回空
class Solution:
def FindKthToTail(self, head, k):
if not head or not k: return
fast = slow = head
for _ in range(k):
if not fast: return # 若还没有走到k步就已经空了,则说明链表长度小于k
fast = fast.next
while fast: # 直到走到尾节点的下一个节点
fast, slow = fast.next, slow.next
return slow
题目: leetcode 142
给定一个链表,若其中包含环,则输出环的入口节点。若其中不包含环,则输出null
。
解析:
class Solution(object):
def detectCycle(self, head):
if not head: return
fast = slow = head
# 检测是否有环
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow == fast:
break
else: # 当fast为空,或者下一个节点为空时则说明没有环
return
# 找出入口节点
while head != slow: # 从头节点开始往前走 x 步,必定相遇
head, slow = head.next, slow.next
return head
题目: leetcode 206
反转一个单链表。
解析:
解法一:循环
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
解法二:递归
递归版本关键在于反向工作。假设列表的其余部分已经被反转,现在我该如何反转它前面的部分?
假设列表为:
假设 n k + 1 n_{k+1} nk+1 到 n m n_m nm 已经反转完,而我们正处于 n k n_{k} nk 。
我们希望 n k + 1 n_{k+1} nk+1 的下一个节点指向 n k n_k nk 。
所以 n k n_k nk.next.next = n k n_k nk
要小心的是 n1 的下一个必须指向空,不然可能会产生循环。
def reverseList(self, head):
if not head or not head.next:
return head
p = self.reverseList(head.next) # 这返回的是尾节点
head.next.next = head
head.next = None # n1 的下一个必须指向空
return p # 返回尾节点
题目: 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 = l1
cur, l1 = l1, l1.next
else:
cur.next = l2
cur, l2 = l2, l2.next
cur.next = l1 or l2
return dummy.next
递归:
递归的定义在两个链表里的 merge 操作
故,每次都是两个链表头部较小的一个与剩下的元素的 merge 操作结果合并。
注意边界条件,确保 l1 和 l2 两链表都不为空,否则返回不为空的那个节点。
def merge1(self, l1: ListNode, l2: ListNode) -> ListNode:
"""递归 优雅
"""
if not l1 or not l2:
return l1 or l2
if l1.val < l2.val:
# 因为l1小,所以l1与剩下的节点merge
l1.next = self.merge1(l1.next, l2)
return l1
else:
# 因为l2小,所以l2与剩下的节点merge
l2.next = self.merge1(l1, l2.next)
return l2
题目: leetcode 572
输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
解析: 递归
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)
题目: 牛客网
输入一个二叉树,将它变换为它的镜像。
输入树:
8
/ \
6 10
/ \ / \
5 7 9 11
输出树:
8
/ \
10 6
/ \ / \
11 9 7 5
解析:
递归:自顶向下递归交换
非递归:使用栈实现dfs,依次交换
class Solution:
def Mirror(self, root):
"""自顶向下递归交换
"""
if not root: return
root.left, root.right = root.right, root.left
self.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
题目: leetcode 101
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3]
是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
解析:
如果同时满足下面的条件,两个树互为镜像:
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的左子树相等返回True
return 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 True
s = [(root.left, root.right)]
while s:
p1, p2 = s.pop()
if not p1 and not p2: continue # 可能提前遇到叶子节点,只能跳过,不能返
if not p1 or not p2: return False
if p1.val != p2.val: return False
s += [(p1.left, p2.right), (p1.right, p2.left)]
return True
def isSymmetric(self, root: TreeNode):
"""队列
"""
if not root: return True
from collections import deque
q = deque()
q.append((root.left, root.right))
while q:
p1, p2 = q.popleft()
if not p1 and not p2: continue
if not p1 or not p2: return False
if p1.val != p2.val: return False
q += [(p1.left, p2.right), (p1.right, p2.left)]
return True
题目: 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 res
row, 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,第二个是横坐标 x
visited[y][x] = True
a, 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) % 4
a, b = x + direct[d][0], y + direct[d][1] # 在新的方向上移动
x, y = a, b
return res
解法二:
先取第一行,删除,逆时针旋转矩阵,再取第一行,直到矩阵为空
def spiralOrder(self, m):
res = []
while m:
res += m[0] # 取第一行
m = list(zip(*m[1:]))[::-1] # 顺时针旋转
return res
题目: leetcode 155
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
解析:
class MinStack:
def __init__(self):
self.s = [] # 数据栈
self.m_s = [] # 最小栈,保存数据栈插入元素后的最小值。最后是和数据栈同步的push pop
def 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]
题目:leetcode 946
两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。
假设压入栈的所有数字均不相等。
例如:
压入序列{1, 2, 3, 4, 5},序列{4,5,3,2,1}对应该压栈序列的一个弹出序列。而{4,3,5,1,2}不可能是该压栈序列的弹出序列。
解析:
用一个辅助栈,模拟pushed序列,依次压栈。
若辅助栈顶元素 == popped序列首元素,两者同时出栈,并接着循环判断是否相等
最后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
题目: 牛客网
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
解析:
使用队列实现层序遍历
class Solution:
def PrintFromTopToBottom(self, root):
from collections import deque
q = deque([root])
res = []
while q:
node = q.popleft()
if node:
res.append(node.val)
q += [node.left, node.right]
return res
题目: leetcode 102
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
解析:
层序遍历,记录每层节点 个数
def levelOrder1(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
from collections import deque
res = []
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
题目: leetcode 103
给定一个二叉树,返回其节点值的 之字形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)
解析:
和上题一样,加一个方向变量
class Solution:
def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: return []
from collections import deque
q = 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
题目: 牛客网
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。
解析:
后续遍历:[左,右,根],又因为是二叉搜索树,所以 左<根<右
序列最后一个元素是根,可以把序列分割为两部分,第一部分都小于它,第二部分都大于它。
若不满足则返回False,依次递归判断。
class Solution:
def VerifySquenceOfBST(self, seq):
def dfs(seq):
if not seq: return True
root = seq[-1] # 根
i = 0
while seq[i] < root: i += 1 # 先找到左边区域,这里最多遍历到最后一个元素,也就是根
for x in seq[i:-1]: # 右边区域若存在,这里可能为空,不满足条件则返回False
if x < root: return False
# 递归判断左右
return dfs(seq[:i]) and dfs(seq[i:-1])
if not seq: return False
return dfs(seq)
题目:
给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。
解析:
前序遍历二叉树,边遍历边加入沿途节点,若遍历到叶子节点且,路径和相等,则加入结果。否则弹出当前节点,回溯。
class Solution:
def pathSum(self, root, sumv):
def dfs(root, sumv):
sumv -= root.val
cur.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
题目: leetcode 138
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的深拷贝。
解析:
解法一:
用一个哈希表存储所有节点和对应的复制,需要O(n) 的辅助空间
遍历哈希表,链接新链表
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
m = n = head
cp = {None:None} # 1. 不用判断head是否为None,2. 不用管节点的random和next是否为None, 省去很多判断
# copy
while m:
cp[m] = Node(m.val, None, None)
m = m.next
while n:
cp[n].next = cp[n.next]
cp[n].random = cp[n.random]
n = n.next
return cp[head]
解法二:
原地修改,三步法,O(1) 的空间
def copyRandomList_1(self, head):
"""O(n) 无需额外空间"""
if not head: return head
p1 = p2 = p3 = head
#
while p1:
p1.next = Node(p1.val, p1.next, None)
p1 = p1.next.next
while p2:
if p2.random:
p2.next.random = p2.random.next
p2 = p2.next.next
new_head = p3.next
while p3.next: # p3.next存在,也就新链表的节点存在!
p3.next, p3 = p3.next.next, p3.next # 注意这里蛛节点依次变换指针,分别指向下一个的下一个节点
return new_head
题目: 牛客网
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。
要求不能创建任何新的结点,只能调整树中结点指针的指向。
注意:
解析:
解法一:
def Convert(self, root):
def inOrder(root):
if not root: return
inOrder(root.left)
res.append(root)
inOrder(root.right)
if not root: return
res = []
inOrder(root)
for i in range(len(res)- 1):
res[i].right = res[i + 1]
res[i+1].left = res[i]
return res[0]
解法二:
def Convert_1(self, root):
"""中序遍历,递归
"""
def dfs(root):
if not root: return
dfs(root.left)
root.left = self.pre
if self.pre:
self.pre.right = root
self.pre = root
dfs(root.right)
if not root: return
self.pre = None # 这里要用全局变量记录pre节点指针,因为pre一直在变。# 因为普通传参递归层数高的不会传给层数低,这个引用可以让递归层数高的改变的pre作用到层数低的函数中。
dfs(root)
while root.left:
root = root.left
return root
题目: 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 root
nodes = iter(data.split(','))
root = dfs(nodes)
return root
题目: leetcode 46
给定一个没有重复数字的序列,返回其所有可能的全排列。
解析: 全排列
回溯法:
是一种通过探索所有可能的候选解来找出所有的解的算法。如果候选解被确认 不是 一个解的话(或者至少不是 最后一个 解),回溯算法会通过在上一步进行一些变化抛弃该解,即 回溯 并且再次尝
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
题目: leetcode 78
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
返回的结果包括空列表。
解析: 子集
解法一:动态规划,递推
dp[i] = dp[i-1] + [each+[nums[i]] for each in dp[i-1]]
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
题目: leetcode 51
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
即任意两个棋子不在同一行,同一列,同一条对角线。
解析:
解法一:回溯
判断该棋子是否在已有棋子的列上,主对角线,次对角线上
用一个一维数组queens保存棋子,索引表示行号,值表示列号。
回溯步骤:
column
中放置皇后.注意 :若把需要回溯的状态放入递归函数的变量可自动回溯,如下面第二个函数
我们如果把 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, False
backtrack() # 向下一行探索
queens.pop() # 回溯,挪开棋子
xy_dif[row - col], xy_sum[row + col] = True, True
queens = []
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)
return
for 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]
解法二:暴搜,慢
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 res
def 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 False
return True
题目: leetocode 168
给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 ⌊ n/2 ⌋
的元素。
解析:
解法一:摩尔投票法
如果我们把众数记为 +1 ,把其他数记为 −1 ,将它们全部加起来,显然和大于 0 ,从结果本身我们可以看出众数比其他数多。
我们维护一个计数器,如果遇到一个我们目前的侯选众数,就将计数器加一,否则减一。只要计算器等于0,我们就将 nums 中之前访问的数字全部忘记,并把下一个数字当作侯选的众数。
class Solution:
def majorityElement(self, nums: List[int]) -> int:
count, res = 0, None
for x in nums:
if count == 0:
res = x
count += 1 if x == res else -1
return res
解法二:先排序,后取中位数
def majorityElement(self, nums: List[int]) -> int:
"""Time: O(nlogn), Space: O(n)
将数组排序后,出现次数大于一半的数一定在数组的中间
"""
nums.sort()
return nums[len(nums) // 2]
题目: 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 randint
i = 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 -= 1
nums[l] = nums[r]
while l < r and nums[l] <= pivot:
l += 1
nums[r] = nums[l]
nums[l] = pivot
return l
def 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]
题目: leetcode 295
数据流中的数是流动的,如何找到一个数据流中的中位数。
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
解析:
由于数据是从一个数据流中读出来的,所以数据的数目随着时间的变化而增加。如果用一个数据容器来保存从流中读出来的数据,则当有新的数据出来时就需要插入数据容器中。这个数据容器用什么定义最合适呢?
因此我们想找到一种相当快速的方法来插入容器,那么所产生的额外操作可能会减少。
并且我们想找到中位数,并不一定要排序,只需保证前一半的元素都小于这个数,后一半的元素都大于这个数。
所以我们需要这样一种数据结构:
事实证明,有两种数据结构符合:
堆是这道题的天然原料!向元素添加元素需要对数时间复杂度。它们还可以直接访问组中的最大/最小元素。
我们可以这样维护两个堆:
其次我们要保证两个堆是平衡的,一个堆的长度最多比另外一个多1。
插入
方法一:
方法二:
取中位数:
注意python自带的只有最小堆。
时间复杂度:O(5*logn) + O(1) = O(logn)
空间复杂度: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_heap
if 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长度+1
hq.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]
题目: 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 个数字结尾的字数组的最大和,那么我们需要求出 m a x [ 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} …
时间复杂度: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 0
f = [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
题目: 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的个数时,
所以得到公式:高位 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, 1
while i <= n: # i 依次个十百位的算,直到大于 n 为止。
cnt += n // (i * 10) * i + min(max(n % (i * 10) - i + 1, 0), i)
i *= 10
return cnt
题目:leetcode 400
在无限的整数序列 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …中找到第 n 个数字。
注意:此处序列是从1开始的。
解析: 解析法
分析得到:
所以解题步骤是:
注意:上面的分析是基于数列从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 i
return int(str(number)[idx - 1])
题目: 牛客网 Acwing
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
例如输入数组[3, 32, 321],则打印出这3个数字能排成的最小数字321323。
解析: 定义排序规则,Time:O(nlogn)
定义 ab < ba 时,a < b
用此规则进行排序,从小到大。拼接出来就是最小的数字。
注意python2和python3自定义排序规则的区别。python3要用cmp_to_key将cmp转化为key。
解释两者为什么等价
cmp(a, b):
- 当a>b, 返回 1
- 当a=b, 返回 0
- 当a
所以等价于如下:
cmp(a, b) <==> (a>b) - (a(a>b) - (a
- 当a>b, 返回 1 - 0 = 1
- 当a=b, 返回 0 - 0 = 0
- 当a
from functools import cmp_to_key
class 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))
# python3
if 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 + b
s2 = b + a
if s1 > s2: return 1
elif s1 < s2: return -1
else: return 0
题目: 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’
状态表示
f[i] 表示前 i 位数字有多少不同的表示方式,求 f[n]
状态转移方程
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]
边界
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, 1
for i in range(2, n + 1): # 2~n 前n个数字
# 此时要得到第 i-1 个和第 i 个数字,对应字符串下标 i-2 和 i-1
if '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不能是字符串的开头!
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 0
else:
if '10' <= s[i - 2:i] <= '26':
f[i] = f[i - 1] + f[i - 2]
else:
f[i] = f[i - 1]
return f[n]
题目: 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 - 1]不会越界。
class Solution(object):
def getMaxValue(self, grid):
if not grid: return
rows, 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]
题目: leetcode 3
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
假设字符串中只包含从’a’到’z’的字符。
解析:
双指针算法,滑动窗口
l, r两个指针看作滑动窗口两端,窗口的长度为 r - l + 1
当窗口内出现重复字符时,移除左边元素直到删除重复元素,继续移动窗口右端。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
"""滑窗法
"""
if not s: return 0
l = 0
mem = set()
max_len = 0
for r in range(len(s)):
while s[r] in mem:
mem.remove(s[l])
l += 1
max_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, 0
pos = {} # 记录每个出现字母的下标
for r, ch in enumerate(s):
if ch in pos:
# 如果重复,则起始点为重复点的下一位开始
l = max(pos[ch] + 1, l) #这里max指l不能后退,有可能此时重复的数字比起始点l还要前
pos[ch] = r
maxlen = max(maxlen, r-l+1)
return maxlen
题目: leetcode 264
编写一个程序,找出第 n
个丑数。丑数就是只包含质因数 2, 3, 5
的正整数。习惯上把 1 当作第一个丑数。
解析: 牛客网
一个丑数一定是由另一个丑数乘以2或者乘以3或者乘以5得到。即丑数为 p = 2 x ∗ 3 y ∗ 5 z p = 2 ^ x * 3 ^ y * 5 ^ z p=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, 0
res = [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 += 1
if res[p3]*3 == min_num:
p3 += 1
if res[p5]*5 == min_num:
p5 += 1
return res[-1]
题目:leetcode 387
给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。
解析:
class Solution:
def firstUniqChar(self, s: str) -> int:
if not s:
return -1
count = {}
for x in s:
if x not in count:
count[x] = 1
else:
count[x] += 1
for key in count:
if count[key] == 1:
return s.index(key)
return -1
题目: 牛客网
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"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 ch
def Insert(self, char):
# 如果是第一次出现,则将对应元素的值改为index
if self.count[ord(char)] == -1:
self.count[ord(char)] = self.index
# 如果已经出现过一次,则修改为-2
elif self.count[ord(char)] >= 0:
self.count[ord(char)] = -2
self.index += 1
题目: 牛客网 Acwing
在数组中的两个数字如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
输入一个数组,求出这个数组中的逆序对的总数。
解析: 分治法
其实就是归并排序。
假设分隔到最小的两个数组,p1指向 left 数组中的元素,p2指向 right数组中的元素,此时两数组内部是有序的。
class Solution:
def inversePairs(self, nums):
"""分治法:O(nlogn) Space: O(n)"""
self.count = 0
self.merge_sort(nums)
return self.count
def merge_sort(self, nums):
if len(nums) <= 1:
return nums
mid = len(nums) >> 1
left = 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 += 1
else:
tmp.append(right[r])
r += 1
# 逆序对数
self.count += len(left[l:])
tmp += left[l:] + right[r:]
return tmp
题目: leetcode 160
编写一个程序,找到两个单链表相交的起始节点。
如链表A和链表B。两链表在节点 c1 处相交。
解析:
解法一:哈希表法
用一个哈希表存储链表A的所有节点,然后检查链表 B
中的每一个结点 b_i 是否在哈希表中。若在,则 b_i 为相交结点。
class Solution(object):
def getIntersectionNode(self, headA, headB):
dic = set()
p1 = headA
while p1:
dic.add(p1)
p1 = p1.next
while headB:
if headB in dic:
return headB
headB = headB.next
解法二:双指针法
消除长度差: 拼接链表A和链表B。p1,p2分别从两个链表的头部开始遍历,当走到尾节点时,则从另外一个链表的头部继续往前走。这样,若存在相交的点,p1,p2必会在一个点相遇。
class Solution(object):
def getIntersectionNode(self, headA, headB):
if not headA or not headB:
return
q, p = headA, headB
while q != p:
q = q.next if q else headB
p = p.next if p else headA
return q
题目: leetcode 34
给定一个按照升序排列的整数数组 nums
,和一个目标值 target
。找出给定目标值在数组中的开始位置和结束位置。
解析: 二分法
因为有序查找,所以二分。O(logn)
第一次二分找到target的第一个位置,第二次二分找到target的最后一个位置。
class Solution(object):
def searchRange(self, nums, target):
if not nums: return -1, -1
l, r = 0, len(nums) - 1 # 第一次二分查找,找到重复数字最左边的索引
while l < r:
mid = l + (r - l) // 2
if nums[mid] < target: # 说明答案在右边,不包含mid
l = mid + 1
else:
r = mid
if nums[l] != target: return -1, -1 # 如果没有找到这个数,则说明不存在
left = l # 重复数字最左边的索引
l, r = 0, len(nums) - 1 # 第二次二分查找,找到重复数字最右边的索引
while l < r:
mid = l + (r - l + 1) // 2
if nums[mid] <= target: # 说明答案在右边,包含mid,故上面加1
l = mid
else:
r = mid - 1
return left, r
def searchRange_2(self, nums, target):
"""暴力法, 一次循环
"""
res = []
for i, x in enumerate(nums):
if x == target:
res.append(i)
if not res: return -1, -1
return res[0], res[-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 0
l, r = 0, len(nums) - 1
while l < r:
mid = l + r >> 1
if nums[mid] == mid:
l = mid + 1
else:
r = mid
if 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) # 假设缺失值为 n
for i, num in enumerate(nums):
miss ^= i ^ num
return miss
题目:
假设一个单调递增的数组里的每个元素都是整数并且是唯一的。
请编程实现一个函数找出数组中任意一个数值等于其下标的元素。
例如,在数组[-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 >> 1
if nums[mid] < mid: l = mid + 1
else: r = mid
if nums[l] != l: return -1
return nums[l]
题目: 牛客网 Acwing
给定一棵二叉搜索树,请找出其中的第k小的结点。
你可以假设树和k都存在,并且1≤k≤树的总结点数。
解析:
中序遍历的同时,没遍历一次,k–,当k = 0时,为答案。
class Solution(object):
def kthNode(self, root, k):
def dfs(root):
if not root: return
dfs(root.left)
self.k -= 1
if self.k == 0: self.res = root
if self.k > 0: dfs(root.right) # 剪枝
self.k = k # k要为全局变量
self.res = None
dfs(root)
return self.res
题目:
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
解析:
递归
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root: return 0
left = self.maxDepth(root.left)
right = self.maxDepth(root.right)
return max(left, right) + 1
题目: leetcdoe 110
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
解析:
递归的同时记录高度,可以考虑加上剪枝
def isBalanced_2(self, root):
def dfs(root):
if not root: return 0
left = dfs(root.left)
right = dfs(root.right)
if abs(left - right) > 1:
self.ans = False
return max(left, right) + 1
self.ans = True
dfs(root)
return self.ans
题目: leetcode
给定一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
解析:
解法一:哈希表
两遍循环,第一遍记录每个数字出现的次数。第二遍筛选出只出现一次的数字。
class Solution:
def singleNumber(self, nums: List[int]) -> List[int]:
dic = {}
for x in nums:
if x not in dic:
dic[x] = 1
else:
dic[x] += 1
res = []
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 = 0
for num in nums:
mask ^= num
k = 0 # 找到最后为1的一位
while not mask >> k & 1:
k += 1
first = 0
for x in nums:
if x >> k & 1:
first ^= x
return first, first ^ mask
题目: leetcode 137
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
解析:
如果一个数字出现三次,那么它的二进制表示的每一位(0或者1)也出现三次。如果把所有出现三次的数字的二进制表示的每一位都分别加起来,那么每一位都能被3整除。
把数组中所有数字的二进制表示的每一位都加起来。如果某一位的和能被 3 整除,那么那个只出现一次的数字二进制对应的那一为是 0,否则就是 1。
def singleNumber(self, nums):
ans = 0
for i in range(32):
bitCount = 0 # 第i位的 1 出现次数的总和
for num in nums:
if num >> i & 1:
bitCount += 1
if bitCount % 3 != 0: # 如果次数不能被 3 整除,则单独的那个数此位为1
ans |= 1 << i
return self.convert(ans)
def convert(self, x):
if x >= 2**31:
x -= 2**32
return x
题目: Acwing
输入一个数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。
如果有多对数字的和等于s,输出任意一对即可。
你可以认为每组输入中都至少含有一组满足条件的输出。
解析:
因为递增有序,l, r指针分别指向最左和最右,即 l 指向最小的元素,r 指向最大的元素,若两者之和大于目标,r 往左移,若两者之和小于目标,l 往右移,等于则返回
class Solution:
def FindNumbersWithSum(self, nums, target):
l, r = 0, len(nums) - 1
while l < r:
if nums[l] + nums[r] == target:
return nums[l], nums[r]
elif nums[l] + nums[r] < target:
l += 1
else:
r -= 1
return []
题目: 牛客网 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。
class Solution(object):
def findContinuousSequence(self, sum):
l, r, cur_sum = 1, 2, 3
res = []
mid = sum + 1 >> 1 # 至少两个数,所以小的那个数不可能大于和的一半。
while l <= mid:
if cur_sum < sum: # 当前和小于sum时
r += 1 # 右移r
cur_sum += r # 加上新的数
elif cur_sum > sum: # 当前和大于sum时
cur_sum -= l # 减去最小数
l += 1 # 右移l
else: # 等于时加入答案,并且继续右移指针
res.append(list(range(l,r+1)))
r += 1
cur_sum += r
return res
题目: 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 = i
while j < len(s) and s[j] != ' ':
j += 1
res += reversed(s[i:j])
i = j
return ''.join(res)
题目: 牛客网 Acwing
请定义一个函数实现字符串左旋转操作的功能。
比如输入字符串"abcdefg"
和数字2,该函数将返回左旋转2位得到的结果"cdefgab"
。
解析:
python 不用多说
class Solution(object):
def leftRotateString(self, s, n):
return s[n:] + s[:n]
题目: 牛客网 Acwing
给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。
例如,如果输入数组[2, 3, 4, 2, 6, 2, 5, 1]及滑动窗口的大小3,那么一共存在6个滑动窗口,它们的最大值分别为[4, 4, 6, 6, 6, 5]。
解析:
使用一个双端队列。队列中存入数组元素的下标。
def maxInWindows_2(nums, k):
"""两端开口的队列
"""
from collections import deque
if 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
题目: 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]状态转移:
dp[i][j]=dp[i-1][j-1]+dp[i-1][j-2]+...+dp[i-1][j-6]
边界:
dp[0][0] = 1
,当一个骰子都没有时,总和为0的情况下的方案数为 1class Solution(object):
def numberOfDice(self, n):
# 每个地方索引+1都是因为方便计算。因为是从骰子个数和一个骰子的范围都是从 1 开始的。
f = [[0]*(6*n + 1) for _ in range(n + 1)]
f[0][0] = 1
for 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 <= j
return [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 res
def dfs(self, n, sum): # n为骰子个数,sum为总和
# 求解总和为某个n时的可能的投掷的所有方案数
if sum < 0: return 0
if 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
题目: 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 False
nums.sort()
k = 0
while not nums[k]: # 找到第一个不为0的元素的索引
k += 1
for i in range(k, len(nums)-1):
if nums[i] == nums[i + 1]: # 若有重复的则不可能为顺子
return False
return nums[-1] - nums[k] <= 4
题目: 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 -1
last = 0 # n=1的时候只剩一个人那么0就活了
for i in range(2, n + 1): # 从两个人开始
last = (last + m) % i # 每次模 上一次 的总人数
return last
def lastRemaining_1(self, n, m):
"""递归, 可能会爆栈
"""
if n==1: return 0
return (self.lastRemaining(n - 1, m) + m)%n
题目: leetcode 121
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
解析:
动态规划。
当前位置的最大利润等于当前价格减去之前的最低价格。
状态表示:
f[i]
为卖出价为第 i 个数字时可能获得的最大利润max(f[i])
转移方程:
f[i] = nums[i] - minValue
,minValue
是 i
元素之前的最小价格minValue = min(minValue, nums[i-1])
边界:
minValue = nums[0]
f[0] = 0
未买入前是不允许卖出的,所以第一个元素不能为卖出价,所以利润是0class Solution:
def maxProfit(self, nums: List[int]) -> int:
if not nums: return 0
f = 0 # f[0] = 0
minPrice = nums[0]
for i in range(1, len(nums)): # 枚举每个状态
minPrice = min(nums[i-1], minPrice)
f = max(nums[i] - minPrice , f)
return f
题目: 牛客网 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))
题目: leetcode 371
不使用运算符 +
和 -
,计算两整数 a
、b
之和。
解析:
将加法分为三部分:
求出无进制加法结果 s1
我们先来观察下位运算中的两数加法,其实来来回回就只有下面这四种:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 0(进位 1)
等价与异或运算。
求出进位 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 左移一位,才是我们所要的进位结果。
s = s1 + s2
因为python正数没有位数限制,可以用numpy来处理。
def getSum(num1, num2):
import numpy as np
while num2:
sumv = np.int32(num1 ^ num2)
carry = np.int32((num1 & num2) << 1)
# 此时应该返回 sumv + carry,但是不能用加法
# 所以循环,直到没有进位,也就不需要相加
num1, num2 = sumv, carry
return int(num1)
题目: 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-2
B = [1]
for i in range(n-1):
B.append(B[-1] * A[i])
# 再算第二部分: 从后往前依次乘剩余的部分
temp = 1 # 最后一项乘以1,B[n-1]=A0A1An-2
for i in range(n-1, -1, -1):
B[i] = B[i] * temp
temp *= A[i]
return B
题目: Acwing
请你写一个函数StrToInt,实现把字符串转换成整数这个功能。
当然,不能使用atoi或者其他类似的库函数。
样例
输入:"123"
输出:123
注意:
你的函数应满足下列条件:
解析:
class Solution:
def strToInt(self, s):
self.input_valid = True
if not s:
self.input_valid = False # 标志输如是否合法
return 0
k = 0
while k<len(s) and s[k] == ' ':
k += 1 # 去掉行首空格
number = 0
is_minus = False
if s[k] == '+':
k += 1
elif s[k] == '-':
k += 1
is_minus = True
while k < len(s) and '0' <= s[k] <= '9':
number = number * 10 + int(s[k])
k += 1
if is_minus:
number *= -1
if number > 2 ** 31 - 1: # MAX_INT
return 2 ** 31 - 1
if number < - 2 ** 31: # MIN_INT
return -2**31
return number
题目: leetcode 236
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
解析:leetcode
解法一:直接在递归的过程中判断。
解法二:搜出路径,再判断。
class Solution:
def lowestCommonAncestor(self, root, p, q):
self.ans = None
self.dfs(root, p, q)
return self.ans
def dfs(self, root, p, q):
if not root: return False
left = self.dfs(root.left, p, q)
right = self.dfs(root.right, p, q)
# If the current node is one of p or q
m = root == p or root == q
# 左右子树和自身只要任意两个存在则返回答案
if m + right + left >= 2:
self.ans = root
# 左中右只要有一个存在就返回True
return m or right or left
def lowestCommonAncestor_1(self, root, p, q):
"""先找到两条根节点到节点的路径
再找到最后一个公共字子节点,则为最近公共祖先
"""
if not root or not p or not q:
return
path_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: return
path.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()