【剑指Offer】个人学习笔记_38_字符串的排列

刷题日期:19:0919 星期四2021年4月29日

个人刷题记录,代码收集,来源皆为leetcode

经过多方讨论和请教,现在打算往Java方向发力

主要答题语言为Java

题目:

剑指 Offer 38. 字符串的排列

难度中等257

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

限制:

1 <= s 的长度 <= 8
题目分析

感觉这个更像是数学问题,返回的是字符串数组对象,因此首先得定义一个res,然后返回为null或者一个元素的结果。限制规定了s长度小于等于8,也就限制了返回的最大值。

怀疑最终的排列种类是n的阶乘。关键在于如何穷举里面的所有可能,而且还要考虑把输入的每个元素分开,作为单个字符串再加起来。

初始解答:

这类问题还是适合采用递归的思想。

在每一次都把字符串分为两部分,第一个和后面所有,用第一个和后面所有字符排列。

接着求后面的排列。

对字符串string的操作还不是很熟练,分解问题的思路也有待提高。

返回的是 字符串数组。

看了下评论,解答都很长,带dfs,还是基础不牢,学习方法一。

class Solution {
    public String[] permutation(String s) {
        Set<String> list = new HashSet<>(); //用哈希表来存放结果
        char[] arr = s.toCharArray(); //字符串转换为字符数组
        StringBuilder sb = new StringBuilder(); //字符串
        boolean[] visited = new boolean[arr.length]; //是否重复
        //进入子程序
        dfs(arr, "", visited, list); //深度优先搜索
        return list.toArray(new String[0]); //转换为数组再返回,将list直接转为Object[] 数组;
    }
    public void dfs(char[] arr, String s, boolean[] visited, Set<String> list) {
        if(s.length() == arr.length) {
            list.add(s);
            return;
        }
        for(int i = 0; i < arr.length; i++) {
            if(visited[i]) continue;
            visited[i] = true;
            //将右边括号内的变量 arr[i] 转换成字符串 
            dfs(arr, s + String.valueOf(arr[i]), visited, list);
            visited[i] = false;
        }
    }
}

好多看不懂的操作,百度才知道。执行结果:通过

显示详情

执行用时:56 ms, 在所有 Java 提交中击败了15.89%的用户

内存消耗:44.4 MB, 在所有 Java 提交中击败了13.25%的用户

学习K神

class Solution {
    //外部变量
    List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
        c = s. toCharArray();
        dfs(0);
        return res.toArray(new String[res.size()]);
    }
    void dfs(int x) {
        if(x == c.length - 1) {
            res.add(String.valueOf(c)); // 添加可能方案
            return;
        }
        HashSet<Character> set = new HashSet<>();
        //Character 类用于对单个字符进行操作,Character 类在对象中包装一个基本类型 char 的值
        for(int i = x; i < c.length; i++) {
            if(set.contains(c[i])) continue; //重复了,所以剪枝
            set.add(c[i]);
            swap(i, x); //交换,将c[i] 固定在x位
            dfs(x + 1); //考虑第x+1位
            swap(i, x); //恢复交换
        }
    } 
    void swap(int a, int b) { //交换子程序
        char tmp = c[a];
        c[a] = c[b];
        c[b] = tmp;
    }
}

效率更高了些 执行结果:通过

显示详情

执行用时:14 ms, 在所有 Java 提交中击败了51.57%的用户

内存消耗:43.6 MB, 在所有 Java 提交中击败了40.37%的用户

学习他人:

方法一:

女马了个b 2021-01-30

//我就不明白了,一帮傻吊公众号把排名第一的答案抄一遍加点comment再发上来有什么意思。其实很简单,不需要用到swap,直接用visited记录一下每次已经访问过的char,然后每次for loop 都从头开始,遇到visited直接跳过就可以。

