数据结构与算法(python最新版)

该文章已生成可运行项目,

第一章:算法基础——时间复杂度 & 空间复杂度

一、为什么要学复杂度

同样实现一个功能,写法不同运行速度、占用内存天差地别。
复杂度就是用来评估算法好坏的标准,不用跑代码,一眼看出谁快谁慢、谁省内存。

二、大O表示法

评判算法统一标准:大 O 符号表示法
只关注最高阶项,忽略常数、低次项。

常见复杂度从快到慢排序:
O(1)<O(log⁡n)<O(n)<O(nlog⁡n)<O(n2)<O(n3)<O(2n)O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2) < O(n^3) < O(2^n)O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)

三、时间复杂度(执行时间)

1. O(1) 常数级

代码执行次数固定,和数据量 n 无关

def get_sum(a, b):
    return a + b

无论传入什么,永远只执行1次,O(1)

2. O(n) 线性级

循环 n 次,数据越大执行次数越多

def print_n(n):
    for i in range(n):
        print(i)

循环跑 n 次,O(n)

3. O(n2n^2n2) 平方级

嵌套循环,一层n,一层n

def double_loop(n):
    for i in range(n):
        for j in range(n):
            print(i, j)

n×nn \times nn×n 次,O(n2n^2n2)

4. O(log⁡n\log nlogn) 对数级

每次数据缩小一半,典型:二分查找
数据翻倍,执行次数只加一点点,效率极高。

5. O(nlog⁡nn\log nnlogn) 线性对数级

代表算法:快速排序、归并排序
排序里最优的稳定复杂度。

四、空间复杂度(占用内存)

衡量算法额外开辟多少内存空间

  • O(1):只用几个变量,不开新数组
  • O(n):额外创建长度为n的列表
  • O(n2n^2n2):开辟二维数组

示例:

# 空间O(1)
def func1(n):
    s = 0
    for i in range(n):
        s += i
    return s

# 空间O(n)
def func2(n):
    lst = [i for i in range(n)]
    return lst

五、三种情况分析

  1. 最好情况:运气最好,执行最少次数
  2. 最坏情况:运气最差,执行最多次(面试常看这个)
  3. 平均情况:所有情况平均耗时

第二章:单向链表

一、什么是链表

1. 数组/列表 的缺点

Python列表底层是顺序存储

  • 中间插入、删除元素,后面元素要整体搬家
  • 数据量越大,效率越低

2. 链表特点

链表由一个个节点串联而成,每个节点包含两部分:

  1. 数据域:存当前节点数据
  2. 指针域:存下一个节点的地址

逻辑结构:

头节点 → 节点1 → 节点2 → 节点3 → 尾节点(指向None)

优点:

  • 插入、删除只改指针,不用批量移动元素
  • 内存不要求连续,灵活分配

缺点:

  • 不能像列表那样按下标随机访问
  • 只能从头节点开始逐个往后遍历

二、节点类设计

每个节点是一个独立对象

class Node:
    def __init__(self, data):
        # 数据域
        self.data = data
        # 指针域,默认指向空
        self.next = None

三、单向链表整体结构

class SingleLinkedList:
    def __init__(self):
        # 初始化头节点为空
        self.head = None

四、链表核心功能实现

1. 尾部添加节点

class SingleLinkedList:
    def __init__(self):
        self.head = None

    # 尾部添加元素
    def append(self, data):
        # 创建新节点
        new_node = Node(data)
        # 如果链表为空,头节点指向新节点
        if self.head is None:
            self.head = new_node
            return
        # 不为空,遍历到尾节点
        cur = self.head
        while cur.next is not None:
            cur = cur.next
        # 尾节点指向新节点
        cur.next = new_node

2. 遍历打印链表

    # 遍历所有节点
    def travel(self):
        cur = self.head
        while cur is not None:
            print(cur.data, end=" → ")
            cur = cur.next
        print("None")

3. 头部插入节点

    # 头部插入
    def add_first(self, data):
        new_node = Node(data)
        # 新节点next指向原头节点
        new_node.next = self.head
        # 头节点换成新节点
        self.head = new_node

4. 指定位置插入节点

    # 指定下标插入
    def insert(self, index, data):
        if index < 0:
            return
        new_node = Node(data)
        # 插在头部
        if index == 0:
            self.add_first(data)
            return
        cur = self.head
        # 找到插入位置的前一个节点
        for i in range(index - 1):
            if cur.next is None:
                break
            cur = cur.next
        # 调整指针
        new_node.next = cur.next
        cur.next = new_node

5. 删除指定数据节点

    # 删除指定元素
    def remove(self, data):
        cur = self.head
        pre = None
        # 找要删除的节点
        while cur is not None:
            if cur.data == data:
                # 如果是头节点
                if pre is None:
                    self.head = cur.next
                else:
                    pre.next = cur.next
                return
            pre = cur
            cur = cur.next

6. 查找元素是否存在

    # 查找元素
    def search(self, data):
        cur = self.head
        while cur is not None:
            if cur.data == data:
                return True
            cur = cur.next
        return False

五、完整代码

# 节点类
class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

# 单向链表类
class SingleLinkedList:
    def __init__(self):
        self.head = None

    def append(self, data):
        new_node = Node(data)
        if self.head is None:
            self.head = new_node
            return
        cur = self.head
        while cur.next is not None:
            cur = cur.next
        cur.next = new_node

    def travel(self):
        cur = self.head
        while cur is not None:
            print(cur.data, end=" → ")
            cur = cur.next
        print("None")

    def add_first(self, data):
        new_node = Node(data)
        new_node.next = self.head
        self.head = new_node

    def insert(self, index, data):
        if index < 0:
            return
        new_node = Node(data)
        if index == 0:
            self.add_first(data)
            return
        cur = self.head
        for i in range(index - 1):
            if cur.next is None:
                break
            cur = cur.next
        new_node.next = cur.next
        cur.next = new_node

    def remove(self, data):
        cur = self.head
        pre = None
        while cur is not None:
            if cur.data == data:
                if pre is None:
                    self.head = cur.next
                else:
                    pre.next = cur.next
                return
            pre = cur
            cur = cur.next

    def search(self, data):
        cur = self.head
        while cur is not None:
            if cur.data == data:
                return True
            cur = cur.next
        return False

# 测试
if __name__ == "__main__":
    sll = SingleLinkedList()
    sll.append(10)
    sll.append(20)
    sll.append(30)
    print("尾部添加后:")
    sll.travel()

    sll.add_first(5)
    print("头部插入后:")
    sll.travel()

    sll.insert(2, 15)
    print("下标2插入15后:")
    sll.travel()

    sll.remove(20)
    print("删除20后:")
    sll.travel()

    print("查找15:", sll.search(15))
    print("查找100:", sll.search(100))

六、单向链表复杂度总结

  • 尾部添加:O(n)
  • 头部插入:O(1)
  • 中间插入/删除:O(n)
  • 查找遍历:O(n)

第三章:栈

一、栈是什么

1. 核心特点

后进先出 LIFO
最后放进去的元素,最先拿出来。

生活例子:

  • 叠盘子:最后放的盘子先拿走
  • 浏览器后退、编辑器撤销 Ctrl+Z 底层都是栈

