记录下最近刷题的情况。
DFS算法适用
DFS算法会用到辅助栈(python中用list),相应的BFS算法则用到队列(python中队列主要用deque、queue或list)。个人偏爱DFS,故而刷这一类型的题都是用DFS算法。
图像渲染
链接: 图像渲染
说明:给你一个初始坐标 (sr, sc) 表示图像渲染开始的像素值(行 ,列)和一个新的颜色值 newColor,让你重新上色这幅图像。
其实就是将图像中与初始坐标像素值相等且连通的区域进行染色
class Solution(object):
def floodFill(self, image, sr, sc, newColor):
"""
:type image: List[List[int]]
:type sr: int
:type sc: int
:type newColor: int
:rtype: List[List[int]]
"""
##思路,dfs
##递归寻找与oldcolor相同的且连通的像素点
def dfs(sr, sc):
# if image[sr][sc] != oldcolor:
# image[sr][sc] = newColor
#以上if语句会与下面冲突
#应先判断边界条件以及像素,再进行填充
if sr < 0 or sr >= r or sc < 0 or sc >= c or image[sr][sc] != oldcolor:
return
#染色后递归就会避免重复遍历
image[sr][sc] = newColor
dfs(sr+1, sc)
dfs(sr-1, sc)
dfs(sr, sc+1)
dfs(sr, sc-1)
oldcolor = image[sr][sc]
##特列处理
if oldcolor == newColor:
return image
r, c = len(image), len(image[0])
dfs(sr, sc)
return image
##调用
##solved = Solution()
##res = solved.floodFill(image, sr, sc, newColor)
被围绕的区域
链接: 被围绕的区域.
说明:给定一个二维的矩阵,包含 ‘X’ 和 ‘O’(字母 O)。找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
补充:边界上的且与之连通的不算被围绕
思路:首先把边界的O以及与它连通的进行替换,避免后面dfs递归遍历进去
class Solution(object):
def solve(self, board):
"""
:type board: List[List[str]]
:rtype: None Do not return anything, modify board in-place instead.
"""
##首先将边界为o的点且与其相连的o点进行替换
##这里用b进行替换
##故而dfs算法的if语句需要找到是O的位置且不超过边界
def dfs(i, j):
if i < 0 or i >= r or j < 0 or j >= c or board[i][j] == 'X' or board[i][j] == 'b':
return
board[i][j] = 'b'
dfs(i-1, j)
dfs(i+1, j)
dfs(i, j-1)
dfs(i, j+1)
##特例
if not board:return
##以下是边界处理
##用isedge表示是否在边界
##在边界上则dfs搜索与之连通的区域,进行替换
r, c = len(board), len(board[0])
i, j = 0, 0
for i in range(r):
for j in range(c):
isedge = bool(i == 0 or j == 0 or i == r-1 or j == c-1)
if isedge and board[i][j] == 'O':
dfs(i, j)
#以下进行被围绕O点的进行X填充,其他则返回原值O
for i in range(r):
for j in range(c):
if board[i][j] == 'O':
board[i][j] = 'X'
if board[i][j] == 'b':
board[i][j] = 'O'
return board
岛屿数量
链接: 岛屿数量.
说明:类似被围绕区域,统计有几个岛屿(连通的算一个)
思路:dfs算法,然后计数
class Solution(object):
def numIslands(self, grid):
"""
:type grid: List[List[str]]
:rtype: int
"""
##特例
if not grid: return 0
r, c = len(grid), len(grid[0])
nums = 0
##边界条件以及岛屿与否
##这里也需要进行替换是避免重复遍历
def dfs(i, j):
if i < 0 or i >= r or j < 0 or j >= c or grid[i][j] != '1' :
return
grid[i][j] = '2'
dfs(i-1, j)
dfs(i+1, j)
dfs(i, j-1)
dfs(i, j+1)
for i in range(r):
for j in range(c):
if grid[i][j] == '1':
dfs(i, j)
nums += 1
return nums
还有一个方法是通过辅助矩阵记录路径,避免重复遍历(评论里面一个老哥一开始写的代码有bug,对其进行了修改后通过,注释的代码是原代码)。他的问题是只在dfs里用辅助矩阵避免重复遍历,然后将辅助矩阵返回原值,这样就不能体现出dfs的搜索路径,最后出来的结果就是在统计一个个岛屿(忽略了连通的岛屿是个整体)。
class Solution:
def __init__(self):
self.num_islands = 0
def numIslands(self, grid):
nr = len(grid)
if nr == 0:
return 0
nc = len(grid[0])
visited = [[False for _ in range(len(grid[0]))] for _ in range(len(grid))]
def dfs(grid, r, c):
# if r < 0 or r >= len(grid) or c < 0 or c >= len(grid[0]):
# return
visited[r][c] = True
# nr, nc = len(grid), len(grid[0])
for x, y in [(r - 1, c), (r + 1, c), (r, c - 1), (r, c + 1)]:
if 0 <= x < nr and 0 <= y < nc and grid[x][y] == "1" and not visited[x][y]:
dfs(grid, x, y)
# visited[r][c] = False
for r in range(nr):
for c in range(nc):
##与辅助矩阵判断,将连通的岛屿当作整体
## if grid[r][c] == "1":
if grid[r][c] == "1" and not visited[r][c]:
dfs(grid, r, c)
self.num_islands += 1
# dfs(grid, r, c)
return self.num_islands
岛屿周长
此题也是典型的dfs算法,也有大佬用数学的思路解题
链接: 元素区间
思路:观察规律,dfs算法
根据nettee大佬的想法写的python版本
class Solution(object):
def islandPerimeter(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
r, c = len(grid), len(grid[0])
def dfs(i, j):
##岛屿走向边界周长+1
##注意边界条件
if i < 0 or i > r-1 or j < 0 or j > c-1:
return 1
##走向水域则周长+1
if grid[i][j] == 0:
return 1
##重复的路径则表示岛屿相连,返回0
if grid[i][j] == 2:
return 0
##避免重复路径
grid[i][j] = 2
return dfs(i+1, j) + dfs(i-1, j) + dfs(i, j+1) + dfs(i, j-1)
for i in range(r):
for j in range(c):
##只有一个岛屿直接返回dfs计算值
if grid[i][j] == 1:
return dfs(i, j)
下面是Jam大佬数学法求解,注释了下为什么这样做(个人理解)。
class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
res=0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j]==1:
res+=4
##if语句只考虑左上的情况是因为从左上开始遍历
##且后面的格子会给前面的格子的右下情况擦屁股
##减2的原因是因为一个格子四个结点,有重合的边则重合两个结点
##格子的周长也等于结点数*边长长度
if i-1>=0 and grid[i-1][j]==1:
res-=2
if j-1>=0 and grid[i][j-1]==1:
res-=2
return res
矩阵最长递增路径
此题在同学笔试中出现过,也是适用dfs算法。笔试中有些题目会要求自己输入,在刷题的过程中也要考虑下怎么输入。
链接: 递增路径
思路:dfs算法+逻辑判断(具体问题具体分析)
笔试要求自己输入的情况下,python一般用input()或sys.stdin.readline().这边的输入是矩阵维度以及矩阵的值。
##输入
"""
3 3
1 9 0
2 8 5
6 3 8
"""
h, w = map(int, input().split())
m = []
for i in range(h):
m.append(list((map(int, input().split()))))
二叉树系列
二叉树常用的方法还是dfs和bfs,结构是树状的。上面dfs算法适用的题都是矩阵网格状。一般对二叉树来说可以用递推或者迭代,迭代就会用到队列、栈,一般二叉树的遍历用栈。
二叉树前序、中序、后序、层序遍历代码参考链接: 二叉树.;二叉树遍历.
直观的理解二叉树三种遍历→理解二叉树遍历.
前序:根节点→左子树→右子树
中序:左子树→根节点→右子树
后序:左子树→右子树→根节点
层序:每层从左往右
二叉搜索树第K大节点
链接:二叉搜索树
思路:二叉搜索树的性质:其节点大小为左子树<根节点<右子树,采用中序遍历就可以得到一个有序递增数组
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def kthLargest(self, root: TreeNode, k: int) -> int:
##二叉搜索树性质节点值左子树<根节点<右子树
##中序遍历则是左子树→根节点→右子树
##通过中序遍历则能得到递增数列
##或者中序倒序遍历
def middle(root):
if not root:
return []
return middle(root.left) + [root.val] + middle(root.right)
res = middle(root)
return res[-k]
'''
##大佬的解法则是中序遍历倒序,同时统计序号也就是K进行递减,记录K=0时候的序号
def middle(root):
if not root:return
middle(root.right)
if self.k == 0:return
self.k -= 1
if self.k == 0:self.res = root.val
middle(root.left)
self.k = k
middle(root)
return self.res
'''
还有就是你可以采用其他的二叉树遍历,缺点就是需要进行排序,可以用list.sort(),也可以写快排、归并进行排序。
class Solution(object):
def kthLargest(self, root, k):
##层序遍历,排序
def bfs(root, level):
if not root:
return
res[level-1].append(root.val)
if len(res) == level:
res.append([])
bfs(root.left, level+1)
bfs(root.right, level+1)
##快排
def fast(lis, l, r):
if l < r:
base_ind = partition(lis, l, r)
fast(lis, l, base_ind-1)
fast(lis, base_ind+1, r)
def partition(lis, l, r):
base = lis[r]
i = l-1
for j in range(l, r):
if lis[j] >= base: ##从大到小
i += 1
lis[i], lis[j] = lis[j], lis[i]
lis[i+1], lis[r] = lis[r], lis[i+1]
return i+1
res = [[]]
bfs(root, 1)
lis = sum(res, [])
fast(lis, 0, len(lis)-1)
return lis[k-1]
二叉树深度
链接:二叉树深度
思路:此题可以根据层序遍历得到的二维数组维度len(res)来确定树的深度
层序遍历可以说是广度优先搜索(bfs算法)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def maxDepth(self, root: TreeNode) -> int:
def bfs(root, level):
if not root:return
else:
res[level-1].append(root.val)
if len(res) == level:
res.append([])
bfs(root.left, level+1)
bfs(root.right, level+1)
res = [[]]
bfs(root, 1)
##最后res会多一个空行[]
return len(res)-1
Krahets大佬的dfs解法思路则是树的深度=max(左子树深度,右子树深度)+1.
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root: return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
平衡二叉树
链接: 平衡二叉树.
思路:平衡二叉树的性质是左右子树相差不超过1,这题跟二叉树深度类似
class Solution(object):
def isBalanced(self, root):
if not root:return True
left_d = self.treehigh(root.left)
right_d = self.treehigh(root.right)
if abs(left_d - right_d) > 1:
return False
else:
# return True
#这里return True错误的原因在于只判断了左右子树
#没有考虑到左右子树中自己的左右子树是否满足
#故而需要进行递推
return self.isBalanced(root.left) and self.isBalanced(root.right)
def treehigh(self, root):
if not root: return 0
return max(self.treehigh(root.right), self.treehigh(root.left)) + 1
二分法
一般有序数组就采用二分法。二分法要注意左右边界问题。
链接: 元素区间
说明:这题是查找目标值在有序数组的位置(start-end)
思路:二分查找法
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
if not nums:return [-1, -1]
i, j = 0, len(nums)-1
##以下是找右边界
##一般来说查完某一边界,就需要将边界初始化,此写法直接沿用查完右边界后的j值,此时j=i-1
##终止条件i>j
while i <= j:
m = (i + j)//2 ##防止溢出就i + (j - i)/2
if nums[m] <= target:
i = m + 1
else: j = m - 1
right = i
##此时j=right-1,若j<0(越界)或j的值不等于target则说明数组中不存在target(target存在于(left,right)区间)
if j < 0 or nums[j] != target:return [-1, -1]
i = 0
while i <= j:
m = (i + j)//2
if nums[m] < target:
i = m + 1
else: j = m - 1
left = j
##可省略
##左边界判断同上,此时i=left+1,若i>len(nums)(越界)或i的值不等于target则说明数组中不存在target(target存在于(left,right)区间)
return [left+1, right-1]
桶排序
排序这边算法很多,一般掌握快排跟归并就行(非科班小白常备)。但是昨天室友在某一面试中的手撕代码就遇到了桶排序,题目是《算法导论》8.4.4单位圆均匀分布这题,要求O(N)时间复杂度。本小白第一反应就是分区域画同心圆,但也就此打住。后面去百度才知道用桶排序。C++\C以及java版本参考链接: 桶排序
def insertionSort(b):
for i in range(1, len(b)):
up = b[i]
j = i - 1
while j >=0 and b[j] > up:
b[j + 1] = b[j]
j -= 1
b[j + 1] = up
return b
def bucketSort(x):
arr = []
slot_num = 10 # 10 means 10 slots, each
# slot's size is 0.1
for i in range(slot_num):
arr.append([])
# Put array elements in different buckets
for j in x:
index_b = int(slot_num * j)
arr[index_b].append(j)
# Sort individual buckets
for i in range(slot_num):
arr[i] = insertionSort(arr[i])
# concatenate the result
k = 0
for i in range(slot_num):
for j in range(len(arr[i])):
x[k] = arr[i][j]
k += 1
return x
动态规划
摆动序列
定义:序列两两差值正负交替出现,如[1,7,4,9]
链接:摆动序列.
思路:状态规划,dp[i][j]其中i是位置,j只有两个值0和1,表示差值正负。分析状态:
1.nums[i] > nums [i-1]:此时dp[i][j]表示正的状态与dp[i-1][0]有关
2.nums[i] < nums [i-1]:此时dp[i][j]表示负的状态与dp[i-1][1]有关
3.nums[i] = nums [i-1]:此时dp[i][j]状态与dp[i-1][j]相等
base case:dp[0] = [1, 1]
可以这样理解,第一个差值不管正负,都假设前面已经有差值的正负满足摆动序列,也就是说如果nums只有一个值的话,不管正负,前面都有了负(正)
class Solution:
def wiggleMaxLength(self, nums: List[int]) -> int:
if len(nums) < 2:return len(nums)
##状态规划
##dp[i][j]其中i是位置,j只有两个值0和1,表示差值正负
##分析状态
##1.nums[i] > nums [i-1]:此时dp[i][j]表示正的状态与dp[i-1][0]有关
##2.nums[i] < nums [i-1]:此时dp[i][j]表示负的状态与dp[i-1][1]有关
##3.nums[i] = nums [i-1]:此时dp[i][j]状态与dp[i-1][j]相等
##base case
##dp[0] = [1, 1]
##可以这样理解,第一个差值不管正负,都假设前面已经有差值的正负满足摆动序列
##也就是说如果nums只有一个值的话,不管正负,前面都有了负(正)
dp = [[0, 0] for i in range(len(nums))]
dp[0] = [1, 1]
for i in range(1, len(nums)):
if nums[i] > nums[i-1]:
dp[i][1] = dp[i-1][0] + 1
dp[i][0] = dp[i-1][0]
elif nums[i] < nums[i-1]:
dp[i][0] = dp[i-1][1] + 1
dp[i][1] = dp[i-1][1]
elif nums[i] == nums[i-1]:
dp[i][0] = dp[i-1][0]
dp[i][1] = dp[i-1][1]
return max(dp[-1][1], dp[-1][0])
打家劫舍
打家劫舍有三道,都是属于动态规划。三道题都放一起。
链接: 打家劫舍.
思路:根据题意写出状态转移方程,容易写出 dp[i] = max(dp[i-1], dp[i-2]+nums[i-1]).
1.当前房子i偷,则i-1的房子不能偷,此时偷到的钱是i-2的金额加上当前i的金额
2.当前房子i不偷,则此时偷到的钱是i-1的金额
class Solution(object):
def rob(self, nums):
if not nums:return 0
dp = [0]*(1 + len(nums))
dp[1] = nums[0]
for i in range(2, len(nums)+1):
dp[i] = max(dp[i-1], dp[i-2]+nums[i-1])
return dp[-1]
打家劫舍Ⅱ
思路:这题就是房子围成一个圈,这样第一个和最后一个房子不能同时抢。那么可以分成分别包含第一个和最后一个的序列,然后求最大金额,最后两个序列最大金额进行比较
class Solution:
##分成两个排列
##不偷第一家以及偷第一家
##考虑nums的长度能不能分成两个排列
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:return nums[0]
nums_last, nums_fir = nums[1:], nums[:-1]
dp_l = [0] * (len(nums_last)+2)
dp_f = [0] * (len(nums_fir)+2)
for l in range(len(nums_last)-1, -1, -1):
dp_l[l] = max(dp_l[l + 1], dp_l[l + 2] + nums_last[l])
for f in range(len(nums_fir)-1, -1, -1):
dp_f[f] = max(dp_f[f + 1], dp_f[f + 2] + nums_fir[f])
return max(dp_f[0], dp_l[0])
打家劫舍Ⅲ
思路:这题就是房子变成二叉树结构,根节点偷了则左右孩子不能偷,孙子节点可以偷。状态定义则为:
1.当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱;
2.当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
class Solution(object):
def rob(self, root):
def dfs(node):
if not node: return 0, 0
l = dfs(node.left)
r = dfs(node.right)
selected = node.val + l[1] + r[1]
notSelected = max(l[0], l[1]) + max(r[0], r[1])
return selected, notSelected
return max(dfs(root))