本博客就蓝桥杯中的基础算法(这一部分说是算法,但更是一些简单的操作)进行罗列,包括:枚举、模拟、前缀和、差分、二分查找、进制转换、贪心、位运算和双指针。
每一个算法都在给出概念解释的同时,给出了示例代码,以供低年级师弟师妹们学习和练习。
前序知识:
(1)Python基础语法
(2)Python OOP(面向对象编程)
一、枚举
算法解释:
枚举就是像“穷举”一样,把所有可能性都列出来,挨个检查是否符合条件。就像你忘记密码时,从0000到9999逐个尝试。
核心要点:
- 暴力遍历:用循环把所有可能情况都试一遍
- 剪枝优化:提前排除不可能的情况(比如检查到中间发现已经不可能,直接跳过后续判断)
- 适用场景:当数据范围较小(比如三位数、四位数)时可用
代码示例:找出所有3位水仙花数
# 枚举算法
# 水仙花数定义:一个3位数,各位数字立方和等于它本身(例如153 = 1³ + 5³ + 3³)
for num in range(100, 1000): # 遍历所有3位数
# 分解各位数字(数学方法)
a = num // 100 # 获取百位数字:整除100(例如153//100=1)
b = (num // 10) % 10 # 获取十位数字:先整除10得15,再取余10得5
c = num % 10 # 获取个位数字:直接取余10(153%10=3)
# 判断是否符合条件
if a**3 + b**3 + c**3 == num:
print(num) # 输出符合条件的数字
二、模拟
算法解释:
实际上就是"仿真"一个过程,严格按照题目描述的步骤来实现程序。就像用乐高积木一步步搭建出场景模型。
核心要点:
- 流程还原:必须完全按照题目要求的步骤来写代码
- 边界处理:特别注意开始、结束、空数据等特殊情况
代码示例:银行排队时间计算(3个窗口,5个客户)
# 模拟算法
# 问题描述:客户按顺序到达,选择最早空闲的窗口办理业务,计算总等待时间
n = 3 # 银行窗口数量
k = 5 # 客户数量
times = [2,5,3,4,1] # 每个客户办理业务需要的时间(假设同时到达)
windows = [0] * n # 记录每个窗口当前业务的结束时间(初始都是0)
total_wait = 0 # 总等待时间统计
for t in times: # 遍历每个客户
# 找到最早可用的窗口(窗口结束时间最小的)
min_time = min(windows) # 获取所有窗口中的最早空闲时间
idx = windows.index(min_time) # 找到这个窗口的索引位置
# 计算当前客户的等待时间(窗口空闲时间 - 到达时间)
# 假设所有客户都是0时刻到达,所以等待时间就是窗口之前的结束时间
wait_time = max(0, min_time - 0) # 如果窗口空闲,等待时间为0
total_wait += wait_time # 累加到总等待时间
print(f"客户{times.index(t)+1}选窗口{idx}, 等待时间:{wait_time} 分钟")
# 更新窗口的结束时间(原结束时间 + 当前客户办理时间)
windows[idx] = min_time + t
print(f"总等待时间:{total_wait} 分钟")
三、前缀和
算法解释:
预先计算数组前N项的和,使得后续求任意区间和的时间复杂度降为O(1)。
代码示例:
# 前缀和算法
# 原始数组(索引从0开始)
arr = [1, 3, 5, 7, 9]
# 创建前缀和数组(比原数组多一个元素,pre_sum[0]=0)
pre_sum = [0] * (len(arr) + 1) # 初始化全0数组
# 计算前缀和数组
for i in range(len(arr)):
# pre_sum[i+1] 表示原数组前i个元素的和(i从0开始计数)
pre_sum[i+1] = pre_sum[i] + arr[i]
# 例如:pre_sum[1] = arr[0], pre_sum[2] = arr[0]+arr[1]
# 查询区间和:获取原数组索引2到4的和(对应元素5+7+9=21)
# 注意题目中的区间定义:如果题目说区间[2,4]指原数组第2到第4个元素(从0开始)
left = 2 # 原数组索引2(第三个元素)
right = 4 # 原数组索引4(第五个元素)
sum_range = pre_sum[right+1] - pre_sum[left] # pre_sum[5] - pre_sum[2]
print(f"区间和:{sum_range}") # 输出21
四、差分
算法解释:
差分是前缀和的逆运算,用于快速对数组的某个区间进行增减操作。
代码示例:
# 差分数组算法
# 原始数组(假设初始全0)
arr = [0] * 5 # 索引0-4
# 创建差分数组(长度比原数组多1)
diff = [0] * (len(arr) + 1) # 索引0-5
# 操作:对原数组的区间[1,3](索引1到3)统一加2
l = 1 # 起始索引(包含)
r = 3 # 结束索引(包含)
val = 2
# 差分数组操作(重点!)
diff[l] += val # 影响从l开始的所有元素
diff[r+1] -= val # 在r+1位置抵消影响
print("原始差分数组:", diff)
# 应用差分数组得到最终结果
for i in range(0, len(arr)): # 注意从1开始
diff[i+1] += diff[i] # 计算前缀和得到每个位置的增量
previous = arr[i] # 原数组当前值
arr[i] += diff[i] # 将增量应用到原数组(注意索引偏移)
print(f"原数组索引{i}的增量:{diff[i]},原数组值:{previous}")
print("最终数组:", arr)
五、二分查找
算法解释:
就和朋友猜某个商品的价格一样,先猜一个中间的价格,朋友告诉你是高了还是低了,再根据这个信息再往高猜或者往低了猜。
核心要点:
- 有序性:必须在已排序的数据上使用
- 边界处理:注意while循环的终止条件和mid计算方式
- 模板记忆:掌握两种写法(找第一个/最后一个匹配项)
代码示例:在有序数组中查找数字5
# 二分查找算法
def binary_search(nums, target):
# 初始化左右指针(表示搜索范围的边界)
left = 0
right = len(nums) - 1 # 数组最后一个元素的索引
while left <= right: # 当左指针<=右指针时继续搜索
mid = (left + right) // 2 # 计算中间位置(取整)
if nums[mid] == target:
return mid # 找到目标,返回索引位置
elif nums[mid] < target:
# 中间值太小,说明目标在右半区
left = mid + 1 # 将左指针移到mid右侧
else:
# 中间值太大,说明目标在左半区
right = mid - 1 # 将右指针移到mid左侧
return -1 # 没找到返回-1
# 测试用例(注意数组必须是有序的!)
nums = [1, 3, 5, 7, 9]
print(binary_search(nums, 5)) # 输出2(索引位置)
print(binary_search(nums, 6)) # 输出-1(未找到)
# 易错点提醒:
# 1. mid计算如果用(left+right)//2可能导致整数溢出(Python不用考虑)
# 2. 循环条件必须是<=,若写成<会漏查元素
六、进制转换
算法解释:
就像用不同规格的盒子装东西,比如十进制是每满10进1,二进制是每满2进1。
核心要点:
- 除基取余法:用目标进制不断除以数值,记录余数
- 逆序输出:最后要把余数倒序排列
- 字母处理:超过9的数字用A-F表示(16进制)
代码示例:将255转换为十六进制
# 进制转换
def convert_base(n, base):
if base < 2 or base > 16:
return "错误:仅支持2-16进制"
# 数字符号库(超过9用字母表示)
digits = "0123456789ABCDEF"
result = [] # 存储余数
# 特殊情况处理:输入为0时直接返回0
if n == 0:
return "0"
# 循环计算余数
while n > 0:
remainder = n % base # 获取当前余数
result.append(digits[remainder]) # 将余数转为对应符号
n = n // base # 更新n为商
# 余数是倒序排列的,需要反转
return ''.join(reversed(result))
print(convert_base(255, 16)) # 输出FF
print(convert_base(10, 2)) # 输出1010
# 执行过程演示(255转16进制):
# 255 ÷ 16 = 15 余 15 → 对应F
# 15 ÷ 16 = 0 余 15 → 对应F
# 结果反转得到FF
七、贪心
算法解释:
像吃自助餐时每次都选最贵的食物,希望最终总价值最高(不一定全局最优,但追求局部最优)。
核心要点:
- 贪心选择:每一步都选择当前最优解
- 无后效性:当前选择不影响后续选择
代码示例:最多能参加的会议(选择不重叠区间)
# 贪心算法
intervals = [[1,3], [2,4], [4,12], [3,6], [5,7], [6,9], [7,10], [8,11]]
# 问题描述:选择最多数量的互不重叠的时间段
# 关键步骤:按结束时间排序(早结束的能给后面留更多时间)
intervals.sort(key=lambda x: x[1]) # 按每个元素的第二个值排序
count = 0 # 统计能选择的数量
last_end = -float('inf') # 初始化上一个选择的结束时间(负无穷)
intervals_list = []
for start, end in intervals:
if start >= last_end: # 当前活动开始时间不冲突
count += 1
intervals_list.append([start, end])
last_end = end # 更新最后结束时间
print(f"最多可以参加{count}个会议, 会议时间分别为:{intervals_list}") # 输出3(选[1,3],[3,6]会冲突吗?)
八、位运算
算法解释:
直接操作二进制位,就像用显微镜观察数据的二进制结构。
核心要点:
操作 | 效果 | 示例(二进制) |
---|---|---|
x & 1 | 判断奇偶 | 1101 & 1 = 1 |
x << 1 | 乘以2 | 5<<1=10 (101→1010) |
x & (x-1) | 消除最后一个1 | 1100 & 1011=1000 |
x ^ x = 0 | 相同数异或得0 | 0110 ^ 0110 = 0000 |
代码示例:计算数字的二进制中1的个数
# 位运算 (二进制)
def count_ones(n):
count = 0
while n:
# n & (n-1) 会消除二进制中最后的一个1
# 例如:n=13(1101), n-1=12(1100) → 1101 & 1100 = 1100(消去最后一个1)
n = n & (n - 1)
count += 1 # 每消去一个1就计数
return count
print(count_ones(13)) # 输出3(1101有3个1)
print(count_ones(7)) # 输出3(0111)
# 执行过程演示(n=13):
# 初始值:1101 → count=0
# 第一次循环:1101 & 1100 = 1100 → count=1
# 第二次循环:1100 & 1011 = 1000 → count=2
# 第三次循环:1000 & 0111 = 0000 → count=3
九、双指针
算法解释:
就像两个人协同工作,一个快一个慢,或者从两头向中间移动。
核心要点:
- 快慢指针:判断链表是否有环
- 左右指针:有序数组的二分查找、两数之和
- 数组合并:将两个独立的数组合并为新的数组
代码示例1:快慢指针法(Floyd判圈算法)
- 快指针每次移动 2步,慢指针每次移动 1步
- 若链表有环,快指针最终会追上慢指针(相遇)
- 若快指针遇到 None,说明无环
# 双指针(快慢指针)
# 定义链表节点类
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def hasCycle(head: ListNode) -> bool:
# 处理空链表或单节点无环的情况
if not head or not head.next:
return False
# 初始化双指针
slow = head # 慢指针,每次走1步
fast = head # 快指针,每次走2步
# 循环移动指针(注意循环条件)
while fast and fast.next: # 保证fast能安全移动两步
# 先移动指针,再判断是否相遇
fast = fast.next.next # 快指针移动两步
slow = slow.next # 慢指针移动一步
if fast == slow: # 相遇说明有环
return True
return False # 循环结束未相遇,说明无环
# 测试用例
# 构造一个带环的链表
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
node4 = ListNode(4)
node5 = ListNode(5)
node6 = ListNode(6)
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5
node5.next = node6
node6.next = node2 # 关键:形成环(6指向2)
print(hasCycle(node1)) # 输出 True
# 构造无环链表
nodeA = ListNode(1)
nodeB = ListNode(2)
nodeA.next = nodeB
print(hasCycle(nodeA)) # 输出 False
代码示例2: 左右指针
# 双指针
def merge(nums1, m, nums2, n):
# nums1有足够空间(总长度m+n),前m个是有效元素
# 从后往前填充,避免覆盖未处理的元素
p1 = m - 1 # nums1有效部分的最后索引
p2 = n - 1 # nums2的最后索引
tail = m + n - 1 # 合并后的最后位置
while p2 >= 0: # 当nums2还有元素需要处理时
# 如果nums1还有元素,且nums1当前值更大
if p1 >= 0 and nums1[p1] > nums2[p2]:
nums1[tail] = nums1[p1]
p1 -= 1
else:
nums1[tail] = nums2[p2]
p2 -= 1
tail -= 1 # 向前移动填充位置
return nums1
# 测试用例
nums1 = [1,3,4,5,7,0,0,0]
nums2 = [2,4,6]
print(merge(nums1, 5, nums2, 3)) # 输出[1,2,3,4,5,6]
# 执行过程演示:
# 初始状态:nums1=[1,3,5,0,0,0], nums2=[2,4,6]
# p1=2(指向5), p2=2(指向6), tail=5
# 第一轮:5<6 → 填入6,tail=4, p2=1
# 第二轮:5>4 → 填入5,tail=3, p1=1
# 第三轮:3<4 → 填入4,tail=2, p2=0
# 第四轮:3>2 → 填入3,tail=1, p1=0
# 第五轮:1<2 → 填入2,tail=0, p2=-1
# 最终nums1变为[1,2,3,4,5,6]