class Solution {
    public String[] permutation(String s) {
        Set<String> list = new HashSet<>();
        char[] arr = s.toCharArray();
        StringBuilder sb = new StringBuilder();
        boolean[] visited = new boolean[arr.length];
        dfs(arr, "", visited, list);
        return list.toArray(new String[0]);
    }
    public void dfs(char[] arr, String s,  boolean[] visited, Set<String> list)
    {
        if(s.length() == arr.length)
        {
            list.add(s);
            return;
        }
        for(int i=0; i<arr.length; i++)
        {
            if(visited[i]) continue;
            visited[i] = true;
            dfs(arr, s+String.valueOf(arr[i]), visited, list);
            visited[i] = false;
        }

    }
}

方法二:

临扬 2021-03-06

javaDFS+剪枝,这题我按照47.全排列2的方法来做的 ps:我发现大佬们在写dfs函数的时候,喜欢把变量定义为全局变量而不是放进函数参数里欸

class Solution {
    public String[] permutation(String s) {
        //对s进行排序
        String [] kk = s.split("");
        Arrays.sort(kk);
        StringBuilder sb = new StringBuilder();
        for(String x:kk) sb.append(x);
        s = sb.toString();

        sb.delete(0,sb.length());
        List<String> list = new LinkedList<>();
        
        boolean [] used = new boolean[s.length()];
        dfs(sb,s,used,list);
        return list.toArray(new String[list.size()]);
    }
    public void dfs(StringBuilder sb,String s,boolean[] used,List<String> list){
        if(sb.length() == s.length()){
            list.add(sb.toString());
            return;
        }
        for(int i=0;i<s.length();i++){
            //与前一个字母相等,且前一个字母标记为false,说明前一个字母已经被回溯过,同级,不能再使用当前字母,否则重复
            if(i>0 && !used[i] && s.charAt(i)==s.charAt(i-1) && !used[i-1]) continue;
            if(!used[i]){
                sb.append(s.charAt(i));
                used[i] = true;
                dfs(sb,s,used,list);
                sb.delete(sb.length()-1,sb.length());
                used[i] = false;
            }
        }
    }
}

方法三:

NekoCharmsL3

2021-03-17

class Solution {
    void dfs(char[] chars, boolean[] visited, StringBuilder buffer, List<String> ans){
        if(buffer.length() == chars.length){
            ans.add(new String(buffer));
            return;
        }
        boolean[] set = new boolean[26];
        for(int i = 0; i < chars.length; i++){
            if(!visited[i] && !set[chars[i] - 'a']){
                set[chars[i] - 'a'] = true; // 表示值为chars[i]的字符已经选择过
                visited[i] = true; // 表示下标i的字符已经选择过
                buffer.append(chars[i]);
                dfs(chars, visited, buffer, ans);
                visited[i] = false;// 回溯状态 重新可选
                buffer.deleteCharAt(buffer.length() - 1);
            }
        }
    }
    public String[] permutation(String s) {
        List<String> ans = new ArrayList<>();
        dfs(s.toCharArray(), new boolean[s.length()], new StringBuilder(), ans);
        return ans.toArray(new String[ans.size()]);
    }
}

class Solution{
    void Swap(char[] chars, int i, int j){
        char tem = chars[i];
        chars[i] = chars[j];
        chars[j] = tem;
    }
    void recursion(char[] chars, int fromIndex, List<String> ans){
        if(fromIndex == chars.length){
            ans.add(new String(chars));
            return;
        }
        boolean[] isAdd = new boolean[26];
        for(int i = fromIndex; i < chars.length; i++){
            if(!isAdd[chars[i] - 'a']){ // 保证交换上来的数是没有出现过的
                isAdd[chars[i] - 'a'] = true;
                Swap(chars, i, fromIndex);
                recursion(chars, fromIndex + 1, ans);
                Swap(chars, i, fromIndex);
            }
        }
    }
    public String[] permutation(String s) {
        List<String> ans = new ArrayList<>();
        recursion(s.toCharArray(), 0, ans);
        return ans.toArray(new String[ans.size()]);
    }
}

方法四:

K神登场

解题思路:
对于一个长度为 nn 的字符串(假设字符互不重复),其排列方案数共有:

n \times (n-1) \times (n-2) … \times 2 \times 1n×(n−1)×(n−2)…×2×1

