算法通识
有太多的天才程序员、知名学者、行业大佬,都或多或少写过算法相关的书籍。目前算法已经是每个程序员必备的技能之一,是进入大厂的第一道门槛。
本文面向初学者,由浅入深的给出一些算法题解,帮助你理解算法的基本概念和实现方式。
基础
大O表示法是一种表示算法时间复杂度的方法。它表示算法运行时间与输入规模之间的关系。
O(1) # 常数时间复杂度
O(log n) # 对数时间复杂度
O(n) # 线性时间复杂度
O(n log n) # 线性对数时间复杂度
O(n^2) # 平方时间复杂度
对于大O表示法,我们只需要关注最高次项并忽略系数。例如O(2n + 1)可以简化为O(n)。
请你完成下面的练习并给出其时间复杂度。
二分查找
描述
给定一个有序列表和一个目标值,请你查找目标值在列表中的索引。如果目标值不存在,则返回-1。
arr = [1, 2, 6, 7, 8, 9, 10]
target = 6
输出: 2
arr = [1, 2, 6, 7, 8, 9, 10]
target = 0
输出: -1
题解
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
print(binary_search([1, 2, 6, 7, 8, 9, 10], 0))# -1
print(binary_search([1, 2, 6, 7, 8, 9, 10], 6))# 2
冒泡排序
描述
给定一个列表,请你对列表的元素进行排序。
示例:
输入: [13, 22, 6, 99, 11, 0]
输出: [0, 6, 11, 13, 22, 99]
输入: [13, 22, 6, 99, 11, 0]
输出: [0, 6, 11, 13, 22, 99]
题解
list1 = [13, 22, 6, 99, 11, 0]
for a in range(len(list1)):
for b in range(a,len(list1)):
if list1[a] < list1[b]: #如果m大于了n
list1[a] ,list1[b] = list1[b],list1[a]#交换位置
print(list1)
分治
分治法是一种将问题分解为更小的子问题,然后解决子问题,最后合并子问题结果的算法。
快速排序
描述
快速排序(Quicksort)是对冒泡排序的一种改进算法。
该算法的实现基本可分为以下几步:
在数组中选一个基准数(通常为数组第一个)。 将数组中小于基准数的数据移到基准数左边,大于基准数的移到右边 对于基准数左、右两边的数组,不断重复以上两个过程,直到每个子集只有一个元素,即为全部有序。
请你编写它的实现代码。
题解
def quick_sort(arr):
"""
快速排序
:param arr: 待排序的List
:return: 排序后的List
"""
# 递归结束条件:如果列表长度小于等于1,直接返回
if len(arr) <= 1:
return arr
# 创建左右2个空列表
left = []
right = []
# 选择一个选中值(pivot),选择第一个元素
pivot = arr[0]
# 将元素分配到左右列表
for i in range(1, len(arr)):
if arr[i] <= pivot: # 小于等于pivot的元素放入左列表
left.append(arr[i])
else: # 大于pivot的元素放入右列表
right.append(arr[i])
# 递归排序左右两部分,并合并结果
return quick_sort(left) + [pivot] + quick_sort(right)
# 测试数据
if __name__ == '__main__':
arr = [17, 56, 71, 38, 61, 62, 48, 28, 57, 42, 10, 21, 12, 90] # 长度为14
sorted_arr = quick_sort(arr)
print("快速排序结果:", sorted_arr)
密码组合
描述
Python中的string模块包含了许多字符,请根据以下提示设计一个函数:
参数1:密码可用字符的列表 参数2:密码的长度 输出:一个符合要求的所有的密码组合
题解
def set_password(chars, length):
passwords = []
def get_password(char, length):
if len(char) == length: # 修复 :检查当前密码长度,而不是字符集长度
passwords.append(char) # 修复:添加当前构建的密码,而不是字符集
return
for i in chars:
get_password(char + i, length)
get_password("", length)
return passwords
print(set_password(['a', 'b', 'c'], 2))# ['aa', 'ab', 'ac', 'ba', 'bb', 'bc', 'ca', 'cb', 'cc']
print(set_password(['a', 'b', 'c'], 3))# ['aaa', 'aab', 'aac', 'aba', 'abb', 'abc', 'aca', 'acb', 'acc', 'baa', 'bab', 'bac', 'bba', 'bbb', 'bbc', 'bca', 'bcb', 'bcc', 'caa', 'cab', 'cac', 'cba', 'cbb', 'cbc', 'cca', 'ccb', 'ccc']
括号组合
描述
给定一个数字n,请你生成所有可能的括号组合
题解
def generate_parenthesis(n):
def backtrack(s, left, right):
if len(s) == 2 * n:
result.append(s)
return
if left < n:
backtrack(s + '(', left + 1, right)
if right < left:
backtrack(s + ')', left, right + 1)
result = []
backtrack('', 0, 0)
return result
print(generate_parenthesis(3))# ['((()))', '(()())', '(())()', '()(())', '()()()']
质数分解
描述
每个数字可以写成多个质数的乘积,给定一个数字,请你分解为多个质数
题解
def fun(num, list=None):
if list is None:
list = []
for i in range(2, num):
while num % i == 0:
list.append(i)
num = int(num / i)
if num > 1:
fun(num)
return list
x = 9*5
print(fun(x))# [3, 3, 5]
图
最少换乘
描述
假设你要从起点去往终点,路上有多个单向站点,怎么设计一个算法,找到最少换乘的路线?
这是一个经典的图论问题,可以用广度优先搜索(BFS)来解决。每条路线可以看作是一条边,换乘次数就是路径的边数。
图数据结构定义:
edges = {
'A': ['B', 'C'],
'B': ['C'],
'C': ['D'],
'D': [],
}
如下图所示:
思路
- 找到起点能去的所有节点
- 检查可去节点是否包含终点
- 如果不包含则继续前往这些节点的可去节点
- 使用广度优先搜索(BFS)确保找到的是最少换乘路线
- 使用队列记录当前层级的节点,逐层扩展直到找到终点
题解
def min_transfers(edges, start, end):
"""
找到从起点到终点的最少换乘路线
:param edges: 图的邻接表表示,edges[u] = [v1, v2, ...] 表示从u可以到达的站点
:param start: 起点
:param end: 终点
:return: 最少换乘次数和路径,如果无法到达返回-1和None
"""
if start == end:
return 0, [start]
# BFS队列,存储(当前站点, 换乘次数, 路径)
queue = [(start, 0, [start])]
visited = {start} # 已访问的站点
# 只要队列不为空,就继续遍历
while queue:
# 获取队列中最左侧(最左边是第一个进入队列的元素)元素,并从队列中删除
# 当前站点, 换乘次数, 路径
current, transfers, path = queue.pop(0)
# 遍历所有可达的下一个站点(即当前站点可去的站点)
# 如果当前站点没有下一个站点,则跳过
# 如果当前站点有下一个站点,则将下一个站点加入队列
for next_station in edges.get(current, []):
if next_station == end:
# 找到终点
return transfers + 1, path + [end]
# 如果下一个站点没有被访问过,则将下一个站点加入已访问集合,并加入队列
# 这一步可以避免重复访问同一个站点,提升算法效率
if next_station not in visited:
# 将下一个站点加入已访问集合
visited.add(next_station)
# 将下一个站点加入队列
# 换乘次数加1,路径加上下一个站点
queue.append((next_station, transfers + 1, path + [next_station]))
# 无法到达终点
return -1, None
# 测试数据
if __name__ == '__main__':
# 示例:站点A->B->D, A->C->D, A->D
edges = {
'A': ['B', 'C'],
'B': ['C'],
'C': ['D'],
'D': []
}
transfers, path = min_transfers(edges, 'A', 'D')
print(f"最少换乘次数: {transfers}")
print(path)
最少价格路径
描述
给定一个带权有向图,每条边都有一个价格(权重),请找到从起点到终点的价格总和最小的路径。
这是一个典型的单源最短路径问题,可以使用Dijkstra算法来解决(假设所有边权非负)。
图数据结构定义:
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('C', 2), ('D', 5)],
'C': [('D', 1)],
'D': []
}
如下图所示:
虽然A -> B -> D 或 A -> C -> D 的路径最短,但价格较高。
其中沿着A -> B -> C -> D 的路径,价格总和最小,为1 + 2 + 1 = 4。
思路
- 初始化起点的距离为0,其他所有节点距离为无穷大
- 找到起点能去的所有节点,计算到达这些节点的价格
- 选择价格最小的节点作为下一个访问节点
- 检查该节点的可去节点,更新到达这些节点的最少价格
- 重复步骤3-4,直到访问到终点或所有可达节点都访问完毕
- 使用优先队列(堆)来快速找到价格最小的节点
题解
def dijkstra(graph, start, end):
"""
使用Dijkstra算法找到从起点到终点的最少价格路径
:param graph: 图的邻接表表示,graph[u] = [(v1, weight1), (v2, weight2), ...]
:param start: 起点
:param end: 终点
:return: 最少价格和路径,如果无法到达返回-1和None
"""
# 初始化节点信息字典:每个节点包含价格、前驱节点等信息
nodes = {}
processed = set() # 已处理的节点集合
# 获取所有节点
# 1. 获取所有节点放入 集合,集合具有去重的特点
all_nodes = set(graph.keys())
# 2. 获取所有邻居节点
for neighbors in graph.values():
for neighbor, _ in neighbors:
all_nodes.add(neighbor) # 将邻居节点加入所有节点集合
# 步骤1:初始化起点的距离为0,其他所有节点距离为无穷大
for node in all_nodes:
nodes[node] = {
'price': 0 if node == start else float('inf'),
'previous': None # 前一个节点
}
"""
all_nodes = {'A', 'B', 'C', 'D'}
nodes = {'A': {'price': 0, 'previous': None},
'D': {'price': inf, 'previous': None},
'C': {'price': inf, 'previous': None},
'B': {'price': inf, 'previous': None}}
"""
def find_lowest_price_node():
"""在所有未处理的节点中,找到价格最小的节点"""
lowest_price = float('inf')
lowest_price_node = None
for node_name, node_info in nodes.items():
# 依次取出每个节点,并检查其价格是否小于最低价格
# 初始选择中,除了起点是0,其他都是无穷大,所以一定会选择起点
if node_name not in processed: # 只从未处理的节点中查找
cost = node_info['price']
if cost < lowest_price:
lowest_price = cost
lowest_price_node = node_name
return lowest_price_node
# 步骤3-5:重复选择价格最小的节点,更新其邻居节点的价格
node = find_lowest_price_node() # 在所有未处理的节点中,找到价格最小的节点
while node is not None: # 只要存在未处理的节点,就继续循环
# 如果到达终点,可以提前结束(可选优化)
if node == end:
break
price = nodes[node]['price'] # 当前节点的价格
# 步骤2和4:找到当前节点能去的所有节点,计算到达这些节点的价格
neighbors = graph.get(node, []) # 当前节点的邻居(从图的邻接表中获取)
for neighbor_name, edge_weight in neighbors: # 遍历当前节点的邻居
# 计算到达邻居的价格 = 当前节点价格 + 边权重
new_price = price + edge_weight
# 如果到达邻居的价格比当前价格更小,则更新邻居的价格
if new_price < nodes[neighbor_name]['price']:
nodes[neighbor_name]['price'] = new_price # 更新邻居的价格
nodes[neighbor_name]['previous'] = node # 更新邻居的前一个节点
processed.add(node) # 将当前节点标记为已处理
node = find_lowest_price_node() # 在所有未处理的节点中,找到价格最小的节点
# 检查是否能到达终点
if nodes[end]['price'] == float('inf'):
return -1, None
# 重建路径
def get_path(node_name):
"""根据前驱节点重建路径"""
path = []
current = node_name
while current is not None:
path.append(current)
current = nodes[current]['previous']
path.reverse()
return path if path else None
path = get_path(end)
return nodes[end]['price'], path
# 测试数据
if __name__ == '__main__':
# 示例图:A->B(1), B->C(2),
# B->D(5),
# A->C(4), C->D(1)
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('C', 2), ('D', 5)],
'C': [('D', 1)],
'D': []
}
price, path = dijkstra(graph, 'A', 'D')
print(f"最少价格: {price}")
print(f"路径: {' -> '.join(path)}") # 最少价格: 4, 路径: A -> B -> C -> D
树
树是一种特殊的图,没有环路。其中更特殊的是二叉树。二叉树是一种每个节点最多有两个子节点的树。
其中左子节点及其所有子节点称为左子树,右子节点及其所有子节点称为右子树。
树兼顾了数组与链表的优点,即可以快速访问任意元素,又可以快速插入和删除元素。
BST树满足普通二叉树的条件,同时:左子节点与左子树总是比父节点要小,右子节点与右子树总是比父节点要大。
假设其根节点为:10 ,左节点为5,右节点为20 则表示为:[10, 5, 20]
如果5有左节点为2,右节点为7。20没有左节点,有右节点为25。则表示为:[10, 5, 20, 2, 7, None,25]
如下图所示:
这棵树从根节点出发,到任意子节点,最多只有两条边。所以树的高度为2。
同样的这组数据,也可绘制以25为根节点的树,所有比25小的都在左侧,但是这样树会很高(去往左侧节点需要走很多步)。
如下图所示: