可变类型与不可变类型:通过回溯算法介绍

总结

可变类型包括以下几种:

  • StringBuffer 和 StringBuilder,这两个类型都是可变的字符串类型。
  • ArrayList 和 LinkedList,这两个类型都是可变的列表类型。
  • HashMap、TreeMap 和 LinkedHashMap,这些类型都是可变的键值对类型。
  • HashSet、TreeSet 和 LinkedHashSet,这些类型都是可变的集合类型。
  • ArrayDeque 和 LinkedList,这两个类型都是可变的队列类型。
  • HashMap、TreeMap 和 LinkedHashMap,这些类型都是可变的映射类型。

不可变类型包括以下几种:

所有的基本类型

  • string类型
  • int
  • 布尔
  • char
  • double
  • long
  • float
  • 数组也是一种不可变类型,在下面的介绍中可以看到的

可变类型与不可变类型在回溯算法中体现:

对于可变类型在回溯算法中一定要恢复到回溯之前的状态,不可变类型则不需要回溯到之前的状态。

一定要切记:可变类型和不可变类型指的是递归函数上参数的类型递归函数内部的变量类型

看下面这段代码,str变量为StringBuffer类型属于可变类型,则需要在append之后deleteCharAt。
而leftNum和rightNum是int类型属于不可变类型,在+1之后并没有-1

    public void search(List<String> result,StringBuffer str,int leftNum,int rightNum,int n){
        if(str.length() == n*2){
            result.add(new String(str));
            return;
        }
        if(leftNum<n){
            search(result,str.append("("),leftNum+1,rightNum,n);
            str.deleteCharAt(str.length()-1);
        }
        if(leftNum>rightNum && rightNum<n){
            search(result,str.append(")"),leftNum,rightNum+1,n);
            str.deleteCharAt(str.length()-1);
        }
    }

正文

使用java语句实现有效括号的生产算法,代码如下,使用回溯实现,哪里有问题呢?

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        search(result,new StringBuffer(),0,0,n);
        return result;
    }
    public void search(List<String> result,StringBuffer str,int leftNum,int rightNum,int n){
        if(str.length() == n*2){
            result.add(new String(str));
            return;
        }
        if(leftNum<n){
            search(result,str.append("("),leftNum+1,rightNum,n);
            str.deleteCharAt(str.length()-1);
        }
        if(leftNum>rightNum && rightNum<n){
            search(result,str.append(")"),leftNum,rightNum+1,n);
            str.deleteCharAt(str.length()-1);
        }
    }

}

您的代码几乎正确,但需要注意的一些问题是:

  1. StringBuffer 对象是可变的,因此需要注意传递引用的情况,特别是当您递归调用 search 函数时。

  2. 在递归调用 search 函数时,您应该使用 str.deleteCharAt(str.length()-1) 删除添加的字符,以便在回溯时回到之前的状态。

  3. leftNum 等于 n 时,只需要添加右括号,因为这是唯一的选择。

  4. 您可以使用字符数组代替 StringBuffer,这样可以更容易地进行字符的添加和删除操作。

下面是更新后的代码:

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        search(result,new StringBuffer(),0,0,n);
        return result;
    }
    public void search(List<String> result,StringBuffer str,int leftNum,int rightNum,int n){
        if(str.length() == n*2){
            result.add(new String(str));
            return;
        }
        if(leftNum<n){
            search(result,str.append("("),leftNum+1,rightNum,n);
            str.deleteCharAt(str.length()-1);
        }
        if(leftNum>rightNum && rightNum<n){
            search(result,str.append(")"),leftNum,rightNum+1,n);
            str.deleteCharAt(str.length()-1);
        }
    }

}

