目录
03. 数组中重复的数字: hash表、数组遍历
04. 二维数组中的查找:二分查找的思路
05. 替换空格:注意“”.join的用法
06. 从尾到头打印链表:python list逆序输出、栈实现
07. 重建二叉树:树的遍历、递归和栈
用栈进行迭代(*):
09. 用两个栈实现队列:辅助栈
10-I. 斐波那契数列:简单的递归、简单的动态规划
动态规划(简单):
10-II. 青蛙跳台阶问题:
11. 旋转数组的最小数字:(二分法**)
12. 矩阵中的路径:dfs(*)
13. 机器人的运动范围:Bfs(*)
14-I. 剪绳子:(数学推导*、贪心*)
14-II. 剪绳子2:(在剪绳子1的基础上多了大数求余(循环求余,快速幂求余)**)
15. 二进制中1的个数:(位运算)
16. 数值的整数次方:(快速幂**)
17. 打印从1到最大的n位数:(将该题推广到大数范围*)
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
PS: 2 <= n <= 100000
一刷:
遍历数组再用hash表存放:
class Solution:
def findRepeatNumber(self, nums: List[int]) -> int:
numberset = dict()
for i in nums:
if i in numberset:
return i
else:
numberset[i] = 1
return -1
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
PS:
限制:0 <= n <= 1000,0 <= m <= 1000
一刷:
从左到右递增,从上到下递增, 那么从最右上角开始查找就像是一棵二分查找树
class Solution:
def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool:
if matrix == []:
return False
i = 0
j = len(matrix[0]) - 1
while((j >= 0) and ( i <= len(matrix) - 1)):
print(i)
if matrix[i][j] == target:
return True
elif matrix[i][j] < target:
i += 1
elif matrix[i][j] > target:
j -= 1
return False
时间复杂度O(n+m)
题目:请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
示例:
输入:s = "We are happy."
输出:"We%20are%20happy."
PS:0 <= s 的长度 <= 10000
class Solution:
def replaceSpace(self, s: str) -> str:
res = []
for c in s:
if c == ' ':
res.append("%20")
else:
res.append(c)
return "".join(res)
题目:输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)
示例:
输入:head = [1,3,2]
输出:[2,3,1]
PS: 0<= 链表长度 <= 10000
一刷:python list可实现逆序输出
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class solution:
def reversePrint(self, head:ListNode) -> List[int]:
res = []
while(head != None):
res.append(head.val)
head = head.next
return res[::-1]
当然也可以用栈来实现:
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class stack(object):
def __init__(self):
self.top=None # 初始栈顶为空
def push(self, n):
n = ListNode(n)
if self.top == None:
self.top = n
else:
n.next = self.top
self.top = n
def pop(self, n):
if self.top == None:
print("stack is empty")
return None
else:
temp = self.top.val
self.top = self.top.next
return temp
def allquit(self): # 打印栈,完成后栈为空
alist = []
while self.top != None:
temp = self.top.val
self.top = self.top.next
alist.append(temp)
print(alist)
class Solution:
def reversePrint(self, head: ListNode) -> List[int]:
res = []
s = stack()
while(head != None):
s.push(head.val)
# res.append(head.val)
head = head.next
while s.top != None:
temp = s.top.val
s.top = s.top.next
res.append(temp)
return res
题目:输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
样例:
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
一刷:
递归:
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
i = 0
n = len(inorder)
if preorder == []:
return None
tree = TreeNode(preorder[0])
while(i < n):
if inorder[i] == preorder[0]:
break
i += 1
tree.left = self.buildTree(preorder[1:1+i],inorder[0:i])
tree.right = self.buildTree(preorder[i+1::],inorder[i+1::])
return tree
class Solution:
def buildTree(self, preorder: List[int], inorder:List[int]) -> TreeNode:
if len(preorder) == 0:
return None
stack = []
i = 0
root = TreeNode(preorder[0])
stack.append(root)
for p in range(1, len(preorder)):
node = stack[-1]
if node.val != inorder[i]:
node.left = TreeNode(preorder[p])
stack.append(node.left)
else:
while((len(stack) != 0) and (stack[-1].val == inorder[i])):
node = stack.pop()
i += 1
node.right = TreeNode(preorder[p])
stack.append(node.right)
return root
题目:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例:
输入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
输出:[null,null,3,-1]
一刷:栈的作用是先入后出,而队列是先入先出,那么用栈实现队列取数顺序,意味着需要将栈内的元素进行反序排序,那么就可以利用辅助栈来进行一个反序操作,这样做插入的时间复杂度是O(n^2),空间复杂度O(n)
class CQueue:
def __init__(self):
# 存放队列元素
self.stack1 = []
# 辅助栈
self.stack2 = []
def appendTail(self, value: int) -> None:
if (self.stack1 == []) and (self.stack2 == []):
self.stack1.append(value)
else:
# 插入新数之前先将栈1(已经按照队列顺序排好)逆序存放到辅助栈中
while(self.stack1 != []):
self.stack2.append(self.stack1.pop())
# 栈1放入存放元素
self.stack1.append(value)
# 辅助栈元素反序进入栈1,则栈1的输出顺序就跟队列一致了
while(self.stack2 != []):
self.stack1.append(self.stack2.pop())
def deleteHead(self) -> int:
if self.stack1 == []:
return -1
else:
return self.stack1.pop()
最佳解法是用一个栈来实现插入,另一个栈来实现删除,当需要删除时对辅助栈进行操作,如果为空则从stack1里弹出所有元素。否则从辅助栈弹出元素,如果都为空就返回-1,这样每个元素都只会移动一次,时间复杂度为O(1),空间复杂度为O(n)
class CQueue:
def __init__(self):
self.stack1 = []
self.stack2 = []
def appendTail(self, value: int) -> None:
self.stack1.append(value)
def deleteHead(self) -> int:
if self.stack2 == []:
if self.stack1 == []:
return -1
else:
while(self.stack1 != []):
self.stack2.append(self.stack1.pop())
return self.stack2.pop()
else:
return self.stack2.pop()
写一个函数,输入 n
,求斐波那契(Fibonacci)数列的第 n
项。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
一刷:递归, 当然递归时间复杂度为O(2^n)(二叉树的节点数),空间复杂度O(n),时间会炸:
class Solution:
def fib(self, n: int) -> int:
if n == 1:
return 1
elif n == 0:
return 0
else:
return (self.fib(n-1) + self.fib(n-2)) % (1000000007)
class Solution:
def fib(self, n: int) -> int:
if n == 1:
return 1
if n == 0:
return 0
dp = [0 for x in range(0,n+1)]
dp[0] = 0
dp[1] = 1
for i in range(2,n+1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n] % (1000000007)
这里踩了一个坑,如果1000000007用1e9+7代替的话会出现精度问题,单独列出来有待思考。
当然上面的动态规划时间复杂度和空间复杂度都是O(n),其实还能在空间上进行优化,因为最后的结果只与之前的两个状态有关
由于 Python 中整形数字的大小限制 取决计算机的内存 (可理解为无限大),因此可不考虑大数越界问题。
如果用java这类严格定义存储类型的语言则要考虑内存溢出问题,就要在循环中添加求余操作
class Solution:
def fib(self, n: int) -> int:
if n == 1:
return 1
if n == 0:
return 0
sum_ = 0
dp1 = 0
dp2 = 1
for i in range(2,n+1):
sum_ = dp1 + dp2
dp1 = dp2
dp2 = sum_
return sum_ % (1000000007)
最简洁的写法,其实实际运行效率还不如上面把0和1单独放一边,sum作为返回可读性也更好:
class Solution:
def fib(self, n: int) -> int:
dp1 = 0
dp2 = 1
for i in range(n):
dp1, dp2 = dp1 + dp2, dp1
return dp1 % (1000000007)
题目:
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 :
输入:n = 2
输出:2
输入:n = 7
输出:21
输入:n = 0
输出:1
一刷:
动态规划的思想,青蛙每次可以跳1步,也可以跳两步,那么青蛙最后一步只能跳一步或者两步,则令f(n)表示n级台阶的跳法,有:f(n) = f(n-1) + f(n-2), 则变成了求解斐波那契数列(上一个问题), 注意初项f(0) = 1
class Solution:
def numWays(self, n: int) -> int:
if n == 0:
return 1
if n == 1:
return 1
dp1 = 1
dp2 = 1
for i in range(2, n+1):
dp2, dp1 = dp2 + dp1, dp2
return dp2%1000000007
题目:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
一刷:
看数字出现的规律,最小的数左边不会比它大,右边不会比它小,最差的解法是暴力搜索,时间复杂度为O(n),很自然可以想到利用折半查找的方式对其进行优化。
分析指针mid指向中间,left指向最左,right指向最优,那么只会出现三种情况
1. mid < right :mid到right是顺序递增的,最小值在mid或者left到mid之间,right指向mid,更新mid
2. mid > right : mid到right是乱序的,说明最小值在mid到right之间,left指向mid+1,更新mid
3. mid == right: 最小值在mid左右两端都有可能,这个时候缩小right边界
循环方式有了,就差一个停止条件,left + 1 :>= right
最后判断一下,mid比左边大,就返回left,否则返回right就行
class Solution:
def minArray(self, numbers: List[int]) -> int:
if numbers == []:
return None
if len(numbers) == 1:
return numbers[0]
left = 0
right = len(numbers) - 1
while(left < right):
mid = ((right - left) >> 1) + left
if numbers[mid] < numbers[right]:
right = mid
elif numbers[mid] > numbers[right]:
left = mid + 1
else:
right -= 1
return numbers[left]
题目:
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[["a","b","c","e"],
["s","f","c","s"],
["a","d","e","e"]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
一刷:很容易想到要用深度优先遍历来进行,写法上需要注意的地方有很多
1. 停止递归条件:
返回False: i和j越界,当前值不等于word[k]
返回True: k == len(word) - 1时
2. 访问过的元素标记
在往后遍历前将其变为‘\’表示访问过了,遍历结束后还得记得变为原来的值
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
word_len = len(word)
row = len(board) - 1
col = len(board[0]) - 1
def dfs(i: int, j: int, k: int) -> bool:
# 超出边界
if (i < 0) or (i > row) or (j < 0) or (j > col) or(board[i][j] != word[k]):
return False
if k == word_len - 1:
return True
# 记录访问过的***********
tmp, board[i][j] = board[i][j], '/'
# 依次向上、下、左、右查找
res = dfs(i+1, j, k + 1) or dfs(i-1, j, k + 1) or dfs(i, j+1, k + 1) or dfs(i, j-1, k + 1)
# 变回来***********
board[i][j] = tmp
return res
for i in range(row+1):
for j in range(col+1):
if dfs(i, j, 0):
return True
return False
ps:注意| 和or的执行时间有何区别?(将res赋值语句中的or改成|为何通过不了)
要知道为什么可以参考Python中 I、& 和 and、or在逻辑运算时有何不同
题目:
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [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, m: int, n: int, k: int) -> int:
# 记录访问过的节点
visited_nodes = []
def qut(num:int) -> int:
return num // 100 + (num % 100) // 10 + (num % 10)
def dfs(row: int, col:int) -> bool:
if (row < 0) or (row >=m) or (col < 0) or (col >= n) or ((row, col) in visited_nodes):
return False
if qut(row) + qut(col) > k:
return False
visited_nodes.append((row, col))
dfs(row - 1, col)
dfs(row + 1, col)
dfs(row, col-1)
dfs(row, col+1)
return True
dfs(0, 0)
return len(visited_nodes)
其实这道题的思路应该是广度优先遍历
题目:
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。ps:2<=n<=58
例如
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
一刷:
数学推导
class Solution:
def cuttingRope(self, n: int) -> int:
# 小于3,按题目要求(m>1),需切除一个1:
if n <= 3:
return n-1
else:
m = n // 3
if n % 3 == 2:
return 2 * 3 ** m
elif n % 3 == 1:
return 4 * 3 ** (m-1)
else:
return 3 ** m
贪心思想——>从初始取值开始,找到合理切分的规律
题目:
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。ps:2<=n<=1000
大数求余————>:
1.当m增大时,最后返回的3^m大小以指数级别增长,可能超出int32甚至int64的取值范围,导致返回值错误。
由于语言特性,理论上 Python 中的变量取值范围由系统内存大小决定(无限大),因此在 Python 中其实不用考虑大数越界问题。
2.在仅使用int32类型存储的前提下,正确计算x^m对p求余的值
3.解决方式有循环求余、快速幂求余,其中后者的时间复杂度更低
4.求余思路——>(xy)⊙p=[(x⊙p)(y⊙p)]⊙p
题目:
请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。
一刷:逐位判断,时间复杂度O(logn)
class Solution:
def hammingWeight(self, n: int) -> int:
num = 0
while(n != 0):
if n % 2 == 1:
num += 1
n = n >> 1
return num
当然用&运算更好,直接将n&1相与,不过在小数据下差别不大
class Solution:
def hammingWeight(self, n: int) -> int:
num = 0
while(n != 0):
num += n & 1
n >>= 1
return num
更优的方法是利用n&(n-1)巧解
n&(n-1): 二进制数字n最右边的1变成0,其余不变, 只需要在循环中消去1即可,时间复杂度从O()降为O(m),m为1的个数
class Solution:
def hammingWeight(self, n: int) -> int:
num = 0
while n:
num += 1
n = n&(n-1)
return num
题目:实现函数duoble power(duoble base, int exponent), 求base的exponet次方,不得使用库函数,同时不需要考虑大数问题
一刷:利用剪绳子2中提到的快速幂思想来解
class Solution:
def myPow(self, x: float, n: int) -> float:
# 将n处理成正数
if n<0: x, n = 1/x, -n
rem = 1
while(n):
if n%2: rem = rem * x
x = x**2
n >>= 1
return rem
题目:输入数字 n
,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999,leetcode上的list类型为int,没考虑大数。
class Solution:
def printNumbers(self, n: int) -> List[int]:
# result = [x for x in range(1, 10**n)]
return list(range(1, 10**n))
(将该题推广到大数范围*)
1. 大数只能用string类型来存储
2. 使用int类型,每轮可以通过+1生成下个数字,而此方法无法应用至String类型。并且,String类型的数字的进位操作效率较低,列如“9999” 至 "10000" 需要从个位到千位循环判断,进位4次。 *生成列表实际上是n位0-9的全排列,因此可避开进位操作,通过递归生成数字的String列表。基于分治算法的思想,先固定高位,向低位递归,当个位已被固定时,添加数字的字符串。列如,当n=2时(数字范围1-99),固定十位为0-9,按顺序依次开启递归,终止递归并添加数字字符串。
class Solution:
def printNumbers(self, n: int) -> List[int]:
def dfs(x):
if x == n: # 终止条件,以固定所有位
# 考虑去除高位的0
s = "".join(num[self.start:])
if s != '0': res.append("".join(s)) #拼接num并添加至res尾部
if n - self.start == self.nine: self.start -= 1
return
for i in range(10): # 遍历0-9
num[x] = str(i) # 固定第x位为i
if i == 9:
self.nine += 1
dfs(x + 1) # 开始固定第x+1位
self.nine -= 1
num = ['0']*n # 起始数字定义为n个0组成的字符列表
res = [] # 数字字符串列表
self.start = n-1
self.nine = 0
dfs(0) #开启全排列递归
# res = [int(x) for x in res]
return ",".join(res)
题目:从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
给定二叉树: [3,9,20,null,null,15,7]
输出:[3,9,20,15,7]
Python 中使用 collections 中的双端队列 deque() ,其 popleft() 方法可达到 O(1) 时间复杂度;列表 list 的 pop(0) 方法时间复杂度为 O(N)
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
if not root: return []
queue, res = [], []
queue.append(root)
while queue:
cur = queue.pop(0)
if cur.left: queue.append(cur.left)
if cur.right: queue.append(cur.right)
res.append(cur.val)
return res
其原因是: list 是列表,也就是 java c++ 中的数组,数组移除头部元素的方式 是把后面的元素全部往前移动一位,所以复杂度是 O(N) ; deque 是双端队列,底层是链表,因此头部和尾部是等价的,插入删除都是 O(1)
class Solution:
def levelOrder(self, root: TreeNode) -> List[int]:
if not root: return []
queue, res = collections.deque(), []
queue.append(root)
while queue:
cur = queue.popleft()
if cur.left: queue.append(cur.left)
if cur.right: queue.append(cur.right)
res.append(cur.val)
return res
题目:从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
给定二叉树: [3,9,20,null,null,15,7]
输出
[ [3], [9,20], [15,7] ]
第一次刷用了两个queue,带来了额外的空间开销
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: return []
queue1, queue2, res, cur_layer = collections.deque(), collections.deque(), [], []
queue1.append(root)
while queue1 or queue2:
cur_layer = []
while(queue1):
cur = queue1.popleft()
if cur.left: queue2.append(cur.left)
if cur.right: queue2.append(cur.right)
cur_layer.append(cur.val)
if cur_layer: res.append(cur_layer)
cur_layer = []
while(queue2):
cur = queue2.popleft()
if cur.left: queue1.append(cur.left)
if cur.right: queue1.append(cur.right)
cur_layer.append(cur.val)
if cur_layer: res.append(cur_layer)
return res
其实只需要添加额外的一个数组,管理一下每层遍历的过程就好
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: return []
queue, res = collections.deque(), []
queue.append(root)
while queue:
tmp = []
for _ in range(len(queue)):
cur = queue.popleft()
if cur.left: queue.append(cur.left)
if cur.right: queue.append(cur.right)
tmp.append(cur.val)
res.append(tmp)
return res
题目:请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
给定二叉树: [3,9,20,null,null,15,7]
输出:
[ [3], [20,9], [15,7] ]
第一次刷用的是层次遍历 + 倒序的方式
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: return []
queue, res = collections.deque(), []
queue.append(root)
count = 0
while queue:
tmp = []
for _ in range(len(queue)):
cur = queue.popleft()
if cur.left: queue.append(cur.left)
if cur.right: queue.append(cur.right)
tmp.append(cur.val)
count += 1
res.append(tmp if len(res)%2 == 0 else tmp[::-1])
return res