2. 栈结构术语

  • 入栈 push:往栈里存数据
  • 出栈 pop:从栈顶取出数据
  • 栈顶 top:当前最后一个元素
  • 栈底 bottom:最先放入的元素

二、Python 实现栈两种方式

  1. 列表模拟栈
  2. 手写栈类

三、方式一:列表直接模拟栈

# 列表尾部当栈顶
stack = []

# 入栈 append
stack.append(10)
stack.append(20)
stack.append(30)
print("入栈后:", stack)

# 出栈 pop
print("出栈:", stack.pop())
print("出栈:", stack.pop())

# 查看栈顶元素
print("栈顶:", stack[-1])

特点:

  • 尾部 append、pop 都是 O(1) 效率极高
  • 简单开发直接用列表当栈

四、方式二:手写封装栈类

完整代码

class Stack:
    def __init__(self):
        # 私有容器
        self.__items = []

    # 入栈
    def push(self, item):
        self.__items.append(item)

    # 出栈
    def pop(self):
        if self.is_empty():
            return None
        return self.__items.pop()

    # 查看栈顶
    def peek(self):
        if self.is_empty():
            return None
        return self.__items[-1]

    # 判断是否为空
    def is_empty(self):
        return len(self.__items) == 0

    # 获取栈大小
    def size(self):
        return len(self.__items)

    # 清空栈
    def clear(self):
        self.__items = []

测试

if __name__ == "__main__":
    s = Stack()
    s.push(1)
    s.push(2)
    s.push(3)

    print("栈大小:", s.size())
    print("栈顶元素:", s.peek())
    print("出栈:", s.pop())
    print("是否为空:", s.is_empty())

五、栈经典实战:括号匹配问题

题目要求

输入字符串包含 () [] {},判断括号是否合法匹配
规则:

  1. 左括号必须有对应右括号
  2. 必须按嵌套顺序闭合
  3. 不能交叉

算法思路

  1. 遍历字符串每个字符
  2. 遇到左括号:入栈
  3. 遇到右括号
    • 栈空 → 直接不匹配
    • 栈顶左括号和当前右括号不配对 → 不匹配
    • 配对成功 → 出栈
  4. 遍历结束,栈必须为空才算完全匹配

完整代码

def is_valid_bracket(s):
    stack = Stack()
    # 建立左右括号映射
    bracket_map = {")":"(", "]":"[", "}":"{"}

    for char in s:
        # 左括号入栈
        if char in bracket_map.values():
            stack.push(char)
        # 右括号匹配
        elif char in bracket_map.keys():
            # 栈空无左括号匹配
            if stack.is_empty():
                return False
            # 取出栈顶左括号
            top = stack.pop()
            if top != bracket_map[char]:
                return False
    # 最后栈必须为空
    return stack.is_empty()

# 测试
print(is_valid_bracket("()[]{}"))      # True
print(is_valid_bracket("([)]"))        # False
print(is_valid_bracket("{[]}"))        # True
print(is_valid_bracket("((()))"))       # True

六、栈复杂度与总结

  1. 入栈、出栈、取栈顶:O(1)
  2. 括号匹配遍历一次字符串:O(n)
  3. 栈核心思想:后进先出
  4. 适用场景:括号匹配、表达式求值、函数递归调用栈、撤销后退

第四章:队列

一、队列核心概念

1. 特点

先进先出 FIFO
先进入队列的元素,先被取出。

生活例子:

  • 排队买票、排队打饭
  • 消息异步队列、任务排队、请求限流

2. 队列术语

  • 入队 enqueue:从队尾添加元素
  • 出队 dequeue:从队头取出元素
  • 队头 front:最先进入的元素
  • 队尾 rearou:最后进入的元素

二、列表模拟普通队列

用列表头部出队 pop(0)

queue = []

# 入队 队尾添加
queue.append(10)
queue.append(20)
queue.append(30)

# 出队 队头弹出
print(queue.pop(0))
print(queue.pop(0))

缺点
pop(0) 会让后面所有元素前移,时间复杂度 O(n),数据量大很慢。

三、collections.deque 双端队列

Python 内置高效双端队列,头尾增删都是 O(1)

常用用法

from collections import deque

# 创建空队列
q = deque()

# 队尾入队
q.append(10)
q.append(20)
q.append(30)

# 队头出队
print(q.popleft())   # 10
print(q.popleft())   # 20

# 队头插入
q.appendleft(5)
# 队尾弹出
print(q.pop())

deque 常用方法

  • append() 队尾加
  • appendleft() 队头加
  • popleft() 队头出(高效)
  • pop() 队尾出
  • clear() 清空
  • len(q) 获取长度

四、手写实现普通队列

封装队列类,遵循先进先出

class Queue:
    def __init__(self):
        self.__items = []

    # 入队:队尾添加
    def enqueue(self, item):
        self.__items.append(item)

    # 出队:队头取出
    def dequeue(self):
        if self.is_empty():
            return None
        return self.__items.pop(0)

    # 判断是否为空
    def is_empty(self):
        return len(self.__items) == 0

    # 获取队列大小
    def size(self):
        return len(self.__items)

    # 查看队头元素
    def front(self):
        if self.is_empty():
            return None
        return self.__items[0]

测试代码

if __name__ == "__main__":
    q = Queue()
    q.enqueue(1)
    q.enqueue(2)
    q.enqueue(3)

    print("队列大小:", q.size())
    print("队头元素:", q.front())
    print("出队:", q.dequeue())
    print("出队:", q.dequeue())
    print("是否为空:", q.is_empty())

五、手写双端队列

支持头尾都可以入队、出队

class DeQueue:
    def __init__(self):
        self.__items = []

    # 队尾添加
    def add_rear(self, item):
        self.__items.append(item)

    # 队头添加
    def add_front(self, item):
        self.__items.insert(0, item)

    # 队头删除
    def remove_front(self):
        if self.is_empty():
            return None
        return self.__items.pop(0)

    # 队尾删除
    def remove_rear(self):
        if self.is_empty():
            return None
        return self.__items.pop()

    def is_empty(self):
        return len(self.__items) == 0

    def size(self):
        return len(self.__items)

六、队列应用场景

  1. 任务排队、消息队列
  2. 广度优先搜索 BFS(树、图遍历)
  3. 多线程任务调度
  4. 窗口滑动、限流排队

七、复杂度总结

  1. 列表模拟队列 pop(0):O(n) 低效
  2. 内置 deque 头尾增删:O(1) 高效
  3. 普通队列:先进先出;栈:后进先出

第五章:哈希表

一、哈希表是什么

哈希表(Hash Table)是键值对映射的数据结构,
核心优势:查询、插入、删除 平均时间复杂度 O(1)

Python 里的 dict 字典,底层就是哈希表。

二、核心原理

1. 哈希函数

把任意长度的 key,通过算法换算成固定数组下标
公式:

数组下标 = hash(关键字) % 数组长度

2. 哈希冲突

不同的 key,算出了同一个数组下标,这就是哈希冲突

3. 解决哈希冲突常用两种方案

  1. 链地址法(拉链法):数组每个位置挂一个链表,冲突就往链表尾部挂
  2. 开放地址法:冲突了就往后找空位

我们这节用面试最常考:链地址法手写哈希表。

三、哈希表结构设计

  1. 底层是一个固定长度数组
  2. 数组每个元素是单向链表
  3. key 经过哈希函数 → 算出下标 → 对应链表插入/查找/删除

