news 2026/10/22 11:38:55

递归算法复杂度分析全攻略:时间与空间复杂度从递推到主定理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
递归算法复杂度分析全攻略:时间与空间复杂度从递推到主定理

递归算法的时间复杂度和空间复杂度,算得上是算法学习路上的一道分水岭。很多人写递归代码手到拈来,一让分析复杂度就开始凭感觉:觉得这函数自己调自己,复杂度肯定很高,结果一算又说不清楚到底多高;或者干脆只会数递归调用的次数,把空间复杂度漏掉。这其实是个很大的误区。递归复杂度的分析是有完整方法论的,学会之后你会发现它比迭代循环的分析还要机械——只需要抓住"递推关系式"和"调用栈深度"这两条主线,一切都能算得明明白白。

这篇文章我会从方法论讲到实操,用二叉树遍历、斐波那契、二分查找、归并排序这几个经典案例,把时间复杂度和空间复杂度的每一步推算都摊开给你看。无论你是正在准备面试,还是想把算法基础补扎实,都能在里面找到可以直接套用的分析套路。

1. 复杂度分析的核心逻辑:递归函数到底在做什么

分析递归算法,核心只有一个问题:这个函数自己调用自己的过程,整体上做了多少工作?同时占了多少额外的内存?前者对应时间复杂度,后者对应空间复杂度。

1.1 递归的两个阶段:递去与归来

一个递归函数执行时,并不是"直接在里面转圈",而是分成两个阶段:

  • 递去(递推):从最外层调用开始,一层一层向内调用,直到到达终止条件(base case)。
  • 归来(回溯):到达终止条件后,逐层返回结果,每一层用子问题的结果计算出当前层的结果。

这两个阶段对应的复杂度分析重点不同。递去阶段决定的是"调用栈有多深",这直接影响空间复杂度;归来阶段决定的是"每一层合并计算要花多少时间",这直接影响时间复杂度。

举个例子,最简单的递归计算阶乘:

def factorial(n): if n == 0: return 1 return n * factorial(n - 1)

调用链是factorial(n) -> factorial(n-1) -> ... -> factorial(0),递去阶段一共要压n+1层栈帧,归来阶段每一层只做一次乘法。这里的"深度 n+1"就是空间消耗的核心来源,而"每一层 O(1) 的乘法"乘以"n+1 层"就是时间消耗的核心来源。

1.2 分析复杂度的核心工具:递推关系式

递推关系式(recurrence relation)是描述递归算法时间消耗的数学表达式。它把"规模为 n 的问题的时间"拆成"子问题的时间"加"合并开销的时间"。

比如上面的阶乘,可以写成:

T(n) = T(n-1) + O(1) T(0) = O(1)

这里T(n-1)是递归调用自己处理规模为 n-1 的子问题所花的时间,O(1)是乘法操作的时间,T(0) = O(1)是终止条件的时间。

空间复杂度的递推关系式更简单,本质上就是"递归调用链的最大深度"。你可以把它看成一层一层往深处走,最深处能走多远,就占多大的栈空间。

1.3 为什么不能用"数调用次数"代替复杂度分析

很多人犯的第一个错误是用"调用次数"直接当时间复杂度。我们看斐波那契数列的朴素递归:

def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)

调用次数确实能反映时间规模,但"调用次数"本身需要算,而且不是一眼看得出来的。它的调用结构是一个二叉树形态,节点数大约是2^n级别。你能感觉到它很大,但如果不会用递推关系式去推导,就永远停在"感觉很大"这个层次,面试时一问"到底多大?为什么?"就答不上来。

再举一个反面例子:有些递归虽然调用次数很多,但每一层合并操作很重,比如每一层都遍历一遍数组,这时时间复杂度不能只看调用次数,还要乘上每层的合并开销。这就是为什么要有递推关系式这个工具——它把"调了多少次"和"每次做了什么"统一到一个公式里。

2. 时间复杂度分析的三种解法:代入、递归树、主定理

拿到递推关系式之后,接下来就是求解。求解有三种主流方法:代入法(猜测 + 数学归纳)、递归树法(画树数节点)、主定理(套公式)。三种方法各有适用场景,我都建议掌握,因为它们在不同场合下效率完全不同。

