题目描述
思路解析
一句话答案:LeetCode 1433 检查一个字符串能否打破另一个:双排序后逐位比,只要 s1 每位都不小于 s2、或反过来每位都不小于,就能打破,时间 O(n log n)、空间 O(n)。
打破一个字符串,到底在要求什么
给两个长度相等的字符串 s1 和 s2。说「s1 打破 s2」,指的是能把 s1 重新排列,让它每位都不小于 s2 对应位的字母,这里按字母表算大小,a 最小、z 最大,两位相等也算打破。题目只要求两个方向里成立一个:s1 能打破 s2,或者 s2 能打破 s1,任一为真就返回 true,两个都做不到才返回 false。
逐位比较其实不必枚举排列
想穷举的话,得把 s1 的所有排法都摆出来一个个和 s2 比,长度到 n 就有 n 的阶乘种排法,稍长一点根本算不完。另一个想当然的错是只验一个方向:s1 打不破 s2 就收手报 false,可题目允许反过来让 s2 打破 s1,漏掉这层会把本该 true 的情形判死。
排好序小对小,能打破就一定看得出来
用不着把排法试遍。把两个串各自从小到大排好,再挨位比一次就够。排序给的是最省力的对齐:s1 最小的字母去顶 s2 最小的、次小对次小,每一位的差距都摊得最匀。这靠贪心撑着,贪心就是每步把能定死的顺序先定死、不回头。想更严实可以用交换论证:假设有个更绕的排列能打破,把它调成小对小、大对大,只会更容易满足。排好序这版都打不破,别的排法更没戏。
两串各排一次序,两个方向轮着验
落到步骤上很干脆。先给 s1、s2 各排一次序,再试方向一:看 s1 是不是每位都不小于 s2 的对应位,只要撞见某位 s1 的字母比 s2 小,方向一当场作废。方向一没成也别急着报 false,掉头试方向二:反过来看 s2 是不是每位都不小于 s1。有一个方向全程扛住就是 true,两个都半路断掉才是 false。这里判的是不小于,某位上两个字母相等这位照样算过,若写成严格的 >,「aaa」打破「aaa」这种全等情形会被误杀成 false。
「abc」对「xya」,true 怎么逐位走出来
拿题面第一组「abc」对「xya」走一遍。先排序,「abc」本就是升序不动,「xya」理成「axy」。试方向一,要求 s1 每位都不小于 s2:第 0 位 a 对 a,相等算过;第 1 位 b 对 x,b 在字母表里排在 x 前面比它小,方向一断在这。掉头试方向二,要求 s2 每位都不小于 s1:第 0 位 a 对 a 过,第 1 位 x 对 b、x 比 b 大过,第 2 位 y 对 c、y 比 c 大过,三位全扛住。方向二成立,结果 true,和题面一致。
O(n log n) 的账,和三处必判的边界
排序 O(n log n) 是大头:给两个串各排一次是 O(n log n),之后逐位比是 O(n),合起来仍是 O(n log n),n 是字符串长度;额外空间 O(n),花在排序的字符副本上。收尾时几处最容易栽。只判一个方向就下结论会漏,像「abe」对「acd」,排序后方向一栽在 b 对 c、方向二栽在 d 对 e,两头都断,得都判完才敢返回 false。不排序直接拿原串比也不行,题目要的是存在某个排列,不是原顺序。还有相等这一层,「aaa」对「aaa」每位都相等、按不小于算就是打破,返回 true,这正是不能把条件写成严格大于的原因。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记牢一句:两个串各自排好序,逐位比;s1 全程 ≥ s2,或者 s2 全程 ≥ s1,任一成立就能打破。下面每帧都在套这句。
原始 · 两个串:上排是 s1 = 「dbace」,右边侧栏是 s2 = 「fcbed」,两个串长度都是 5。直接看原始顺序没法逐位比,所以第一步,把它们各自从小到大排好序。
第一步 · s1 排序:先排 s1。把 「dbace」 从小到大理一遍,得到 「abcde」,现在上排就是排好的样子。接着排 s2。
第一步 · s2 排序:s2 也排一遍,「fcbed」 理顺成 「bcdef」,放进右边侧栏。现在上排 s1 是 「abcde」,侧栏 s2 是 「bcdef」,位置一一对齐,可以逐位比了。
就位 · 两个方向二选一:两行都排好了。接下来要试两个方向:方向一,看 s1 是不是每一位都 ≥ s2;方向二,看 s2 是不是每一位都 ≥ s1。任意一个方向全程成立,就能打破。先试方向一。
方向一 · s1 能打破 s2 吗:方向一的要求很明确:排好序后,s1 的每一位都要 ≥ s2 的对应位。只要有一位 s1 比 s2 小,方向一当场就不成立。从第 0 位开始。
方向一 · 第 0 位:第 0 位,上排 s1 是 「a」,侧栏 s2 是 「b」。方向一要的是 s1 ≥ s2,也就是 「a」 要不小于 「b」。比一下。
方向一 · 断在第 0 位:「a」 在字母表里排在 「b」 前面,也就是 「a」 < 「b」。s1 的第 0 位反而比 s2 小,方向一的「每位都 ≥」当场被打破,这一格标红。方向一不成立。
方向一不行 → 试方向二:方向一断了,可别急着说答案是 false。题目是「两个方向有一个成立就行」,s1 打不破 s2,还要反过来看 s2 能不能打破 s1。清空颜色,试方向二。
方向二 · s2 能打破 s1 吗:方向二的要求对称过来:排好序后,s2 的每一位都要 ≥ s1 的对应位。同样从第 0 位开始,一位一位往后比,中途有一位栽了就算方向二也失败。
方向二 · 第 0 位:看第 0 位。侧栏 s2 是 「b」,上排 s1 是 「a」。方向二要 s2 ≥ s1,也就是 「b」 要不小于 「a」。
方向二 · 第 0 位通过:「b」 在字母表里不比 「a」 靠前,也就是 「b」 ≥ 「a」,这一位满足方向二的要求,标绿。已经连过 1 位,接着看下一位。
方向二 · 第 1 位:看第 1 位。侧栏 s2 是 「c」,上排 s1 是 「b」。方向二要 s2 ≥ s1,也就是 「c」 要不小于 「b」。
方向二 · 第 1 位通过:「c」 在字母表里不比 「b」 靠前,也就是 「c」 ≥ 「b」,这一位满足方向二的要求,标绿。已经连过 2 位,接着看下一位。
方向二 · 第 2 位:看第 2 位。侧栏 s2 是 「d」,上排 s1 是 「c」。方向二要 s2 ≥ s1,也就是 「d」 要不小于 「c」。
方向二 · 第 2 位通过:「d」 在字母表里不比 「c」 靠前,也就是 「d」 ≥ 「c」,这一位满足方向二的要求,标绿。已经连过 3 位,接着看下一位。
方向二 · 第 3 位:看第 3 位。侧栏 s2 是 「e」,上排 s1 是 「d」。方向二要 s2 ≥ s1,也就是 「e」 要不小于 「d」。
方向二 · 第 3 位通过:「e」 在字母表里不比 「d」 靠前,也就是 「e」 ≥ 「d」,这一位满足方向二的要求,标绿。已经连过 4 位,接着看下一位。
方向二 · 第 4 位:看第 4 位。侧栏 s2 是 「f」,上排 s1 是 「e」。方向二要 s2 ≥ s1,也就是 「f」 要不小于 「e」。
方向二 · 第 4 位通过:「f」 在字母表里不比 「e」 靠前,也就是 「f」 ≥ 「e」,这一位满足方向二的要求,标绿。已经连过 5 位。
方向二 · 成立:五位全部走完,s2 的每一位都 ≥ s1 的对应位,一格都没栽。方向二成立,s2 可以打破 s1,上排整排标绿。
完成 · 答案 true:回看全程:方向一在第 0 位就断了,但方向二每一位都成立。题目只要两个方向有一个能打破就行,所以答案是 true,跟开头说的对上了。这就是排序后逐位比、两个方向二选一的全过程。
边界:abc 对 xya 靠方向二成立;abe 对 acd 两个方向都断为 false;全 a 对全 a 因为相等也算打破,返回 true。
面试重点:排序后逐位比是贪心最优配对(交换论证);两个方向不对称、都要试;字符集 26 时可用计数排序把整体压到 O(n)。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def checkIfCanBreak(self, s1: str, s2: str) -> bool: cs1 = sorted(s1) cs2 = sorted(s2) return all(a >= b for a, b in zip(cs1, cs2)) or all( a <= b for a, b in zip(cs1, cs2) )复杂度
- 时间:O(n log n),n 是字符串长度。主要开销是给两个串各排一次序,各 O(n log n);之后逐位比最多扫一两遍,是 O(n)。合起来由排序主导,O(n log n)。字符集只有 26 个小写字母,想更快可以改用计数排序把排序降到 O(n),整体就成 O(n)
- 空间:O(n),按峰值算:三种语言都为排序生成了串的副本。Python 的 sorted 返回两个长度 n 的字符列表;Java 用 toCharArray 得到两个 char 数组;C++ 的 check 按值或对已排序串操作,排序本身也在长度 n 的串上进行。这些 O(n) 的副本是空间主项,排序内部的递归栈 O(log n) 被它盖过
易错点
面试追问把动画讲成自己的话
追问为什么排序后逐位比就一定对,不会漏掉某个更好的排列?
追问为什么必须试两个方向?只看 s1 能不能打破 s2 不够吗?
追问这道题复杂度还能再压吗?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
不同整数的最少数目
LeetCode 1481 · 中等 · 沿着 贪心套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题