资讯详情

LeetCode 1333:过滤+排序题型通解,轻松搞定餐厅筛选

📅 2026/9/24 19:02:55 | 华诺云谱 👁 阅读
LeetCode 1333:过滤+排序题型通解,轻松搞定餐厅筛选
1. 先看懂题目到底在考什么过滤排序的流程题LeetCode 1333这道题光看标题Filter Restaurants by Vegan-Friendly, Price and Distance容易觉得是道菜谱题实际上它是非常典型的按条件筛选 按规则排序的流程题。我当年刷这道题的时候第一反应是这不就是SQL里WHERE加ORDER BY吗后来仔细一想它比SQL多了一个自定义排序规则的细节这才是真正值得琢磨的地方。先看题目给的原始数据。每个餐厅有五个字段分别是id、rating评分、veganFriendly是否提供素食友好选项、price人均价格、distance距离。然后题目会给三个过滤条件veganFriendly1表示只筛选素食友好餐厅0表示不过滤、maxPrice最大可接受价格、maxDistance最大可接受距离。最终要返回符合条件的餐厅id列表排序规则是先按rating降序rating相同的按id降序。这道题标的是Medium但我个人觉得它本质上是一道Easy题。它不是考你多复杂的算法而是考你能不能把题目描述准确无误地翻译成代码逻辑尤其要注意题目里两个一字之差的坑一个是veganFriendly这个过滤条件的0和1语义一个是排序时rating同等比id的稳定排序问题。用生活场景来类比一下。你打开外卖App筛掉不送外卖的店筛掉价格超过心理价位的店筛掉距离太远的店剩下的按评分高到低排评分一样的按最近上新的排。就这么个事但LeetCode希望大家用代码把这套筛选排序的业务逻辑写清楚。理解到这一层这道题就已经解决了一半。说白了这是一道业务模拟题考察的不是算法思维而是工程上的流程完整性和边界意识。你写代码的时候每一步都要能回答为什么这么做这样才能保证换一种语言、换一套写法都不会出错。2. 核心考点不止排序三个条件藏着多少边界问题很多人在LeetCode评论区说这道题是无脑题排序就完了。但真正动手写的时候会遇到一堆小问题。我把题目做结构化拆解之后整理出了三个值得反复确认的考点。2.1 veganFriendly为0时的语义陷阱题目的veganFriendly参数有个特殊设计当它等于1时只返回veganFriendly字段为1的餐厅当它等于0时不做任何过滤素食餐厅和非素食餐厅都可以出现在结果里。这个设计本身很合理但新手特别容易写成餐厅的veganFriendly字段等于传入的veganFriendly参数这种严格相等判断结果当传入0时把所有非素食餐厅返回了素食餐厅反倒被过滤掉了。这是个非常经典的参数意图理解问题。参数是用户的需求字段是餐厅的属性它俩并不是一一对应的关系。比如用户不Care是否素食这时候你把不Care写成只要非素食逻辑就完全拧了。我在代码里遇到这类开关型过滤条件时通常会这样处理先判断过滤开关是否开启只有在开启的情况下才做字段匹配。这样既保留了过滤能力又避免了0不限制这种语义被误读。同样的技巧在写电商筛选、订单状态过滤时也特别常见。2.2 价格和距离的边界比较maxPrice和maxDistance这两个参数用的是less than or equal to也就是小于等于。这意味着边界值是要被包含进来的。比如maxPrice是50那price字段正好等于50的餐厅必须通过筛选。这个细节看着微不足道但确实有相当一部分人会写成严格小于导致边界案例挂掉。LeetCode的判定数据里几乎一定会出现边界等于的情况所以我刷题时养成的一个习惯是所有比较类条件先看题目是less than还是less than or equal to宁可多写一个等号也不要漏掉边界。2.3 排序规则的优先级题目要求先按rating降序rating相同的按id降序。注意是id降序不是id升序。这个点看起来简单但真实场景中餐厅id越大往往代表越新入驻的店评分相同的时候优先展示新店符合业务直觉。实现层面有两种思路一是直接调用排序函数传入自定义比较器二是利用复合键排序。从代码可读性角度看我推荐在对象上先算好综合排序值再对排序值降序排列这样后续要调整排序权重也方便。不过两种都能AC选顺手的那种即可。3. 两种主流解法对比从跑通到优雅这道题在LeetCode的Accepted解里主流的写法大概可以分成两种一种是先过滤后排序一种是排序时根据条件跳过不符合项。两种思路都能通过但代码的清晰度和可扩展性差别挺大。3.1 解法一先过滤后排序推荐这个解法很直观先把餐厅列表里不符合条件的餐厅去掉得到一个过滤后的候选列表再对这个候选列表按题目规则排序。整个过程拆成两个阶段每个阶段只做一件事思路非常清晰。我用JavaScript写了一个版本如下var filterRestaurants function(restaurants, veganFriendly, maxPrice, maxDistance) { const filtered restaurants.filter(([id, rating, vegan, price, distance]) { if (veganFriendly 1 vegan ! 1) return false; if (price maxPrice) return false; if (distance maxDistance) return false; return true; }); filtered.sort((a, b) { if (a[1] ! b[1]) return b[1] - a[1]; return b[0] - a[0]; }); return filtered.map(item item[0]); };这里的filter里用了数组解构直接把每个餐厅的五个字段解出来名字起得清晰每个判断条件的含义一目了然。先比较rating如果rating不同就直接按rating降序rating相同再比较id按id降序。最后用map把id取出来返回。这个实现的时间复杂度是O(n log n)主要消耗在排序上空间复杂度是O(n)因为filter会生成一个新数组。对于LeetCode的输入规模这个复杂度完全够用。3.2 解法二一次遍历构造候选后排序如果你不想用filter也可以在一次遍历里收集符合条件的餐厅id及排序信息然后排序。这种方式和第一种本质上一样只是把过滤和提取字段合在了一起。class Solution: def filterRestaurants(self, restaurants: List[List[int]], veganFriendly: int, maxPrice: int, maxDistance: int) - List[int]: candidates [] for r in restaurants: rid, rating, vegan, price, distance r if veganFriendly 1 and vegan ! 1: continue if price maxPrice or distance maxDistance: continue candidates.append((rating, rid)) candidates.sort(keylambda x: (-x[0], -x[1])) return [rid for _, rid in candidates]Python版我用的是构造二元组key函数的做法。key返回一个元组(-rating, -id)这样sort默认升序就等价于rating降序、id降序。写起来比较简洁而且不容易出错因为Python的元组比较天然支持多级排序。两种解法在LeetCode上跑都是几十毫秒的事单选题没必要过度优化性能真正值得在意的是代码是否容易让下一个人看懂。我在做Code Review的时候最怕看到的就是能跑但谁也看不懂的代码。这道题用第一种方式写几乎不需要注释谁来看都知道在干什么。3.3 两种方案的取舍建议如果你的编程习惯偏函数式用filter sort更舒适如果偏命令式用for循环收集候选也更自然。但无论是哪种都不要把过滤条件直接写到sort的比较器里那样会让排序函数的职责不纯粹后续要调整规则会很痛苦。举个反例有人会写成下面这样restaurants.sort((a, b) { if (a[3] maxPrice) return 1; // ... 一堆过滤逻辑混在排序里 });这种写法最大的问题在于过滤是要不要这个元素的问题排序是这个元素排在哪的问题把两个问题混在一起代码的意图就被稀释了。将来如果过滤条件变了你得去改排序函数的内部逻辑很容易改出隐藏Bug。4. 我踩过的坑排序比较器里有两个隐藏的规则死角刷这道题的时候我第一次提交其实没有一次通过。查了半天才发现自己栽在了两个规则死角上这里展开说说希望大家别重复踩。4.1 只按rating排序漏了id降序我第一次写排序的时候图省事只比较了rating写完自信满满地提交结果在某个测试用例上挂了。那个用例里有两家餐厅评分一样但id不同预期结果里id大的排前面而我按原顺序返回导致顺序不一致。这个坑属于读题不仔细。LeetCode这类题凡是排序规则里写了如果……相同则按……排序基本就意味着排序规则是复合的。你要做的是把比较器补完整而不是赌测试用例不会出现评分相同的场景。后来我给自己定了个规矩只要题目排序规则里出现并列、相同、then之类的字眼一律改成多级比较绝不偷懒。类似的题还有按分数排序、学生按总分和学号排序等套路一模一样。4.2 比较器里用a[1] - b[1]还是b[1] - a[1]搞反JavaScript的Array.prototype.sort方法如果不传比较器默认会把元素转成字符串按字典序排序这显然不是我们想要的。传入比较器后规定返回值大于0时a排在b后小于0时a排在b前。于是排序规则rating降序对应的就是b[1] - a[1]或直接return b[1] - a[1]。这个符号逻辑我经常搞混。后来我总结了一个简单粗暴的记忆方法你希望大的排前面就让后面的减前面的返回b[1] - a[1]。如果你希望小的排前面就返回a[1] - b[1]。每次写完比较器我都会用一两组实际数据在脑子里过一遍排序流程确认没有把方向搞反。Python里的sorted和list.sort默认都是升序配合reverse参数或者负号key逻辑上更直观一些。但如果用sorted(..., reverseTrue)就要注意它会同时翻转所有比较维度如果还想在某个维度上升序就会比较麻烦这时候用负号key反而更好控制。4.3 把id降序理解成越晚加入的排越前的直觉陷阱还有一个大家不太注意的是id降序到底意味着什么。题目没明说id和入驻时间的关系但既然要求id降序你就照做别自己脑补id越小越靠前。这种脑补在工程里是做需求的大忌——需求怎么说代码就怎么写不要在需求之上再加自己的假设。当然如果实际业务中确实需要一套不同的排序逻辑那时再改不迟。5. 从这道题延伸出去LeetCode过滤排序题型的通解思路这道1333刷完之后我对LeetCode里一大类场景模拟过滤排序题产生了很强的亲切感。这类题包括但不限于按条件筛选航班、筛选电影、筛选歌曲、按综合评分排序等。它们的核心骨架几乎一模一样只要能总结出一套通用套路再遇到类似题基本就是套模板。5.1 通解四步法遇到给定一批对象每个对象有若干属性按若干条件过滤再按某个规则排序这种题我一般按以下四步走读题把过滤条件和排序规则分别拆出来用列表写清楚。切忌混在一起。判断过滤条件里有没有开关型参数比如这里的veganFriendly0代表不过滤。有的话单独处理。写过滤逻辑时优先用continue或return false来跳过不符合项尽量让过滤代码独立成一个函数或一个块。排序时根据题目要求设计多级比较器务必覆盖主排序键相同时的次级排序键。这套流程我不仅用在做LeetCode上日常写代码处理表格数据、做数据分析筛选时也在用。你会发现LeetCode很多题的本质其实是把工作里常见的数据处理流水线浓缩成了一个小题目。5.2 与LeetCode 994腐烂橘子的对比有人可能好奇为什么这道题的热搜词里同时出现了LeetCode 994 腐烂的橘子。因为两道题虽然都是Medium但考察方向完全不一样。994腐烂的橘子考的是多源BFS的遍历次序它更依赖状态变化的先后顺序而1333考的是过滤和排序它本质上不依赖元素间的相对位置变化顺序只依赖属性值。明白这个区别后你在选题的时候就能更快判断一道题值不值得花时间。如果你正在练BFS去刷994如果你正在练自定义排序和过滤逻辑1333是个不错的起点刷完还可以顺便刷按身高排序 根据身高重建队列这类同门题把复合排序这个点练透。5.3 给刷题新手的一个建议说句题外话。很多人刷LeetCode喜欢按题号顺序刷从1刷到1333这其实效率很低。我建议把题按题型标签来刷比如排序类过滤类双指针类动规类每个标签选三五道经典题集中突破。1333就是过滤排序类里很好的入门题标着Medium但实际难度偏低适合拿来建立信心。刷题不是比数量而是比每类题你是否能形成稳定的解题框架。有框架的人遇到新题哪怕没见过也能一步步推理出来没框架的人只能靠背题一旦题目换个包装就抓瞎。这道1333作为框架养成题非常合适因为它把拆解条件、设计比较器、完成映射三个能力一次全练到了。6. 手写代码的完整过程从出声思考到提交通过我想再花一段篇幅完整还原一下我拿到这道题之后从读题到提交的全过程包括中间那些心里的小嘀咕。过程比结果重要因为LeetCode刷题学会如何思考比记住标准答案更有价值。6.1 第一步手动模拟一遍样例拿到题目后我没有立刻开写。我先把题目给的样例数据在纸上手动算了一遍哪些餐厅会被过滤掉剩下的餐厅按rating排完是什么顺序rating相同的怎么排。这个过程很笨但极其有效——它帮我确认了我对题意没有理解偏。LeetCode的题目描述有时会写得比较绕尤其是这种带多个过滤参数的题。手动模拟一遍相当于用真实数据校验你的理解比自己埋头写代码然后靠编译器反馈要快得多。6.2 第二步先搭骨架再抠细节我的习惯是先把大框架写出来也就是过滤、排序、取id三步先把函数壳子搭好。骨架就位之后再往里面填过滤条件和排序逻辑。每一步的代码尽量保持短小方便中间打印调试。比如我写JavaScript版的时候先在过滤块里console.log输出过滤后的数组确认过滤结果正确再写排序。排序后再打印一次确认顺序正确。分阶段验证比写完一大坨再一起调要舒服得多。6.3 第三步边界条件自测写完代码后我会自己在脑子里构造几个特殊测试用例所有餐厅都不满足条件时应该返回空数组。只有一个餐厅满足条件时返回这个餐厅的id。rating完全相同、id不同时id降序是否正确。veganFriendly参数为0时素食和非素食餐厅都要保留。price或distance恰好等于最大值时餐厅应保留。这些用例不需要真跑去LeetCode提交心里过一遍或者本地跑一遍都行。能把这几个边界想清楚这道题基本就拿下了。LeetCode判定虽然严格但大多逃不出这些常规边界。6.4 第四步提交并对照最优解我提交通过后还会去看一眼讨论区的最优解跟自己的代码做对比。有几次对比真能学到不少东西比如有人用tuple做多级排序代码比我的简短很多我顺手就记下来了也有人的代码虽然短但可读性差我看了之后会更加确定自己清晰优先的选择是对的。这种对照习惯是我刷题后期提升最快的方法之一。7. 实际业务中类似的餐厅过滤需求长什么样如果只看LeetCode1333很容易被当成一道纯刷题用的题目。但这道题的原型在真实业务里比比皆是比如外卖App的筛选排序、酒店预订的价格距离筛选、招聘网站的职位过滤、二手交易平台的商品筛选核心逻辑几乎一模一样。这里我拿外卖App举例说说生产环境里要怎么把1333的思维落地。7.1 用SQL表达这道题的需求如果用SQL来写餐厅筛选大概是这样的SELECT id FROM restaurants WHERE (veganFriendly 1 OR :veganFriendly 0) AND price :maxPrice AND distance :maxDistance ORDER BY rating DESC, id DESC;这就是1333题的SQL版。注意veganFriendly条件在SQL里写成了(veganFriendly 1 OR :veganFriendly 0)意思就是只有当参数为1时才过滤为0时该条件恒真不过滤。这是业务代码里处理开关型条件最标准的写法之一。排序部分直接用ORDER BY rating DESC, id DESC跟代码里的复合比较器完全对应。你可以看到LeetCode题说白了就是一道可以用SQL直接搞定的数据查询题用编程语言实现反而绕了一点。7.2 Java/Go等语言的实现思路如果你平时写Java代码思路也完全一样public ListInteger filterRestaurants(int[][] restaurants, int veganFriendly, int maxPrice, int maxDistance) { Listint[] filtered new ArrayList(); for (int[] r : restaurants) { if (veganFriendly 1 r[2] ! 1) continue; if (r[3] maxPrice || r[4] maxDistance) continue; filtered.add(r); } filtered.sort((a, b) - { if (a[1] ! b[1]) return b[1] - a[1]; return b[0] - a[0]; }); ListInteger res new ArrayList(); for (int[] r : filtered) res.add(r[0]); return res; }Go的话用sort.Slice实现比较器代码会稍微长一点但逻辑一致。我一般会跟别人说算法思路一旦想通语言只是表达工具别在语言写法上卡太久。7.3 真实业务的加码分页、多字段排序、动态条件LeetCode只要求返回全部符合条件的id但真实业务一般会加分页比如一次只返回20条。这时候你还要考虑第20名和第21名rating相同时id排序是否稳定这类细节否则翻页可能出现重复数据。类似1333的这种复合排序在分页场景下尤其重要因为数据库的排序如果不稳定翻页就乱了。另外真实业务里排序条件通常是用户可选的默认按综合排序也可以切到按距离排序、按评分排序、按价格排序。这时候我会用策略模式封装不同的排序比较器而不是写一个巨大的if-else分支。这个思路其实在1333里已经埋下伏笔了——比较器本身就是可以独立抽出来的策略单元。8. 回顾这道题的价值不是简单而是标准刷完1333后我最大的感受是有些题简单但简单得有价值。这道题没有高深的算法却在训练你把需求转代码这件事做得规范。过滤条件要准确排序规则要完整边界情况要覆盖输出格式要正确——这四件事恰恰是写生产代码最基础也最重要的能力。它提醒我LeetCode刷题不必只盯着难题、偏题偶尔刷一刷这种标准题反而能巩固基本功。就像运动员训练不是每天都要冲刺极限重量更多时候练的是动作标准度和稳定性。如果你现在正在刷LeetCode我建议你给1333留个位置。它既能帮你快速上手过滤排序题型又能让你在面试时遇到类似场景题时心里有底。面试官其实不指望你现场发明什么新算法更多时候是看你面对一个明确需求时能不能写出结构清爽、逻辑完整的代码——这恰恰就是1333在训练的东西。9. 关于这道题最后的几个私房建议文章写到最后分享几个我刷这道题以及刷整个sorted-filter题型时的私房建议都是踩过坑之后总结出来的第一写完比较器后养成代入两三条数据手动演算的习惯。LeetCode的判题虽然能告诉你错没错但手动演算能让你知道为什么错、错在哪一步。我见过不少人同一个比较器错误反复出现三四次就是因为没有真正理解比较器的返回值和排序方向的关系。第二不要忽略题目里的小于等于和严格小于。LeetCode很多边界判定都藏在有没有等号上。1333的价格和距离都是小于等于幸好容易注意到我遇到不少题明明思路全对就因为不等号的细节没注意白交了两次。第三做完题花五分钟看一下讨论区里别人的写法。不是为了抄答案而是为了看看别人怎么组织代码结构。同一道题有人用一行lambda解决有人用完整函数封装没有绝对的好坏但多看几种写法能帮你拓宽代码组织方式的视野。第四遇到这种场景型题目试着把题目翻译成你熟悉的另一套技能树去解一遍。比如把1333用SQL写一遍用Python写一遍再用JavaScript写一遍。多语言对比能帮你从记住解法提升到理解本质这种理解力在面试和实际工作中都非常值钱。最后刷题这件事进度慢一点没关系关键是每道题都刷扎实。一道1333或许在很多人眼里只是五分钟速通的简单题但我觉得弄懂它背后的过滤排序套路比盲目刷十道同类型题更有价值。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。