2.1 代入法:先猜后证

代入法分两步:

  1. 猜测一个上界,比如T(n) = O(n log n)。
  2. 用数学归纳法证明猜测成立。

这个方法适合你已经对结果有直觉的场景。比如你知道某个递归是二分结构,大概率是O(n log n),那就假设它是,然后代入递推关系式验证。

实际推导过程是这样的——假设要证明T(n) ≤ c · n log n,我们把递推关系式T(n) = 2T(n/2) + O(n)代进去:

T(n) ≤ 2 · c · (n/2) · log(n/2) + n = c · n · (log n - 1) + n = c · n · log n - c · n + n

只要取c ≥ 1,-c·n + n ≤ 0,就证出来了。这个方法严谨,但有个前提:你得先猜中。对于复杂一点的关系式,猜错方向会浪费大量时间。

2.2 递归树法:最直观,也最适合手算

递归树法的思路是把递归的每一层展开,计算每一层的总代价,然后全部加起来。这个方法我最推荐给初学者,因为它的每一步都看得见摸得着。

以T(n) = 2T(n/2) + n为例:

  • 第一层:规模为 n,合并开销是 n。
  • 第二层:有两个规模为 n/2 的子问题,每个的合并开销是 n/2,合计2 × n/2 = n。
  • 第三层:有四个规模为 n/4 的子问题,合计4 × n/4 = n。
  • 每一层加起来都是 n。
  • 树的高度是log₂ n(因为每次规模减半,直到 1)。

所以总时间 =n × log₂ n,也就是O(n log n)。

这个方法唯一的难点是"每一层加起来是多少"需要观察规律,但绝大多数递归都能用这个方式解出来,而且还能顺带看到空间消耗的路径(从根沿着一条路走到叶子有多长)。

2.3 主定理:套公式的工业级解法

主定理(Master Theorem)是专门针对形如T(n) = aT(n/b) + f(n)的递推关系式的公式化解法。它的判断条件围绕f(n)与n^{log_b a}的比较展开:

  • 如果f(n) = O(n^{log_b a - ε}),说明合并开销比子问题开销小得多,时间复杂度由子问题数量主导,结果是Θ(n^{log_b a})。
  • 如果f(n) = Θ(n^{log_b a}),两边势均力敌,结果是Θ(n^{log_b a} · log n)。
  • 如果f(n) = Ω(n^{log_b a + ε}),且存在常数c < 1使a·f(n/b) ≤ c·f(n)对足够大的 n 成立,说明合并开销主导,结果是Θ(f(n))。

用归并排序的关系式T(n) = 2T(n/2) + Θ(n)来套:

  • a = 2, b = 2,所以n^{log_b a} = n^{log₂ 2} = n。
  • f(n) = n,正好等于n^{log_b a},属于第二种情况。
  • 结果是Θ(n log n)。

主定理最大的价值在于快速判断,不用展开递归树。但它的局限也很明显:只适用于"子问题规模等比缩小"的递归(即n/b形式的),对于T(n) = T(n-1) + T(n-2)这种斐波那契形式就完全失效了。所以三种方法缺一不可。

2.4 常见递推关系式的时间复杂度速查表

递推关系式对应场景时间复杂度
T(n) = T(n-1) + O(1)线性递归(阶乘、顺序遍历)O(n)
T(n) = T(n-1) + O(n)递归中每层遍历一次O(n²)
T(n) = T(n/2) + O(1)二分查找O(log n)
T(n) = T(n/2) + O(n)快速选择等O(n)
T(n) = 2T(n/2) + O(n)归并排序O(n log n)
T(n) = 2T(n/2) + O(1)二叉树后序遍历O(n)
T(n) = T(n-1) + T(n-2) + O(1)朴素斐波那契O(2ⁿ)

这张表建议保存,刷题时遇到递归函数,先尝试把它的递推关系式写出来,然后对号入座,多数情况几秒钟就能搞定。

3. 空间复杂度分析:真正容易踩坑的地方

时间复杂度的讨论已经够多了,但空间复杂度反而是面试里最容易暴露问题的地方。原因很简单:很多递归代码看起来"没开数组没开字典",大家就默认空间复杂度是O(1),这完全是误区。

