资讯动态

PHP数组并集、交集、差集详解:从array_merge到array_diff的实战避坑指南

发布时间:2026/10/10 18:22:10 来源:尧图企业网站定制
平时处理用户权限、商品ID、标签这类数据时PHP数组的并集、交集与差集操作几乎是绕不开的三板斧。你可能已经用过array_merge合并数组、array_intersect找共同值、array_diff剔除多余数据但真到了实际项目里键名规则、比较方式和顺序问题往往会踩出一堆坑。这篇文章我把这三类操作掰开揉碎讲清楚结合我平时在后台管理、数据筛选和接口对接中积累的经验把每个函数的边界条件、典型误区和性能取舍都过一遍适合刚接触PHP数组、或者用了多年但对细节依然含糊的朋友。1. 并集别小看array_merge键名规则才是真正的分水岭1.1 字符串键和数字键合并行为完全不同很多人以为并集就是array_merge把两个数组拼在一起结果拼接时发现“合并”后的数组和预期不一致。其实array_merge对字符串键和数字键的处理是两套逻辑字符串键冲突时后面的值覆盖前面的值数字键不管是否冲突一律按顺序重新索引不会覆盖。$userBase [uid 1001, role admin]; $userExtra [role editor, score 99]; $merged array_merge($userBase, $userExtra); // [uid 1001, role editor, score 99] $tagsA [php, mysql]; $tagsB [php, redis, nginx]; $mergedTags array_merge($tagsA, $tagsB); // [php, mysql, php, redis, nginx]第一个例子中role被后面的editor覆盖了因为字符串键遵循“后声明的赢”。第二个例子全是数字键结果变成了五个元素的索引数组php重复出现并没有自动去重。这里最容易被忽略的是连续数组的场景。如果两个数组都是索引数组array_merge会重新编号比如[10 a, 20 b]和[30 c]合并后是[0 a, 1 b, 2 c]。但如果你是想保留键名的合并比如两个配置数组键名本身有业务含义那么array_merge的重新索引行为就会破坏数据语义。这时候运算符反而更适合。$a $b在键冲突时以$a为准且数字键不会重新索引只会把$b中在$a里不存在的键追加进来。它和array_merge的差别不只是“左边覆盖右边”还是“右边覆盖左边”关键是是否保留原始键。我习惯把理解为“按位补全配置”把array_merge理解为“合并列表数据”两者应用场景完全不同。1.2 去重并没有想象中那么“自动”PHP没有内置的array_union函数所以我们做真正的数学意义上的并集时通常要组合两个函数先用array_merge合并再用array_unique去重。$tagsA [php, mysql, redis]; $tagsB [php, nginx, redis]; $union array_unique(array_merge($tagsA, $tagsB)); // [0 php, 1 mysql, 2 redis, 4 nginx]这里有几个值得注意的点array_unique默认使用字符串比较1和1会被当成同一个值。这在多数业务场景下是合理的但如果你比较的是不同类型的数据要留意这个默认行为。去重后键名保留的是第一次出现时的索引所以php保留的是下标0而不是合并后出现的3。如果想拿到连续索引接着套一层array_values即可。array_unique只处理同一数组内部重复元素它和“比较两个数组”没有任何关系。如果你只是想让两个数组的并集结果里不出现重复项用array_merge array_unique是等价方法但如果你连键名也想一并控制需要先想清楚结果应该是关联数组还是索引数组。我在实际项目中处理标签合并时更多是直接走“键名不冲突”的路子把标签ID作为数组的键然后直接两个数组合并这样天然去重$tagMapA [1 php, 2 mysql]; $tagMapB [2 mysql, 3 redis]; $tagUnion $tagMapA $tagMapB; // 键 2 保留的是 $tagMapA 里的值不会覆盖也天然没有重复这种写法在“主键ID集合”场景里特别高效因为键名本身就是唯一标识省掉了array_unique的额外扫描。1.3 一个实用频率没想象中高的array_merge_recursivearray_merge_recursive的行为和普通array_merge区别在于当两个字符串键相同时它不会用后面的值覆盖前面的值而是把两个值合并成一个数组。$default [theme light, lang [en, zh]]; $custom [theme dark, lang [fr]]; $result array_merge_recursive($default, $custom); // [theme [light, dark], lang [en, zh, fr]]这个函数不常用因为大多数配置合并场景需要的是“覆盖”而不是“堆叠”。什么时候适合呢当你需要把多个来源的同类配置累加到一起比如把多个规则文件里的相同字段汇成规则列表array_merge_recursive就很有用。但要注意它不会对数字键做同样的归并数字键依然是追加并重新索引。也就是说它并不是完全意义上的“递归并集”只是对字符串键的冲突值做数组化。如果嵌套数组很深比如三维甚至四维配置array_merge_recursive的递归层级和最终形状需要实测确认我建议在新项目里先用小样例跑一遍再落业务代码。2. 交集array_intersect家族的四件武器比“共同值”多了一层含义2.1array_intersect最直接的“值出现即可”array_intersect返回第一个数组里那些“值出现在后续所有数组中”的元素返回时保留第一个数组的键名。$a [uid 1, tid 2, gid 3]; $b [100, 1, 300, 3]; $result array_intersect($a, $b); // [uid 1, gid 3]这里有个关键点交集比较的是值不是键名。第一个数组里的tid 2虽然在数组$b里不存在值2所以被剔除了。返回值里键名仍然是uid和gid这通常是好事后续能直接知道匹配的元素来自哪里。不过按值匹配也有它的问题当两个数组分别来自不同数据源时值相同的语义可能完全不同。比如第一个数组里的id是用户自增ID第二个数组里的id是商品ID两边都有123array_intersect也会认为它们是共同点业务上就闹笑话了。所以用array_intersect前要先确认“值的含义一致”这个前提成立。2.2array_intersect_assoc把键名也拉进来如果交集必须同时满足键名相同、值相同那就用array_intersect_assoc。它比较严格适合做“精确匹配”的数据核对。$configA [theme dark, width 100, cache true]; $configB [theme dark, width 200, debug true]; $result array_intersect_assoc($configA, $configB); // [theme dark]width的值一个是 100、一个是 200不匹配cache和debug键名不同只有theme同时满足键和值一致。这个函数在配置对比、接口参数校验里非常实用。比如校验前端传参和本地配置是否有冲突项或者对比两个数据行的字段内容是否变化。但注意它和array_intersect一样仍然是宽松类型比较100和100会被认为是相等的如果要严格区分类型还是得走自定义比较器。2.3array_intersect_key只认键名适合做主键冲突检测有时候我要的不是“值相同”而是“这个键是否同时存在”。比如两组数据一个是数据库查出来的记录集另一个是客户端上报的ID集合我只关心ID有没有交集值是什么不重要。array_intersect_key就是干这个的它只看键名。$dbRows [3 [name A], 7 [name B], 9 [name C]]; $clientIds [3, 9, 11]; $matched array_intersect_key($dbRows, array_flip($clientIds)); // [3 [name A], 9 [name C]]这里我把$clientIds翻转成以ID为键的数组再用array_intersect_key快速筛出dbRows中命中的记录。这是处理主键集合最舒服的姿势语义清晰效率也高因为键名的查找是哈希查找比线性扫描更快。2.4 自定义比较器array_uintersect处理对象和复杂值如果数组里放的是对象、数组或者你想用严格等号而不是宽松等号array_intersect家族帮不上忙得用array_uintersect配合回调函数。$ordersA [order_1, order_2, order_10]; $ordersB [order_1, order_10]; $result array_uintersect($ordersA, $ordersB, fn($a, $b) $a $b);回调返回 0 表示两个值相等是宇宙飞船操作符能同时处理字符串、数字和数组的排序比较。如果是对象比较就在回调里手动指定属性对比规则。我一次真实经历是合并两个第三方平台返回的订单列表两边字段名不同但订单号一样。按订单号取交集直接用array_intersect会误判因为订单号有时带前导零有时不带。我用了array_uintersect回调里先ltrim去掉前导零再才得到准确交集。3. 差集array_diff最容易上手但也最容易搞反方向3.1 值差集的反直觉点保留的是第一个数组的视角array_diff返回第一个数组中有、但后续数组中没有的值。它的反直觉点在于计算结果是“站在第一个数组角度”的补集不是数学上的对称差集。$users [alice, bob, carol]; $blocked [bob]; $available array_diff($users, $blocked); // [0 alice, 2 carol]这个例子很直观从可用用户里剔除被封禁用户alice和carol被保留而且保留了原有下标0和2中间空了一个1。如果你接下来要拿这个数组做循环或者合成 SQL 的IN查询建议用array_values先重排下标否则拼接出来的IN语句没问题但继续做数组运算时可能会有奇奇怪怪的键偏移问题。另一个容易踩坑的地方是array_diff只比较值不比较键。哪怕两个数组的键名完全不同只要某个键对应的值出现在后面数组里它就会被剔除。$a [red #f00, green #0f0]; $b [primary #f00]; $result array_diff($a, $b); // [green #0f0]red #f00虽然键名不同但值#f00在$b中存在所以被去掉。3.2array_diff_assoc值和键一起比过滤才够严当键名本身代表业务含义时比如“用户对某个角色的启用状态”除了值键名也必须参与对比这时可以用array_diff_assoc。$current [uid_1 on, uid_2 off, uid_3 on]; $changed [uid_2 off, uid_3 off]; $diff array_diff_assoc($current, $changed); // [uid_1 on, uid_3 on]这里uid_2 off在两边完全一致被剔除uid_3一个是on一个是off被保留uid_1只出现在第一个数组里被保留。这样一来我就得到了真正“发生变化或者新增”的用户配置而不是把值相等但键不同的元素也误判成变化。做数据同步、冲突检测的时候这个函数比array_diff可靠得多。3.3array_diff_key按主键排除最像数据库操作如果不关心值内容只想根据键名把后面数组中出现过的键全部去掉array_diff_key是首选。我经常把它用在“已经处理过的记录ID”和“全部待处理ID”之间。$pending [ 101 [title task1], 102 [title task2], 103 [title task3], ]; $processed [ 102 [title task2], ]; $unprocessed array_diff_key($pending, $processed); // [101 [title task1], 103 [title task3]]这个操作可以避免手动写一层foreach去逐个查isset代码更短意图也更清晰。需要留神的是传入的$processed数组必须也是以相同键名为索引的数组如果它是一串普通索引数组比如[102, 103]array_diff_key就匹配不上了因为它的键名是0和1。此时要先array_flip($processed)把值转为键名再传入。3.4 方向敏感性——为什么顺序不能乱array_diff($a, $b)和array_diff($b, $a)的结果完全不同。$a [1, 2, 3]; $b [2, 3, 4]; array_diff($a, $b); // [0 1] array_diff($b, $a); // [1 4]第一次计算保留的是“在$a中但不在$b中”的元素结果是1第二次保留的是“在$b中但不在$a中”的元素结果是4。两边合起来才是对称差集[1, 4]。如果你想一次拿到对称差集可以把两个array_diff的结果用array_merge合并起来$onlyA array_diff($a, $b); $onlyB array_diff($b, $a); $symmetric array_values(array_merge($onlyA, $onlyB));注意这里我说了array_values因为两个差集的索引都是从原数组继承的直接合并会出现键名重复或错乱普通业务逻辑里重新索引更安全。4. 大规模数组与混合类型效率和类型陷阱才是重头戏4.1 内置函数用的还是比较宽松的字符串比较PHP 的array_intersect、array_diff在没有指定比较器时实际执行的是字符串方式的宽松比较。也就是说123和123会被视作相等。var_dump(array_intersect([123], [123])); // [0 123]看起来很方便但它也会掩盖数据类型问题。比如一个数组来自数据库 WHERE 条件值是整形另一个数组来自 CSV 导入值是字符串两边值都是123实际内容可能完全不是同一个东西。这种隐藏的类型转换在线上环境比较难排查因为本地测试时数据量小一眼能看出来到了十几万行数据时错误的交集结果往往很难定位。如果你希望严格区分类型用array_intersect和array_diff的带u版本回调里写fn($a, $b) $a $b ? 0 : ($a $b ? -1 : 1)。这样数字和字符串不会互相误判代价是性能差一些并且回调要求比较器满足一致性和传递性否则结果不可预估。4.2 大数据量下的算法改造用哈希思想取代暴力扫描PHP 内置的array_intersect、array_diff性能事实上并不差内部做了排序和哈希优化百万级数据也不会真的两两 O(n^2) 暴力比较。但在特定场景下比如频繁循环里多次调用、或者每次参与运算的数组都很大我更倾向于用array_flip配合isset做手动哈希查找。$targetMap array_flip($targetIds); $filtered array_values(array_filter($sourceRows, fn($row) isset($targetMap[$row[id]])));这样isset的键名查找是 O(1) 级别整体是 O(n m)而屡次调用array_intersect可能会在多轮筛选中浪费不少不必要的时间。我实测过一个导出功能原来在 5 万条记录里循环调用array_intersect单次要十几毫秒改成array_flip后整体耗时降到个位数毫秒。还有一个更隐蔽的性能问题array_intersect和array_diff会把数组元素先转成字符串进行比较。当数组元素是数组、对象这些复杂结构时转换成本极高。所以我建议如果数组元素本身就是复杂的关联数组优先直接用键名做比较或者把每个元素映射成简单标量比如id之后再交给内置函数。这样写出来的代码清晰性能也可控。4.3 空值、重复元素、键名保留三个容易翻车的细节空值在集合运算里经常会出现意外。比如array_intersect([0, 1], [null, ])会把0匹配上因为在宽松比较里0 null为真0 也为真。这会导致你应该得到空交集却拿到了一堆匹配结果。处理含空值数据的标准姿势是先array_filter把空元素清掉再做并集、交集、差集运算或者直接用带u的版本进行严格比较。重复元素是另一个容易翻车的点。array_intersect和array_diff在比较时不会自动去重如果一个数组里有重复值结果会原样保留多个重复项。比如$a [1, 1, 2, 3]; $b [1]; $result array_intersect($a, $b); // [0 1, 1 1]很多人的预期是交集结果里最多出现一个1但 PHP 返回了两个。如果你需要的是数学意义上的集合记得先用array_unique对结果再做一次收缩。键名保留问题前面提到过几次这里统一强调array_intersect系列和array_diff系列都保留第一个数组的键名它们不会帮你重置索引。如果后续要拿到有序列表比如直接传给前端表格组件必须补一个array_values。我见过不少同事就是忘了这一步导致 JSON 输出变成对象而不是数组前端循环逻辑直接崩掉。5. 拿一个真实场景串起来权限管理中的并集交集差集5.1 从“全部权限”和“用户已有权限”算出可分配集合权限管理是集合运算最经典的落地场景。假设系统有一个allPermissions数组存着所有权限编码用户当前拥有userPermissions我需要知道三件事用户拥有哪些有效权限array_intersect用户权限里已经失效的编码array_diff后台还可以新分配给用户的权限array_diff($allPermissions, $userPermissions)$allPermissions [article.view, article.edit, article.delete, user.view, user.edit]; $userPermissions [article.view, user.view, system.config]; $valid array_values(array_intersect($userPermissions, $allPermissions)); $expired array_values(array_diff($userPermissions, $allPermissions)); $assignable array_values(array_diff($allPermissions, $userPermissions));这里有个实际操作经验如果$userPermissions里的权限编码是“从旧版本配置文件读出来的”可能会包含已经被系统移除的编码直接拿去数据库比对容易报错。先用array_intersect把有效权限筛出来再用有效权限集合去写缓存或做逻辑判断能省掉不少异常处理。每次权限变更时还要把旧权限和新权限做差集、对称差集才能生成“新增权限”和“移除权限”两条审计日志。这时前面提到的array_diff_assoc也派得上用场因为权限状态可能是启停状态键相同但值不同也应该算作变更。5.2 结合数据表ID处理并集差集后端接口经常收到前端传过来的一组 ID比如用户勾选的标签 ID。此时数据库里可能还有一批默认标签 ID合并逻辑用并集过滤逻辑用差集。$defaultTagIds [5, 6, 7]; $userTagIds [6, 7, 8]; $allTagIds array_values(array_unique(array_merge($defaultTagIds, $userTagIds))); // [5, 6, 7, 8] $onlyUserTagIds array_values(array_diff($userTagIds, $defaultTagIds)); // [8]如果这个场景再加上“某些标签已被禁用”就差集操作服务端过滤了$disabledTagIds [7]; $enabledAllTagIds array_values(array_diff($allTagIds, $disabledTagIds));这样一套下来数据库查询里只需要一条SELECT * FROM tags WHERE id IN (...)不需要在 SQL 里写麻烦的NOT IN和IN组合条件。PHP 数组运算在这里代替了部分 SQL 逻辑代码可读性反而更高。5.3 组合使用的代码骨架我整理一下自己项目里常用的集合运算骨架可以直接套用function arrayUnion(array $a, array $b): array { return array_values(array_unique(array_merge($a, $b))); } function arrayIntersectByValue(array $a, array $b): array { return array_values(array_intersect($a, $b)); } function arrayDiff(array $a, array $b): array { return array_values(array_diff($a, $b)); }这套函数要注意的是arrayUnion内部用了array_unique当数组元素是数组时会报“Array to string conversion”的警告所以只适合元素为标量的场景应对。如果比较的是复杂结构就换array_udiff和array_uintersect并自定义比较回调。骨架的价值不在代码量而在于固定“第一个数组是谁”的方向约定。项目里多人协作时差集方向最容易搞混我在团队规范里强制要求凡是array_diff调用必须注释“结果是第一个数组减去后续数组的补集”注释虽然啰嗦但确实避免了大量无谓的口水仗。做完这套封装之后我处理类似需求时基本不再裸写array_intersect和array_diff因为裸写的调用点一多键名保留和方向问题就会被反复触发。反而封装成带返回固定格式的工具函数后业务代码干净很多出问题时也只需要在一个地方排查。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