链表和数组
多见链表的全反转,K个一组反转链表 head 等表示指定Node,不是index也不是指针 head.next = node1 表示该Node的后继节点是node1 node2 = node1.next, 表示将node1的后继节点node赋值给 node2
0. 交换链表节点
剑指 Offer 反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
def reverseList(self, head: ListNode) -> ListNode:
cur = head
prev = None
while cur:
cur.next, prev, cur = prev, cur, cur.next
return prev
两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 给定 1->2->3->4, 你应该返回 2->1->4->3.
def swapPairs(self, head):
a, a.next = self, head
while a.next and a.next.next:
b,c = a.next, a.next.next
a.next, b.next, c.next = c, c.next, b
a = b
return self.next
1. 判断链表是否有环
判断是否有环可用快慢指针, 起点相同,快指针每次走2个步,满指针每次都1步,如果最终快慢指针相遇,则证明此链表中有环。 用set将路径上每个节点存起来,每走一步判断set中是否有当前节点,若有则证明有环
环形链表
给定一个链表,判断链表中是否有环
def hasCycle(self, head: ListNode) -> bool:
# 快慢指针法
fast = slow = head
while fast and slow and fast.next:
fast = fast.next.next
slow = slow.next
if (fast == slow):
return True
return False
环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
def detectCycle(self, head: ListNode) -> ListNode:
# 快慢指针不好使,快指针可能会跳过那个节点
# 所以还是使用的缓存法,把走过的路缓存起来
cur = head
cache = set()
while cur:
if cur not in cache:
cache.add(cur)
cur = cur.next
else:
return cur
return None
K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。 k 是一个正整数,它的值小于或等于链表的长度。 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
2. 判断括号字符串是否有效
用堆栈,如果符号是左边,入栈,如果符号是右边则与栈顶符号做匹配。最终堆栈为空则有效 将字符串中所有{} [] () 替换为空字符串,如果最后又剩余则表示字符串无效
有效的括号
给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。
def isValid(self, s: str) -> bool:
#堆栈的方式
symbols = {'}':'{',']':'[',')':'('}
stack = []
for i in s:
if i not in symbols:
stack.append(i)
# if stack为空意思是 如果栈还是空的就来一个右符号,则肯定是无效的
# 或者是栈取出来的不一样
elif not stack or symbols[i] != stack.pop():
return False
return not stack
3. 用队列实现栈&用栈实现队列
队列-> 栈: 每个元素入栈后,将前面n-1个元素pop然后压入栈,保证第n个元素总是位于第一位的。 栈->队列: 用2个栈, A为输入栈,B为输出栈。 当pop时将A所有元素依次出栈并压入B栈,形成倒叙的A栈数据。后续若B栈为空,则继续讲A倒叙压入B栈中再执行pop
用队列实现栈
def __init__(self):
self.q = []
def push(self, x: int) -> None:
self.q.append(x)
def pop(self) -> int:
temp = self.q[-1]
del self.q[-1]
return temp
def top(self) -> int:
return self.q[-1]
def empty(self) -> bool:
return not len(self.q)
用栈实现队列
def __init__(self):
self.s1 = []
self.s2 = []
def push(self, x: int) -> None:
self.s1.append(x)
def pop(self) -> int:
if not len(self.s2):
for _ in range(len(self.s1)):
self.s2.append(self.s1.pop())
return self.s2.pop()
def peek(self) -> int:
if not len(self.s2):
for _ in range(len(self.s1)):
self.s2.append(self.s1.pop())
return self.s2[len(self.s2)-1]
def empty(self) -> bool:
return len(self.s1) == 0 and len(self.s2) == 0
4. 返回数据流中的第K大元素
用最小堆,维护一个大小为K的最小堆,每次比较当前元素与堆顶元素,若当前元素大,则删除堆顶元素,加入当前元素并重新排序,然后继续扫描下一个元素
5. 返回滑动窗口中的最大值
使用最大堆,同上理 使用双端队列, 每次窗口扫到一个新元素,则将新元素与窗口中第一个元素比较大小,如果新元素大,则将窗口中前面所有元素删除,新元素成为第一个元素。如果旧窗口中的元素大,则不作处理,新元素加入窗口中。 此举目的是保证窗口中始终把最大元素放在最左边位置即第一位,后续扫描只需要比较一次大小即可。
6. 有效的字母异位词
单词中每个字母个数相同,排序不同。 使用排序,排序后比较是否一致 使用hashMap, 将每个字母出现的次数统计在map中,最后比较map是否一致
有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
# 排序的方式
def isAnagram(self, s: str, t: str) -> bool:
return sorted(s) == sorted(t)
# hashMap 的方式
def isAnagram(self, s: str, t: str) -> bool:
voc1 = {}
voc2 = {}
for char in s:
voc1[char] = voc1.get(char, 0) + 1
for char in t:
voc2[char] = voc2.get(char, 0) + 1
return voc1 == voc2
7. 两数之和
2个for循环,暴力计算 用set, 遍历第一层,计算set中是否包含target-item的值
两数之和
def twoSum(self, nums: List[int], target: int) -> List[int]:
res = {}
# 遍历nums, 输出index和每个item值, 这里map中存的是数值的index,方便返回
for i, item in enumerate(nums):
if (target-item) not in res:
res[item] = i
else:
return [res[target-item], i]
return []
8. 三数之和,输出所有三个数字之和为0的组合
3层for循环,暴力计算,同上 用set,2个for循环,计算set中是否包含-item1-item2的值
三数之和
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
def threeSum(self, nums):
nums.sort()
res = set()
for i,item in enumerate(nums[:-2]):
# 此判断为了能快速判断,若item与前一位值相同则继续循环
# 因为如果前面相同值有解,后面item的解一定与前面的相同,要求是答案中是不重复的,所以可以排除不遍历
if i >= 1 and item == nums[i-1]:
continue
d = {}
for item2 in nums[i+1:]:
if item2 not in d:
# 这里的处理方式同两数之和,前2个数的负数就是第3个解,3数值和为0.
d[-item-item2] = 1
else:
res.add((item, -item-item2, item2))
return map(list, res)
延伸 输出三数之和为0的所有index组合
9. 验证二叉排序树
中序遍历整个树,判断是否为有序数组 递归,每次递归当前节点的左子树,然后判断根节点的值是否比前继节点值大
验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树即二叉平衡树
def isValidBST(self, root: TreeNode) -> bool:
self.prve = None
def isBST(root):
if not root:
return True
if not isBST(root.left):
return False
# 这里为什么要小于 前继节点的值?
# 因为这是中序遍历,会把左子树全部走完才会走到每个树的根节点,所以prve一定是比根节点小的值,因为都是左子树嘛
# 那么这里一定是更节点 > prve的值
if self.prve and self.prve.val >= root.val:
return False
# 这里赋值的原因是,要拿根节点与右子树比较
self.prve = root
return isBST(root.right)
return isBST(root)
10. 二叉树&二叉搜索树的最近公共祖先
从链表角度考虑,由子节点往根节点遍历,路径上所有点记录下来,比较2个路径中最后的相同节点。 递归,分别寻找左子树和右子树中是否包含P 和 Q这2个节点,如果包含返回节点,不包含则继续递归直到子树中出现节点或者到达叶子节点返回空,那么这个树就将单一的P或Q一直往上透传,直到在某个节点中与取到的P或Q组成一对,则返回当前根节点的值即为公共祖先
二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# 简单递归
def lower(root, p, q):
if not root or root == p or root == q : return root
left = lower(root, p,q) # 结果是p 或 q 或叶子节点
right = lower(root, p ,q)
if not left: return right # 如果左子树没有结果,结果一定在右边
if not right: return left # 同理
if left and right : return root # 如果左右子树都有值,说明当前节点就是左右子树的共同根节点
return lower(root, left, right)
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# 排序递归
if root.val< p and root.val< q:
return lowestCommonAncestor(root.right, p,q)
if root.val> p and root.val> q:
return lowestCommonAncestor(root.left, p,q)
if root.val>p and root.val < q:
return root
二叉树的最近公共祖先
# 同上题简单递归,因为此题无序,无法使用排序逻辑
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
def lowest(root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# 此处如果root是空,也可以返回,表示后续没找到,走8,9行判断
if not root or root == p or root == q: return root
left = lowest(root.left, p, q)
right = lowest(root.right, p, q)
if not left: return right
if not right:return left
if left and right: return root
return lowest(root, p, q)
11. Pow(x,n) 即x的n次方
递归,分治的思想,x的n次方可以拆分为 x的n/2次方*x的n/2次方, 还可以继续往下拆分,有效避免了重复计算,时间复杂度是logn.
Pow(x, n)
实现 pow(x, n) ,即计算 x 的 n 次幂函数。
# 使用递归方式,分治
def myPow(self, x: float, n: int) -> float:
if not n : return 1
if n < 0 : return (1 / self.myPow(x, -n))
if n %2 : # 奇数
return x * self.myPow(x, n-1)
return self.myPow(x*x, n/2) # 偶数
12. 求众数,即元素出现次数大于n/2
暴力计算,2个loop, O(n²) HashMap, 遍历所有元素,把出现次数保存到map中,再查找map中多次数的元素 排序后获取数组中间位置即为众数 分治思想,获取左右子集中的众数,如果左右众数一致则得出结果,不一致则继续分治拆分左右子集,统计出所有子集的众数,最多的为众数
多数元素
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
def majorityElement(self, nums: List[int]) -> int:
# 摩尔投票法, 当一个数的重复次数超过数组长度的一半,每次将两个不相同的数删除,最终剩下的就是要找的数。
res = 0
count = 0
for item in nums:
if count == 0:
res = item
count = 1
elif item == res:
count += 1
else:
count -= 1
if count > 0: return res
def majorityElement(self, nums: List[int]) -> int:
# 哈希法
res = {}
maxItem = 0
maxNums = 0
for item in nums:
res[item] = res.get(item, 0) + 1
for item, value in res.items():
if maxNums < value :
maxItem = item
maxNums = value
return maxItem
13. 买卖股票的最佳时机
DFS 深度优先搜索 贪心算法: 选择局部最优 动态规划
14. 二叉树层次遍历
BFS, Breadth First Search 广度优先搜索 DFS, Depth First Search 深度优先搜索
二叉树的层序遍历
def levelOrder(self, root: TreeNode) -> List[List[int]]:
# BFS 用队列
queue = []
queue.append(root)
result = []
while queue:
size = len(queue)
current_level = []
for _ in range(size):
node = queue.pop(0)
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(current_level)
return result
def levelOrder(self, root: TreeNode) -> List[List[int]]:
# DFS用栈
result = []
def dfs(node, level):
if not node: return []
if len(result) < level+1:
result.append([])
result[level].append(node.val)
dfs(node.left, level+1)
dfs(node.right, level+1)
dfs(root,0)
return result
15. 二叉树的最大和最小深度
同上,可以使用BFS 和 DFS方法解决。 BFS则是使用 while循环遍历当前层级的queue中所有对象,(最小)如果其中一个节点是叶子节点,则结束遍历,输出当前层级即为最小深度。(最大)一直遍历整棵树,输出最后的层级即为最大深度 DFS则使用递归,递归每层的左右节点,结束条件是若当前节点是空返回0.
16. 生成有效括号组合
同上,使用DFS递归来解决,注意2个递归条件,已使用的右括号必须小于左括号,才可继续增加右括号,否则没有多于的左括号去配对。当左右括号全部使用完毕将字符串添加到结果list中
17. N皇后问题
使用DFS递归, 思想是类似围棋或象棋算法每一步都会产生一个分支,每个分支下一步棋又会产生一个分支,每条分支的每一步都会有成功或者失败的判断即剪枝的条件,如剪枝成立则此分支结束,回溯到上一分支继续走其他步。上上一步也可回溯继续走下去。只要某个条件下能够走将N个皇后走完,则代表前面的每一步都是成立的,且路径都会有记录。 使用DFS+位运算, 太复杂, 后续再研究
18. 数独问题
依然使用DFS递归然后回溯去搜索解,剪枝的条件是横竖现有值不能重复,33的9宫格中数字不能重复。除此判断条件外,直接递归回溯直到走完所有99输出答案
19. 实现一个求解平方根的函数
用二分查找法,因为y=x²是单调递增,所以是有序有界的,可以通过mid*mid来逼近真实的x 用牛顿迭代法,有数学公式
20. 实现一个字典树即Trie树 字典树
字典里面套字典,表示每个节点有多个边相对应,穿起来所有路径即为最终的单词,Trie树如下图, 例如insert “apple” ,则生成Trie树为 {‘a’: {‘p’: {‘p’: {‘l’: {‘e’: {‘#’: ‘#’}}}}}} 最后一个字母e的分支只有结束标识符”#” 在上面的基础上insert “app”, 则树为 {‘a’: {‘p’: {‘p’: {‘l’: {‘e’: {‘#’: ‘#’}}, ‘#’: ‘#’}}}} 在p的分支有2个,一个是”l”, 一个是结束符”#” ,代表app可以结束,也可以只是前缀
21. 二维网格中的单词搜索问题
在二维网格中搜索单词,可以用DFS,去递归+回溯。 也可用DFS+Trie树+回溯。把单词编制成Trie树,遍历网格中每个单词,若是树中的首字母,则接着递归首字母附件的4个字母是否存在于树中,不存在则回溯
22. 统计位1的个数
先学习一下常见的位运算 x&1 == 1 or == 0 判断奇偶性 x = x & (x-1) 清除x最低位的1 x&-x 拿到最低位的1
23. 2的幂次方问题&比特位计数问题
2的幂次方有个特点,2进制只有1位是1,其他全是0,可以用上面的位运算解决 比特位计数问题如果用上面的暴力算法是可以解决的,问题是每个数字都要计算一遍浪费时间,最好的方式是前面计算的结果,后面直接使用,公式是count[x] = count[x&(x-1)] +1 ,就是 x比x小1位的数字的个数+1. 这个一个循环得到结果,很巧妙
2的幂
# 2的幂次方 只有一位是1,其他全是0, 所以打掉1之后 就是0了
def isPowerOfTwo(self, n: int) -> bool:
if n != 0 and n&(n-1) == 0:
return True
else:
return False
24. 动态规划
- 递推 (递归+记忆化)
- 状态的定义: opt[n], dp[n]…
- 状态转移方程: opt[n] = best_of(opt[n-1] ,opt[n-2] …)
- 最优子结构: 最终的结果是由最优的每一步得到的,每一步的最优结果都得到保存,所以如果问子结构中最优结果就可以直接得到 例如斐波拉切数列,F[n] = F[n-1] + F[n-2], 直接递归会有很多重复计算,例如F[4]=F[3]+F[2] = F[2]+F[1]+F[2],重复计算了F[2],如果将F[2]结果保存起来,就节省很多时间。
25. 动态规划DP vs 回溯 vs 贪心
回溯(递归) -> 重复计算, 可以做到总体最优,但是计算量大,性能低下 贪心 -> 永远局部最优,如果简单的每步局部最优就是总体最优,这样是可以的,但是往往没这么简单 DP -> 记录局部最优子结构, 其实如果能够递归的问题,一般都可通过DP解决
26. 爬楼梯
递推公式同 斐波拉切 F[n] = F[n-1] + F[n-2] 因为最后一步有多少种方法等于 前一步的方法 加上前2步的方法, 因为可以一次可以1个台阶或者2个台阶 DP定义:到第n层有多少种方法
27. 三角形的最小路径和
一种递归+回溯,能解决,只是时间很长,重复计算 动态规划,从底层开始,往上遍历,上层每次都选择下面2个其中小的一个, 推导出的DP转移方程为F[j]= min(F[j], F[j+1]) + t[i][j] F[j]为指定层第j个位置到达底部最小路径,以此类推,顶部只需要计算下一层到他的最小路径即可。 DP定义:到第j层最小路径
28. 乘积最大子数组
动态规划,计算连续的子数组积的最大值, 由于元素可能有正,有负, 如果始终只保留正数,可能后续元素出现负数积就变小了,可能之前的负数与负数的积是最大值。所以需要保留之前的最大值和最小值,乘后续数组元素后再覆盖最大值最小值 DP定义:到第i位最大值和最小值
29. 买卖股票的最佳时机 & 买卖股票的最佳时机 III & 买卖股票的最佳时机 II
用3维数组,分别代表0~n-1天, 0/1是否持股, 0~k次交易 能够满足大部分股票买卖的状态定义
30. 最长上升子序列
dp[i]状态为 在i点包含i的最长上升子序列, 不是i点的最长上升子序列,所以最后的结果是选择数组中最大的,而不是指定位置的
31. 零钱兑换
dp[i] 为凑成当前金额最少的硬币数,则dp方程为 dp[i] = min{dp[i-conins[j] ]} +1, 例如银币金额是[1,2,5], 则dp[11] = min{ dp[10], dp[9], dp[6]} +1 可以把此题当成爬楼梯,到达11层,一次可以跨1步,2步,5步,最少需要多少步一样。
32. 编辑距离
把A单词转换成B单词最少需要多少步, 可以插入,删除,替换。dp[i][j] 表示从A单词的 i 位置到B单词 j 位置需要的最小操作数。 dp方程为dp[i][j] = if A[i] ==B[j] : dp[i-1][j-1] else: min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}+1 如果当前ij字符相等,则dp[i][j]就等于上一步的结果,不需要变动。如果不等,则等于插入,删除,替换其中一个的最小步数+1
33. DP套路 三部曲
- 套路步骤1, 创建dp状态定义
- 套路步骤2, 初始化dp首位状态
- 套路步骤3, 大循环开始写递推公式,并赋值给dp状态
34. 并查集
几个重要方法, 初始化,查找父类,合并。 查找父类可以做路径压缩的优化。
35. 岛屿数量
染色法:遍历节点,把节点为“1”的相邻节点赋值为“0”, 并记录为一个岛。可用DFS也可BFS。 并查集:对为“1”的节点初始化,把相邻的合并,最后遍历有多少个roots
36. 布隆过滤器
是一个很长的二进制向量和一个映射函数,用于检索一个元素是否在集合中,如果判断不在集合中,那一定不在集合中。如果判断在集合中,不一定再 特点: 空间效率和查询时间远远超过一般算法,但是有一定误识别率和删除困难。