目录
面试题4 二维数组中的查找--左下角二分查找 &240
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
思路
类似于二分查找,根据题目,如果拿数组中任意一个元素与目标数值进行比较,如果该元素小于目标数值,那么目标数值一定是在该元素的下方或右方,如果大于目标数值,那么目标数值一定在该元素的上方或者左方。对于二分查找来说,每次比较只能移动一个指针,在二维数组的查找中,两个指针是一个上下方向移动,一个是左右方向移动 。
两个指针可以从同一个角出发。 假设我们从左上角出发,也就是row=0 和 col=0,如果元素小于目标数值,我们会将row往下移或着col往右移,这样,被屏蔽的区域可能会是目标元素所在的区域。比如row+=1,那么第一行除左上角以外的元素就被忽略了,如果col+=1,那么第一列出左上角以外的元素就被忽略了。因此这样是不可行的。所以本题从左下角出发寻找解题思路。
col1 col2 col3
row1 1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
class Solution: def findNumberIn2DArray(self, matrix: List[List[int]], target: int) -> bool: if matrix == []: return False #重要 rows = len(matrix) - 1 cols = len(matrix[0]) - 1 i = rows j = 0 while i >= 0 and j <= cols: if target > matrix[i][j]: j += 1 #不能写成j++ elif target < matrix[i][j]: i -= 1 else: return True return False
能够得知 i += 1 的效率往往要比 i = i + 1 更高一些
时间复杂度 O(M+N) :其中,N 和 M 分别为矩阵行数和列数,此算法最多循环 M+N 次。
空间复杂度 O(1): i, j 指针使用常数大小额外空间。
51. N皇后-回溯
给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。
PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。
这是 N = 8 的一种放置方法:
这个问题本质上跟全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行的任意一列放置一个皇后。
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
class Solution: def solveNQueens(self, n: int) -> List[List[str]]: res = [] track = [] def isValid(track, row, col): if not track: return True for i in range(row): if track[i][col] == 'Q': # 本列 return False i = row - 1 j = col - 1 while i >= 0 and j >= 0: #左上 if track[i][j] == 'Q': return False i -= 1 j -= 1 i = row - 1 j = col + 1 while i >= 0 and j < n: # 右上 if track[i][j] == 'Q': return False i -= 1 j += 1 return True def backtrack(track, row): #print(track) if len(track) == n: res.append(track[:]) return for i in range(n):#n:col ans = ['.'] * n #改在这里,就不需要回退时处理了不然会出现[Q,Q] if isValid(track, row, i): ans[i] = 'Q' track.append(list(ans)) backtrack(track,row + 1) track.pop() backtrack([], 0) res2 = [[] for _ in range(len(res))] for index,re in enumerate(res): for r in re: r_str = "".join(r) res2[index].append(r_str) return res2
37. 解数独-回溯
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 '.' 表示。
求解数独的思路很简单粗暴,就是对每一个格子所有可能的数字进行穷举
class Solution: def solveSudoku(self, board: List[List[str]]) -> None: if not board: return def isValid(board, row, col, c): for i in range(9): if board[row][i] == c: return False if board[i][col] == c: return False if board[3 * (row//3) + i//3][3 * (col//3) + i%3] == c: return False return True def backtrack(board): for i in range(len(board)): for j in range(len(board[0])): if board[i][j] == '.': for c in ["1","2","3","4","5","6","7","8","9"]: if isValid(board, i, j, c): board[i][j] = c if backtrack(board): return True else: board[i][j] = '.' return False return True backtrack(board)
36. 有效的数独-回溯
判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。
一个有效的数独(部分已被填充)不一定是可解的。
- 数字 1-9 在每一行只能出现一次。
- 数字 1-9 在每一列只能出现一次。
- 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
class Solution: def isValidSudoku(self, board: List[List[str]]) -> bool: def isValid(board, row, col, c): count1,count2,count3 = 0,0,0 for i in range(9): if board[row][i] == c: count1 += 1 if board[i][col] == c: count2 += 1 if board[3 * (row//3) + i//3][3 * (col//3) + i%3] == c: count3 += 1 if count1==1 and count2==1 and count3==1: return True else: return False for i in range(len(board)): for j in range(len(board[0])): c = board[i][j] if c !='.' and not isValid(board,i,j,c): return False return True
778. 水位上升的泳池中游泳-优先队列
在一个 N x N 的坐标方格 grid 中,每一个方格的值 grid[i][j] 表示在位置 (i,j) 的平台高度。
现在开始下雨了。
当时间为 t 时,此时雨水导致水池中任意位置的水位为 t 。你可以从一个平台游向四周相邻的任意一个平台,但是前提是此时水位必须同时淹没这两个平台。假定你可以瞬间移动无限距离,也就是默认在方格内部游动是不耗时的。当然,在你游泳的时候你必须待在坐标方格里面。
你从坐标方格的左上平台 (0,0) 出发。最少耗时多久你才能到达坐标方格的右下平台 (N-1, N-1)?
输入: [[0,2],[1,3]]
输出: 3
解释:
时间为0时,你位于坐标方格的位置为 (0, 0)。
此时你不能游向任意方向,因为四个相邻方向平台的高度都大于当前时间为 0 时的水位。
等时间到达 3 时,你才可以游向平台 (1, 1). 因为此时的水位是 3,坐标方格中的平台没有比水位 3 更高的,所以你可以游向坐标方格中的任意位置
用优先队列保存下一步可以游向的平台,每次都选择高度最小的平台。以这种方式到达终点时,路径中遇到的最高平台就是答案。
class Solution(object): def swimInWater(self, grid): N = len(grid) seen = {(0, 0)} #标记已访问 pq = [(grid[0][0], 0, 0)] ans = 0 while pq: d, i, j = heapq.heappop(pq)#弹出堆中最小值 ans = max(ans, d) if i == j == N-1: return ans #达到边界返回 for a, b in ((i-1, j), (i+1, j), (i, j-1), (i, j+1)): if 0 <= a < N and 0 <= b < N and (a, b) not in seen: heapq.heappush(pq, (grid[a][b], a, b)) seen.add((a, b))
面试题 16.04. 井字游戏
设计一个算法,判断玩家是否赢了井字游戏。输入是一个 N x N 的数组棋盘,由字符" ","X"和"O"组成,其中字符" "代表一个空位。
以下是井字游戏的规则:
- 玩家轮流将字符放入空位(" ")中。
- 第一个玩家总是放字符"O",且第二个玩家总是放字符"X"。
- "X"和"O"只允许放置在空位中,不允许对已放有字符的位置进行填充。
- 当有N个相同(且非空)的字符填充任何行、列或对角线时,游戏结束,对应该字符的玩家获胜。
- 当所有位置非空时,也算为游戏结束。
- 如果游戏结束,玩家不允许再放置字符。
如果游戏存在获胜者,就返回该游戏的获胜者使用的字符("X"或"O");如果游戏以平局结束,则返回 "Draw";如果仍会有行动(游戏未结束),则返回 "Pending"。
输入: board = ["O X"," XO","X O"]
输出: "X"
输入: board = ["OOX","XXO","OXO"]
输出: "Draw"
解释: 没有玩家获胜且不存在空位
输入: board = ["OOX","XXO","OX "]
输出: "Pending"
解释: 没有玩家获胜且仍存在空位
class Solution: def tictactoe(self, board: List[str]) -> str: n = len(board) def check(c): s = c * n return any(( any(row == s for row in board), any(col == s for col in map(''.join, zip(*board))), all(board[i][i] == c for i in range(n)), all(board[i][n - i - 1] == c for i in range(n)) )) if check('X'): return 'X' if check('O'): return 'O' if ' ' in ''.join(board): return 'Pending' return 'Draw'
10 矩阵中的路径——深度优先搜索DFS
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。
[["a","b","c","e"],
["s","f","c","s"],
["a","d","e","e"]]
但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
本问题是典型的矩阵搜索问题,此类问题通常可使用 深度优先搜索(DFS)+ 剪枝 解决。
算法原理:
深度优先搜索: 可以理解为暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到 这条路不可能和目标字符串匹配成功 的情况(例如:此矩阵元素值和目标字符值不同、路径已访问此元素),则应立即返回,称之为 可行性剪枝 。
从每个节点 DFS 的顺序为:下、上、右、左
递归参数: 当前元素在 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k 。
终止条件:
返回 false: ① 行列索引越界 或 ② 当前矩阵元素与目标字符不同 或 ③ 当前矩阵元素已访问过 (③ 可合并至 ② ) 。
返回 true : k = len(word) - 1,即字符串 word 已匹配完成。
递推工作:
标记当前矩阵元素: 将 board[i][j] 值暂存于变量 tmp ,并修改为字符 '/' ,代表此元素已访问过,防止之后搜索时重复访问。
搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需一条可行路径) ,并记录结果至 res 。
还原当前矩阵元素: 将 tmp 暂存值还原至 board[i][j] 元素。
回溯返回值: 返回 res ,代表是否搜索到目标字符串。
class Solution: def exit(self, board, word): def dfs(i,j,k): if not 0=<i< len(board) or not 0<=j<len(board[0]) or board[i][j]!=work[k]: return False if k == len(word) - 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(len(board)): for j in range(len(board[0])): if dfs(i,j,0): return True return False
11 机器人运动范围
地上有一个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。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
class Solution: def movingCount(self, m, n, k): visited=[[False for _ in range(n)] for _ in range(m)] return self.dfs(0,0,m,n,visited,k) def dfs(self,i,j,m,n,visited,k): if not 0<=i<m or not 0<=j<n or self.cal(i)+self.cal(j)>k or visited[i][j]==True: #边界条件 return 0 visited[i][j]=True #回溯子状态 return self.dfs(i-1,j,m,n,visited,k)+self.dfs(i,j-1,m,n,visited,k)+self.dfs(i+1,j,m,n,visited,k)+self.dfs(i,j+1,m,n,visited,k)+1 def cal(self,num): #计算行坐标和列坐标数位和 total=0 while num>0: total+=num%10 num//=10 return total
29. 顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
class Solution: def spiralOrder(self, matrix:[[int]]) -> [int]: if not matrix: return [] l, r, t, b, res = 0, len(matrix[0]) - 1, 0, len(matrix) - 1, [] while True: for i in range(l, r + 1): res.append(matrix[t][i]) # left to right t += 1 if t > b: break for i in range(t, b + 1): res.append(matrix[i][r]) # top to bottom r -= 1 if l > r: break for i in range(r, l - 1, -1): res.append(matrix[b][i]) # right to left b -= 1 if t > b: break for i in range(b, t - 1, -1): res.append(matrix[i][l]) # bottom to top l += 1 if l > r: break return res
200 岛屿数量——DFS
目标是找到矩阵中 “岛屿的数量” ,上下左右相连的 1 都被认为是连续岛屿。
dfs方法: 设目前指针指向一个岛屿中的某一点 (i, j),寻找包括此点的岛屿边界。
1. 从 (i, j) 向此点的上下左右 (i+1,j),(i-1,j),(i,j+1),(i,j-1) 做深度搜索。
2. 终止条件:
(1)(i, j) 越过矩阵边界;
(2)grid[i][j] == 0,代表此分支已越过岛屿边界。
主循环:
遍历整个矩阵,当遇到 grid[i][j] == '1' 时,从此点开始做深度优先搜索 dfs,岛屿数 count + 1 且在深度优先搜索中删除此岛屿。
最终返回岛屿数 count 即可。
class Solution: def numIslands(self, grid: [[str]]) -> int: m = len(grid) if m == 0: return 0 n = len(grid[0]) def dfs(i, j): if i<0 or i>=m or j<0 or j>=n or grid[i][j]=='0': return grid[i][j] = '0' dfs(i + 1, j) dfs(i, j + 1) dfs(i - 1, j) dfs(i, j - 1) count = 0 for i in range(m): for j in range(n): if grid[i][j] == '1': dfs(i, j) count += 1 return count
695. 岛屿的最大面积
仍然可以采用原位修改的方式避免记录visited的开销。我们的做法是将grid[i][j] = 0
时间复杂度:O(m∗n)
空间复杂度:O(m∗n)
class Solution: def maxAreaOfIsland(self, grid): m = len(grid) if m ==0: return 0 n = len(grid[0]) ans = 0 def dfs(i,j): if i<0 or i >=m or j<0 or j>=n: return 0 if grid[i][j] == 0: return 0 grid[i][j] = 0 top = dfs(i+1,j) bottom = dfs(i-1,j) left = dfs(i,j-1) right = dfs(i,j+1) return 1+sum([top,bottom,left,right]) for i in range(m): for j in range(n): ans = max(ans,dfs(i,j)) return ans
1162. 地图分析-BFS
不用visited,而是原地修改。由于这道题求解的是最远的距离,而距离我们可以使用BFS来做。算法:
对于每一个海洋,我们都向四周扩展,寻找最近的陆地,每次扩展steps加1。
如果找到了陆地,我们返回steps。
我们的目标就是所有steps中的最大值。
class Solution: def maxDistance(self, grid: List[List[int]]) -> int: n = len(grid) steps = -1 queue = [(i, j) for i in range(n) for j in range(n) if grid[i][j] == 1] if len(queue) == 0 or len(queue) == n ** 2: return steps while len(queue) > 0: for _ in range(len(queue)): x, y = queue.pop(0) for xi, yj in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]: if xi >= 0 and xi < n and yj >= 0 and yj < n and grid[xi][yj] == 0: queue.append((xi, yj)) grid[xi][yj] = -1 steps += 1 return steps
207 课程表——DFS
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
思路: 通过DFS判断图中是否有环
借助一个标志列表flags,用于判断每个节点i(课程)的状态:
- 未被DFS访问: i == 0
- 已被其他节点启动的DFS访问: i == -1
- 已被当前节点启动的DFS访问:i == 1
对 numCourses 个节点依次执行DFS,判断每个节点起步DFS是否存在环,若存在环直接返回FALSE。
DFS流程
终止条件:当flag[i] == -1, 说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True.当flag[i] ==1, 说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环,直接返回False。
将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False
当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 −1并返回 True
若整个图 DFS 结束并未发现环,返回 True
class Solution: def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool: def dfs(i, adjacency, flags): if flags[i] == -1: return True if flags[i] == 1: return False flags[i] = 1 for j in adjacency[i]: if not dfs(j, adjacency, flags): return False flags[i] = -1 return True adjacency = [[] for _ in range(numCourses)] flags = [0 for _ in range(numCourses)] for cur, pre in prerequisites: adjacency[pre].append(cur) for i in range(numCourses): if not dfs(i, adjacency, flags): return False return True
47. 礼物的最大价值--动态规划
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
动态规划
这次用的方法不需要额外的存储空间,直接在原数组上进行修改
时间复杂度O(m*n),空间复杂度O(1)
class Solution: def maxValue(self, grid): for i in range(len(grid)): for j in range(len(grid[0])): if i == 0 and j==0: continue if i== 0 and j >= 1: grid[i][j] = grid[i][j-1]+grid[i][j] elif j == 0 and i >=1: grid[i][j] = grid[i-1][j] +grid[i][j] else: A = grid[i-1][j] + grid[i][j] B = grid[i][j-1]+ grid[i][j] grid [i][j] = max(A,B) return grid[len(grid)-1][len(grid[0])-1]
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
class Solution: def uniquePaths(self, m: int, n: int) -> int: dp = [[0]*(n) for _ in range(m)] for i in range(m): for j in range(n): if i==0 or j==0: dp[i][j] = 1 else: dp[i][j] = dp[i-1][j]+dp[i][j-1] return dp[-1][-1]
63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
class Solution: def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int: m = len(obstacleGrid) #每一行 n = len(obstacleGrid[0]) #每一列 for i in range(m): for j in range(n): if obstacleGrid[i][j]: #如果有障碍物 obstacleGrid[i][j] = 0 else: #没有障碍物 if i==j==0: obstacleGrid[i][j]=1 else: a = obstacleGrid[i-1][j] if i!=0 else 0 #上方格 b = obstacleGrid[i][j-1] if j!=0 else 0 #下方格 obstacleGrid[i][j] = a+b return obstacleGrid[-1][-1]