排列方案的生成:

根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符交换,先固定第 11 位字符( nn 种情况)、再固定第 22 位字符( n-1n−1 种情况)、… 、最后固定第 nn 位字符( 11 种情况)。

重复排列方案与剪枝:

当字符串存在重复字符时,排列方案中也存在重复的排列方案。为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。从 DFS 角度看,此操作称为 “剪枝” 。

作者:jyd
链接:https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/solution/mian-shi-ti-38-zi-fu-chuan-de-pai-lie-hui-su-fa-by/
来源:力扣(LeetCode)

class Solution {
    List<String> res = new LinkedList<>();
    char[] c;
    public String[] permutation(String s) {
        c = s.toCharArray();
        dfs(0);
        return res.toArray(new String[res.size()]);
    }
    void dfs(int x) {
        if(x == c.length - 1) {
            res.add(String.valueOf(c));      // 添加排列方案
            return;
        }
        HashSet<Character> set = new HashSet<>();
        for(int i = x; i < c.length; i++) {
            if(set.contains(c[i])) continue; // 重复,因此剪枝
            set.add(c[i]);
            swap(i, x);                      // 交换,将 c[i] 固定在第 x 位
            dfs(x + 1);                      // 开启固定第 x + 1 位字符
            swap(i, x);                      // 恢复交换
        }
    }
    void swap(int a, int b) {
        char tmp = c[a];
        c[a] = c[b];
        c[b] = tmp;
    }
}
//饭胡啦 (编辑过)2020-04-22
//大佬那个图画的太好了,加上些我的理解,应该是这样子的吧,有错误欢迎指正

class Solution {
    //为了让递归函数添加结果方便,定义到函数之外,这样无需带到递归函数的参数列表中
    List<String> list = new ArrayList<>();
    //同;但是其赋值依赖c,定义声明分开
    char[] c;
    public String[] permutation(String s) {
        c = s.toCharArray();
        //从第一层开始递归
        dfs(0);
        //将字符串数组ArrayList转化为String类型数组
        return list.toArray(new String[list.size()]);
    }

    private void dfs(int x) {
        //当递归函数到达第三层,就返回,因为此时第二第三个位置已经发生了交换
        if (x == c.length - 1) {
            //将字符数组转换为字符串
            list.add(String.valueOf(c));
            return;
        }
        //为了防止同一层递归出现重复元素
        HashSet<Character> set = new HashSet<>();
        //这里就很巧妙了,第一层可以是a,b,c那么就有三种情况,这里i = x,正巧dfs(0),正好i = 0开始
        // 当第二层只有两种情况,dfs(1)i = 1开始
        for (int i = x; i < c.length; i++){
            //发生剪枝,当包含这个元素的时候,直接跳过
            if (set.contains(c[i])){
                continue;
            }
            set.add(c[i]);
            //交换元素,这里很是巧妙,当在第二层dfs(1),x = 1,那么i = 1或者 2, 不是交换1和1,要就是交换1和2
            swap(i,x);
            //进入下一层递归
            dfs(x + 1);
            //返回时交换回来,这样保证到达第1层的时候,一直都是abc。这里捋顺一下,开始一直都是abc,那么第一位置总共就3个交换
            //分别是a与a交换,这个就相当于 x = 0, i = 0;
            //     a与b交换            x = 0, i = 1;
            //     a与c交换            x = 0, i = 2;
            //就相当于上图中开始的三条路径
            //第一个元素固定后,每个引出两条路径,
            //     b与b交换            x = 1, i = 1;
            //     b与c交换            x = 1, i = 2;
            //所以,结合上图,在每条路径上标注上i的值,就会非常容易好理解了
            swap(i,x);
        }
    }

    private void swap(int i, int x) {
        char temp = c[i];
        c[i] = c[x];
        c[x] = temp;
    }
}

Java HashSet

HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。

HashSet 允许有 null 值。

HashSet 是无序的,即不会记录插入的顺序。

HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。

HashSet 实现了 Set 接口。

