文章目录
-
- 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 = 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
3 找出数组中重复的数字
题目描述:
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,
但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
解析:
长度为n,数字范围0~n-1,如果这个数组不存在重复的数字,那么当数组排序后数字 i 将出现在下标为 i 的位置。
即下方跳出 while 循环。
- 让我们重新在排列这个数组,从头到尾依次扫描每个数字。当扫到下标为 i 的数字,首先判断这个数字(m)是否等于 i。如果是,则扫描下一个数字
- 若不是,则再拿它和下标为 m 的数字比较,相等则找到一个重复的数字 (该数字在下标为 i 和 m 的位置都出现了),若不等则交换两者位置。使得数字 m 对应下标 m。
- 接着继续重复这个过程,直到找到重复数字为止。
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
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: 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
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 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
5 替换空格
题目:
请实现一个函数,把字符串中的每个空格替换成"%20"。
解析:
- 首先遍历一遍原数组,求出最终答案的长度length;
- 将原数组resize成length大小;
- 使用两个指针,指针i指向原字符串的末尾,指针j指向length的位置;
- 两个指针分别从后往前遍历,如果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 = 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')
6 从尾到头打印链表
题目:
输入一个链表的头结点,按照 从尾到头 的顺序返回节点的值。
解析:
- 遍历+倒序
- 递归
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)
7 重建二叉树
题目: leetcode 105.
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解析:
- 找到各个子树的根节点 root
- 递归构建该根节点的左子树
- 递归构建该根节点的右子树
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
8 二叉树的下一个节点
题目: 牛客网
给定一棵二叉树的其中一个节点,请找出中序遍历 [左,根,右] 序列的下一个节点。
- 如果给定的节点是中序遍历序列的最后一个,则返回空节点;
- 二叉树一定不为空,且给定的节点一定不是空节点;
解析:
- 当此节点有右子树,下一个节点就是右子树中最左侧的节点。
- 当此节点没有右子树时,
- 若它是它父节点的左节点,那么下一个节点就是它的父节点
- 若不是,就沿着父指针向上遍历,直到找到一个是它父节点的左节点,这个父节点就是我们要找的下一个节点。
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
9 用两个栈实现队列
题目: leetcode 232
请用栈实现一个队列,支持如下四种操作:
- push(x) – 将元素x插到队尾;
- pop() – 将队首的元素弹出,并返回该元素;
- peek() – 返回队首元素;
- empty() – 返回队列是否为空;
解析:
-
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.
9.1 用两个队列实现一个栈
题目: leetcode 225
使用队列实现栈的下列操作:
push(x) – 元素 x 入栈
pop() – 移除栈顶元素
top() – 获取栈顶元素
empty() – 返回栈是否为空
解析:
- push的时候保证栈的特性即可,栈是先进候出,队列先进先出,入队时,将队1所有元素放入队2,将元素x入队1,再将队2所有元素入队1,则保证了栈的特性
- pop直接返回队1队首元素即可。
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
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 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]
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, 0
for _ in range(3, number+1):
fn = f1 + f2
f1, f2 = f2, fn
return 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)
- 第一个小矩形竖着放时,剩余的 2x(n-1) 的矩形的摆法是 f(n-1)
- 第一个小矩形横着放时,下面必须再横着放一个小矩形,剩余的 2x(n-2) 的矩形的摆法是 f(n-2)
- 故 f(n) = f(n-1) + f(n-2),f(1) = 1, f(2) = 2
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
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) - 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 >>