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

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

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
相关推荐
黎宇幻生7 小时前
Java全栈学习笔记39
java·笔记·学习
诸葛悠闲9 小时前
XCP协议在以太网上实现的配置
学习
遇印记10 小时前
大二java学习笔记:二维数组
java·笔记·学习
爱吃甜品的糯米团子11 小时前
Linux 学习笔记之进程管理、网络基础与常用软件安装
linux·网络·学习
Purple Coder12 小时前
面试-上海电力大学研一的学习经验
学习
饮浊酒13 小时前
Python学习-----小游戏之人生重开模拟器(普通版)
python·学习·游戏程序
QT 小鲜肉13 小时前
【个人成长笔记】在Ubuntu中的Linux系统安装 anaconda 及其相关终端命令行
linux·笔记·深度学习·学习·ubuntu·学习方法
QT 小鲜肉13 小时前
【个人成长笔记】在Ubuntu中的Linux系统安装实验室WIFI驱动安装(Driver for Linux RTL8188GU)
linux·笔记·学习·ubuntu·学习方法
急急黄豆14 小时前
MADDPG学习笔记
笔记·学习