一、排序与查找(必问思想,常要求手写)
1.1 6种经典排序算法
1.1.1冒泡排序(Bubble Sort)
(1)通俗原理
像水里泡泡上浮:相邻两个元素两两比较,如果左边数字>右边,就交换位置;每一轮遍历,当前未排序里最大的数字会被一步步 “冒泡” 到末尾,每完成一轮,末尾多 1 个排好的大数。
举例子:原数组[5,3,8,4,2] 第 1 轮相邻对比: 5 和 3 换→[3,5,8,4,2] →5 和 8 不换→8 和 4 换→[3,5,4,8,2]→8 和 2 换→[3,5,4,2,8] 本轮结束:最大值8已经固定在最后一位,后续不用再动 8。 下一轮只排前面[3,5,4,2],重复逻辑。
(2)Python代码(基础版 + 优化版)
# 基础冒泡排序
def bubble_sort(arr):
# 获取数组长度
n = len(arr)
# 需要循环n-1轮,n个数最多n-1次冒泡
for i in range(n - 1):
# 每一轮后末尾i个数字已经排好,不用再比较
for j in range(n - 1 - i):
# 左边>右边,交换
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
# 优化版:如果某一轮没有发生任何交换,说明数组已经有序,直接提前退出
def bubble_sort_optimize(arr):
n = len(arr)
for i in range(n - 1):
swap_flag = False # 标记本轮是否交换
for j in range(n - 1 - i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
swap_flag = True
if not swap_flag: # 无交换=全有序,跳出循环
break
return arr
# 测试
test = [5,3,8,4,2]
print(bubble_sort_optimize(test)) # 输出[2,3,4,5,8]
1.1.2选择排序(Selection Sort)
(1)通俗原理
把数组分成「左边有序区」「右边无序区」,每一轮从无序区找到最小值,和无序区第一个元素交换,最小值归入有序区末尾。
举例[5,3,8,4,2]: 第一轮无序区全数组,最小值是 2,和第一位 5 交换 →[2,3,8,4,5],2 进入有序区; 第二轮无序区[3,8,4,5],最小值 3,不用交换,有序区变成[2,3]; 后续循环处理剩余无序区。
(2)代码
def select_sort(arr):
n = len(arr)
# i代表有序区末尾下标,从0到倒数第二个元素
for i in range(n - 1):
min_index = i # 先假设无序区第一个是最小值下标
# 遍历无序区:i+1 ~ 末尾
for j in range(i + 1, n):
if arr[j] < arr[min_index]:
min_index = j # 更新最小值下标
# 找到最小值后,和无序区首位交换
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
test = [5,3,8,4,2]
print(select_sort(test))
1.1.3插入排序(Insert Sort,打牌理牌逻辑)
(1)通俗原理
模拟手里摸牌整理:数组左侧是已经排好的有序牌,逐个拿右侧无序的数字,向前插入到有序区里合适的位置。
举例[5,3,8,4,2]: 默认第一个数 5 是有序区[5]; 取第二个数 3,比 5 小,插到 5 前面→有序区[3,5]; 取第三个数 8,比 5 大,直接放末尾→[3,5,8]; 取第四个数 4,往前找:8>4、5>4、3<4,插在 3 和 5 中间→[3,4,5,8],以此类推。
(2)代码
def insert_sort(arr):
n = len(arr)
# 从第二个元素开始(下标1),逐个插入
for i in range(1, n):
temp = arr[i] # 待插入的数字
j = i - 1 # 有序区末尾下标
# 往前遍历有序区:比temp大的数字全部后移
while j >= 0 and arr[j] > temp:
arr[j+1] = arr[j]
j -= 1
arr[j+1] = temp # 空位放入temp
return arr
test = [5,3,8,4,2]
print(insert_sort(test))
小结前三种:冒泡 / 选择 / 插入适合短数组、少量数据,数据量大的时候排序很慢,面试只需要能手写插入即可,冒泡选择看懂逻辑。
1.1.4速排序(Quick Sort|面试 TOP1 必考手写)
(1)通俗原理
- 随便选一个数字当「基准 pivot」(一般选数组第一个 / 最后一个)
- 把数组分成两堆:小于基准放左边,大于基准放右边(分区 partition)
- 左边一堆、右边一堆重复上面两步,递归拆分,直到子数组只剩 1 个数(天然有序)
举例[5,3,8,4,2],基准选第一个数 5: 分区后:小于 5:[3,4,2] | 基准 5 | 大于 5:[8] 再分别对[3,4,2]、[8]递归排序。
(2)代码(面试最常用单边分区写法)
def quick_sort(arr):
# 递归终止条件:数组长度<=1不用排序
if len(arr) <= 1:
return arr
pivot = arr[0] # 选第一个元素作为基准
left = [] # 存小于基准的数
mid = [pivot] # 等于基准
right = [] # 存大于基准的数
for num in arr[1:]:
if num < pivot:
left.append(num)
elif num > pivot:
right.append(num)
else:
mid.append(num)
# 递归拼接:左+中+右
return quick_sort(left) + mid + quick_sort(right)
test = [5,3,8,4,2]
print(quick_sort(test))
进阶:原地版快排(不开新数组,真正面试手写原地 partition,先掌握上面简易版理解思想)
1.1.5归并排序(Merge Sort|面试TOP2)
(1)通俗原理
- 拆分:数组从中间劈成两半,不断对半拆分,直到每个子数组只剩 1 个元素(单个元素天然有序)
- 合并:两个有序小数组,用双指针拼成一个新的有序数组,逐层向上合并。
举例[5,3,8,4,2]: 拆分→[5,3,8] 和 [4,2] →再拆→[5][3][8]、[4][2] 合并:[3,5,8]、[2,4] →最后合并[2,3,4,5,8]
(2)代码递归代码
def merge_sort(arr):
# 递归终止:长度<=1直接返回
if len(arr) <= 1:
return arr
mid = len(arr) // 2 # 中间分割点
left = merge_sort(arr[:mid]) # 左半部分递归拆分排序
right = merge_sort(arr[mid:]) # 右半部分递归拆分排序
# 合并两个有序数组
return merge(left, right)
# 合并函数:把两个有序数组合成一个有序数组
def merge(left, right):
res = []
i = j = 0 # 双指针,分别指向左右数组起始
while i < len(left) and j < len(right):
if left[i] < right[j]:
res.append(left[i])
i += 1
else:
res.append(right[j])
j += 1
# 把剩下没遍历完的元素直接追加
res += left[i:]
res += right[j:]
return res
test = [5,3,8,4,2]
print(merge_sort(test))
1.1.6堆排序(Heap Sort|重点 TopK,RAG 高频)
(1)通俗原理
堆是用数组实现的完全二叉树,大顶堆:父节点数字 ≥ 两个子节点
- 把原数组构建成大顶堆(堆顶是整个数组最大值)
- 堆顶最大值和数组末尾元素交换,最大值固定到末尾;
- 剩下前面的数组重新调整成大顶堆,重复交换,逐步排完。
(2)代码
def heap_sort(arr):
n = len(arr)
# 第一步:构建大顶堆
for i in range(n//2 -1, -1, -1):
heap_adjust(arr, n, i)
# 第二步:逐个交换堆顶和末尾,调整堆
for i in range(n-1, 0, -1):
arr[0], arr[i] = arr[i], arr[0] # 堆顶最大值放末尾
heap_adjust(arr, i, 0) # 剩余数组重新堆化
return arr
# 堆调整函数:以i为根,把对应子树调成大顶堆
def heap_adjust(arr, size, root):
max_idx = root # 最大值下标默认根节点
left = 2 * root + 1 # 左孩子下标
right = 2 * root + 2 # 右孩子下标
# 左孩子更大
if left < size and arr[left] > arr[max_idx]:
max_idx = left
# 右孩子更大
if right < size and arr[right] > arr[max_idx]:
max_idx = right
# 最大值不是根,交换+递归调整
if max_idx != root:
arr[root], arr[max_idx] = arr[max_idx], arr[root]
heap_adjust(arr, size, max_idx)
test = [5,3,8,4,2]
print(heap_sort(test))
1.2查找算法
1.3时间复杂度 & 空间复杂度
意义:时间、空间复杂度就是脱离电脑配置、编程语言,标准化衡量算法优劣的标尺:
- 时间复杂度:衡量运行快慢(随数据变多,耗时涨得多快)
- 空间复杂度:衡量额外内存开销(随数据变多,占用内存涨得多快)





1.3.1时间复杂度
(1)大 O 记号 O () 是什么?
只看数据规模 n 变大时,算法耗时 / 空间的增长趋势,忽略常数、低次项
n:输入数据长度(比如数组长度
len(nums)=n)
(2)定义
代码跑多久(执行次数随 n 变化)
(3)常见复杂度从优到劣(背顺序)
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)


前面所有相向双指针、快慢、滑动窗口:全部 O (n),这就是比暴力O(n2)优秀的原因。




(4)
1.3.2空间复杂度
(1)定义
空间复杂度 = 算法运行时,额外新开的内存大小,不包含题目本来给你的输入数据。
重点规则:原始输入的数组、字符串,是题目传进来的,不算在额外空间里; 只有你自己在代码里:新建数组、列表、集合、字典、变量才算占用空间。
记号依旧:大 O O(...),只看数据规模n(输入长度)变大时,额外空间增长趋势。
(2)举例分析
1.O(1) 常数空间|原地算法(额外空间固定,和 n 无关)
无论输入数组多长(n=10 /n=10000),你定义的变量永远就几个,占用内存不变。 只定义普通数字变量:a,b,left,right,sum 这种单个变量全是O(1)。
示例1:
def add(arr):
total = 0 # 只1个变量
for num in arr:
total += num
return total
输入arr是题目给的,不算空间;只开了total,空间O(1)。
示例2(最简单双指针雏形,只两个变量):
def check(arr):
left = 0
right = len(arr)-1
while left < right:
left +=1
right -=1
return True
只创建left、right两个变量,不管 arr 长度 n 是 10 还是 10w,永远 2 个变量 → 空间 O (1)
这就是之前双指针大多 O (1) 的原因:只开几个标记指针,不建新数组。
2.O(n) 线性空间|额外开容器,空间跟着 n 变大
自己新建列表 / 集合 / 字典,容器存储的数据量和输入长度n成正比。
def copy_arr(arr):
new_list = [] # 自己新建数组
for x in arr:
new_list.append(x)
return new_list
输入 arr 长度 n,new_list最后存 n 个元素,n 越大占用内存越大 → 空间 O (n)
滑动窗口里的set()存字符就是这类:数组全不重复,集合存满 n 个元素 → O (n)。
3.O(n2) 平方空间(少见)
新建二维数组,n 行 n 列:
def create2d(n):
mat = [[0]*n for _ in range(n)]
return mat
一共n×n个数据,空间O(n2)。
4.易错点汇总
(a) 输入参数不算空间!!
(b)循环里临时单个变量不算 O (n)。以下代码空间依旧O(1)。
def calc(arr):
s = 0
for i in arr:
tmp = i+1 # tmp每次循环复用同一块内存,不是新建n个变量
s += tmp
return s
二、数组 + 双指针 / 滑动窗口(超级高频)
2.1数组
算法里的数组,在 Python 中就是 list(列表),是面试最基础的数据结构。下面从概念、下标、特性、算法常用规则一步步讲,这是双指针的前提。
2.1.1数组基本概念
数组是一段连续内存存放一组数据,元素按顺序排列,每个元素有唯一的下标(索引)。
- Python 中
nums = [2,5,7,9]就是一个数组; - 数组长度:
len(nums),上面例子长度为 4。
2.1.2 下标(索引)—— 双指针的核心基础(必记)
数组下标从 0 开始,从左到右依次递增:
数组: [ 2 , 5 , 7 , 9 ]
下标: 0 1 2 3
规则总结:
- 第一个元素下标:
0 - 最后一个元素下标:
len(nums) - 1(上面例子:4-1=3) - 通过下标取值:
nums[下标],比如nums[1] = 5 - 下标范围:
0 ≤ 下标 ≤ len(nums)-1,超出会报错(越界)
双指针本质就是用两个变量存储两个下标,不断修改下标实现遍历。
2.1.3内存布局与地址计算
内存布局:
以C语言为例,arrary[9]=[1,2,3,4,65,6,7,8,9],由于int类型是4个字节,所以当arry[0]=1的地址是0x1000时,内存分布如下图所示

地址计算:
地址 = 基地址 + 偏移量
addr = 0x1000 + (4*type_size)
2.1.4 数组核心性能特性(结合复杂度理解)
- 随机访问极快:O (1) 直接按下标取元素,不管数组多长,耗时固定。
- 中间插入 / 删除很慢:O (n) 如果删除数组中间元素,后面所有元素都要往前移位,数据量越大越慢。 这也是算法题常要求原地修改数组的原因:避免新建数组、减少移位操作。
2.1.5算法里两个关键名词(做题必懂)
- 原地操作 不新建额外数组,直接修改原数组,空间复杂度通常为 \(O(1)\),面试高频要求。
- 子数组 数组中连续的一段元素(滑动窗口只处理连续子数组)。 例:
[2,5,7,9]的子数组有[5,7]、[2,5,7];[2,7]不是子数组(不连续)。
2.2指针
2.2.1区分两种指针

2.2.2 第一部分:传统底层指针(C/C++ 原生指针,了解原理)



简单示例(伪代码,看懂逻辑即可)
// 1. 定义普通变量 a,存数据 10
int a = 10;
// 2. 定义指针变量 p,用来存放变量 a 的地址
int *p = &a;
// &a :取变量 a 的内存地址
// *p :代表这是一个指针变量
// 3. 通过指针 p,找到 a 对应的内存格子,读取数据
printf("%d", *p);
// *p :解引用,根据地址找到对应数据,输出 10

2.2.2 第二部分:算法刷题中的「指针」(核心重点)




nums = [2,5,7,9]
for i in nums:
print(i)










2.3双指针
结合你已经掌握的数组、单指针基础,今天系统讲解双指针。 双指针就是两个单指针配合工作,是数组、字符串算法面试的第一高频考点。 它的核心价值:把暴力双层循环 \(O(n^2)\) 的时间复杂度,优化到线性 \(O(n)\);多数场景支持原地操作,额外空间 \(O(1)\),效率拉满。
2.3.1 双指针整体分类(3 大类型,覆盖 100% 面试题型)

(1)第一类:快慢指针(同向双指针)






def removeElement(nums, val):
slow = 0
for fast in range(len(nums)):
# 保留不等于 val 的元素
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
# 测试
nums = [3, 2, 2, 3]
val = 3
new_len = removeElement(nums, val)
print(new_len) # 输出 2
print(nums[:new_len])# [2, 2]







(2)第二类:相向对撞指针(左右指针)







def threeSum(nums):
res = []
nums.sort() # 排序
n = len(nums)
for i in range(n):
# 去重:跳过和上一个相同的元素
if i > 0 and nums[i] == nums[i-1]:
continue
left = i + 1
right = n - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total == 0:
res.append([nums[i], nums[left], nums[right]])
# 左右指针分别去重
while left < right and nums[left] == nums[left+1]:
left += 1
while left < right and nums[right] == nums[right-1]:
right -= 1
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1
return res
# 测试
print(threeSum([-1,0,1,2,-1,-4]))
# 输出:[[-1, -1, 2], [-1, 0, 1]]
(3)滑动窗口(同向双指针进阶,连续区间专属)







2.3.2做题快速选型口诀(面试秒判断题型)
拿到数组 / 字符串题,按下面规则快速选择双指针类型:
- 原地去重、移除元素、移动零 → 快慢指针
- 有序数组求和、反转、回文判断 → 相向对撞指针
- 求固定长度连续子数组 → 定长滑动窗口
- 求最长 / 最短连续子数组、子串 → 变长滑动窗口
- 无序数组非连续查找 → 不用双指针,改用哈希表
2.3.3面试高频通用追问(必背)

2.3.4刷题建议
- 优先攻克快慢指针4 道题:是双指针基础,上手最简单;
- 再练相向指针,重点理解「有序数组」这个前提;
- 最后突破滑动窗口(面试区分度最高);
- 每做完一题,手动模拟一遍指针移动过程,彻底吃透逻辑。
三、二分查找
四、 栈 & 队列
五、哈希表(Hash)
六、字符串(重点,贴近实际开发)
七、二叉树(仅次于链表,校招核心难点)
***************************持续更新**********************************

813

被折叠的 条评论
为什么被折叠?