结构示意:

数组下标0 → 链表节点
数组下标1 → 链表节点→节点
数组下标2 → 空
...

四、先复用之前的节点类

class Node:
    def __init__(self, key, value):
        self.key = key
        self.value = value
        self.next = None

五、手写哈希表(链地址法完整实现)

具备功能:

  • 新增/修改键值对 put
  • 根据key取值 get
  • 根据key删除 remove
  • 打印全部元素
class HashTable:
    def __init__(self, capacity=10):
        # 底层数组容量
        self.capacity = capacity
        # 初始化数组,每个位置初始为None
        self.table = [None] * self.capacity

    # 哈希函数:计算下标
    def _hash(self, key):
        return hash(key) % self.capacity

    # 添加/修改键值对
    def put(self, key, value):
        idx = self._hash(key)
        new_node = Node(key, value)

        # 当前下标位置为空,直接放头节点
        if self.table[idx] is None:
            self.table[idx] = new_node
            return

        # 不为空,遍历链表
        cur = self.table[idx]
        # 如果key已存在,覆盖value
        while cur:
            if cur.key == key:
                cur.value = value
                return
            if cur.next is None:
                break
            cur = cur.next
        # 尾部挂载新节点
        cur.next = new_node

    # 根据key获取value
    def get(self, key):
        idx = self._hash(key)
        cur = self.table[idx]
        while cur:
            if cur.key == key:
                return cur.value
            cur = cur.next
        # 找不到返回None
        return None

    # 删除指定key
    def remove(self, key):
        idx = self._hash(key)
        cur = self.table[idx]
        pre = None

        while cur:
            if cur.key == key:
                # 是头节点
                if pre is None:
                    self.table[idx] = cur.next
                else:
                    pre.next = cur.next
                return True
            pre = cur
            cur = cur.next
        return False

    # 打印哈希表所有数据
    def show(self):
        for i in range(self.capacity):
            print(f"下标{i}: ", end="")
            cur = self.table[i]
            while cur:
                print(f"({cur.key}:{cur.value})", end=" → ")
                cur = cur.next
            print("None")

六、测试手写哈希表

if __name__ == "__main__":
    ht = HashTable(10)

    # 添加键值对
    ht.put("name", "张三")
    ht.put("age", 20)
    ht.put("city", "北京")
    ht.put("score", 95)

    # 打印整个哈希表
    ht.show()

    # 查询
    print("姓名:", ht.get("name"))
    print("年龄:", ht.get("age"))

    # 修改
    ht.put("age", 21)
    print("修改后年龄:", ht.get("age"))

    # 删除
    ht.remove("city")
    print("删除city后:")
    ht.show()

七、哈希表复杂度

  • 理想无冲突:插入、查找、删除 O(1)
  • 大量冲突退化成链表:最坏 O(n)
  • Python dict 底层会自动扩容,把冲突降到很低

八、哈希表适用场景

  1. 键值对快速存储查找
  2. 数据去重、统计频次
  3. 缓存设计、索引映射
  4. 算法题:两数之和、字母异位词

第六章:三大基础排序——冒泡、选择、插入排序

一、排序算法说明

三大简单排序:

  1. 冒泡排序
  2. 选择排序
  3. 插入排序
    平均复杂度都是:O(n²),适合小规模数据。

一、冒泡排序

原理

相邻两个元素两两比较,大的往后冒泡,每一轮把当前最大值沉到末尾。

  • 每一轮确定一个末尾有序元素
  • 共需 n-1 轮

完整代码

def bubble_sort(arr):
    # 拷贝一份,不修改原数组
    nums = arr.copy()
    n = len(nums)
    # 外层:共比较 n-1 轮
    for i in range(n - 1):
        # 内层:每轮比较到无序区域末尾
        for j in range(n - 1 - i):
            if nums[j] > nums[j + 1]:
                # 交换
                nums[j], nums[j + 1] = nums[j + 1], nums[j]
    return nums

# 测试
if __name__ == "__main__":
    test = [5, 3, 8, 4, 2]
    res = bubble_sort(test)
    print("冒泡排序结果:", res)

优化版(已有序提前退出)

def bubble_sort_optimize(arr):
    nums = arr.copy()
    n = len(nums)
    for i in range(n - 1):
        flag = False  # 标记本轮是否发生交换
        for j in range(n - 1 - i):
            if nums[j] > nums[j + 1]:
                nums[j], nums[j + 1] = nums[j + 1], nums[j]
                flag = True
        # 没交换说明已经有序,直接退出
        if not flag:
            break
    return nums

复杂度

  • 最好:O(n)(优化后已有序)
  • 最坏/平均:O(n²)
  • 空间:O(1) 原地排序

二、选择排序

原理

每一轮找到最小值下标,和当前无序区间第一个位置交换。

  • 不频繁交换,只记下标,一轮只交换一次

完整代码

def select_sort(arr):
    nums = arr.copy()
    n = len(nums)
    for i in range(n - 1):
        # 假设当前i为最小值下标
        min_idx = i
        # 找后续更小值的下标
        for j in range(i + 1, n):
            if nums[j] < nums[min_idx]:
                min_idx = j
        # 最小值和i位置交换
        nums[i], nums[min_idx] = nums[min_idx], nums[i]
    return nums

# 测试
test = [5, 3, 8, 4, 2]
print("选择排序结果:", select_sort(test))

复杂度

  • 最好/最坏/平均:O(n²)
  • 空间:O(1)
  • 特点:交换次数比冒泡少,实际效率略高于冒泡

三、插入排序

原理

把数组分成有序区 + 无序区
逐个把无序区第一个元素,插入到前面有序区的合适位置。
打牌理牌,一张一张插到正确位置。

完整代码

def insert_sort(arr):
    nums = arr.copy()
    n = len(nums)
    # 从第二个元素开始往前插
    for i in range(1, n):
        # 当前要插入的数
        temp = nums[i]
        j = i - 1
        # 往前挪,比temp大的后移
        while j >= 0 and nums[j] > temp:
            nums[j + 1] = nums[j]
            j -= 1
        # 放到正确位置
        nums[j + 1] = temp
    return nums

# 测试
test = [5, 3, 8, 4, 2]
print("插入排序结果:", insert_sort(test))

复杂度

  • 最好:O(n)(已有序)
  • 最坏/平均:O(n²)
  • 空间:O(1)
  • 实际效率:插入 > 选择 > 冒泡

四、三大基础排序总结

排序平均复杂度最好复杂度原地排序实际效率
冒泡O(n²)O(n)最差
选择O(n²)O(n²)中等
插入O(n²)O(n)最好

第七章:进阶排序——快速排序、归并排序

一、进阶排序概述

前面冒泡、选择、插入都是 O(n²) 简单排序;
快速排序、归并排序属于 O(nlogn) 高效排序,面试必考、工程常用,都基于 分治+递归 思想。


一、快速排序

1. 核心原理

  1. 选一个基准值 pivot
  2. 把数组划分成:小于pivot | pivot | 大于pivot
  3. 左右两边递归再做同样划分
    分治思想:挖坑 + 分区域 + 递归

2. 基础版快速排序

