拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

分形不止是图形:从自相似原理到递归算法与工程应用

分形不止是图形:从自相似原理到递归算法与工程应用

分形这个词,近些年出镜率极高——曼德博集合的绚丽图像、游戏里逼真的程序化植被、电影特效中的山脉与云海,后台好像都藏着它的影子。但如果你去翻定义,看到的往往是一堆“自相似”“分数维”“迭代动力系统”的术语,越解释越玄乎。我刚接触这个概念时也很头疼,直到把一条主线梳理清楚:分形不是在描述某种固定图形,而是在描述一类生成逻辑——用简单的局部规则,在无穷的尺度上重复叠加,最终涌现出复杂结构。这个逻辑一旦打开,再看花菜、海岸线、天线、图像压缩,全都串起来了。这篇文章想讲的就是这套总体逻辑:它从哪来、数学上怎么刻画、代码里怎么实现、真实世界里怎么用。

这篇文章适合两类人:一是只听过“分形”这个名字、想系统弄明白它核心脉络的朋友;二是已经会写几行递归代码,但知识比较零散,想把点连成线的开发者或设计师。我会尽量少堆公式,每个概念都配上直觉解释、可运行的代码和踩坑提醒。

1. 分形到底在讲什么:自相似性与标度

1.1 自相似性不是“长得像”那么简单

分形的第一块基石是“自相似性”,但这里的相似和日常说的“长得像”不一样。俄罗斯套娃是自相似的最直观例子:打开一个,里面还有一个小一号的自己,再打开,还有更小的。罗马花椰菜更典型——掰下一朵,外形几乎就是整棵的缩小版;把那一朵再掰开,又得到一个小一号的版本。这种“局部缩放后与整体结构一致”的性质,就是自相似。

严格来说,分形里的自相似分两种。一种是精确自相似,比如科赫雪花、谢尔宾斯基三角形,任何一小块放大之后,和整体完全一样,这种主要在数学构造里出现。另一种是统计自相似,也叫统计意义上的分形,自然界的山脉、云层、血管、树冠都属这一类:局部和整体在统计特征上相似,但具体细节并不完全复制。你去看一片真实蕨叶,每个小叶片的形状虽然接近整株,但不会一模一样,角度、长度都有波动。理解这个区别很重要,因为很多人在自然里找不到“精确自相似”就以为分形是骗人的,其实是把两种自相似混淆了。

为什么这个性质重要?因为它提供了复杂度的一个来源:不需要每一条褶皱都单独设计,只需要定义一套规则,然后在不同尺度上持续重复,就能得到细节丰富、观感自然的形态。换句话说,分形是用少量信息压缩了大量视觉复杂度。

1.2 一个经典难题:海岸线的长度为什么测不准

理解分形起源最好的入口,是曼德博1967年那篇论文《英国的海岸线有多长》。这个问题看起来无聊,细想却很吊诡。你用一把1公里的尺子去量海岸线,得到长度A;换一把100米的尺子,得到的长度B会比A明显更大;再换10米的尺子,结果又涨一截。尺子越小,能塞进海湾和岬角的细节就越多,测出来的总长度就越长。理论上,尺子无限小,海岸线长度就趋于无穷。

为什么会这样?因为海岸线的形状在多个尺度上都有细节,它不是一条光滑曲线,而是由不同尺度的锯齿叠加而成。曼德博在这个问题上引入了一个关键视角:不要问海岸线的“真实长度”是多少,而要看“长度如何随测量尺度变化”。如果满足关系:

L(s) = K × s^(1-D)

其中s是尺子长度,K是常数,D就是这条海岸线的分形维数。这条关系式说明,长度随尺子变小按幂律增长,增长的快慢由D决定。D越接近1,曲线越光滑;D越接近2,曲线越曲折、越接近铺满平面。真实海岸线的D通常在1.2到1.5之间。

这个思想是分形概念的总体逻辑里最关键的一环:当常规测量量(长度、面积)变得不稳定或趋于无穷时,换一个视角,去观察物理量随尺度的变化关系,反而能得到稳定的特征值。后面讲分形维数、讲信号处理中的分形特征,全都是沿这个思路展开的。

2. 分形的数学内核:迭代、维数与混沌

2.1 复杂从哪来:迭代产生的自反馈

分形的第二个基石是“迭代”。所谓迭代,就是把上一次的结果重新作为输入,下一次运算重复同样的规则。小学玩过的“按计算器把一个数不断乘自己”,就是一种迭代。关键在于:只要规则里带有非线性,反复反馈就可能产生极其复杂的行为。

最著名的例子是曼德博集合。它定义在复平面上,考察复数的迭代:

z_(n+1) = z_n² + c

给定一个复数c,从z_0 = 0开始反复迭代。如果z_n的模长始终不超过某个界限(即不发散到无穷),就认为c属于曼德博集合;如果迭代若干次后就爆炸了,c就不属于集合。把所有属于集合的点画出来,得到那个经典的“黑色肾形+无数细小突起”图形。

这里最令人震撼的地方在于:生成规则的数学表达式只有一行,但边界处的细节无穷无尽,放大多少倍都有新结构。这就是“简单规则产生复杂涌现”的典型证明。对程序员来说,这条逻辑特别熟悉——一个几行的递归函数可以生成整棵结构复杂的树,和这个本质完全一致。

需要提醒的是,实际渲染曼德博集合时,不可能真的迭代无穷次,只能设置一个最大迭代次数阈值,判断“到这一步还没发散就当成收敛”。这个阈值的选取直接影响成图质量,后面在避坑部分会细讲。

2.2 分形维数:用对数定义“有多曲折”

普通几何里,维度只能是整数:直线是1维、平面是2维、空间是3维。可海岸线这种曲线,你说它是1维,它细看又似乎占了一些面积;你说它是2维,它又没有填满平面。分形维数(也叫豪斯多夫维数、计盒维数)就是用来描述这种“介于两个整数维度之间”的状态。

怎么理解维数的计算?看缩放关系:一条直线放大2倍,长度变成原来的2倍,2^1 = 2,指数就是维数1;一个正方形放大2倍,面积是原来的4倍,2² = 4,维数2;一个立方体放大2倍,体积是原来的8倍,2³ = 8,维数3。

反过来,如果某个对象放大m倍后,结构数量变成原来的N倍,那么它的维数D满足:

D = log(N) / log(m)

以科赫雪花为例:它的生成方式是把每条线段中间1/3替换成等边三角形的两条边。每次迭代后,线段数量变成原来的4倍,同时整体长度放大为原来的3倍。所以科赫雪花的维数是:

D = log(4) / log(3) ≈ 1.26

这个1.26非常直观:它比普通曲线(维数1)更曲折,但又远没到铺满平面(维数2)。分形维数就是这样一个介于整数之间的实数,刻画对象在不同尺度上的“填充能力”。

实际应用中很少靠解析公式算维数,更多是用计盒维数法(box counting):把图形用不同边长的方格覆盖,统计至少包含一个图形点的格子数N(ε),然后对log(N(ε))和log(1/ε)做线性拟合,斜率就是维数的估计值。这个方法对图像、信号、点云都能用,是工程场景里最省事的量化手段。

2.3 分形与混沌:一对孪生兄弟

分形和混沌常被放在一起讲。混沌指系统对初始条件极度敏感(所谓“蝴蝶效应”),轨道看似随机,整体却有结构;分形则常常是这种混沌系统在几何空间里留下的“指纹”。比如洛伦兹吸引子,它由三个常微分方程迭代生成,轨迹永不自交、永不确定,但画出来的三维曲线有一种明确的蝴蝶状结构,这种结构本身就具有分形特征。

对咱们实操的人来说,不需要啃透混沌理论,但理解“迭代可能产生混沌,混沌的几何痕迹往往呈分形”这一点就够了。它解释了为什么分形能出现在天气预报、流体、股票波动这些看起来毫无共同点的领域——因为底层都是动力学系统的迭代反馈,而反馈系统的长期行为天然容易产生自相似结构。

3. 动手生成分形:从代码到可复现实验

3.1 工具选型:Python 是最省心的入口

分形实现方式很多,可以用Shader在GPU上实时渲染,可以用Processing写交互装置,也可以用JavaScript在网页里跑。但对大多数想理解原理的人来说,我推荐Python + matplotlib + numpy。理由很简单:生态成熟、语法直白、能把“算法逻辑”和“视觉呈现”之间的映射关系看得最清楚。matplotlib画线画点足够,numpy处理大规模采样点足够快。

环境准备也不复杂,创建一个虚拟环境,安装两个包就行:

python -m venv fractal_env source fractal_env/bin/activate # Windows下是 fractal_env\Scripts\activate pip install matplotlib numpy

不想折腾虚拟环境的话,直接pip install到用户目录也能跑,只是养成隔离环境的习惯在后续做实验时会省很多麻烦。

