【Python】你还不了解数据结构与算法?

🟡 数据结构角度

数据结构可以从两个角度来分类:

  • 逻辑结构:描述数据间的逻辑关系,决定了数据之间的关系。
  • 物理结构 :描述数据在计内存中的物理存储方式,存储方式进而影响我们如何设计算法
    • 从计算机内存的物理特性来看,无论数据结构多么复杂,其底层存储最终都归结为顺序存储或链式存储。
  • 顺序存储:数据在内存中连续排列,直接映射到线性地址空间(数组就是顺序存储的具象化)。
  • 链式存储:数据通过指针分散存储,逻辑上通过链接形成关系(链表就是跳跃存储的具象化)。
graph TD LS["逻辑结构"] %% 逻辑结构分类 LS --> Linear["线性结构"] LS --> NonLinear["非线性结构"] %% 具体数据结构作为逻辑和物理的组合 Linear --> Array["数组
逻辑: 线性
物理: 顺序"] Linear --> LinkedList["链表
逻辑: 线性
物理: 链式"] Linear --> Stack["栈
逻辑: 线性
物理: 顺序或链式"] Linear --> Queue["队列
逻辑: 线性
物理: 顺序或链式"] NonLinear --> Tree["树
逻辑: 非线性
物理: 链式(常见)"] NonLinear --> Graph["图
逻辑: 非线性
物理: 链式(邻接表)或顺序(邻接矩阵)"] %% 样式 classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px; classDef main fill:#4285f4,color:white,stroke:#333,stroke-width:1px; classDef logical fill:#0f9d58,color:white,stroke:#333,stroke-width:1px; classDef physical fill:#db4437,color:white,stroke:#333,stroke-width:1px; classDef type fill:#f4b400,stroke:#333,stroke-width:1px; class DS main; class LS,PS logical; class Linear,NonLinear,Sequential,Linked physical; class Array,LinkedList,Stack,Queue,Tree,Graph type;

顺序存储就不用说了,位置隐含了逻辑关系。而链式存储的本质是通过指针(或引用)将分散在内存中的数据节点连接起来。因此不同数据结构之间的差异就是体现在指针的设计上:最基础的链式结构就是链表,每个节点只有一个指针指向下一个节点。然后复杂一点的比如二叉树 ,它每个节点有两个指针,分别指向左子节点和右子节点。再复杂一些比如图,它每个节点可能有多个指针,指向它的邻居节点(如邻接表)。还有跳表,每个节点有多个指针,指向不同层级的后续节点。

数组和链表是两种最原始的数据结构,所有其他数据结构都可以基于它们实现。

graph TD %% 核心数据结构 Array["数组"]:::arrayClass LinkedList["链表"]:::listClass %% 顶部说明 BaseStructure[基础数据结构
]:::titleClass BaseStructure --> Array BaseStructure --> LinkedList %% 数组的扩展 Array --> StackQueueArray["栈/队列
(顺序实现)"]:::arrayExtClass Array --> CompleteBinaryTree["完全二叉树
(数组实现)"]:::arrayExtClass Array --> AdjacencyMatrix["邻接矩阵
(图的表示)"]:::arrayExtClass Array --> HashTable["哈希表
(数组 + 链表)"]:::arrayExtClass %% 链表的扩展 LinkedList --> StackQueueList["栈/队列
(链式实现)"]:::listExtClass LinkedList --> BinaryTree["二叉树
(链式实现)"]:::listExtClass LinkedList --> AdjacencyList["邻接表
(图的表示)"]:::listExtClass LinkedList --> SkipList["跳表
(多级索引链表)"]:::listExtClass %% 样式定义 classDef arrayClass fill:#ffcccb,stroke:#ff6b6b,stroke-width:2px,color:black classDef listClass fill:#c2e0ff,stroke:#4a90e2,stroke-width:2px,color:black classDef arrayExtClass fill:#ffecb3,stroke:#ffa726,stroke-width:1px,color:black classDef listExtClass fill:#b2dfdb,stroke:#00897b,stroke-width:1px,color:black classDef titleClass fill:#f9f9f9,stroke:#666,stroke-width:1px,color:black,font-weight:bold

🔘逻辑结构:线性 or 非线性

plaintext 复制代码
     线性结构                 非线性结构
A -> B -> C -> D        树               图
                        A              A -- B
                       / \             |    |
                      B   C            C -- D
                     / \
                    D   E

🔘物理结构:链式 or 顺序

plaintext 复制代码
顺序结构(数组)
内存地址: 0x1000 | 值: 10
内存地址: 0x1004 | 值: 20
内存地址: 0x1008 | 值: 30
内存地址: 0x100C | 值: 40

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

链式结构(链表)
节点1: 地址: 0x2000 | 值: 10 | 指针: 0x3000
节点2: 地址: 0x3000 | 值: 20 | 指针: 0x4000
节点3: 地址: 0x4000 | 值: 30 | 指针: NULL

链式结构(二叉树)
节点A: 地址: 0x5000 | 值: A | 左指针: 0x6000 | 右指针: 0x7000
节点B: 地址: 0x6000 | 值: B | 左指针: NULL  | 右指针: NULL
节点C: 地址: 0x7000 | 值: C | 左指针: NULL  | 右指针: NULL

链式结构(图)
节点A: 地址: 0x8000 | 值: A | 邻居: [0x9000, 0xA000]
节点B: 地址: 0x9000 | 值: B | 邻居: [0x8000]
节点C: 地址: 0xA000 | 值: C | 邻居: [0x8000, 0xB000]
节点D: 地址: 0xB000 | 值: D | 邻居: [0xA000]

🔘数据结构与算法的关系

不同的数据结构对应不同的操作需求,因此需要不同的算法来优化这些操作:

  • 数组:适合随机访问,常用二分查找。
  • 链表:适合动态插入/删除,常用遍历和反转算法。
  • 栈和队列:适合特定的顺序操作,分别用于 DFS 和 BFS。
  • 树和图 :适合表示复杂关系,常用遍历和路径规划算法。

🟡 访问方式


🔘顺序访问 ☆

从头到尾逐一访问数据元素 的方式。它的时间复杂度通常是 O(n)

  • 逐一遍历:无法直接跳转到任意位置,必须按照顺序逐步移动。
  • 灵活性:适合动态插入和删除操作。
  • 依赖非连续存储:数据可以分散存储(如链表)。

典型场景

  • 链表:从头节点开始逐一访问每个节点。
  • 文件流:按顺序读取文件内容。
  • 磁带存储:只能按顺序访问数据。

🔘索引访问

通过索引快速定位数据,但索引本身可能需要额外维护。

  • 类似于随机访问,但索引可能是间接的。索引可以加速访问,但需要额外的空间和维护成本。

典型场景

  • 数据库中的索引(如 B+ 树)。
  • 跳表(Skip List):通过多级索引加速查找。

🔘分块访问

将数据分成若干块,每块内部可以顺序访问,块之间可以通过索引随机访问。

  • 结合了随机访问和顺序访问的优点。
  • 适合大规模数据集。

典型场景

  • 分块存储的文件系统。
  • 数据库中的分页查询。

🔘缓存访问

利用缓存机制加速访问,优先访问最近使用过的数据。

  • 访问速度取决于数据是否在缓存中。
  • 时间复杂度介于 O(1) 和 O(n) 之间。

典型场景

  • CPU 缓存。
  • 数据库查询缓存。

🔘并行访问

同时访问多个数据元素,通常用于分布式系统或并行计算。

  • 可以显著提高访问效率。
  • 需要复杂的同步机制。

典型场景

  • 分布式数据库。
  • 并行计算中的共享内存模型。

🔘随机访问 ☆

直接访问任意位置的数据 ,而不需要从头开始逐一查找。它的时间复杂度通常是 O(1)

  • 直接定位:通过索引或地址直接定位目标,无论数据量多大访问任意位置的时间固定。
  • 前提是连续存储:通常需要数据在内存中连续存储(如数组)。

典型场景

  • 数组:arr[3] 直接访问第 4 个元素。
  • 哈希表:通过键直接映射到值的位置。
  • 数据库索引:B+ 树等结构优化了随机访问性能。

举个例子:如果你在一本书中查找第 100 页的内容,你可以直接翻到第 100 页,而不需要从第 1 页开始一页一页地翻过去。这就是随机访问。在计算机内存中,随机访问之所以可能,是因为内存的存储方式支持按地址直接访问:内存被划分为一个个连续的存储单元,每个单元都有一个唯一的地址,当你需要访问某个数据时,只需要提供它的地址,CPU 就可以直接读取该地址的内容。

python 复制代码
arr = [10, 20, 30, 40, 50]
print(arr[3])  # 输出 40

在这个例子中,arr[3] 的访问是随机的,因为数组在内存中是连续存储 的,程序可以通过计算偏移量 直接找到第 3 个元素的位置,而不需要遍历前面的元素。数组结构就支持随机访问?原因在于它的物理存储方式 ,数组在内存中是连续存储的,每个元素占用固定大小的空间。通过公式 地址 = 起始地址 + 索引 × 元素大小,可以直接计算出任意元素的内存地址。地址 = 0x1000 + 2 × 4 = 0x1008,直接读取地址 0x1008 的内容。

plaintext 复制代码
内存布局:
地址 0x1000: 10 (4 字节)
地址 0x1004: 20 (4 字节)
地址 0x1008: 30 (4 字节)
地址 0x100C: 40 (4 字节)
地址 0x1010: 50 (4 字节)

很显然,如果元素大小不固定(比如一个元素是4字节,另一个是8字节),计算机就无法用简单的公式计算地址,必须逐个检查每个元素的位置,随机访问效率会降到O(n),失去数组的优势。所以下面趁热来点语言效率差异的拓展:

在C、C++等静态语言中:你必须在声明变量时严格指定类型(intfloat),因为类型决定了内存大小,所以编译器在编译时就确定数组的内存布局,确保所有元素连续存储,且大小固定。这些严格的规定让数组的随机访问成为可能,因为计算机总能通过公式直接计算任何元素的地址,无需额外检查。

而Python是一种动态语言,类型和内存布局不像C、C++那样严格。Python中常见的"数组"实际上是list,它不是C/C++那样的传统数组,而是基于动态数组(有时也用链表或混合结构)实现的。允许列表中的元素类型不同(比如[1, "hello", 3.14]),元素大小不固定(整数可能是4字节,字符串可能是几十字节)。为了支持动态类型和大小调整,Python会在内存中额外存储元数据(比如每个元素的类型和大小) ,导致无法直接用地址 = 起始地址 + 索引 × 元素大小计算地址。

C/C++的数组像一个整齐的书架,所有书大小相同,找书直接算位置。Python的list像一个书架,但书大小不固定(有些是厚书,有些是薄书),虽然你也能快速找到第3本书,但背后可能需要多看几眼标签(元数据),效率略低。

Python中真正的"数组"是array模块,array模块,可以创建类似C的固定类型数组(比如array('i', [1, 2, 3])表示整数数组)。这种数组元素大小固定(比如每个整数4字节),支持真正的随机访问,效率接近C/C++。 但是也有局限性,array只能存储相同类型的数据,不支持动态类型(如Python的list),使用范围较小,Python开发者更常使用灵活的list

链表不支持随机访问的本质也在于它的物理存储方式:链表的节点分散存储在内存中,每个节点包含数据和指向下一个节点的指针。要访问某个节点,必须从头节点开始,沿着指针逐一跳转,直到找到目标节点。

plaintext 复制代码
链表: 10 -> 20 -> 30 -> 40 -> 50
内存布局:
地址 0x1A3F: 数据 10,指针 0x7B8C
地址 0x7B8C: 数据 20,指针 0x4D2E
地址 0x4D2E: 数据 30,指针 0x9F1A
地址 0x9F1A: 数据 40,指针 0x2C6B
地址 0x2C6B: 数据 50,指针 NULL

每个节点的地址(如 0x1A3F, 0x7B8C)是随机的,没有固定的间隔。要访问第 3 个节点(值为 30),必须从头节点开始,依次访问 0x1A3F -> 0x7B8C -> 0x4D2E,无法直接跳转到目标节点。

链表的节点分散存储在内存中,地址随机分配,因此无法通过索引直接访问,只能通过逐一遍历实现顺序访问。 链表通过调整指针实现高效的插入/删除,而数组需要整体移动元素,因此在动态场景下链表更具优势。

🟡 LangChian vs LangGraph

LangChain最初设计用于简单的LLM应用,比如问答系统或RAG。这些任务通常是线性的:输入→处理→输出,或者几个步骤按顺序执行。当任务复杂到需要多代理协作、动态决策或循环逻辑时,链的if-else和手动循环会变得很复杂 ,也难以修改业务逻辑,难以维护。LangGraph针对更复杂的AI场景,比如多代理协作(多个AI角色实时互动)、循环任务(不断优化直到满足条件)或动态工作流(根据状态选择路径)。这些场景需要非线性的、灵活的结构。图结构天生适合表示复杂关系和动态逻辑,开发者可以用节点和边定义任务,系统自动根据状态选择路径。

🔘链结构就不能路径规划了?

"我用链结构(比如通过if-else、循环等逻辑)也能实现路径规划或复杂逻辑,为什么说图结构'天生自带路径规划'呢?"首先,用链结构(比如LangChain中的Chains)确实可以通过条件判断(如if-else)、循环(while/for)等实现路径规划或复杂逻辑。就像你在超市购物清单上写"如果没牛奶就买豆奶",或者在代码中写:

python 复制代码
if condition1:
    step1()
elif condition2:
    step2()
else:
    step3()

甚至可以用循环实现反复检查或动态调整,比如:

python 复制代码
while not inventory_enough:
    adjust_order()

所以,你完全可以通过链结构实现路径规划或复杂的决策逻辑,比如一个AI代理根据输入选择不同路径("如果用户问天气就查天气API,如果问餐厅就查地图API")。但尽管链结构能实现路径规划,但图结构在路径规划和复杂逻辑上的"天生优势"体现在以下几个关键点上。

🔘链结构的路径------人工驱动

链的路径规划是"程序员驱动"的,手动定义每条路径

在链结构中,路径规划需要你(程序员)手动写出所有可能的条件和分支。比如在咖啡店订单例子中,你需要明确写出:如果点咖啡→调用咖啡代理、如果点甜点→调用甜点代理、如果点两者→先咖啡后甜点、如果库存不足→调整订单(可能需要嵌套if-else或循环)。一旦路径变得复杂,if-else和循环会变得非常繁琐,代码可能长到难以维护。而且链的路径是预定义的,运行时只能按照你写好的逻辑走,无法根据实时状态灵活调整,你只能手动添加更多条件。而且一旦业务需求变化(比如新增一个代理或新路径),你需要修改代码,重新设计整个链,工作量大。

🔘图结构的路径------数据驱动

图的路径规划是"数据驱动"的,动态选择路径

路径规划是通过节点(Nodes)和边(Edges)定义的,系统会根据数据的状态(state)或条件动态选择路径。比如在咖啡店订单例子中:

  • 你定义节点:接收输入、检查库存、制作咖啡、制作甜点、调整订单。
  • 你定义边:从"检查库存"节点到"制作咖啡"或"调整订单",条件是"如果有库存就制作,如果没库存就调整"。

运行时,图会根据实时状态自动选择路径,比如"如果库存不足,跳到调整订单节点;如果库存够,跳到制作节点",甚至可以设置循环(反复检查直到库存满足)。你不需要手动写出所有if-else逻辑,只需要定义节点和条件,系统自动根据状态选择路径。如果业务需求变化(新增代理或路径),只需添加新节点和边,无需重构整个结构,扩展非常灵活。当然图结构在某些情况下也可以被视为"预定义的规则",但与基于 if-else 或链式逻辑的方式有本质区别。

图是一种非线性数据结构,用于表示节点之间的复杂关系,可以随时添加或删除节点和边。

  • 节点(Node):表示实体(如状态、任务、代理等)。
  • 边(Edge):表示节点之间的关系(如依赖、路径、权重等)。

🔘为什么链结构无法取代图结构

为了简化,我们选定5个具体的条件,基于咖啡店订单系统:

  • condition1: order_coffee - 客户是否点了咖啡(True/False)。
  • condition2: order_dessert - 客户是否点了甜点(True/False)。
  • condition3: stock_available - 咖啡和甜点的库存是否充足(True/False)。
  • condition4: payment_success - 支付是否成功(True/False)。
  • condition5: is_vip - 客户是否是VIP(True/False,影响折扣)。

这些条件组合理论上有2^5 = 32种可能,但我们会通过不同方法处理,展示代码量和灵活性的差异。


🔘链结构------平铺

手动写出所有可能的if-else分支,5个条件有32种组合,需写32个分支,代码冗长。增加1个条件(6个)就到64种,维护困难。

python 复制代码
# 未优化的链结构实现咖啡店订单 ,手动枚举所有32种组合(2^5),代码冗长。
def process_order_unoptimized(order_coffee, order_dessert, stock_available, payment_success, is_vip):
    
    # 组合1:点咖啡+甜点,库存充足,支付成功,VIP
    if order_coffee and order_dessert and stock_available and payment_success and is_vip:
        print("客户点了咖啡和甜点,库存充足,支付成功,是VIP...")
        make_coffee()
        make_dessert()
        apply_vip_discount()
        print("订单完成!")
    
    # 组合2:点咖啡,库存不足,支付成功,非VIP
    elif order_coffee and not order_dessert and not stock_available and payment_success and not is_vip:
        print("客户点了咖啡,库存不足,支付成功,非VIP...")
        adjust_order("咖啡")
        print("订单调整完成!")
    
    # 组合3:点甜点,库存充足,支付失败,VIP 
    # 组合4:...(省略其余28种组合,每种都需要类似分支)

# 测试
process_order_unoptimized(True, True, True, True, True)

🔘链结构------嵌套

你要说,我又不傻,实际设计肯定不会这样平铺,是的,if 嵌套确实比平铺的 if-else 更紧凑,但它的致命弱点在于:一旦需要修改某个条件或逻辑,代码会迅速陷入"嵌套地狱" 。因为所有条件和逻辑紧密耦合在一起,修改一个条件可能会影响其他条件的执行。未来需要支持 "奶茶",需要在多个嵌套层级中插入新的逻辑。再比如顺序一旦想改,比如支付检查需要优先于库存检查,必须重新组织整个嵌套结构。

🔘链结构------分而治之

通过分步骤和循环处理,减少if-else嵌套,分3步(订单收集、库存检查、支付处理),用循环动态处理每个项。5个条件生成约4-8种路径(取决于订单项和状态),远少于32种,代码约10行。但是仍需手动定义步骤和条件,灵活性有限。其实链结构优化和神经网络分层的核心思想非常相似:传统链结构直接枚举所有条件组合,假设我们要构建一个神经网络,总权重数为10000。可以3层结构(10×10×10×10)总共40个节点。可以1000×10结构总共1010个节点。所以优化链结构不是一个特定的"算法"(像排序算法或搜索算法),而是一种设计模式编程思想 。它通过结构化代码和动态逻辑,改进传统链结构的局限。想象你在规划一天的行程(链结构),最初你可能为每种天气(雨/晴)和每种活动(工作/娱乐)写出所有组合(未优化)。优化后,你按步骤规划(先查天气,再选活动),减少重复决策。核心思想是:分而治之 + 动态处理 + 模块化

python 复制代码
def process_order_optimized(order_coffee, order_dessert, stock_available, payment_success, is_vip):
   
    # 步骤1:收集订单项 - 就像先问客户要点啥,记下来
    order_items = []  # 空列表,记录客户点的东西
    if order_coffee:
        order_items.append("咖啡")  # 如果点咖啡,加入列表
    if order_dessert:
        order_items.append("甜点")  # 如果点甜点,加入列表
    print(f"客户点了:{order_items}")  # 显示订单项
    
    # 步骤2:逐个检查库存并制作 - 像一个一个处理订单
    for item in order_items:  # 循环每个订单项
        if item == "咖啡" and stock_available:  # 如果是咖啡且有库存
            print("库存够,制作咖啡...")
            make_coffee()  # 做咖啡
        elif item == "咖啡" and not stock_available:  # 如果是咖啡但没库存
            print("咖啡没货,调整订单...")
            adjust_order("咖啡")  # 调整(比如换产品)
        elif item == "甜点" and stock_available:  # 如果是甜点且有库存
            print("库存够,制作甜点...")
            make_dessert()  # 做甜点
        elif item == "甜点" and not stock_available:  # 如果是甜点但没库存
            print("甜点没货,调整订单...")
            adjust_order("甜点")  # 调整
    
    # 步骤3:处理支付和VIP - 最后结账和加优惠
    if payment_success:  # 如果支付成功
        print("支付成功!")
        if is_vip:  # 如果是VIP
            print("VIP客户,给你折扣...")
            apply_vip_discount()  # 给折扣
        print("订单处理完成!")
    else:  # 如果支付失败
        print("支付失败,取消订单...")
        cancel_order()  # 取消
思想/方式 代码量 路径数量 可读性 灵活性 适用场景
平铺设计if 几十行(32+) 32种(2^5) 差(冗长) 低(静态) 简单场景,条件少
if嵌套 15-20行 4-8种 中(嵌套多) 中(层次调整) 条件有优先级场景
分而治之优化 10-15行 4-8种 好(分步清晰) 中(需步骤) 顺序任务,动态调整

平铺设计if 像单层神经网络(1000×10,10000权重),枚举所有输入组合。if嵌套 :像浅层网络(10×100,1000权重),按层次处理。分而治之优化:像深层网络(10×10×10,300权重),分层动态优化。

🔘图结构

用节点和边动态建模,运行时根据状态选择路径,无需预定义组合。

python 复制代码
# 图结构实现咖啡店订单
def process_order_graph(order_coffee, order_dessert, stock_available, payment_success, is_vip):
   
    # 定义图
    graph = {
        "起点": ["接收订单"],
        "接收订单": ["检查咖啡" if order_coffee else "检查甜点" if order_dessert else "无订单"],
        "检查咖啡": ["制作咖啡" if stock_available else "调整订单"],
        "制作咖啡": ["完成" if payment_success else "支付失败"],
        "检查甜点": ["制作甜点" if stock_available else "调整订单"],
        "制作甜点": ["完成" if payment_success else "支付失败"],
        "调整订单": ["完成"],
        "支付失败": ["取消订单"],
        "取消订单": [],
        "无订单": ["完成"],
        "完成": []
    }
    
    # 动态遍历
    def traverse(current):
        """
        遍历图,根据状态选择下一节点。
        用简单队列模拟动态路径。
        """
        if current in graph:
            print(f"状态: {current}")
            for next_node in graph[current]:
                if isinstance(next_node, str):
                    traverse(next_node)
                elif callable(next_node):
                    next_node = next_node()
                    if next_node:
                        traverse(next_node)
    
    # 从起点开始
    traverse("起点")

# 测试
process_order_graph(True, True, True, True, True)

再比如用一个大家熟悉的交通导航例子对比链和图:假设你用链结构设计一个导航系统,从家(A)到公司(D),中间可能经过B(公园)或C(商场),但路径取决于交通状况::

python 复制代码
if traffic_to_B < traffic_to_C:
    drive_to_B()
    if traffic_from_B_to_D < 30_minutes:
        drive_to_D()
    else:
        drive_back_to_A()
else:
    drive_to_C()
    if traffic_from_C_to_D < 30_minutes:
        drive_to_D()
    else:
        drive_back_to_A()
  • 如果交通状况有10种可能(堵车、施工、事故等),你需要写出所有if-else组合,代码会变得非常复杂。
  • 如果中途需要动态调整(比如发现B点堵车后直接跳到C点),你需要手动修改逻辑,运行时缺乏灵活性。
  • 如果需要循环(比如反复检查交通状况直到找到最佳路径),你得手动写while循环,代码更复杂。

假设你用图结构(类似LangGraph)设计导航系统:你定义:

  • 节点:A(家)、B(公园)、C(商场)、D(公司)。
  • 边:从A到B、A到C、B到D、C到D,每条边带条件(如"如果交通畅通就走这条路")。
  • 状态:实时交通数据(堵车、畅通等)。

运行时系统从A开始,根据交通状态自动选择路径,比如"如果B点堵车,就走A→C→D;如果C点也堵车,跳回A重新规划"。如果需要循环(反复检查交通直到找到最佳路径),只需在图中设置"回到A检查"的边,无需手动写代码。可见根本不需要手动写if-else,系统根据状态动态选择路径,代码简洁,而且扩展简单,如果新增E点(新商场),只需添加节点和边,不用重构整个系统。

图结构是路径规划经典算法的基础。例如:

  • 广度优先搜索(BFS):用于寻找最短路径(无权图)。
  • Dijkstra 算法:用于寻找加权图中的最短路径。
  • A 算法*:结合启发式信息,进一步优化路径搜索。

🟡图结构与强化学习

图结构与强化学习之间存在一种深刻而有趣的联系,这种联系源于它们在处理复杂问题时的天然契合,但强化学习并不完全依赖图结构,二者的关系是强关联而非唯一。

图结构是一种非常强大的工具,尤其在需要表示离散状态空间和状态转移关系时表现得淋漓尽致。想象一个导航问题:地图上的每个格子是一个节点,移动方向是连接节点的边,这种结构不仅直观,还天然适合路径规划任务。许多经典的强化学习场景本质上都可以看作路径规划问题 。图结构在这里的优势在于它能够清晰地表达状态之间的"真实路径 ",即从一个节点到另一个节点的实际连接,比如社交网络中的用户互动、知识图谱中的实体关联,或者交通路网中的道路布局。不仅如此,图结构还具备动态调整的能力,可以随时添加或删除节点和边,非常适合实时变化的环境。

然而,强化学习的本质却不仅仅是路径规划。强化学习的真正目标是在一个高维空间中找到"最佳路径" ,但这里的"路径"并不是图结构中的实体路径,而是一个更抽象的概念------最优策略 。这种策略是将状态映射到动作的规则,它可能是通过图结构的路径规划来实现,但也可能完全不依赖图。比如在一个连续动作空间中,比如自动驾驶中的油门控制,最优策略可能由一个神经网络直接输出,而不是依赖节点间的显式连接。这正是图结构与强化学习关系微妙的地方:强化学习的灵活性在于,它可以通过多种数据结构和方法来逼近这个抽象的"最优路径",图结构也只是实现强化学习的一种方式,但绝不是唯一的方式。比如说,深度强化学习中常用的数组和矩阵是不可或缺的。Q-learning 中的 Q 表可以用二维数组存储状态-动作对的价值,而深度 Q 网络(DQN)则依赖矩阵运算来表示神经网络的权重和输入输出。树结构也在某些场景下大放异彩,比如蒙特卡洛树搜索(MCTS)在 AlphaGo 中通过构建博弈树来探索策略。哈希表则在状态空间较大时派上用场,帮助快速查找和存储稀疏的状态价值。甚至在实现细节上,像经验回放中的队列,或优先级采样中的堆结构,都为强化学习的效率提供了支持。这些数据结构各有千秋,它们在特定任务中的作用是图结构无法完全替代的。

近年来,图神经网络GNN的兴起进一步深化了图结构与强化学习的结合。GNN 是一种专门处理图数据的深度学习技术,它通过聚合邻居节点的信息,能够捕捉图中的局部和全局特征。交通流量优化的例子就很贴切:路网可以用图表示,交叉口是节点,道路是边,GNN 可以提取拥堵模式等特征,而强化学习则通过优化信号灯的切换策略来最大化交通效率。这种组合不仅限于单智能体任务,在多智能体强化学习中也展现出强大潜力,比如在社交网络中预测用户行为,或在多人游戏中协调多个智能体的行动。所以,图结构在强化学习中的地位是毋庸置疑的,它擅长建模复杂的依赖关系,支持动态变化,。但强化学习的灵魂在于寻找最优策略,这个过程可以借助图结构,也可以跳出图结构的框架,通过其他工具实现。

🟡数据结构与算法

  • 顺序存储:连续内存布局让随机访问很快(O(1)),但插入删除很慢(O(n))。
  • 链式存储:动态指针连接让插入删除很快(O(1)),但随机访问很慢(O(n))。

数据结构是理解算法的本质,每种数据结构都有其擅长的操作,算法就是根据数据结构的特性解决问题

  1. 分治法:把大问题拆成小问题,递归解决(快速排序、归并排序)。
  2. 贪心法:每步最优,期望全局最优(Dijkstra、Kruskal)。
  3. 动态规划:记录子问题结果,优化重复计算(背包、LCS)。
  4. 回溯法:尝试所有可能,找到解(DFS、全排列)。
  5. 迭代法:逐步逼近答案(二分查找、链表反转)。

🔘 分治思想

将大问题分解为小问题,分别解决后再合并结果。

💠二分查找

  • 数据结构:数组(要求有序)。每次将搜索范围缩小一半,利用数组的随机访问特性。
  • 适应场景:可快速查找有序数组中的目标值。不适用无序数组或链表(无法随机访问)。

开发一个电商网站,用户输入了一个商品的价格范围(如 500------1000 元),找到所有符合条件的商品。如果采用暴力解法那就遍历整个数组,逐一检查每个商品的价格是否在范围内。时间复杂度为(O(n)),100 万条记录那就判断100万次。而数组本身又支持随机访问 ,可以通过索引直接定位元素。每次比较中间值,判断目标值在哪一半,逐步缩小范围。时间复杂度:(O(\log n)),比暴力解法快得多。

再次强调:二分查找的前提就是"有序"! 如果数据是无序的,那二分查找根本没法用,因为它是基于"排除一半"的策略。如果没有顺序,就无法判断下一步该往哪边缩小范围。

  • 数据有序 → 二分查找 O(log n)
  • 数据无序 → 先排序 O(n log n),再二分查找
  • 频繁查找 → 用平衡搜索树 (BST) 或 B+ 树
  • 单点查询快 → 用哈希表 O(1)

💠归并排序

  • 数据结构:数组或链表。先将数组分成两部分,分别排序后合并。
  • 适应场景:需要稳定排序且时间复杂度为 (O(n \log n)) 的情况。不适用内存有限时(归并排序需要额外空间)。

假设你在开发一个社交平台,需要对用户的活跃度(如点赞数)进行排序,以便展示"最活跃用户排行榜"。首先用户数据量可能非常大(如 100 万用户),而且数据可能是动态生成的(如实时统计的点赞数)。暴力解法即使用简单的冒泡排序或选择排序,时间复杂度:(O(n^2))。我们可以将数据分成小块分别排序,再合并结果,即利用分治思想减少比较次数

🔘 贪心思想

每一步都要局部最优解,希望最终得到全局最优解。

💠Dijkstra最短路径

  • 数据结构:图:邻接表或邻接矩阵。每次选择当前距离最短的节点扩展,逐步逼近目标。
  • 适应场景:加权图中求单源最短路径。但不适用存在负权边的图(需用 Bellman-Ford 算法)。

开发一个导航应用,用户输入起点和终点,计算出从起点到终点的最短路径。地图可以用图表示,节点是地点,边是道路,权重是距离或时间。 暴力解法枚举所有可能的路径,计算每条路径的总距离,这个时间复杂度是指数级。我们可以每次基于当前距离最短的节点进行扩展,逐步逼近目标,可以避免重复计算已经确定的最短路径。时间复杂度:(O((V + E) \log V)),适合加权图。

💠霍夫曼编码

  • 数据结构:树、队列。每次合并频率最小的两个节点,构建最优编码树。
  • 适应场景:数据压缩。但不适合动态更新的场景。

开发一个文件压缩工具,用户希望将一个大文件压缩成更小的文件以便存储或传输。文件中的数据通常包含重复的字符,压缩的核心目标是用更少的比特表示高频字符,同时用更多的比特表示低频字符。 暴力解法使用固定长度编码(ASCII ),每个字符占用相同的比特数,这样低频字符浪费空间,高频字符没有优化。可以使用变长编码,高频字符用短编码,低频字符用长编码,时间复杂度:(O(n \log n))(n 是字符种类数)。

🔘动态规划

将问题分解为子问题,通过保存中间结果避免重复计算。

💠最长公共子序列(LCS)

  • 数据结构:二维数组。通过递推公式逐步填表,记录子问题的解。
  • 适用场景:字符串匹配、生物信息学。不适用输入规模过大时(空间复杂度过高)。

💠背包问题

  • 数据结构:一维数组。通过状态转移方程逐步计算最优解。
  • 适用场景:资源分配、组合优化。不适用非线性约束问题。

假设你要去旅行,但你的行李箱容量有限(最多只能装 10kg)。你有多个物品,每个都有重量和价值(比如重要性、实用性)。你的目标是在不超重的情况下,装入价值最高的物品。 直觉方法 (贪心算法)直接选价值最高的物品(💻 笔记本 10 分,先拿!)但剩下 7kg 空间,能否搭配更好的方案?暴力解法 穷举所有物品组合,计算总重量和总价值。但组合太多,计算量太大。所以这时候就需要动态规划解法,我们用一个二维表 dp[i][w],表示前 i 个物品,在重量限制 w 下,能获得的最大价值 。引入状态转移公式 保证价值最大化!。在电商购物优惠 (选最优商品组合,最大化折扣) 投资选择 (有限预算下买最优资产) 服务器资源分配(最大化计算力,优化成本)下都好使!

🔘回溯与递归

通过试探所有可能的解,找到满足条件的解。

💠深度优先搜索(DFS):

  • 数据结构:栈(显式或隐式递归)。沿着一条路径尽可能深地搜索,直到找到解或穷尽可能性。
  • 适用场景:迷宫求解、组合问题这种找到任意解,但不适用需要找到最优解(如最短路径问题)。

假设你在开发一个游戏,玩家需要在一个迷宫中找到出口。你需要编写一个算法,帮助玩家找到从起点到终点的路径。 迷宫可以用图表示,每个格子是一个节点,相邻格子之间有边连接, 需要找到从起点到终点的一条路径(不一定是最短路径)。 暴力解法枚举所有可能的路径,逐一检查是否到达终点。时间复杂度:指数级。可以使用 DFS 沿着一条路径尽可能深地搜索,如果当前路径无法继续,回溯到上一步,尝试其他路径。

💠八皇后问题:

  • 数据结构:递归调用栈。尝试放置皇后,回溯到上一步重新尝试。
  • 适用场景:排列组合、约束满足问题。不适合解空间过大或约束过于复杂时。

🔘广度优先搜索(BFS)

逐层扩展搜索,确保最先找到的解是最优解。

💠广度优先搜索(BFS)

  • 数据结构:队列。从起点开始,逐层扩展相邻节点。
  • 适用场景:无权下的最短路径、连通性问题。不适用有权图(需用 Dijkstra 或其他算法)。

假设你在开发一个社交平台,用户 A 想知道他和用户 B 是否有共同好友,或者他们之间的"社交距离"是多少。 社交网络可以用图表示,每个用户是一个节点,好友关系是边。需要找到用户 A 到用户 B 的最短路径(即最少经过的好友数)。 暴力解法枚举所有可能的路径,计算每条路径的长度,时间复杂度:指数级。采用用 BFS 逐层扩展,从用户 A 开始,逐层访问其好友,确保最先找到的路径是最短路径。

深度优先搜索是一种基于试探性探索 的算法,沿着一条路径尽可能深地搜索,直到找到解或穷尽可能性。如果当前路径无法继续,就回溯到上一步,尝试其他路径。属于回溯与递归思想?回溯体现在DFS 的过程本质上是通过试探所有可能的路径来寻找解,当某条路径不可行时,就回溯到上一步重新尝试。地柜体现在DFS 的实现通常依赖递归调用 ,隐式地保存当前路径的状态。适用需要找到任意解(如迷宫求解、组合问题),但不需要保证最优解(如八皇后问题)。广度优先搜索是一种基于逐层扩展的算法,从起点开始,逐层访问相邻节点,它确保最先找到的解是最优解(如最短路径问题)。它不需要回溯,也不依赖递归,而是通过队列显式地管理待访问节点。适用需要找到最优解(如无权图中的最短路径),解空间较大但解在较浅层时(如连通性问题)。

相关推荐
shinelord明2 分钟前
【软件设计】23 种设计模式解析与实践指南
数据结构·设计模式·软件工程
驼驼学编程16 分钟前
决策树,Laplace 剪枝与感知机
算法·决策树·剪枝
坚强小葵16 分钟前
实验8-2-1 找最小的字符串
c语言·算法
apcipot_rain36 分钟前
【密码学——基础理论与应用】李子臣编著 第三章 分组密码 课后习题
python·算法·密码学
lucky1_1star43 分钟前
FX-函数重载、重写(覆盖)、隐藏
java·c++·算法
KuaCpp3 小时前
蓝桥杯15届省C
算法·蓝桥杯
橘颂TA3 小时前
【C++】数据结构 栈的实现
开发语言·数据结构·c++··学生
奔跑的废柴4 小时前
LeetCode 738. 单调递增的数字 java题解
java·算法·leetcode
武乐乐~4 小时前
欢乐力扣:合并区间
算法·leetcode·职场和发展
安忘9 小时前
LeetCode 热题 -189. 轮转数组
算法·leetcode·职场和发展