def quick_sort(arr):
    # 递归终止条件:数组长度小于等于1直接返回
    if len(arr) <= 1:
        return arr
    # 选第一个当基准值
    pivot = arr[0]
    # 划分三区
    left = [x for x in arr[1:] if x < pivot]
    mid = [pivot]
    right = [x for x in arr[1:] if x >= pivot]
    # 递归拼接
    return quick_sort(left) + mid + quick_sort(right)

# 测试
if __name__ == "__main__":
    test = [6, 3, 8, 5, 2, 9, 1]
    res = quick_sort(test)
    print("快速排序结果:", res)

3. 原地交换版(不额外开数组)

def quick_sort_in_place(arr, low, high):
    def partition(l, r):
        pivot = arr[l]
        while l < r:
            # 从右找比pivot小的
            while l < r and arr[r] >= pivot:
                r -= 1
            arr[l] = arr[r]
            # 从左找比pivot大的
            while l < r and arr[l] <= pivot:
                l += 1
            arr[r] = arr[l]
        # 把基准值放到中间
        arr[l] = pivot
        return l

    if low < high:
        # 划分基准下标
        pi = partition(low, high)
        # 递归排左边
        quick_sort_in_place(arr, low, pi - 1)
        # 递归排右边
        quick_sort_in_place(arr, pi + 1, high)

# 测试
test = [6, 3, 8, 5, 2, 9, 1]
quick_sort_in_place(test, 0, len(test)-1)
print("原地快排结果:", test)

4. 复杂度

  • 平均:O(nlogn)
  • 最坏:O(n²)(已有序且选最左当基准)
  • 空间:递归栈空间 O(logn)
  • 特点:实际工程最快,默认内置排序底层就是改良快排

二、归并排序

1. 核心原理

分治两步:

  1. :把数组不断对半拆分,直到拆成单个元素
  2. :两两有序合并,最后合成一个完整有序数组

2. 完整代码实现

def merge_sort(arr):
    # 递归终止
    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.extend(left[i:])
    res.extend(right[j:])
    return res

# 测试
test = [6, 3, 8, 5, 2, 9, 1]
res = merge_sort(test)
print("归并排序结果:", res)

3. 复杂度

  • 最好/最坏/平均:O(nlogn) 稳定
  • 空间:O(n) 需要额外数组存合并结果
  • 特点:稳定排序,适合外排序、大数据分片排序

三、高效排序对比总结

排序时间复杂度稳定性原地排序特点
快速排序O(nlogn)不稳定实际最快,工程首选
归并排序O(nlogn)稳定稳定适合大数据、外排

第八章:查找算法——顺序查找、二分查找

一、查找算法介绍

查找:从一组数据中找到目标元素的位置或判断是否存在。
常用两种:

  1. 顺序查找(线性查找)
  2. 二分查找(折半查找,重点、面试必考)

一、顺序查找

原理

从头到尾逐个遍历,逐一对比,找到就返回下标,遍历完没找到返回-1。

  • 数据无需有序
  • 算法简单,效率低

代码实现

def linear_search(arr, target):
    for idx, val in enumerate(arr):
        if val == target:
            return idx
    return -1

# 测试
if __name__ == "__main__":
    nums = [5, 2, 9, 1, 7]
    print(linear_search(nums, 9))
    print(linear_search(nums, 3))

复杂度

  • 时间:平均/最坏 O(n)
  • 空间:O(1)

二、二分查找(普通版)

前提

数组必须有序(升序)

原理

  1. 设定左边界 left、右边界 right
  2. 取中间下标 mid
  3. 中间值等于目标:直接返回mid
  4. 中间值 < 目标:去右半边找
  5. 中间值 > 目标:去左半边找
  6. 不断折半,范围缩小一半

循环版实现(最常用)

def binary_search(arr, target):
    left = 0
    right = 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

# 测试 必须有序数组
nums = [1, 2, 5, 7, 9, 12, 15]
print(binary_search(nums, 7))
print(binary_search(nums, 3))

递归版二分查找

def binary_search_recur(arr, left, right, target):
    if left > right:
        return -1
    mid = (left + right) // 2
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search_recur(arr, mid + 1, right, target)
    else:
        return binary_search_recur(arr, left, mid - 1, target)

# 测试
nums = [1, 2, 5, 7, 9, 12, 15]
print(binary_search_recur(nums, 0, len(nums)-1, 9))

复杂度

  • 时间:O(logn) 极快,数据量越大优势越明显
  • 空间:循环版O(1),递归版O(logn)

三、二分查找扩展:找左边界、右边界

适用场景:有重复元素,找第一次出现、最后一次出现的位置

1. 二分找左边界

def binary_left_bound(arr, target):
    left = 0
    right = len(arr) - 1
    res = -1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            res = mid
            right = mid - 1  # 继续往左找
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return res

# 测试
nums = [1,2,2,2,3,4,5]
print(binary_left_bound(nums, 2))

2. 二分找右边界

def binary_right_bound(arr, target):
    left = 0
    right = len(arr) - 1
    res = -1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            res = mid
            left = mid + 1  # 继续往右找
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return res

nums = [1,2,2,2,3,4,5]
print(binary_right_bound(nums, 2))

四、两种查找对比

查找方式是否需要有序时间复杂度适用场景
顺序查找不需要O(n)小规模、无序数据
二分查找必须有序O(logn)大规模、静态有序数据

第九章:二叉树

一、树基础概念

1. 什么是树

树是层次化非线性数据结构,区别于链表、栈、队列的线性结构。

术语:

  • 根节点:最顶层,没有父节点
  • 父节点、子节点、兄弟节点
  • 叶子节点:没有子节点
  • 高度:从当前节点到最远叶子的边数
  • 深度:从根到当前节点的边数

2. 二叉树定义

每个节点最多只有2个子节点:左孩子、右孩子

满二叉树、完全二叉树、普通二叉树。

二、二叉树节点类

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None   # 左子树
        self.right = None  # 右子树

三、手动构建一棵二叉树

# 构建树:
#       1
#      / \
#     2   3
#    / \
#   4   5

def build_tree():
    root = TreeNode(1)
    root.left = TreeNode(2)
    root.right = TreeNode(3)
    root.left.left = TreeNode(4)
    root.left.right = TreeNode(5)
    return root

四、二叉树三种遍历(递归版 最简单)

遍历规则:

  • 前序:根 → 左 → 右
  • 中序:左 → 根 → 右
  • 后序:左 → 右 → 根

1. 前序遍历

def pre_order(node):
    if not node:
        return
    print(node.val, end=" ")
    pre_order(node.left)
    pre_order(node.right)

2. 中序遍历

def in_order(node):
    if not node:
        return
    in_order(node.left)
    print(node.val, end=" ")
    in_order(node.right)

3. 后序遍历

def post_order(node):
    if not node:
        return
    post_order(node.left)
    post_order(node.right)
    print(node.val, end=" ")

测试运行

if __name__ == "__main__":
    root = build_tree()
    print("前序遍历:")
    pre_order(root)
    print("\n中序遍历:")
    in_order(root)
    print("\n后序遍历:")
    post_order(root)

输出:

前序遍历:
1 2 4 5 3 
中序遍历:
4 2 5 1 3 
后序遍历:
4 5 2 3 1 

五、层序遍历(BFS 广度优先)

按从上到下、从左到右一层一层遍历,用队列实现

from collections import deque