3.1 递归调用栈是怎么吃内存的

每次函数调用,系统都会在调用栈上分配一块栈帧(stack frame),用来存局部变量、参数、返回地址。递归函数一层层往里调,如果最深调到第 k 层,那栈上就同时压着 k 层栈帧,谁也释放不了——最外层还在等最内层的结果呢。

这个"最深层数"就是递归空间复杂度的核心。

举个例子,二叉树的遍历:

def inorder(root): if root is None: return inorder(root.left) visit(root) inorder(root.right)

这个函数在极端情况下(比如树退化成链表)会递归调用n层,所以空间复杂度是O(n)。很多人在笔试里写二叉树中序遍历,空间复杂度直接写O(1),这是错的。即使在平衡树中,调用深度也是O(log n),不是O(1)。

3.2 空间复杂度的两个组成部分

递归算法的空间占用不是只有调用栈。总共分两块:

  1. 递归栈空间:从根调用到最深叶子调用路径上的栈帧总数,也就是递归深度。
  2. 辅助空间:每个栈帧里面的局部变量、临时数组占用的空间。

如果每一层都复制了一个长度为 n 的数组,那"每一层的辅助空间"乘以"深度"才是总空间。比如某些递归实现,每层都slice数组,那空间复杂度会变成O(n²)——因为深度是O(n),每层复制O(n),相乘就是O(n²)。

但多数情况下,如果每一层的辅助空间是O(1),总空间就等于递归深度。

3.3 空间复杂度的递推表达式

空间复杂度的递推表达式和时间不同。它更像一条"最深路径"的计算:

S(n) = S(子问题规模) + O(每层额外空间)

以二分查找为例,S(n) = S(n/2) + O(1),一路除到 1,深度是log₂ n,空间复杂度O(log n)。

以斐波那契为例,递归树虽然庞大,但空间复杂度不是看树有多大,而是看"从根到叶子的最长路径"有多长。fib(n)调fib(n-1),fib(n-1)调fib(n-2),最长路径是n层,所以空间复杂度是O(n)。

这是一道经典的面试陷阱题:斐波那契递归的时间复杂度是O(2ⁿ),但空间复杂度却是O(n)。很多人把两者都答成指数级,就栽了。

3.4 尾递归优化对空间复杂度的影响

尾递归是指递归调用是函数的最后一个操作,且返回值直接返回,不再参与任何运算。比如:

def factorial_tail(n, acc=1): if n == 0: return acc return factorial_tail(n - 1, acc * n)

这种写法在支持尾调用优化的语言(如某些函数式语言)里,可以复用当前栈帧,空间复杂度从O(n)降到O(1)。但要注意:Python、Java 等主流语言默认不做尾调用优化,即使写成尾递归形式,空间复杂度依然是O(n)。这是面试场上很容易说错的一个细节。

判断一个递归是不是尾递归有个简单标准:递归调用之后还有没有后续操作?如果还有乘法、加法、合并等操作,就不是尾递归。阶乘的朴素写法是return n * factorial(n-1),乘法在递归返回之后才做,所以不是尾递归。改成acc * n作为参数传下去,才是尾递归。

4. 经典案例完整分析:从递推式到最终结论

光讲理论太抽象,我选四个最经典的递归场景,把从递推关系式到最终复杂度结论的完整推算过程写一遍。这些都是面试和工作中出现频率最高的。

4.1 案例一:阶乘的线性递归

代码:

def factorial(n): if n == 0: return 1 return n * factorial(n - 1)

递推关系式:

T(n) = T(n-1) + O(1) T(0) = O(1)

求解:

展开T(n) = T(n-1) + 1 = T(n-2) + 2 = ... = T(0) + n = O(1) + n,所以T(n) = O(n)。

空间复杂度:

每次递归调用压一层栈,从factorial(n)到factorial(0)一共n+1层,每层栈帧大小O(1),所以S(n) = O(n)。

递归树视角:递归树是一条链,高度 n,每层一个节点,总节点数 n+1,每条路径长度 n。

4.2 案例二:斐波那契的朴素递归

代码:

def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2)

递推关系式:

T(n) = T(n-1) + T(n-2) + O(1) T(0) = T(1) = O(1)

求解:

