【数据结构与算法_学习精华】

一、学习数据结构与算法的框架思维

1、核心结论:

种种数据结构,皆为数组 (顺序存储)和链表 (链式存储)的变换。(索引访问与指针)

数据结构的关键点在于遍历访问,具体一点就是:增删查改等基本操作。

种种算法,皆为穷举 。穷举的关键点在于无遗漏和无冗余

各种数据结构的遍历 + 访问仅两种形式:迭代(for / while)与 递归(函数自调:自己调用自己)

2、迭代与递归

2.1 迭代

python 复制代码
迭代(iterative)------ 自己一步一步数台阶

脚下踩一根计数器 i=0→1→2...

每数一步,状态全在你自己口袋里(循环变量、栈、指针)

CPU 只关心"下一步去哪",不回头找导游

线性结构,适合 循环迭代,以数组为例:

python 复制代码
def traverse(arr: List[int]):
    for i in range(len(arr)):
        # 迭代访问 arr[i]
2.1.1 数组求和
python 复制代码
def sum_iter(a):
    total = 0
    for x in a:        # 迭代遍历
        total += x
    return total

print(sum_iter([7, 3, 5]))   # 15
2.1.2 链表遍历
python 复制代码
# 基本的单链表节点
class ListNode:
    def __init__(self, val):
        self.val = val
        self.next = None

#遍历方式1:迭代访问

def traverse(head: ListNode) -> None:
    p = head
    while p is not None:
        # 迭代访问 p.val
        p = p.next
2.1.3 图遍历
python 复制代码
from typing import List, Dict, Set, Deque
from collections import deque

# ---------- 图定义 ----------
Graph = Dict[int, List[int]]   # 邻接表:{节点: [邻居, ...]}

# ---------- 广度优先 BFS(迭代) ----------
def bfs(graph: Graph, root: int) -> None:
    if root not in graph:
        return
    visited: Set[int] = set()
    q: Deque[int] = deque([root])
    visited.add(root)

    while q:                       # 标准队列迭代
        node = q.popleft()
        print(node)                # 访问节点
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                q.append(neighbor)

2.2 递归(套娃-自己调自己)

python 复制代码
概念理解:

递归(recursive)------ 导游们接力,【每层】的【导游-函数】负责一层处理

你问导游 A:"到山顶多少阶?"

A 只记 "【我这一层的阶数】 + 【下一层导游 B 的答案】", 然后把问题原样扔给 导游B。

每层导游拍照存档自己的阶数,直到最后一阶不再接力。回程时,照片从后往前逐层收集,结果逐层返回。

每层只做"自己这一层 + 剩余结果",而剩余结果交给下一层复制粘贴的这同一段代码去完成,这就是递归。
2.2.1 数组求和-递归方式
python 复制代码
def sum_rec(a, i=0):         # 函数签名:a是数组,i是元素索引
    if i == len(a):          # 基准case:停止条件。到最后一阶直接返回,避免越界
        return 0             # 避免越界
    return a[i] + sum_rec(a, i + 1)  

# 把当前元素 a[i] "拿在手里",每层的元素累计操作

#如果是return sum_rec(a, i + 1),则表示跳过当前元素,只算"剩余的部分"


运行结果:
sum_rec([7,3,5], 0)
= 7 + sum_rec([7,3,5], 1)
= 7 + (3 + sum_rec([7,3,5], 2))
= 7 + 3 + (5 + sum_rec([7,3,5], 3))
= 7 + 3 + 5 + 0          ← i==3 越界,返回 0
= 15

一些递归的概念

python 复制代码
1、函数栈帧:

就是函数被调用时,在内存里临时建立的一小块"工作台",(如例子上的 每层的照片)
里面放着:
1)当前函数的局部变量
2)返回地址(调用完后回到哪条指令)
3)参数值
4)一些控制信息(保存的寄存器、上一帧指针等)

调用函数 → 压入一帧;返回 → 弹出这一帧。

所有帧按"后调用先弹出"的顺序串在系统调用栈上,这就是递归深度过深会爆 StackOverflow 的原因。

上面例子中:

函数栈帧里只记录"当前这一层"的局部变量名 i 和指向列表 a 的引用(指针),不会把整个列表或元素复制进帧。所以 a[i] 只是通过帧里的引用去堆上读取数据,本身不是栈帧的一部分。但其实就是可以代指栈帧
2.2.1 链表遍历

既可以迭代 又可以递归的,以链表为例:

python 复制代码
# 基本的单链表节点
class ListNode:
    def __init__(self, val):
        self.val = val
        self.next = None


