阅读视图
jQuery-ui源代码重点难点分析
【React】React初体验--手把手教你写一个自己的React初始项目
不再死记硬背!帮你理解原型相关八股概念!
vue3+vite 项目中怎么引入 elementplus 组件库
告别 useState 噩梦:useReducer 如何终结 React 状态管理混乱?
排序(Python/Java/C++/C/Go/JS/Rust)
如果 $B$ 是 $A$ 的子文件夹,那么:
- $A + \texttt{/}$ 是 $B$ 的前缀。
比如 $\texttt{/a/b}$ 是 $\texttt{/a}$ 的子文件夹,其中 $\texttt{/a/}$ 是 $\texttt{/a/b}$ 的前缀。(当然,$\texttt{/a}$ 也是 $\texttt{/a/b}$ 的前缀。)
暴力的想法是,枚举路径 $B$,枚举其他路径 $A$,判断是否满足上述要求,这需要 $\mathcal{O}(\ell\cdot n^2)$ 时间,其中 $\ell\le 100$ 为单个字符串的长度。这太慢了。
可以不枚举 $A$ 吗?
如果 $\textit{folder}$ 列表是有序的(按照字典序从小到大排序),那么当 $B$ 是 $A$ 的子文件夹时,$A$ 是 $B$ 的前缀,所以 $A$ 一定排在 $B$ 的左边。
比如 $\textit{folder} = [\texttt{/a},\texttt{/a/b},\texttt{/c},\texttt{/d}]$:
- $\texttt{/a}$ 不可能是任何文件夹 $A$ 的子文件夹(否则 $A$ 会排在 $\texttt{/a}$ 前面)。
- $\texttt{/a/b}$ 是 $\texttt{/a}$ 的子文件夹,删除。
- $\texttt{/c}$ 不是 $\texttt{/a}$ 的子文件夹。
- $\texttt{/d}$ 不是 $\texttt{/c}$ 的子文件夹。注意,不需要判断 $\texttt{/d}$ 是否为 $\texttt{/a}$ 的子文件夹。因为一定不是,为什么?如果 $\texttt{/d}$ 是 $\texttt{/a}$ 的子文件夹,由于列表是有序的,$\texttt{/a}$ 的下一个字符串到 $\texttt{/d}$ 之间的字符串都是以 $\texttt{/a/}$ 开头的,所以上一个文件夹 $\texttt{/c}$ 也是以 $\texttt{/a/}$ 开头的,一定是 $\texttt{/a}$ 的子文件夹,一定会被删除,但这与实际矛盾。
因此,排序后,只需比较当前路径与上一个未被删除的路径。
代码实现时,为避免 $A + \texttt{/}$ 生成新字符串花费额外时间和空间,判断 $A + \texttt{/}$ 是否为 $B$ 的前缀,可以分解为:
- 判断 $A$ 是否为 $B$ 的前缀。
- 判断 $B[m]$ 是否为 $\texttt{/}$,其中 $m$ 是 $A$ 的长度。注意题目保证所有字符串互不相同,所以如果 $A$ 是 $B$ 的前缀,那么 $B$ 的长度一定大于 $A$ 的长度。所以 $B[m]$ 不会下标越界。
###py
class Solution:
def removeSubfolders(self, folder: List[str]) -> List[str]:
folder.sort()
ans = [folder[0]]
for s in folder[1:]: # 切片可以用 for i in range(1, n) 代替,做到 O(1) 空间
last = ans[-1]
if not s.startswith(last) or s[len(last)] != '/':
ans.append(s)
return ans
###java
class Solution {
public List<String> removeSubfolders(String[] folder) {
Arrays.sort(folder);
List<String> ans = new ArrayList<>();
ans.add(folder[0]);
for (int i = 1; i < folder.length; i++) {
String s = folder[i];
String last = ans.getLast();
if (!s.startsWith(last) || s.charAt(last.length()) != '/') {
ans.add(s);
}
}
return ans;
}
}
###cpp
class Solution {
public:
vector<string> removeSubfolders(vector<string>& folder) {
ranges::sort(folder);
vector<string> ans = {folder[0]};
for (int i = 1; i < folder.size(); i++) {
string& s = folder[i];
string& last = ans.back();
if (!s.starts_with(last) || s[last.size()] != '/') {
ans.emplace_back(s);
}
}
return ans;
}
};
###c
int cmp(const void* a, const void* b) {
return strcmp(*(char**)a, *(char**)b);
}
char** removeSubfolders(char** folder, int folderSize, int* returnSize) {
qsort(folder, folderSize, sizeof(char*), cmp);
*returnSize = 1;
for (int i = 1; i < folderSize; i++) {
char* s = folder[i];
char* last = folder[*returnSize - 1];
int len = strlen(last);
if (strncmp(last, s, len) != 0 || s[len] != '/') {
folder[(*returnSize)++] = s; // 原地保存
}
}
return folder;
}
###go
func removeSubfolders(folder []string) []string {
slices.Sort(folder)
ans := folder[:1]
for _, s := range folder[1:] {
last := ans[len(ans)-1]
if !strings.HasPrefix(s, last) || s[len(last)] != '/' {
ans = append(ans, s)
}
}
return ans
}
###js
var removeSubfolders = function(folder) {
folder.sort();
const ans = [folder[0]];
for (let i = 1; i < folder.length; i++) {
const s = folder[i];
const last = ans[ans.length - 1];
if (!s.startsWith(last) || s[last.length] !== '/') {
ans.push(s);
}
}
return ans;
};
###rust
impl Solution {
pub fn remove_subfolders(mut folder: Vec<String>) -> Vec<String> {
folder.sort_unstable();
let mut ans = vec![folder[0].clone()];
for s in folder.into_iter().skip(1) {
let last = ans.last().unwrap();
if !s.starts_with(last) || s.as_bytes()[last.len()] != b'/' {
ans.push(s);
}
}
ans
}
}
复杂度分析
- 时间复杂度:$\mathcal{O}(\ell\cdot n\log n)$ 时间,$n$ 是 $\textit{folder}$ 的长度,$\ell\le 100$ 为单个字符串的长度。排序有 $\mathcal{O}(n\log n)$ 次比较,每次 $\mathcal{O}(\ell)$ 时间。
- 空间复杂度:$\mathcal{O}(1)$。返回值不计入。忽略排序的栈开销。
分类题单
- 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
- 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
- 单调栈(基础/矩形面积/贡献法/最小字典序)
- 网格图(DFS/BFS/综合应用)
- 位运算(基础/性质/拆位/试填/恒等式/思维)
- 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
- 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
- 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
- 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
- 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
- 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
- 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)
欢迎关注 B站@灵茶山艾府
揭秘Chrome DevTools:从原理到自定义调试工具
我终于也是会写3d小游戏的人了,发个掘金显摆显摆
WebSocket:断线、心跳与重连
“移动端优先”在隐藏元素方面的问题 - tailwindcss 系列
大屏应用中的动态缩放适配工具
别再硬啃文档了!用 AI 十分钟上手任意 GitHub 。文档不全,调用出错,快速解决
Three.js 滚动条 3D 视差动画原理解析
前端学C++可太简单了:预处理器
括号生成算法
你在开发一个动态表单生成器,用户可以通过拖拽组件构建界面。当处理嵌套组件时(如折叠面板中的表格、弹窗内的表单),你需要生成所有可能的有效嵌套组合——这正是括号生成问题在前端的实际应用。
// 组件嵌套示例:
<Modal> // 开括号
<Table> // 开括号
<Form /> // 内容
</Table> // 闭括号
</Modal> // 闭括号
问题定义:寻找有效括号组合
给定正整数 n
,要求生成所有有效的括号组合,满足:
- 每个组合包含
n
对圆括号 - 每个开括号
(
都有对应闭括号)
- 闭括号不能出现在对应开括号之前
例如 n = 3
时:
有效的组合: ["((()))", "(()())", "(())()", "()(())", "()()()"]
无效的组合: ["())()(", "())(()"] // 括号不匹配
解法一:回溯法(深度优先搜索)
核心思路:递归构建所有可能的组合,同时确保每一步都符合有效条件。
function generateParenthesis(n) {
const result = []; // 存储所有有效组合
// 递归生成函数
const backtrack = (str, openCount, closeCount) => {
// 已生成完整组合
if (str.length === 2 * n) {
result.push(str);
return;
}
// 添加开括号的条件:开括号数少于n
if (openCount < n) {
backtrack(str + '(', openCount + 1, closeCount);
}
// 添加闭括号的条件:闭括号数小于开括号数
if (closeCount < openCount) {
backtrack(str + ')', openCount, closeCount + 1);
}
};
backtrack('', 0, 0); // 初始调用
return result;
}
执行过程可视化(n=2):
开始: "" (0开, 0闭)
├─ 添加( → "( "(1,0)
│ ├─ 添加( → "((" (2,0) → 闭括号只能添加)
│ │ └─ 添加) → "(()" (2,1)
│ │ └─ 添加) → "(())" (2,2) ✅
│ └─ 添加) → "()" (1,1)
│ └─ 添加( → "()(" (2,1)
│ └─ 添加) → "()()" (2,2) ✅
└─ 添加) → 无效路径(闭括号前无开括号)
复杂度分析:
- 时间复杂度:O(4^n/√n) - 卡特兰数增长速度
- 空间复杂度:O(n) - 递归栈深度
解法二:动态规划 - 优雅的递推解法
核心思路:n
对括号的组合 = (
+ i
对括号的组合 + )
+ n-1-i
对括号的组合
function generateDP(n) {
// 存储不同数量组合的结果
const dp = [...Array(n+1)].map(() => []);
dp[0] = ['']; // 基础情况:0对括号为空字符串
for (let i = 1; i <= n; i++) {
for (let j = 0; j < i; j++) {
// 内部组合可能有j种情况
for (const left of dp[j]) {
// 外部组合有i-1-j种情况
for (const right of dp[i - 1 - j]) {
// 构建新组合: (left)right
dp[i].push(`(${left})${right}`);
}
}
}
}
return dp[n];
}
计算过程展示(n=3):
dp[0] = [""]
dp[1] = ["()"]
dp[2]:
从dp[0]和dp[1]: ( ) + "" = "()" → 但需要外括号包裹 → 变为 "( )" + "" → 错误
正确计算:
j=0: ("") → dp[2] 加入 ( ) + dp[1]的内容 → ( )() → "()()"
j=1: (dp[1]内容) → (()) + dp[0]="" → "(())"
dp[3]:
j=0: ("") → ( ) + dp[2]的内容 → ["()(())", "()()()"]
j=1: 使用dp[1] → (dp[1]内容) + dp[1] → (())()
j=2: 使用dp[2] → (dp[2]内容) → 包括两种情况:("(())")和("()()") 加上空外部
→ ["((()))", "(()())"]
两种方法对比:选择最优方案
特性 | 回溯法 | 动态规划 |
---|---|---|
时间复杂度 | O(4ⁿ/√n) | O(4ⁿ/√n) |
空间复杂度 | O(n) 调用栈 | O(4ⁿ/√n) 存储所有组合 |
实现难度 | ★★★☆ | ★★★★ |
适用场景 | 仅需结果列表 | 需要中间结果集 |
优势 | 内存占用低,实现直观 | 数学证明清晰 |
前端适用度 | ⭐⭐⭐⭐ | ⭐⭐ |
前端推荐选择回溯法:动态规划需存储所有中间结果,当
n>10
时可占用数百MB内存,不适合前端环境。
实际项目应用:React状态管理优化
当组件有多层依赖关系时,我们需要生成有效依赖树:
// 动态生成嵌套组件结构
function DependencyTree({ dependencies }) {
// 计算依赖关系树
const generateTree = (level) => {
const brackets = generateParenthesis(level);
return brackets.map((pattern, index) => (
<div key={index} className="dependency-branch">
{renderPattern(pattern)}
</div>
));
};
// 将括号模式转换为可视化组件
const renderPattern = (pattern) => {
return pattern.split('').map((char, i) => (
char === '(' ?
<div className="nested-block"> :
</div>
));
};
return <div className="dependency-tree">{generateTree(dependencies)}</div>;
}
高级技巧:使用剪枝优化性能
对于大规模应用场景,可在回溯法中添加记忆化缓存:
function generateOptimized(n) {
const memo = new Map();
function backtrack(open, close) {
const key = `${open}_${close}`;
if (memo.has(key)) return memo.get(key);
if (open === n && close === n) return [''];
const results = [];
if (open < n) {
backtrack(open + 1, close).forEach(seq => {
results.push('(' + seq);
});
}
if (close < open) {
backtrack(open, close + 1).forEach(seq => {
results.push(')' + seq);
});
}
memo.set(key, results);
return results;
}
return backtrack(0, 0);
}
边界情况与问题处理
- 输入验证逻辑:
function safeGenerate(n) {
if (typeof n !== 'number' || n < 0 || !Number.isInteger(n)) {
throw new Error('输入必须是正整数');
}
if (n > 12) console.warn('n>12可能导致性能问题');
return generateParenthesis(n);
}
- 空结果处理:
// 当n=0时应返回空数组还是包含空字符串?
// 根据实际需求决定:
function generateParenthesis(n) {
if (n === 0) return []; // 或 return [''];
// ...
}
扩展应用场景
- CSS嵌套生成器:
function generateCSSNesting(depth) {
return generateParenthesis(depth).map(seq => {
return seq.split('').map(char =>
char === '(' ? '.container' : '}'
).join('\n');
});
}
// 输出:
// .container {
// .container {
// }
// }
- 代码缩进验证工具:
function validateCodeStructure(code) {
const stack = [];
for (const char of code) {
if (char === '(') stack.push(char);
if (char === ')') {
if (stack.pop() !== '(') return false;
}
}
return stack.length === 0;
}
前端框架中的实际应用:React Hooks依赖链
当需要动态生成Hook的执行顺序时:
function useNestedHooks(depth) {
const hooks = generateParenthesis(depth);
return hooks.map(pattern => {
const hooksChain = [];
pattern.split('').forEach(char => {
if (char === '(') hooksChain.push(useState);
// 模拟Hook执行顺序
});
return hooksChain;
});
}
小结
-
关键:理解有效括号字符串在任何子串中开括号数量不少于闭括号的性质
-
方法选择指南:
- 需要所有组合 → 回溯法
- 需要数学推导 → 动态规划
- 内存敏感 → 迭代法
-
性能优化:
// 提前终止无效分支 function backtrack(str, open, close) { if (close > open) return; // 关键剪枝! // ... }
-
前端适用技巧:
// 流式处理大n值的情况 function* generateLazy(n) { function* backtrack(...) { ... yield str; } yield* backtrack('', 0, 0); }
掌握括号生成算法,能在开发动态布局、代码解析器等场景中提供高效解决方案。在保证有效性的前提下逐步构建,及时剪枝避免无效路径。