3.2 第一步:用递归函数画一棵分形树

分形树是理解“递归 + 自相似”的最佳起点。规则很简单:从树干出发,到达顶部后分成两个子枝,子枝长度按比例缩短,角度分别偏左偏右,对每个子枝重复这个过程。伪代码只有几行,但运行结果却非常接近真实树木的拓扑结构。

import matplotlib.pyplot as plt import numpy as np def draw_branch(ax, x, y, angle, length, depth, shrink=0.7, spread=30): if depth == 0: return # 计算当前分支终点 x2 = x + length * np.cos(np.radians(angle)) y2 = y + length * np.sin(np.radians(angle)) # 画树干 linewidth = depth * 0.8 ax.plot([x, x2], [y, y2], color='saddlebrown', linewidth=linewidth) # 左右两个子分支,角度偏移 draw_branch(ax, x2, y2, angle - spread, length * shrink, depth - 1, shrink, spread) draw_branch(ax, x2, y2, angle + spread, length * shrink, depth - 1, shrink, spread) fig, ax = plt.subplots(figsize=(6, 6)) ax.axis('off') draw_branch(ax, 0, 0, 90, 1.0, 10) plt.show()

这里有两个关键参数值得玩味。一个是shrink(长度收缩比),取0.7意味着每一层子枝比父枝短30%。它必须小于1,否则整棵树会在有限区域内向外爆炸式延伸,永远出不了图。另一个是spread(分叉角度),取30度左右时树形最自然,过小会挤成一束,过大会扁平成扫帚。参数的具体取值没有绝对标准,但理解它的边界条件比背参数更重要:任何递归绘图都要保证“整体空间有限”,否则深度稍大就会数值溢出或者图形飞掉。

递归深度的选择也直接影响结果。深度8时大约有2^7=128条末端枝,10层就是512条,15层就到上万条。我建议家用机器上做静态图控制在10到12层,既能体现细节密度,又不会让渲染等待过久。

3.3 用L系统生成植物形态

L系统(Lindenmayer System)是分形生成里另一个强大的工具。它把生长规则抽象成字符串重写:一条公理(初始字符串),若干条替换规则,然后反复把字符串里的字符替换成规则定义的新字符串。规则迭代完成后再把字符串“翻译”成绘图指令。

一个经典的“分形树”L系统定义如下:

公理: F 规则: F -> F[+F][-F]F

其中F表示向前画线,+表示向左转30度,-表示向右转30度,[表示保存当前状态,]表示恢复状态。把这条规则迭代5次,字符串会指数级膨胀,翻译出来就是一棵层次分明的树。

Python里用turtle或者matplotlib都能解释这套指令,核心模式是维护一个栈:

import turtle def draw_lsystem(axiom, rules, steps, angle, size=10): s = axiom for _ in range(steps): s = ''.join(rules.get(ch, ch) for ch in s) stack = [] t = turtle.Turtle() t.speed(0) t.left(90) for ch in s: if ch == 'F': t.forward(size) elif ch == '+': t.right(angle) elif ch == '-': t.left(angle) elif ch == '[': stack.append((t.position(), t.heading())) elif ch == ']': pos, heading = stack.pop() t.penup() t.goto(pos) t.setheading(heading) t.pendown() turtle.done()

L系统真正的威力在于:替换规则的微小改动会带来形态上的巨大差异,所以植物学界用它模拟不同物种的生长模式。对图形学从业者来说,L系统是生成植被外观时灵活性很高的方案,配合随机化参数还能得到形态各异但风格一致的植株群。

3.4 迭代函数系统(IFS):巴恩斯利蕨

另一种截然不同的分形生成方式是迭代函数系统,体现了“用随机游走绘制确定结构”的逆向思维。巴恩斯利蕨是经典案例:用四组仿射变换描述蕨叶各部分的关系,每组变换把点(x, y)映射到新位置,每个变换按一定概率被选中。

四组变换的参数如下:

编号abcdef概率含义
10.000.000.000.160.000.000.01茎干
20.850.04-0.040.850.001.600.85逐级向上的小叶
30.20-0.260.230.220.001.600.07左侧大叶
4-0.150.280.260.240.000.440.07右侧大叶

实现时,从一个任意初始点出发,每次按概率随机选一个变换作用于当前点,把新点画到图上,重复数万次。点数足够多时,蕨叶的整体轮廓自然浮现。