或者

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        char[] chars = new char[n*2];
        search(result, chars, 0, 0, 0, n);
        return result;
    }

    public void search(List<String> result, char[] chars, int pos, int leftNum, int rightNum, int n) {
        if (pos == n * 2) {
            result.add(new String(chars));
            return;
        }
        if (leftNum < n) {
            chars[pos] = '(';
            search(result, chars, pos+1, leftNum+1, rightNum, n);
        }
        if (leftNum > rightNum) {
            chars[pos] = ')';
            search(result, chars, pos+1, leftNum, rightNum+1, n);
        }
    }
}` 

这个算法使用回溯法生成有效的括号序列,它的时间复杂度为 O(4^n / \sqrt{n}),因为对于每个位置,我们有两种选择:插入左括号或右括号。在最坏的情况下,我们需要插入 n 个左括号和 n 个右括号,因此总共有 2n 个位置,每个位置有两种选择。此外,由于我们只在满足一定条件时才进行递归,因此回溯法可以在相当短的时间内生成所有有效的括号序列。


为什么使用字符数组的话就不需要恢复到回溯之前的状态呢?不也应该删除add的元素吗

实际上,在使用字符数组时,我们仍然需要恢复到回溯之前的状态。与使用 StringBuffer 不同,字符数组是不可变的,因此我们需要将每个字符添加到正确的位置,并在回溯时将其删除。因此,在递归调用 search 函数之前,我们将字符添加到字符数组中的正确位置,并将位置向前移动一位。在递归调用返回时,我们需要将位置向后移动一位,并将添加的字符删除,以恢复到回溯之前的状态。这是因为我们不想在下一次递归调用中重复使用这个字符。

下面是修改后的代码:

class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        char[] chars = new char[n*2];
        search(result, chars, 0, 0, 0, n);
        return result;
    }

    public void search(List<String> result, char[] chars, int pos, int leftNum, int rightNum, int n) {
        if (pos == n * 2) {
            result.add(new String(chars));
            return;
        }
        if (leftNum < n) {
            chars[pos] = '(';
            search(result, chars, pos+1, leftNum+1, rightNum, n);
            chars[pos] = '\0';
        }
        if (leftNum > rightNum) {
            chars[pos] = ')';
            search(result, chars, pos+1, leftNum, rightNum+1, n);
            chars[pos] = '\0';
        }
    }
}

注意,我们在每个分支中使用 chars[pos] = '\0' 将添加的字符删除,以恢复到回溯之前的状态。


与使用 StringBuffer 不同,字符数组是不可变的,这个不可变是指什么呢?

对于 Java 中的字符数组 char[],其大小是固定的,即一旦创建就无法改变。也就是说,我们不能向现有的字符数组中添加或删除元素,而只能覆盖已经存在的元素。这就是字符数组不可变的含义。

相比之下,StringBufferStringBuilder 可以被动态地扩展,我们可以通过调用它们的方法(如 append())向字符串缓冲区中添加新的字符或字符串,也可以通过调用方法(如 delete()replace())删除或替换已经存在的字符或字符串。因此,这些字符串缓冲区可以看作是可变的。

在使用回溯算法时,我们通常需要在每个递归调用之间维护变量的状态,以便正确地恢复到之前的状态。由于字符数组是不可变的,我们必须创建新的字符数组来保存每个状态,而不能像 StringBuffer 一样直接在原始字符串缓冲区上进行修改。


class Solution {
    public List<String> generateParenthesis(int n) {
        List<String> result = new ArrayList<>();
        char[] chars = new char[n*2];
        search(result, chars, 0, 0, 0, n);
        return result;
    }

    public void search(List<String> result, char[] chars, int pos, int leftNum, int rightNum, int n) {
        if (pos == n * 2) {
            result.add(new String(chars));
            return;
        }
        if (leftNum < n) {
            chars[pos] = '(';
            search(result, chars, pos+1, leftNum+1, rightNum, n);
        }
        if (leftNum > rightNum) {
            chars[pos] = ')';
            search(result, chars, pos+1, leftNum, rightNum+1, n);
        }
    }
}`

这段代码,在添加chars[pos]='(',也没有删除'('啊,为什么也能正确执行呢?

这是因为在回溯算法的实现中,每次递归返回时,都会将添加到字符数组中的字符删除,这样就不需要手动删除字符了。在代码中,当我们在字符数组的 pos 位置添加一个字符后,在递归调用 search 函数时,会将 pos 向前移动一位,也就是将下一个字符添加到 pos+1 的位置上。在递归返回时,我们会将 pos 向后移动一位,并将添加到 pos 位置的字符删除,以恢复到回溯之前的状态。这样,我们就能在不删除字符的情况下,正确地执行回溯算法了。


除了StringBuffer之外,哪些类型还是可变的呢?回溯回来之后要恢复到原来的状态

除了 StringBufferStringBuilder 之外,Java 还提供了一些其他的可变类型,比如 ArrayListLinkedListHashMapHashSet 等。这些类型都支持动态地添加、删除、修改和查询元素,因此被称为可变类型。

在使用回溯算法时,我们需要在每个递归调用之间维护变量的状态,以便正确地恢复到之前的状态。对于这些可变类型,我们需要特别注意回溯操作,以保证回溯时状态的正确性。一般来说,我们需要在递归之前将当前状态保存下来,然后在递归返回时将状态恢复到之前的状态。对于不同的类型,具体的保存和恢复操作可能有所不同,需要根据具体情况进行调整。


将这些可变类型全部列举下,并且全部列举出哪些类型是不可变的

Java 中常用的可变类型包括:

  1. StringBufferStringBuilder,这两个类型都是可变的字符串类型。
  2. ArrayListLinkedList,这两个类型都是可变的列表类型。
  3. HashMapTreeMapLinkedHashMap,这些类型都是可变的键值对类型。
  4. HashSetTreeSetLinkedHashSet,这些类型都是可变的集合类型。
  5. ArrayDequeLinkedList,这两个类型都是可变的队列类型。
  6. HashMapTreeMapLinkedHashMap,这些类型都是可变的映射类型。

除了 String 类型和基本数据类型(如 intboolean 等)之外,Java 中的大多数类型都是可变的。

不可变的类型包括:

  1. StringString 类型是不可变的字符串类型。
  2. 基本数据类型(如 intboolean 等)都是不可变的。


数组是可变的吗?

在 Java 中,数组是一种固定长度的数据结构,一旦创建后就不能改变其长度。因此,从长度的角度来看,数组是不可变的。但是,我们可以通过修改数组中的元素来实现对数组的修改,因此,从元素的角度来看,数组是可变的。


那是不是可以说数组在回溯时,其实不需要恢复到恢复之前的状态呢?因为上边的char[]并没有恢复到回溯之前的状态?

是的,在回溯算法中,对于可变类型的数据结构,通常需要恢复到回溯之前的状态,以确保后续的搜索不会受到前面搜索的影响。但对于数组这样的可变类型,由于数组本身就是可修改的,所以不需要额外的恢复操作。在回溯过程中,对数组元素的修改会直接反映在原数组上,所以不需要将其恢复到之前的状态。


确定吗?数组在回溯过程中是不需要恢复到之前的状态的

是的,对于可变类型的数据结构,需要在回溯过程中恢复到之前的状态,以确保后续的搜索不会受到前面搜索的影响。但对于数组这样的可变类型,由于数组本身就是可修改的,所以不需要额外的恢复操作。在回溯过程中,对数组元素的修改会直接反映在原数组上,所以不需要将其恢复到之前的状态。因此,可以肯定地说,数组在回溯过程中不需要恢复到之前的状态。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容