def level_order(root):
    if not root:
        return
    q = deque()
    q.append(root)
    while q:
        node = q.popleft()
        print(node.val, end=" ")
        if node.left:
            q.append(node.left)
        if node.right:
            q.append(node.right)

六、迭代版遍历(不用递归,栈模拟)

前序迭代

def pre_order_iter(root):
    if not root:
        return
    stack = [root]
    while stack:
        node = stack.pop()
        print(node.val, end=" ")
        # 栈后进先出,先压右再压左
        if node.right:
            stack.append(node.right)
        if node.left:
            stack.append(node.left)

中序迭代

def in_order_iter(root):
    stack = []
    cur = root
    while cur or stack:
        while cur:
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()
        print(cur.val, end=" ")
        cur = cur.right

七、二叉树常见基础小题

  1. 求二叉树节点个数
  2. 求二叉树叶子节点个数
  3. 求二叉树最大深度
  4. 翻转二叉树

示例:求树的节点总数

def count_node(node):
    if not node:
        return 0
    return 1 + count_node(node.left) + count_node(node.right)

示例:求二叉树深度

def tree_depth(node):
    if not node:
        return 0
    left_dep = tree_depth(node.left)
    right_dep = tree_depth(node.right)
    return max(left_dep, right_dep) + 1

第十章:二叉搜索树 BST

一、二叉搜索树 BST 定义

二叉搜索树是特殊的二叉树,满足严格规则:

  1. 左子树所有节点值 < 根节点值
  2. 右子树所有节点值 > 根节点值
  3. 左右子树也都是二叉搜索树

核心特性:

  • 中序遍历 = 升序有序数组
  • 查找、插入、删除 平均 O(logn),最坏 O(n)

二、复用树节点

class TreeNode:
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

三、二叉搜索树类封装

基础框架

class BST:
    def __init__(self):
        self.root = None

四、BST 插入操作

思路

  1. 树空:新节点作为根
  2. 不为空:和当前节点比较
    • 小于当前值 → 往左子树走
    • 大于当前值 → 往右子树走
  3. 直到遇到空位置,挂载新节点

插入代码(递归版)

class BST:
    def __init__(self):
        self.root = None

    # 插入入口
    def insert(self, val):
        self.root = self._insert(self.root, val)

    # 递归内部实现
    def _insert(self, node, val):
        # 找到空位置,新建节点
        if not node:
            return TreeNode(val)
        if val < node.val:
            node.left = self._insert(node.left, val)
        elif val > node.val:
            node.right = self._insert(node.right, val)
        # 相等值 直接忽略(BST一般不存重复值)
        return node

五、BST 查找操作

思路

从根开始比较:

  • 相等 → 找到
  • 更小 → 左子树
  • 更大 → 右子树
  • 走到空 → 不存在
    # 查找
    def search(self, val):
        return self._search(self.root, val)

    def _search(self, node, val):
        if not node:
            return False
        if val == node.val:
            return True
        elif val < node.val:
            return self._search(node.left, val)
        else:
            return self._search(node.right, val)

六、中序遍历(验证升序)

BST 中序遍历一定是从小到大有序

    # 中序遍历
    def in_order(self):
        res = []
        self._in_order(self.root, res)
        return res

    def _in_order(self, node, res):
        if not node:
            return
        self._in_order(node.left, res)
        res.append(node.val)
        self._in_order(node.right, res)

七、BST 删除操作

删除分三种情况:

  1. 叶子节点:直接删,置为 None
  2. 只有一个子节点:用子节点顶替自己
  3. 有左右两个子节点
    • 右子树最小值 或者 左子树最大值 替换当前节点
    • 再删掉那个替补节点

完整删除代码

    # 删除入口
    def remove(self, val):
        self.root = self._remove(self.root, val)

    def _remove(self, node, val):
        if not node:
            return None

        # 先找到要删除的节点
        if val < node.val:
            node.left = self._remove(node.left, val)
        elif val > node.val:
            node.right = self._remove(node.right, val)
        else:
            # 情况1:叶子节点 或 只有一个孩子
            if not node.left:
                return node.right
            if not node.right:
                return node.left

            # 情况2:左右都有孩子,找右子树最小节点
            min_right = self._get_min(node.right)
            node.val = min_right.val
            # 删除右子树最小值节点
            node.right = self._remove(node.right, min_right.val)
        return node

    # 获取以node为根的最小节点
    def _get_min(self, node):
        cur = node
        while cur.left:
            cur = cur.left
        return cur

八、完整测试代码

if __name__ == "__main__":
    bst = BST()
    # 插入数据
    nums = [5,3,7,2,4,6,8]
    for n in nums:
        bst.insert(n)

    # 中序遍历 升序输出
    print("BST中序遍历:", bst.in_order())

    # 查找
    print("查找4:", bst.search(4))
    print("查找9:", bst.search(9))

    # 删除
    bst.remove(7)
    print("删除7后:", bst.in_order())

输出:

BST中序遍历: [2, 3, 4, 5, 6, 7, 8]
查找4: True
查找9: False
删除7后: [2, 3, 4, 5, 6, 8]

九、BST 复杂度与特点

  1. 平均查找、插入、删除:O(logn)
  2. 最坏退化成单链链表:O(n)
  3. 中序遍历天然升序,适合排序、有序查找
  4. 底层 TreeSet、TreeMap 原理就是改良版 BST(红黑树)

第十一章:堆 & 优先队列、大顶堆/小顶堆、heapq、TopK经典题

一、堆是什么

1. 堆的本质

堆是完全二叉树,同时满足堆序性质

  • 小顶堆:父节点值 ≤ 左右孩子节点值;堆顶是最小值
  • 大顶堆:父节点值 ≥ 左右孩子节点值;堆顶是最大值

2. 存储方式

堆不用链式节点,直接用数组/列表顺序存储
完全二叉树数组下标规律:

  • 下标 i 的左孩子:2*i + 1
  • 下标 i 的右孩子:2*i + 2
  • 下标 i 的父节点:(i - 1) // 2

二、Python 内置 heapq 模块

Python 只自带小顶堆,没有原生大顶堆。

1. heapq 常用API

import heapq
  • heapq.heappush(heap, item) 推入堆
  • heapq.heappop(heap) 弹出堆顶最小元素
  • heapq.heapify(list) 列表原地建堆 O(n)
  • heapq.nlargest(k, iterable) 取前k大
  • heapq.nsmallest(k, iterable) 取前k小

2. 基础使用:小顶堆

import heapq

heap = []
# 入堆
heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 8)
heapq.heappush(heap, 1)

# 依次弹出,从小到大
while heap:
    print(heapq.heappop(heap), end=" ")

3. 列表原地建堆 heapify

import heapq

nums = [3,1,4,2,5]
heapq.heapify(nums)  # 原地变成小顶堆
print(nums)

4. 模拟大顶堆

思路:元素取负数,用小顶堆模拟大顶堆

import heapq

max_heap = []
nums = [3,1,4,2,5]

for n in nums:
    heapq.heappush(max_heap, -n)

# 弹出时再取反
while max_heap:
    print(-heapq.heappop(max_heap), end=" ")

三、手写小顶堆

封装堆:初始化、上浮、下沉、入堆、出堆