import numpy as np import matplotlib.pyplot as plt transforms = [ (0.00, 0.00, 0.00, 0.16, 0.00, 0.00), (0.85, 0.04, -0.04, 0.85, 0.00, 1.60), (0.20, -0.26, 0.23, 0.22, 0.00, 1.60), (-0.15, 0.28, 0.26, 0.24, 0.00, 0.44), ] probabilities = [0.01, 0.85, 0.07, 0.07] points = [] x, y = 0.0, 0.0 rng = np.random.default_rng(42) for _ in range(30000): r = rng.random() if r < 0.01: idx = 0 elif r < 0.86: idx = 1 elif r < 0.93: idx = 2 else: idx = 3 a, b, c, d, e, f = transforms[idx] x, y = a*x + b*y + e, c*x + d*y + f points.append((x, y)) xs, ys = zip(*points) plt.figure(figsize=(6, 8)) plt.scatter(xs, ys, s=0.1, c='green') plt.axis('off') plt.show()

IFS背后的“拼贴定理”给了这种随机绘制一个理论基础,但实操中你更需要的其实是经验:概率越大,对应变换产生的点密度越高,形态的“主骨架”越强。如果你想调节生成效果,优先调概率阈值,而不是手调变换矩阵,因为后者牵一发而动全身。

3.5 迈向程序化地形:分形噪声

从分形树到IFS,覆盖的是“形态重复”这一分支;而自然地形这类“随机自相似”场景,需要另一套思路——分形噪声。最朴素的做法是“中点位移法”:从一条线段开始,取中点,在中点坐标上加上一个随机偏移量,偏移量与当前尺度成正比;然后对两条子线段重复操作。尺度越小,偏移越小,最终得到的折线在宏观和微观两个层面都起伏自然,形成模拟山脉轮廓的曲线。

扩展到二维就是菱形-方形算法,可以生成高度场。这类算法在游戏程序化生成里用得极多:控制初始粗糙度参数,就能在“平滑丘陵”和“锋利山地”之间切换。分形噪声和之前几种生成方式的区别在于:这里不只是自相似,还叠加了随机性,是“统计自相似”的工程落地。

4. 分形的真实用武之地:应用场景底层逻辑

4.1 计算机图形学与游戏开发

分形在图形学里最大的价值,是“低成本制造无穷细节”。游戏中的树木、草、灌木如果每个都用美术手工建模,资源量不可想象;而用L系统或递归规则生成,程序只需要存储规则和几个参数,渲染时现场展开,就能获得形态丰富、贴近自然的植被。程序化地形也是同理,分形噪声生成了大片地形的高度数据。

这里有个容易被忽略的点:分形生成不只解决“造形”,还解决了“细节层次(LOD)”。离摄像机远时减少递归深度,只画大致轮廓;离得近时提高深度,补充细枝末节,性能和画面质量都兼顾。递归天然适合做这种“可缩放复杂度”的任务,这正是分形逻辑和工程需求的一次完美契合。

4.2 信号医学与图像分析

分形维数作为一个统计特征,在信号处理和医学图像分析里很实用。病灶组织的边缘往往比正常组织更粗糙,用计盒维数测量MRI图像中特定区域的复杂度,就可以作为辅助诊断特征。骨小梁结构的影像、肺部支气管的形态、眼底血管的分支,都被研究过用分形维数做量化。

做这类应用时,一定要清楚:分形维数不是“这个区域有多病”的直接指标,它只是一个特征维度,需要结合其它特征一起做分类或回归。常见的坑是图像分辨率不足,导致测出来的维数在标度区间内根本不合理,这个在下一章避坑部分会细说。

4.3 天线设计与建筑结构

工程领域里,分形天线是知名度最高的应用之一。科赫雪花曲线这样的分形结构作为天线时,可以在更小的物理尺寸里获得类似多频段的谐振特性,因为分形结构的自相似性会在不同频率下重复出现相似的电学行为。超材料与吸波结构也常利用分形图案设计多频段响应,本质是对“尺度选择性”的人为控制。

建筑领域的分形则更偏视觉与结构逻辑。很多高层建筑的外立面分格、窗墙比节奏都带有自相似分级的秩序,让建筑在不同距离上都有层次感。结构工程里,分形还被用来设计轻量高强度的桁架和网格壳,利用自相似分层的传力路径实现材料的高效利用。这里的核心逻辑依然是:通过尺度上的自我重复,在局部规则和整体性能之间建立一种可设计的关系。

4.4 分形是一种思维工具:从局部规则到全局涌现

