一、排序与查找(必问思想,常要求手写)
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)定义
空间复杂度 = 算法运行时,额外新开的内存大小,不包含题目本来给你的输入数据。
重点规则:原始输入的数组、字符串,是题目传进来的,不算在额外空间里; 只有你自己在代码里:新建数组、列表、集合、字典、变量才算占用空间。
记号依旧:大 OO(...),只看数据规模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二、数组 + 双指针 / 滑动窗口(超级高频)
三、二分查找
四、 栈 & 队列
五、哈希表(Hash)
六、字符串(重点,贴近实际开发)
七、二叉树(仅次于链表,校招核心难点)
***************************持续更新**********************************