#遍历方式2:递归访问

def traverse(head: ListNode) -> None:
    if head is None:          # 终止条件
        return
    print(head.val)           # 访问当前节点
    traverse(head.next)       # 处理后续链表
2.2.3 二叉树遍历

非线性结构,适合递归,以二叉树为例:

与上面的链表相似

python 复制代码
# 基本的二叉树节点
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
        
def traverse(root: TreeNode) -> None:
    if root is None:        # 终止条件
        return
    print(root.val)         # 访问当前节点
    traverse(root.left)     # 递归左子树
    traverse(root.right)    # 递归右子树
2.2.4 N叉数的遍历

与二叉树相似

python 复制代码
# 基本的 N 叉树节点
from typing import List

class TreeNode:
    val: int
    children: List['TreeNode']

def traverse(root: TreeNode) -> None:
    if root is None:                # 基准:空树
        return
    print(root.val)                 # 先访问当前节点(前序)
    for child in root.children:     # 再依次递归每棵子树
        traverse(child)
2.2.5 图的遍历

与N叉数相似,对于环,要单独使用

python 复制代码
from typing import List, Dict, Set, Deque
from collections import deque

# ---------- 图定义 ----------
Graph = Dict[int, List[int]]   # 邻接表:{节点: [邻居, ...]}

# ---------- 深度优先 DFS(递归) ----------
def dfs(graph: Graph, root: int, visited: Set[int]) -> None:
    if root not in graph:          # 停止条件:空图或孤立点
        return
    print(root)                    # 访问当前节点(前序)
    visited.add(root)
    for neighbor in graph[root]:   # 依次递归每条边
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

带环的图,需要使用 布尔数组 visited 做标记

python 复制代码
from typing import Dict, List, Set, Deque
from collections import deque

Graph = Dict[int, List[int]]

# ---------- DFS(递归) ----------
def dfs(graph: Graph, root: int, visited: Set[int]) -> None:
    if root not in graph:
        return
    print(root)
    visited.add(root)
    for nxt in graph[root]:
        if nxt not in visited:   # 环被这一步剪掉
            dfs(graph, nxt, visited)

3、数据结构根本

3.1 数组

连续存储,可通过索引随机访问元素。内存空间需要一次性分配好。如果需要扩容,时间复杂度是O(N),如果是在数组中间插入/删除元素,时间复杂度也是O(N)。因为都需要移动其他元素保证连续存储。

3.2 链表

非连续存储,所以不能随机访问元素。需要指针指向下一元素。删除/插入元素,只需要操作某一元素的前后指针,所以时间复杂度是O(1)。因为需要存储前后指针,需要额外的存储空间。

4、数据结构基本类型

4.1、数组

4.2、链表

4.3、哈希表

通过散列函数把键映射到一个大数组里。

拉链法需要链表特性。

线性探查法需要数组连续访问特性。

4.4、队列

可以使用链表也可以使用数组实现。

用数组实现,就要处理扩容缩容的问题;

用链表实现,没有扩/缩容问题,但需要更多的内存空间存储节点指针

复制代码
1

4.5、栈

可以使用链表也可以使用数组实现。

用数组实现,就要处理扩容缩容的问题;

用链表实现,没有扩/缩容问题,但需要更多的内存空间存储节点指针

复制代码
可以使用链表也可以使用数组实现

用数组实现,就要处理扩容缩容的问题;
用链表实现,没有扩/缩容问题,但需要更多的内存空间存储节点指针

4.6、树

用数组实现的是:完全二叉树、二叉堆

用链表实现的是:二叉搜索树、红黑树、B数、AVL树

4.7、图

图的两种存储方式,邻接表就是链表,邻接矩阵就是二维数组。

5、算法的本质

计算机算法,最笨但最通用的是:穷举。然后可以变形成聪明的穷举

python 复制代码
大部分开发岗位工作中都是基于现成的开发框架做事,不怎么会碰到底层数据结构和算法相关的问题,但另一个事实是,只要你想找技术相关的岗位,数据结构和算法的考察是绕不开的,因为这块知识点是公认的程序员基本功。

为了区分,不妨称算法工程师研究的算法为「数学算法」,称刷题面试的算法为「计算机算法」,我们的目标主要聚焦的是「计算机算法」。

找一份开发岗位的工作,所以你真的不需要有多少数学基础,只要学会用计算机思维解决问题就够了

5.1 排列组合问题

排列组合问题抽象成一棵树,要精确地使用代码遍历这棵树的所有节点,不能漏不能多,才能写出正确的代码