这个递推式没法直接套主定理,用递归树展开:

  • 根节点做一次加法,代价O(1)。
  • 第二层有两个节点:fib(n-1)和fib(n-2)。
  • 第三层有四个节点。
  • 第 k 层有大约2^k个节点。

树的高度大约到 n,总节点数 ≈2^0 + 2^1 + ... + 2^n = 2^{n+1} - 1。所以T(n) = O(2ⁿ)。

更严格地,这个递推式的解可以借助特征方程:x² = x + 1,特征根是黄金比例φ ≈ 1.618,所以T(n) = O(φⁿ)。这就是O(2ⁿ)的精确来源——它是指数级的,底数大约是 1.618。

空间复杂度:

沿任意一条路径从根走到叶子,最多走 n 步(比如一直走fib(n-1)→fib(n-2)→ ...),所以调用栈深度是O(n),空间复杂度S(n) = O(n)。

这是最典型的时间大、空间小的案例。如果追问优化方案,可以从递归改成带备忘录的自顶向下递归,时间复杂度降到O(n),空间复杂度仍是O(n);或者改成自底向上的迭代,时间O(n),空间O(1)。

4.3 案例三:二分查找的递归实现

代码:

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

递推关系式:

每次只调用一个子问题,规模减半:

T(n) = T(n/2) + O(1) T(1) = O(1)

求解:

T(n) = T(n/2) + 1 = T(n/4) + 2 = ... = T(1) + log₂ n,所以T(n) = O(log n)。

空间复杂度:

递归深度也是log₂ n,每层O(1)额外空间,S(n) = O(log n)。注意这个空间是递归造成的,如果改成迭代实现,空间可以压到O(1)。

这里有个容易犯的错误:二分查找的递归写法里没有复制数组,每次只是改下标,所以空间只看调用栈。如果有人写成binary_search(arr[mid+1:], ...)这种切片传参,那每层都会复制数组,时间和空间复杂度都会恶化到O(n)甚至O(n log n),面试时这种细节经常被追问。

4.4 案例四:归并排序的递归实现

代码:

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)

递推关系式:

每次分成两个规模为 n/2 的子问题,合并操作的代价是O(n):

T(n) = 2T(n/2) + O(n) T(1) = O(1)

求解(主定理):

a = 2, b = 2,n^{log_b a} = n,f(n) = n,属于主定理第二种情况,所以T(n) = Θ(n log n)。

求解(递归树):

每一层合并总代价都是 n,树高log₂ n,总代价n log n。两种方法结果一致。

空间复杂度:

这个案例的最大坑在于:上面的 Python 写法每层递归都通过切片arr[:mid]和arr[mid:]复制数组,每一层复制总长度是 n,深度是log₂ n。但因为切片产生的数组在函数返回后会被回收,同一时间栈上存活的空间不是n log n,而是从根到当前叶子这条路径上的所有切片之和:

n + n/2 + n/4 + ... + 1 ≈ 2n

所以空间复杂度是O(n),不是O(n log n)。这一点特别容易算错——如果只把"每层 O(n)"乘以"深度 log n"得到O(n log n),那是把不同层的空间错误地叠加了。实际上不同层的切片存在时间有重叠(父层的left数组在子层递归时还活着),但总和的几何级数依然是O(n)级别。

核心结论:归并排序时间是O(n log n),空间是O(n)。这个结论面试必考,但背后的"为什么空间是 n 而不是 n log n"很多人说不清,因为调用栈是串行深度,不是并行宽度。

5. 递归与非递归的权衡:何时该用递归,何时该改写

学完复杂度分析,很多人会产生一个疑问:"递归空间复杂度常常是 O(n),而迭代能压到 O(1),那是不是递归就该被淘汰?" 不是的。实际问题中要权衡的东西远比复杂度公式复杂。

5.1 递归的优势:代码可读性与问题建模

递归最擅长的是分治类问题——归并排序、快速排序、树的遍历、回溯搜索、动态规划的记忆化递归。这些问题如果用迭代写,要么需要手动模拟系统栈(退化成显式栈),要么需要维护复杂的循环不变量,代码量和出错概率都会显著上升。

以二叉树前序遍历为例,递归写法 5 行搞定:

def preorder(root): if not root: return visit(root) preorder(root.left) preorder(root.right)

迭代写法要用栈模拟,至少多写十行,还要注意入栈顺序(先右后左)。在可读性面前,O(log n)的空间开销(平衡树场景)是完全值得的。

5.2 递归的劣势:栈溢出风险

递归的隐藏炸弹是栈溢出(stack overflow)。在系统栈大小一定(通常是 8MB 左右)的情况下,递归深度超过几万层就会崩。Python 默认递归深度上限是 1000,即使你写成无限递归也会被RecursionError拦下来。

所以判断是否使用递归,有个简单的经验法则:

  • 递归深度在几百到几千级别、且代码复杂度和可读性明显优于迭代 → 用递归。
  • 递归深度可能达到几万甚至更多(比如单链表反转、遍历超长链表、处理极大数组的 DFS)→ 优先考虑迭代或显式栈。

在实际工程项目中,很多分布式系统处理的数据规模大到递归深度不可控,这时一般会用迭代 + 显式栈来实现,避免生产环境栈溢出。面试时你如果能主动说出这个权衡,会是一个很加分的点。

5.3 优化手段:备忘录、剪枝、尾递归

在必须用递归但性能不理想的情况下,有三项优化手段:

  1. 备忘录(Memoization):把已经算过的子问题结果缓存,避免重复计算。斐波那契从O(2ⁿ)降到O(n)就是靠这个。空间换时间,适合子问题重叠度高的场景。
  2. 剪枝(Pruning):在递归搜索中提前判断当前分支不可能产生最优解,直接跳过。回溯算法里的大量优化都属于这一类。
  3. 尾递归改写:把递归调用变成尾调用,在支持 TCO 的语言里能省栈空间。但对于不支持的语言,这只是把栈帧里的内容换了一换,深度本身没变,别对内存优化抱太大期待。

6. 实用技巧与速查清单:把复杂度分析变成肌肉记忆

最后分享一些我在实际刷题和写代码时总结的小技巧。这些技巧没法替代上面的理论推导,但能帮你快速定位答案,减少出错概率。

6.1 五分钟快速定位递归复杂度的检查单

拿到一个递归函数,按以下顺序检查:

  1. 画出递归调用形态:是一个子问题(线性递归)还是多个子问题(树形递归)?
  2. 子问题规模怎么变:是n-1还是n/2?这决定了深度是n还是log n。
  3. 每层合并开销是多少:O(1)还是O(n)?这决定了是否会给总时间再乘一项。
  4. 写出递推关系式:只要形态清楚,关系式就是直接翻译。
  5. 求解关系式:优先套主定理,不适用就画递归树,再不行才用代入法猜。
  6. 空间复杂度只看调用栈深度和每层辅助空间:不要被递归树的节点总数带偏。

6.2 面试中常见追问与应对

面试官最喜欢在递归这里连环追问,我把自己遇到过的几个高频追问列出来:

追问一:这个递归是不是尾递归?优化后的空间复杂度是多少?

应对:先回答案判断标准,再看语言支持不支持尾调用优化。比如 Python 即使写出尾递归形式也不省栈,空间复杂度依然是O(n);在支持 TCO 的函数式语言里才是O(1)。

追问二:如果输入数据大到递归栈溢出怎么办?

应对:讲清楚递归深度达到多少会溢出,给出两个方向——一是改成迭代 + 显式栈,二是查一下能不能用备忘录缩小递归深度(比如自顶向下改自底向上)。

追问三:为什么斐波那契递归时间是指数级,空间却是线性级?

应对:从递归树的"宽度"和"深度"两个维度讲。时间消耗看整棵树的节点总数(宽度),空间消耗看从根到叶子的一条路径长度(深度)。树形递归是宽度爆炸、深度线性,所以时间是O(2ⁿ),空间是O(n)。

6.3 高手习惯:先写递推关系式,再写代码

我自己养成了一个习惯:写递归代码之前,先在注释里把递推关系式和空间复杂度写出来。这样做有两个好处:

  • 逼着自己想清楚每个递归调用处理的是哪个子问题,避免出现"无限递归"或"重复计算"的隐藏 bug。
  • 代码 review 时别人一眼就能看到你的复杂度分析,沟通成本低很多。