class MinHeap:
    def __init__(self):
        self.heap = []

    # 上浮:新元素往上换到合适位置
    def _sift_up(self, idx):
        while idx > 0:
            parent_idx = (idx - 1) // 2
            if self.heap[idx] >= self.heap[parent_idx]:
                break
            # 交换父子
            self.heap[idx], self.heap[parent_idx] = self.heap[parent_idx], self.heap[idx]
            idx = parent_idx

    # 下沉:删除堆顶后,最后元素往下换
    def _sift_down(self, idx):
        n = len(self.heap)
        while True:
            left = 2 * idx + 1
            right = 2 * idx + 2
            smallest = idx

            if left < n and self.heap[left] < self.heap[smallest]:
                smallest = left
            if right < n and self.heap[right] < self.heap[smallest]:
                smallest = right

            if smallest == idx:
                break
            self.heap[idx], self.heap[smallest] = self.heap[smallest], self.heap[idx]
            idx = smallest

    # 入堆
    def push(self, val):
        self.heap.append(val)
        self._sift_up(len(self.heap)-1)

    # 出堆
    def pop(self):
        if not self.heap:
            return None
        res = self.heap[0]
        # 最后一个元素放到堆顶
        self.heap[0] = self.heap[-1]
        self.heap.pop()
        self._sift_down(0)
        return res

    # 判空
    def is_empty(self):
        return len(self.heap) == 0

测试手写小顶堆

if __name__ == "__main__":
    h = MinHeap()
    for num in [5,3,8,1,2]:
        h.push(num)

    while not h.is_empty():
        print(h.pop(), end=" ")

四、经典算法题:TopK 问题

题目

从一堆数据中,找出前 K 大的数

最优思路

维护一个大小为K的小顶堆

  1. 前K个元素直接入堆
  2. 后面每个元素比堆顶大,就弹出堆顶、加入当前元素
  3. 最后堆里就是前K大
import heapq

def top_k(nums, k):
    heap = []
    for n in nums:
        if len(heap) < k:
            heapq.heappush(heap, n)
        else:
            if n > heap[0]:
                heapq.heappop(heap)
                heapq.heappush(heap, n)
    # 排序返回结果
    return sorted(heap, reverse=True)

# 测试
arr = [9,3,7,1,8,2,6,4,5]
print(top_k(arr, 3))

五、堆排序原理

  1. 把数组建成堆
  2. 不断弹出堆顶,放到数组末尾
    时间复杂度:O(nlogn)

六、堆总结

  1. 小顶堆堆顶最小,大顶堆堆顶最大
  2. Python heapq 只有小顶堆,负数模拟大顶堆
  3. 核心操作:上浮、下沉
  4. 经典应用:TopK、堆排序、优先队列、任务调度

第十二章:并查集(Union-Find)原理 + 手写实现 + 经典例题

一、并查集是什么

并查集是一种处理不相交集合合并与查询的数据结构。
核心两个操作:

  1. 查 Find:查找某个元素的根节点,判断两个元素是否在同一集合
  2. 并 Union:把两个集合合并成一个

适用场景:

  • 朋友圈关系、亲戚连通
  • 岛屿数量、连通分量
  • 最小生成树 Kruskal 算法
  • 网络节点连通性

二、并查集核心思想

  • 初始每个元素自己是一个集合,父节点指向自己
  • 合并:把一个集合的根,挂到另一个集合根下面
  • 查询:一路往上找根节点
  • 优化:路径压缩 + 按秩合并,几乎接近 O(1)

三、基础版并查集(无优化)

class UnionFind:
    def __init__(self, size):
        # 父节点数组,初始自己指向自己
        self.parent = list(range(size))

    # 查找根节点
    def find(self, x):
        # 自己就是根
        if self.parent[x] == x:
            return x
        # 递归往上找父节点
        return self.find(self.parent[x])

    # 合并两个集合
    def union(self, x, y):
        x_root = self.find(x)
        y_root = self.find(y)
        # 根不同就合并
        if x_root != y_root:
            self.parent[y_root] = x_root

    # 判断是否连通
    def is_connected(self, x, y):
        return self.find(x) == self.find(y)

测试基础版

if __name__ == "__main__":
    uf = UnionFind(5)
    uf.union(0,1)
    uf.union(1,2)
    print(uf.is_connected(0,2))  # True
    print(uf.is_connected(0,3))  # False

四、优化版并查集(路径压缩 + 按秩合并)

效率拉满,面试标准写法

  • 路径压缩:查找时把沿途所有节点直接挂到根,下次查找超快
  • 按秩合并:树矮的挂到树高的下面,避免树变深
class UnionFindPro:
    def __init__(self, size):
        self.parent = list(range(size))
        # 秩:记录树的高度
        self.rank = [1] * size

    # 路径压缩查找
    def find(self, x):
        if self.parent[x] != x:
            # 递归找根,顺便把父节点直接设为根
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    # 按秩合并
    def union(self, x, y):
        x_root = self.find(x)
        y_root = self.find(y)
        if x_root == y_root:
            return
        # 矮树挂到高树下面
        if self.rank[x_root] < self.rank[y_root]:
            self.parent[x_root] = y_root
        else:
            self.parent[y_root] = x_root
            # 高度相等时,合并后根高度+1
            if self.rank[x_root] == self.rank[y_root]:
                self.rank[x_root] += 1

    def is_connected(self, x, y):
        return self.find(x) == self.find(y)

五、经典实战:岛屿数量(并查集解法)

题目

给定二维网格,1是陆地,0是水域,求连通的岛屿数量
上下左右相连的1算同一个岛屿。

解题思路

  1. 遍历每个格子,是陆地就初始化自己为一个集合
  2. 往右、往下看相邻陆地,合并集合
  3. 最后统计根节点总数就是岛屿数量

完整代码

def num_islands(grid):
    if not grid or not grid[0]:
        return 0
    m = len(grid)
    n = len(grid[0])
    # 把二维坐标转一维下标:i * n + j
    def get_idx(i,j):
        return i * n + j

    uf = UnionFindPro(m * n)
    count = 0

    # 方向:右、下
    dirs = [(0,1), (1,0)]

    for i in range(m):
        for j in range(n):
            if grid[i][j] == "1":
                count += 1
                # 遍历相邻
                for dx, dy in dirs:
                    x = i + dx
                    y = j + dy
                    if 0 <= x < m and 0 <= y < n and grid[x][y] == "1":
                        # 不在同一集合就合并,数量减1
                        if not uf.is_connected(get_idx(i,j), get_idx(x,y)):
                            uf.union(get_idx(i,j), get_idx(x,y))
                            count -= 1
    return count

# 测试
grid = [
    ["1","1","0","0","0"],
    ["1","1","0","0","0"],
    ["0","0","1","0","0"],
    ["0","0","0","1","1"]
]
print(num_islands(grid))  # 输出3

六、并查集复杂度总结

  • 优化后(路径压缩+按秩合并):近似 O(1)
  • 适合处理大规模连通性问题
  • 代码模板固定,刷题直接套

第十三章:图

一、图的基本概念

1. 图组成

  • 顶点(Vertex):节点
  • 边(Edge):顶点之间的连线

2. 图分类

  • 无向图、有向图
  • 无权图、带权图
  • 连通图、非连通图
  • 环、有向无环图DAG