5.2 有序数组中,寻找一个元素

在有序数组中寻找一个元素,用一个 for 循环暴力穷举谁都会,但 二分搜索算法 就是更聪明的穷举方式,拥有更好的时间复杂度

5.3 动态规划

动态规划是无冗余地穷举所有解,然后找一个最值

5.4 贪心算法

贪心算法就是在题目中发现一些规律(专业点叫贪心选择性质),使得你不用完整穷举所有解就可以得出答案

5.5 计算连通分量

想判断图中的两个节点是否连通,用 DFS/BFS 暴力搜索(穷举)肯定可以做到,但 Union Find 算法硬是用数组模拟树结构,把连通性相关的操作复杂度给干到 O(1)

6、常见的算法技巧

链表和数组

1、单链表常考的技巧就是双指针

判断单链表是否成环,暴力解是用一个 HashSet 之类的数据结构来缓存走过的节点,遇到重复的就说明有环。

但用快慢指针可以避免使用额外的空间,这就是聪明地穷举

2、数组常用的技巧也是双指针
3、二分搜索技巧

可以归为两端向中心的双指针。

如果在数组中搜索元素,一个 for 循环花 O(N)时间穷举肯定能搞定,但是二分搜索告诉你,如果数组是有序的,它只要 O(logN)的复杂度,这就是一种更聪明的搜索方式。

4、滑动窗口算法

典型的快慢双指针。用嵌套 for 循环花 O(N^2) 的时间肯定可以穷举出所有子数组。但是滑动窗口算法表示,在某些场景下,它可以用一快一慢两个指针,只需 O(N) 的时间就可以找到答案,这就是更聪明地穷举方式。

5、前缀和 -技巧

频繁地让你计算子数组的和,每次用 for 循环去遍历肯定没问题,但前缀和技巧预计算一个 preSum 数组,就可以避免循环。

6、差分数组技巧

频繁地让你对子数组进行增减操作,也可以每次用 for 循环去操作,但差分数组技巧维护一个 diff 数组,也可以避免循环。

二叉树系列

二叉树模型几乎是所有高级算法的基础。叉树题目的递归解法可以分两类思路:

第一类是遍历一遍二叉树得出答案:回溯算法,

第二类是通过分解问题计算出答案:动态规划算法

1、遍历二叉树最大深度

这个逻辑就是用 traverse 函数遍历了一遍二叉树的所有节点,维护 depth 变量,在叶子节点的时候更新最大深度。

python 复制代码
class Solution:
    def __init__(self):
        # 记录最大深度
        self.res = 0
        # 记录当前遍历节点的深度
        self.depth = 0
        
    def maxDepth(self, root: TreeNode) -> int:
        self.traverse(root)
        return self.res
        
    def traverse(self, root: TreeNode) -> None:
        if not root:
            # 到达叶子节点
            self.res = max(self.res, self.depth)
            return
        # 前序遍历位置
        self.depth += 1
        self.traverse(root.left)
        self.traverse(root.right)
        # 后序遍历位置
        self.depth -= 1
2、全排列问题:

本质就是多叉树的遍历,所以说回溯算法本质就是遍历多叉树,只要能把问题抽象成树结构,就一定能用回溯算法解决。

python 复制代码
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        # 记录所有全排列
        res = []
        # 记录当前正在穷举的排列
        track = []

        # track 中的元素会被标记为 true,避免重复使用
        used = [False] * len(nums)

        # 主函数,输入一组不重复的数字,返回它们的全排列
        def backtrack(nums):
            # 到达叶子节点,track 中的元素就是一个全排列
            if len(track) == len(nums):
                res.append(track[:])
                return

            for i in range(len(nums)):
                # 排除不合法的选择
                if used[i]:
                    # nums[i] 已经在 track 中,跳过
                    continue
                # 做选择
                track.append(nums[i])
                used[i] = True

                # 进入递归树的下一层
                backtrack(nums)

                # 取消选择
                track.pop()
                used[i] = False

        backtrack(nums)
        return res
3、二叉树最大深度 --分解问题
python 复制代码
# 定义:输入根节点,返回这棵二叉树的最大深度
def maxDepth(root: TreeNode) -> int:
    if root is None:
        return 0
    # 递归计算左右子树的最大深度
    leftMax = maxDepth(root.left)
    rightMax = maxDepth(root.right)
    # 整棵树的最大深度就是左右子树的最大深度加一
    res = max(leftMax, rightMax) + 1

    return res
相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习