字符串的排列

剑指offer里的一道经典题目。

难度:中等

描述

输入一个长度为 n 字符串,打印出该字符串中字符的所有排列,你可以以任意顺序返回这个字符串数组。

例如输入字符串ABC,则输出由字符A,B,C所能排列出来的所有字符串ABC,ACB,BAC,BCA,CBA和CAB。

数据范围:n<10
要求:空间复杂度 O(n!),时间复杂度 O(n!)

思路:相当于求字符串的全排列,为了方便去重,先按照字典序给字符排序。主要利用递归与回溯解决此问题。每一层递归找到一个字符添加进临时字符串,当字符串长度合适时则存入字符串数组。而每一层递归相当于给剩下的字符再进行全排列。

具体做法:

  • 第一步:先对字符串按照字典序排序,获取第一个排列情况。
  • 第二步:准备一个空串暂存递归过程中组装的排列情况。使用额外的vis数组用于记录哪些位置的字符被加入了。
  • 第三步:每次递归从头遍历字符串,获取字符加入:首先根据vis数组,已经加入的元素不能再次加入了;同时,如果当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经使用过,也不需要将其纳入。
  • 第四步:进入下一层递归前将vis数组当前位置标记为使用过。
  • 第五步:回溯的时候需要修改vis数组当前位置标记,同时去掉刚刚加入字符串的元素,
  • 第六步:临时字符串长度到达原串长度就是一种排列情况。
import java.util.*;
public class Solution {
    public void recursion(ArrayList<String> res, char[] str, StringBuffer temp, boolean[] vis){
        //临时字符串满了加入输出
        if(temp.length() == str.length){ 
            res.add(new String(temp));
            return;
        }
        //遍历所有元素选取一个加入
        for(int i = 0; i < str.length; i++){ 
            //如果该元素已经被加入了则不需要再加入了
            if(vis[i]) 
                continue;
            if(i > 0 && str[i - 1] == str[i] && vis[i - 1])
                //当前的元素str[i]与同一层的前一个元素str[i-1]相同且str[i-1]已经用过了(为了去重)
                continue;  
            //标记为使用过
            vis[i] = true;  
            //加入临时字符串
            temp.append(str[i]); 
            recursion(res, str, temp, vis);
            //回溯
            vis[i] = false; 
            temp.deleteCharAt(temp.length() - 1);
        }
    }
    
    public ArrayList<String> Permutation(String str) {
        ArrayList<String> res = new ArrayList<String>();
        if(str == null || str.length() == 0) 
            return res;
        //转字符数组
        char[] charStr = str.toCharArray();
        // 按字典序排序
        Arrays.sort(charStr); 
        boolean[] vis = new boolean[str.length()];
        //标记每个位置的字符是否被使用过
        Arrays.fill(vis, false); 
        StringBuffer temp = new StringBuffer();
        //递归获取
        recursion(res, charStr, temp, vis); 
        return res;
    }
}

复杂度分析:

时间复杂度

- 排序操作 `sort(str.begin(), str.end())` 的时间复杂度是 O(n log n),其中 n 是输入字符串 `str` 的长度。
- 递归函数 `recursion` 实现了全排列,对于一个长度为 n 的字符串,其全排列的数量是 n!(n 的阶乘)。
- 在每个排列中,我们需要执行的操作包括检查访问数组 `vis`、将字符添加到临时字符串 `temp` 中以及回溯时移除字符。这些操作都是 O(1) 的。
- 因此,除了初始的排序之外,生成所有排列的过程需要 O(n * n!) 的时间。这是因为对于每一个排列,我们都需要遍历整个字符串(O(n)),而总共有 n! 个排列。

综上所述,整个算法的时间复杂度是 O(n * n!) + O(n log n)。由于 n! 增长得非常快,当 n 较大时,n log n 可以忽略不计,所以我们可以简化为 O(n * n!)。

空间复杂度

- 输入字符串 `str` 需要 O(n) 的空间。
- 访问标记数组 `vis` 也需要 O(n) 的空间。
- 递归调用栈的最大深度是 n,因为每次递归调用都会增加临时字符串 `temp` 的长度,直到达到与 `str` 相同的长度。
- 结果列表 `res` 在最坏的情况下可以包含 n! 个不同的字符串,每个字符串长度为 n,因此 `res` 所需的空间是 O(n * n!)。
- 临时字符串 `temp` 在递归过程中最多会存储 n 个字符,因此它占用 O(n) 的额外空间。

综上所述,算法的空间复杂度主要由结果列表 `res` 决定,即 O(n * n!)。加上其他辅助数据结构所需的空间 O(n),总的额外空间复杂度仍然可以表示为 O(n * n!)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值