3. 两种存储方式

  1. 邻接矩阵:二维数组,适合顶点少
  2. 邻接表:字典/列表嵌套,适合顶点多、稀疏图(开发最常用

二、邻接表存储图

字典存:key顶点,value相邻顶点列表

1. 无向图构建

# 邻接表表示无向图
graph = {
    "A": ["B", "C"],
    "B": ["A", "D", "E"],
    "C": ["A", "F"],
    "D": ["B"],
    "E": ["B", "F"],
    "F": ["C", "E"]
}

2. 封装图类(增顶点、增边)

class Graph:
    def __init__(self):
        # 邻接表
        self.adj = {}

    # 添加顶点
    def add_vertex(self, v):
        if v not in self.adj:
            self.adj[v] = []

    # 添加无向边
    def add_edge(self, v1, v2):
        self.add_vertex(v1)
        self.add_vertex(v2)
        # 互相加入邻接列表
        self.adj[v1].append(v2)
        self.adj[v2].append(v1)

    # 打印邻接表
    def show(self):
        for k, v in self.adj.items():
            print(f"{k}: {v}")

测试构建图

if __name__ == "__main__":
    g = Graph()
    g.add_edge("A","B")
    g.add_edge("A","C")
    g.add_edge("B","D")
    g.add_edge("B","E")
    g.add_edge("C","F")
    g.add_edge("E","F")
    g.show()

三、图深度优先遍历 DFS

思路

  1. 用集合记录已访问顶点
  2. 访问当前顶点,标记已访问
  3. 递归遍历所有未访问邻接点

递归版 DFS

class Graph:
    # ... 接上上面代码 ...

    def dfs(self, start):
        visited = set()
        self._dfs(start, visited)

    def _dfs(self, v, visited):
        # 访问当前节点
        print(v, end=" ")
        visited.add(v)
        # 遍历邻接点
        for neighbor in self.adj[v]:
            if neighbor not in visited:
                self._dfs(neighbor, visited)

迭代版 DFS(栈实现)

    def dfs_iter(self, start):
        visited = set()
        stack = [start]
        while stack:
            v = stack.pop()
            if v in visited:
                continue
            print(v, end=" ")
            visited.add(v)
            # 逆序入栈,保证遍历顺序和递归一致
            for neighbor in reversed(self.adj[v]):
                if neighbor not in visited:
                    stack.append(neighbor)

四、图广度优先遍历 BFS

思路

  • 借助队列
  • 先访问当前层所有节点,再访问下一层
  • 最短路径、最少步数 首选BFS

BFS 完整实现

from collections import deque

class Graph:
    # ... 省略前面代码 ...

    def bfs(self, start):
        visited = set()
        q = deque()
        q.append(start)
        visited.add(start)

        while q:
            v = q.popleft()
            print(v, end=" ")
            # 邻接点入队
            for neighbor in self.adj[v]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    q.append(neighbor)

五、完整测试 DFS / BFS

if __name__ == "__main__":
    g = Graph()
    g.add_edge("A","B")
    g.add_edge("A","C")
    g.add_edge("B","D")
    g.add_edge("B","E")
    g.add_edge("C","F")
    g.add_edge("E","F")

    print("深度优先DFS:")
    g.dfs("A")
    print("\n广度优先BFS:")
    g.bfs("A")

六、有向图简单改造

只需要改加边函数,不用双向添加:

# 有向边 v1 -> v2
def add_direct_edge(self, v1, v2):
    self.add_vertex(v1)
    self.add_vertex(v2)
    self.adj[v1].append(v2)

七、图算法应用场景

  1. DFS:连通分量、拓扑排序、岛屿问题、路径搜索
  2. BFS:最短路径、最少换乘、迷宫最短步数
  3. 进阶:Dijkstra最短路径、Prim/Kruskal最小生成树

第十四章:贪心算法

一、贪心算法核心原理

1. 什么是贪心

每一步只做当前局部最优选择,期望最终得到全局最优。

  • 不往后看、不回头、不考虑未来,只管当下最优
  • 不是所有问题都能用贪心,必须满足两个条件:
    1. 贪心选择性质:局部最优能导出全局最优
    2. 最优子结构:子问题最优 包含在全局最优里

2. 适用题型特征

  • 区间调度、活动安排
  • 零钱兑换(特定面额)
  • 跳跃游戏
  • 哈夫曼编码
  • 分发饼干、贪心区间覆盖

二、例题1:分发饼干(入门经典)

题目

孩子有胃口 g,饼干尺寸 s
每个孩子最多一块饼干,饼干尺寸 >= 胃口才能满足。
求最多能满足几个孩子。

思路

  1. 孩子胃口、饼干都从小到大排序
  2. 小饼干优先喂胃口小的孩子
  3. 双指针逐个匹配

代码

def find_content_children(g, s):
    g.sort()
    s.sort()
    i = j = count = 0
    while i < len(g) and j < len(s):
        if s[j] >= g[i]:
            # 能满足
            count += 1
            i += 1
            j += 1
        else:
            # 饼干太小,换下一块
            j += 1
    return count

# 测试
print(find_content_children([1,2,3], [1,1]))     # 1
print(find_content_children([1,2], [1,2,3]))     # 2

三、例题2:活动选择 / 区间调度

题目

给若干活动开始时间、结束时间,同一时间只能参加一个活动,求最多能参加几个活动

贪心策略

优先选结束时间最早的,给后面留更多时间。

代码

def activity_selection(activities):
    # 按结束时间升序排序
    activities.sort(key=lambda x: x[1])
    res = [activities[0]]
    last_end = activities[0][1]

    for act in activities[1:]:
        start, end = act
        if start >= last_end:
            res.append(act)
            last_end = end
    return res

# 测试:(开始, 结束)
acts = [(1,3), (2,4), (3,5), (4,6)]
print(activity_selection(acts))

四、例题3:跳跃游戏

题目

数组每个元素代表当前位置最多能跳几步,判断能不能跳到最后下标。

贪心思路

实时维护能跳到的最远位置,遍历不断更新。

def can_jump(nums):
    max_reach = 0
    n = len(nums)
    for i in range(n):
        if i > max_reach:
            return False
        max_reach = max(max_reach, i + nums[i])
        if max_reach >= n - 1:
            return True
    return True

# 测试
print(can_jump([2,3,1,1,4]))  # True
print(can_jump([3,2,1,0,4]))  # False

五、例题4:零钱兑换(贪心可行版)

注意:只有纸币面额是规范整除体系时贪心才正确

题目

面额 [1,5,10,20,50,100],凑总金额,求最少纸币张数

贪心策略

从大面额开始优先选

def coin_change_greedy(amount):
    coins = [100,50,20,10,5,1]
    res = []
    for c in coins:
        while amount >= c:
            res.append(c)
            amount -= c
    return res

print(coin_change_greedy(136))

六、贪心算法总结

  1. 核心:局部最优 → 推全局最优
  2. 套路:先排序 + 依次贪心选择
  3. 常见模型:
    • 排序+双指针
    • 区间按结束时间排序
    • 维护最远可达边界
  4. 局限性:不满足贪心性质的问题,只能用动态规划

第十五章:动态规划DP入门

一、动态规划核心思想

1. 什么是DP

动态规划 Dynamic Programming,把大问题拆解成子问题
保存子问题的结果,避免重复计算,以空间换时间

2. DP三要素

  1. 状态定义:dp[i] 代表什么含义
  2. 状态转移方程:大问题怎么由子问题推导出来
  3. 初始边界条件:最开始’s的基础值

3. 解题套路

  • 拆分子问题
  • 画dp数组、找规律
  • 写状态转移
  • 初始化边界
  • 循环计算

二、例题1:斐波那契数列(DP入门必学)

题目

1, 1, 2, 3, 5, 8 …
第n项 = 前两项之和

状态定义

dp[i]:第 i 个斐波那契数

转移方程

dp[i] = dp[i-1] + dp[i-2]

代码

def fib(n):
    if n <= 2:
        return 1
    dp = [0] * (n + 1)
    dp[1] = 1
    dp[2] = 1
    for i in range(3, n+1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

print(fib(10))

空间优化(只用两个变量)

def fib_optimize(n):
    if n <= 2:
        return 1
    a, b = 1, 1
    for _ in range(3, n+1):
        c = a + b
        a = b
        b = c
    return b

print(fib_optimize(10))

三、例题2:爬楼梯

题目

一次可以爬 1 阶 或 2 阶,爬到 n 阶有多少种方式?

分析

  • 到第n阶:只能从 n-1 走1步 或 n-2 走2步
  • 本质就是斐波那契变形

状态定义

dp[i]:爬到第 i 阶的方法数

转移方程

dp[i] = dp[i-1] + dp[i-2]

代码

def climb_stairs(n):
    if n <= 2:
        return n
    dp = [0]*(n+1)
    dp[1] = 1
    dp[2] = 2
    for i in range(3, n+1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

print(climb_stairs(10))

四、例题3:最小路径和(二维DP)

题目

给定 m x n 网格,只能向右、向下走,从左上角到右下角,路径数字和最小是多少。

状态定义

dp[i][j]:走到第i行第j列的最小路径和

转移方程

dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])

代码

def min_path_sum(grid):
    m = len(grid)
    n = len(grid[0])
    # 复制网格当dp数组
    dp = [[0]*n for _ in range(m)]
    dp[0][0] = grid[0][0]

    # 初始化第一行 只能从左边来
    for j in range(1, n):
        dp[0][j] = dp[0][j-1] + grid[0][j]

    # 初始化第一列 只能从上面来
    for i in range(1, m):
        dp[i][0] = dp[i-1][0] + grid[i][0]

    # 遍历填充其他位置
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
    return dp[m-1][n-1]

# 测试
grid = [
    [1,3,1],
    [1,5,1],
    [4,2,1]
]
print(min_path_sum(grid))  # 7

五、例题4:打家劫舍(一维DP经典)

题目

数组每个元素是房间金额,不能偷相邻房间,求能偷的最大金额。

状态定义

dp[i]:前 i 个房间能偷的最大金额

转移方程

  • 偷第i间:dp[i-2] + nums[i]
  • 不偷第i间:dp[i-1]
dp[i] = max(dp[i-1], dp[i-2] + nums[i])

代码

def rob(nums):
    if not nums:
        return 0
    n = len(nums)
    if n == 1:
        return nums[0]
    dp = [0]*n
    dp[0] = nums[0]
    dp[1] = max(nums[0], nums[1])
    for i in range(2, n):
        dp[i] = max(dp[i-1], dp[i-2] + nums[i])
    return dp[-1]

print(rob([1,2,3,1]))
print(rob([2,7,9,3,1]))

六、动态规划入门总结

  1. DP核心:拆分子问题 + 记录子问题结果
  2. 三步走:状态定义 → 转移方程 → 初始化边界
  3. 常见题型:
    • 一维DP:斐波那契、爬楼梯、打家劫舍
    • 二维DP:最小路径和、背包、子序列问题
  4. 大部分DP都可以空间优化,从数组压缩成变量

第十六章:双指针算法

一、双指针算法总览

1. 核心作用

把暴力两层循环 O(n²) 优化成 O(n)
是数组、字符串、链表刷题最常用、必掌握技巧。

2. 三大分类

  1. 左右指针(相向指针):一头一尾往中间收缩
  2. 快慢指针(同向指针):一慢一快同方向走
  3. 滑动窗口(双指针进阶):左右指针框住区间,动态伸缩

二、类型一:左右指针(相向型)

适用场景:
有序数组两数之和、反转数组、回文串判断、二分类题目

1. 反转数组

def reverse_array(nums):
    left = 0
    right = len(nums) - 1
    while left < right:
        # 交换
        nums[left], nums[right] = nums[right], nums[left]
        left += 1
        right -= 1
    return nums

# 测试
print(reverse_array([1,2,3,4,5]))

2. 判断回文字符串

def is_palindrome(s):
    left = 0
    right = len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True

print(is_palindrome("abba"))
print(is_palindrome("abcde"))

3. 有序数组两数之和

def two_sum_sorted(nums, target):
    left = 0
    right = len(nums) - 1
    while left < right:
        total = nums[left] + nums[right]
        if total == target:
            return [left + 1, right + 1]
        elif total < target:
            left += 1
        else:
            right -= 1
    return []

# 测试
print(two_sum_sorted([2,7,11,15], 9))

三、类型二:快慢指针(同向型)

适用场景:
有序数组去重、链表找中点、链表判环、删除指定元素

1. 有序数组原地去重

def remove_duplicates(nums):
    if not nums:
        return 0
    slow = 0
    for fast in range(1, len(nums)):
        if nums[fast] != nums[slow]:
            slow += 1
            nums[slow] = nums[fast]
    # 不重复元素个数
    return slow + 1

# 测试
arr = [1,1,2,2,3,4,4,4]
print(remove_duplicates(arr))

2. 链表快慢指针:找中间节点

class Node:
    def __init__(self, val):
        self.val = val
        self.next = None

# 链表找中间节点
def find_middle_node(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow.val

3. 移除数组指定元素

def remove_element(nums, val):
    slow = 0
    for fast in range(len(nums)):
        if nums[fast] != val:
            nums[slow] = nums[fast]
            slow += 1
    return slow

arr = [3,2,2,3]
print(remove_element(arr, 3))

四、类型三:滑动窗口(双指针进阶)

核心思想:
leftright 框出一个窗口

  • right 右扩:纳入新元素
  • left 右缩:满足条件就缩小窗口

适用:子数组、子串、最长无重复子串、最小长度子数组

1. 长度最小的子数组

def min_subarray_len(nums, target):
    left = 0
    total = 0
    res = float("inf")

    for right in range(len(nums)):
        total += nums[right]
        # 满足条件,收缩左边界
        while total >= target:
            res = min(res, right - left + 1)
            total -= nums[left]
            left += 1
    return res if res != float("inf") else 0

# 测试
print(min_subarray_len([2,3,1,2,4,3], 7))

2. 最长无重复字符子串

def length_of_longest_substring(s):
    window = set()
    left = 0
    max_len = 0

    for right in range(len(s)):
        # 有重复就一直左移
        while s[right] in window:
            window.remove(s[left])
            left += 1
        window.add(s[right])
        max_len = max(max_len, right - left + 1)
    return max_len

# 测试
print(length_of_longest_substring("abcabcbb"))

五、第十六章 双指针总结

  1. 左右指针:有序、两头往中间靠
  2. 快慢指针:同向走,慢指针留有效元素,快指针遍历
  3. 滑动窗口:区间动态伸缩,解决子数组、子串最值问题
  4. 所有双指针题型,时间复杂度都是 O(n),是刷题必备神器
本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值