七、快捷键

代码生成常用的快捷键

(1)使用psvm快捷键生成main方法

(2)使用sout快捷键生成输出语句

(3)使用fori快捷键生成普通for循环

(4)增强for循环的快捷键生成,使用 集合.for的快捷键方式生成

(5)使用ctrl + alt + t快捷键,可以生成流程控制语句。

(6)使用alt+insert快捷键,可以生成构造方法、get和set方法、重写toString()的方法、重写父类的方法等。

IntelliJ IDEA必备快捷键

Ctrl + D

复制光标所在行或复制选择内容,并把复制内容插入光标位置下面

Ctrl + F

在当前文件进行文本查找

Ctrl + H

查看类的继承结构

Ctrl + N

通过类名定位文件

Ctrl + O

快速重写父类方法

Ctrl + X

剪切所选中行

Ctrl + Y

删除光标所在行或删除选中的行

Ctrl + Z

撤销

Ctrl + F12

弹出当前文件结构层,可以在弹出的层上直接输入进行筛选

Ctrl + Space

基础代码补全默认在Windows系统上被输入法占用,需要进行修改,

建议修改为Ctrl + 分号

Ctrl + /

注释光标所在行代码,会根据当前不同文件类型使用不同的注释符号

Ctrl+Shift+/

多行注释,如果按第二次就是反注释

Alt + Enter

根据光标所在问题,提供快速修复选择,用的最多的是生成变量

Ctrl + Alt + B

在某个调用的方法名上使用会跳到具体的实现处

Ctrl + Alt + L

格式化代码 可以对当前文件和整个包目录使用

Ctrl + Alt + T

对选中的代码弹出环绕选项弹出层

Ctrl + Alt + 左方向键

退回到上一个操作的地方,查看源码的时候很方便

Ctrl + Alt + 右方向键

前进到上一个操作的地方,查看源码的时候很方便

Ctrl + Shift + R

根据输入内容替换对应内容,

范围为整个项目或指定目录内文件

Ctrl + Shift + U

对选中的代码进行大/小写轮流转换

Ctrl + Shift + Z

取消撤销,习惯使用ctrl+y,可以修改快捷键

需要先把ctrl+y删除一行的快捷键解绑

Ctrl + Shift+ /

String、StringBuffer、StringBuilder区别

StringBuffer、StringBuilder和String一样,也用来代表字符串。String类是不可变类,任何对String的改变都 会引发新的String对象的生成;StringBuffer则是可变类,任何对它所指代的字符串的改变都不会产生新的对象。既然可变和不可变都有了,为何还有一个StringBuilder呢?相信初期的你,在进行append时,一般都会选择StringBuffer吧!

HashTable是线程安全的,很多方法都是synchronized方法,而HashMap不是线程安全的,但其在单线程程序中的性能比HashTable要高。StringBuffer和StringBuilder类的区别也是如此,他们的原理和操作基本相同,区别在于StringBufferd支持并发操作,线性安全的,适 合多线程中使用。StringBuilder不支持并发操作,线性不安全的,不适合多线程中使用。新引入的StringBuilder类不是线程安全的,但其在单线程中的性能比StringBuffer高。

从上面的结果可以看出,不考虑多线程,采用String对象时(我把Count/100),执行时间比其他两个都要高,而采用StringBuffer对象和采用StringBuilder对象的差别也比较明显。由此可见,如果我们的程序是在单线程下运行,或者是不必考虑到线程同步问题,我们应该优先使用StringBuilder类;如果要保证线程安全,自然是StringBuffer。

从后面List的测试结果可以看出,除了对多线程的支持不一样外,这两个类的使用方式和结果几乎没有任何差别。

总结

以上就是本题的内容和学习过程了,这道题还是比较难的,还涉及到很多其他变量的转换,最近因为生病加补习数据结构相关的课程,刷题进度放慢了。

本来昨天又有华为的机试,最后胆怯了没上,还是好好踏实学习吧,把基础的分类排序二分,表图数链栈都弄清楚再上场。

欢迎讨论,共同进步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值