抛开具体技术,分形最有价值的部分是它提供了一套组织复杂系统的思维方式。软件设计里的组件复用、递归遍历、CSS里响应式设计随视口宽度缩放,都和分形的“尺度自我重复”逻辑有神似之处。你不需要依赖一个全局规划去手写每一个细节,而是定义好局部规则和反馈机制,让整体结构在重复中自发生成。

这种思维在日常工作中很实用。当你面对一个层级复杂、细节繁多的系统,第一反应不该是“把所有特殊情况都列出来”,而是思考“有没有一组统一的规则,能在不同尺度上自行生成应有的复杂度”。这和“用算法生成而非手绘所有细节”是同一件事,我认为这才是分形概念总体逻辑里最值得迁移到其它领域的部分。

5. 分形实验中的常见问题与避坑

5.1 递归深度不是越大越好

很多新手第一次调递归树,上来就设深度20,结果程序卡死或者内存爆炸。原因是线段数量随深度指数增长:一棵二叉分形树深度为d时,末端枝数量是2^(d-1)。深度20意味着超过50万条线段,再加上中间交互状态,matplotlib一次要画几百万个对象,自然扛不住。

我的建议:静态非交互实验控制在8到12层;需要更大深度时,改用numpy批量生成线段数组再一次性plot,而不是逐条调用ax.plot;或者引入“达到最小长度就停止”的终止条件,而不是死磕固定深度。后者更接近真实物理:细节在肉眼不可见时就不再重要,可以直接截断。

5.2 分形维数“算不准”的三个根源

用计盒维数给真实数据估维数时,结果经常忽高忽低,可能的原因有三个:一是数据点太少或图像分辨率太低,精细尺度的网格里很多格子是空的,曲线尾部塌陷;二是噪声太多,把原本光滑的结构干扰成“伪粗糙”;三是标度区间选择不当,用太细或太粗的网格拟合,得到的是噪声或宏观趋势,不是自相似区间。

实操建议是:改变网格最小边长和最大边长的跨度,画出log-log曲线,找到线性良好的中间段,只在那个区间做线性拟合。如果曲线中段弯曲、没有稳定直线,说明数据本身不具备分形特征,不要硬套维数。这个“先看标度区间,再报维数”的流程,是分形应用里最有价值的工程习惯。

5.3 曼德博集合渲染为何总是“糊”或“脏”

用逃逸时间算法画曼德博集合时,最常见的两个参数错误。第一是最大迭代次数太小,比如只有50,集合边界收敛判定不充分,边界区域会被错误画成内部颜色,整体显得糊成一片。通常渲染清晰图像需要几百到上千次迭代。第二是逃逸半径设置不当,半径太小时发散判定过早,边界细节丢失;太大则纯属浪费计算量。常用逃逸半径是2,因为曼德博集合内所有点在某次迭代后模长都不会超过2,超过2必发散。

想得到丝滑的渐变色,关键是不要用“发散时的迭代次数n”直接映射颜色,而是用规范化迭代次数做平滑过渡,细节会柔和很多。这个优化虽然只差几行数学,观感差异却是天壤之别。

5.4 “分形=混乱”是误解

还有一个认知上的坑。分形图形看起来纷繁复杂,但它绝不是无规律可言的随机涂鸦。恰恰相反,分形是“由明确规则通过迭代产生的复杂结构”,它的每一个层级的形态都服从同一套生成逻辑。随机性只是在生成过程中加入了噪声扰动(比如分形噪声地形),但底层框架仍是确定的规则。理解了这一点,你在看分形时就会有完全不同的问题意识:不是“它为什么这么乱”,而是“它是由什么规则、经过几次迭代生成的”。

结尾

分形概念对我来说,最迷人的不是那些炫目的图,而是它在很多领域反复印证同一个道理:复杂可以从简单规则中涌现,尺度之间的关系可以被量化。你在递归画树时学的不是画树,而是如何用局部规则组织整体复杂度;你在测海岸线维数时学的也不是地理测量,而是如何在不确定的尺度里寻找稳定特征。

最后分享两个我常用的小技巧:第一,做分形实验时不要一次性追求大而全,先从深度3的树、5000个IFS点开始,确认逻辑无误,再逐步增加复杂度,能节省大量调试时间。第二,看到一张分形图,先问一句“它的自相似体现在哪个尺度区间、生成规则是什么”,这比记住任何公式都更能帮你真正理解它。希望这篇梳理能帮你也把分形的散点知识串成一条能落地的线。

返回列表