常考算法题:无重复字符串的排列组合

简介: 前端西瓜哥

大家好我西瓜哥,今天做一道比较常考的算法题。

编写一种方法,计算某字符串的所有排列组合,字符串每个字符均不相同。

如输入 "qwe",要求返回 ["qwe", "qew", "wqe", "weq", "ewq", "eqw"]

LeetCode 题目链接:

https://leetcode-cn.com/problems/permutation-i-lcci/

回溯解法

常见的解法是回溯。

我们使用一个 toVisited 数组,用于维护递归过程中尚未访问的字符。

回溯函数的 TypeScript 签名如下:

(combs: string[], s: string, toVisit: string[]) => void
  • combs:字符串数组。排列组合被保存的数组,回溯结束后用于返回
  • s:字符串
  • toVisit:尚未访问的字符数组。

先看下回溯函数的代码实现。

function r(combs, s, toVisit) {
  if (toVisit.length === 0) {
    combs.push(s);
    return;
  }
  for (let i = 0; i < toVisit.length; i++) {
    r(combs, s + toVisit[i], toVisit.filter(item => item != toVisit[i]));
  }
}

我们会遍历 toVisit 数组,将这些尚未被访问的字符,追加到 s 末尾,然后生成一个新的移除了该字符的 toVisited,作为下一个递归调用的参数。

当所有字符都被访问过了(toVisite 数组为空)时,我们就拿到了一种排列方式,将它放到 combs,然后结束当前的这一个迭代。

当所有的迭代都结束后,我们的 combs 中就包含了所有的排列,将其返回即可。

完整代码实现如下:

function permutation(S: string): string[] {
  const combs = [];
  r(combs, '', [...S]);
  return combs;
};
function r(combs: string[], s: string, toVisit: string[]) {
  if (toVisit.length === 0) {
    combs.push(s);
    return;
  }
  for (let i = 0; i < toVisit.length; i++) {
    r(combs, s + toVisit[i], toVisit.filter(item => item != toVisit[i]));
  }
}

我们注意到,每次递归,都要拷贝一份待访问字符数组,即 toVisit.filter(item => item != toVisit[i]),这个效率实在是不怎么高啊。

其实这个可以优化一下。如果你实现过排序算法,比如选择排序,你就会想到一个原地使用原数组的思路。排序算法中将原数组的元素进行交换,分为 “排序区间” 和 “未排序区间”。

我们这里也可以致敬一下,也交换一下嘛,在原数组中原地分为 “已访问字符区间” 和 “待访问数组”。然后在下一个递归函数执行完后,再复原一下数组。

改良后的实现如下:

function permutation(S): string[] {
  const combs = [];
  r(combs, [...S], 0);
  return combs;
};
function r(combs, chars, first) {
  if (first === chars.length) {
    combs.push(chars.join(''));
    return;
  }
  for (let i = first; i < chars.length; i++) {
    swap(chars, i, first);
    r(combs, chars, first + 1);
    swap(chars, i, first);
  }
}
function swap(chars, i, j) {
  const tmp = chars[i];
  chars[i] = chars[j];
  chars[j] = tmp;
}

结尾

本文讲解了如何用回溯的思考解全排列问题,回溯解法的核心思路是每次迭代,从待访问字符集中挑选字符,直到所有字符都被访问完。

为此你需要维护待访问字符集数组,这里如果想要高效一些,可以使用数组原地分区或是使用栈的方式,直接拷贝一份效率很低,但优点是简单直接(所以我做题时经常这么干)

我是每周做五道算法题的前端西瓜哥,欢迎关注我。

相关文章
|
4月前
|
算法
【算法】滑动窗口——找到字符串中所有字母异位词
【算法】滑动窗口——找到字符串中所有字母异位词
|
2月前
|
算法
两个字符串匹配出最长公共子序列算法
本文介绍了最长公共子序列(LCS)问题的算法实现,通过动态规划方法求解两个字符串的最长公共子序列,并提供了具体的编程实现细节和示例。
90 1
两个字符串匹配出最长公共子序列算法
|
4月前
|
算法 Java
掌握算法学习之字符串经典用法
文章总结了字符串在算法领域的经典用法,特别是通过双指针法来实现字符串的反转操作,并提供了LeetCode上相关题目的Java代码实现,强调了掌握这些技巧对于提升算法思维的重要性。
|
5月前
|
自然语言处理 算法 搜索推荐
字符串相似度算法完全指南:编辑、令牌与序列三类算法的全面解析与深入分析
在自然语言处理领域,人们经常需要比较字符串,这些字符串可能是单词、句子、段落甚至是整个文档。如何快速判断两个单词或句子是否相似,或者相似度是好还是差。这类似于我们使用手机打错一个词,但手机会建议正确的词来修正它,那么这种如何判断字符串相似度呢?本文将详细介绍这个问题。
284 1
|
5月前
|
数据采集 算法 JavaScript
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
JavaScript字符串搜索涵盖`indexOf`、`includes`及KMP算法。`indexOf`返回子字符串位置,`includes`检查是否包含子字符串。KMP是高效的搜索算法,尤其适合长模式匹配。示例展示了如何在数据采集(如网页爬虫)中使用这些方法,结合代理IP进行安全搜索。代码示例中,搜索百度新闻结果并检测是否含有特定字符串。学习这些技术能提升编程效率和性能。
127 1
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
|
4月前
|
算法 C++
惊爆!KPM算法背后的秘密武器:一行代码揭秘字符串最小周期的终极奥义,让你秒变编程界周期大师!
【8月更文挑战第4天】字符串最小周期问题旨在找出字符串中最短重复子串的长度。KPM(实为KMP,Knuth-Morris-Pratt)算法,虽主要用于字符串匹配,但其生成的前缀函数(next数组)也可用于求解最小周期。核心思想是构建LPS数组,记录模式串中每个位置的最长相等前后缀长度。对于长度为n的字符串S,其最小周期T可通过公式ans = n - LPS[n-1]求得。通过分析周期字符串的特性,可证明该方法的有效性。提供的C++示例代码展示了如何计算给定字符串的最小周期,体现了KPM算法在解决此类问题上的高效性。
87 0
|
6月前
|
存储 算法 Cloud Native
C++ bcrypt算法 字符串加密,亲测有效
C++ bcrypt算法 字符串加密,亲测有效
|
6月前
|
存储 算法
算法训练,牛客.判断是不是平衡二叉树 牛客.最大子矩阵两个数组的交集牛客.数组中两个字符串的最小距离
算法训练,牛客.判断是不是平衡二叉树 牛客.最大子矩阵两个数组的交集牛客.数组中两个字符串的最小距离
算法训练,牛客.判断是不是平衡二叉树 牛客.最大子矩阵两个数组的交集牛客.数组中两个字符串的最小距离
|
5月前
|
算法 Java
KMP算法详解及其在字符串匹配中的应用
KMP算法详解及其在字符串匹配中的应用
|
7月前
|
算法
KPM算法求字符串的最小周期证明
公式 `ans = n - LPS[n-1]` 描述了最小周期,其中 `n` 是子串长度,`LPS[n-1]` 是前缀函数值。证明分为特殊情况和一般情况:对于完整周期字符串,`LPS[n-1] = 3*T`,故 `ans = T`;对于非完整周期,通过分析不同长度的 `[末部分]` 和 `[前部分]`,展示 `ans` 始终等于周期 `T` 或由 `[e][b]` 构成的最小周期,从而证明公式正确。