举个例子,写归并排序前格式大概是这样的:

# T(n) = 2T(n/2) + O(n) # S(n) = O(n) (调用栈深度 O(log n) + 切片辅助空间 O(n))

这种习惯看着小,但对代码质量的影响很大。很多高级工程师写复杂递归逻辑时,靠的就是这一步额外的思考。

6.4 最后一点个人体会

我在实际工作中用过很多次递归,最近一次是处理一个树状的权限配置解析,每个节点下挂着一堆子节点,匹配规则层层嵌套。当时我第一版用的是递归加备忘录,逻辑清晰,几层嵌套读起来毫不费力。后来数据量涨上来了,树的深度极端情况下超过几万层,生产环境直接栈溢出。改写成显式栈之后,代码确实丑了一些,但稳定性明显提升。

这段经历让我对递归有了更实在的认识:递归不是一个"绝对好"或"绝对坏"的工具,而是一个需要你精确计算代价的能力。会写递归只是入门,能算出它的时间复杂度和空间复杂度、知道它在什么数据规模下会出问题、知道什么时候该改用迭代——这才是真正吃透了递归。希望这篇分析能帮你把这一步走扎实。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 11:37:04

GNSS基线解算差异分析:五款软件结果为何相差2.3毫米

简介&#xff1a;本资源是一份面向测绘工程、大地测量及GNSS高精度定位领域从业者与高校研究者的专业技术分析报告&#xff0c;聚焦五款主流GNSS数据处理软件&#xff08;CGO、TBC、SOUTH、国产CGO、RINEX&#xff09;在基线解算中的性能差异与参数优化规律。报告基于真实工程控…

作者头像 李华
网站建设 2026/10/22 11:37:03

Hermes Agent 自动化任务管理:从安装配置到实战避坑指南

1. 从一个真实困境说起&#xff1a;为什么需要 Hermes Agent第一次接触 Hermes Agent 是在一个自动化任务频繁出问题的项目里。当时团队维护着一套跨平台的定时任务系统&#xff0c;每天要处理几十个不同来源的数据抓取、格式转换和消息推送。最初用传统的脚本堆叠方案&#xf…

作者头像 李华
网站建设 2026/10/22 11:36:25

Malleable C2实战解析:HTTP流量伪装与内存隐蔽技术全指南

做攻防对抗的人&#xff0c;绝大部分时间其实不是花在“怎么打进去”&#xff0c;而是花在“怎么让通信不像通信&#xff0c;让文件不像文件”。我最近在复盘一批仿真C2样本时&#xff0c;一个词反复出现&#xff1a;Malleable C2。它看起来像某个具体项目&#xff0c;实际上更…

作者头像 李华
网站建设 2026/10/22 11:36:13

ZIP伪加密识别与修复:用WinHex分析文件头标志位的完整实操指南

在处理压缩包文件时&#xff0c;我们偶尔会遇到一种令人困惑的情况&#xff1a;一个ZIP文件被标记为加密&#xff0c;但用任何密码都能“蒙混过关”——或者说文件其实根本没有真正加密&#xff0c;只是通过修改ZIP文件头部的一个标志位&#xff0c;让它在表面上看起来是加密的…

作者头像 李华
网站建设 2026/10/22 11:35:47

Qt网络编程必备:QUrlQuery从编码原理到实战技巧

1. 从一次URL拼接翻车说起&#xff1a;为什么需要QUrlQuery 如果你写过Qt网络程序&#xff0c;一定经历过这种场景&#xff1a;要构造一个带有查询参数的请求地址&#xff0c;于是拿起 QString 就是一顿拼接—— QString url "https://api.example.com/search?keywo…

作者头像 李华
网站建设 2026/10/22 11:34:17

VulnHub靶机hackNos-2.1实战:Web渗透到Linux提权全记录

1. 开局准备&#xff1a;Host-Only网络下如何挖出靶机真实IP 最近在整理VulnHub上适合练手的Linux靶机时&#xff0c;把0s-hackNos-2.1这台机器完整打了一遍。整体感受是&#xff1a;这台靶机对刚接触渗透测试、想建立完整攻击链路的人非常友好&#xff0c;难度不算高&#xff…

作者头像 李华