Java 空间复杂性示例

Java 空间复杂性示例,java,algorithm,big-o,space-complexity,Java,Algorithm,Big O,Space Complexity,所以我想知道,当对象(或原语)在for循环中创建时,这对空间复杂性有何影响 例如,下面是一个代码示例: public boolean checkUnique(String p){ int term = -1; int len = p.length(); for (int i =0; i<len; i++) { char c = p.charAt(i); StringBuilder sb = new StringBuilder(p.su

所以我想知道,当对象(或原语)在for循环中创建时,这对空间复杂性有何影响

例如,下面是一个代码示例:

public boolean checkUnique(String p){
    int term = -1;
    int len = p.length();
    for (int i =0; i<len; i++) {
        char c = p.charAt(i);
        StringBuilder sb = new StringBuilder(p.substring(0, i));
        sb.append(p.substring(i+1, len));
        String str = sb.toString();
        if (str.indexOf(c) != term) {
            return false; 
        }
    }
    return true;
}

但该算法具有O(1)空间复杂度。那么我的理解一定是错误的?在这种情况下,checkUnique算法的空间复杂度也为O(1)

为了进行复杂性分析,您必须非常清楚您的机器是如何工作的。机器将如何运行您的代码?机器的功能是什么

运行该代码的机器至少有两种非常相似的工作方式,每种方式都会对您的问题给出不同的答案

假设每个新变量声明都会导致分配一个唯一的内存位,并且一旦分配,该内存就无法重用。这可能像磁带存储器,或者像你用墨水在纸上写下步骤一样。如果这样做,空间复杂度将与循环迭代次数成正比,因为您在循环体中分配内存

相反,假设新的变量声明使用分配的第一个可用内存位,并且一旦变量超出范围,该内存就会被释放并自由地重新分配。在这种情况下,到函数结束时,除了常量之外,所有变量都超出了范围,因此空间复杂度是常量

Java具有自动垃圾收集功能,因此我们可以合理地说,我们处于第二种情况,即使是w.r.t.堆分配内存(堆栈分配内存,与原语一样,肯定以第二种方式工作)。事实上,垃圾收集并非在所有情况下都是瞬间发生的,因此我们可能处于两种情况之间。但在令人高兴的情况下,我们可以有把握地说,在Java中,这是O(1)

在C++中,故事会有所不同。在那里,我们需要
new
delete
(或等效的)堆分配内存,以便在第二种场景中使用;否则,我们会在第一


正如您所看到的,这在很大程度上取决于代码的真正含义,而这只能从它所执行的系统的角度来完全理解。

我把算法的这种实现非常糟糕的事实放在一边

你说:

迭代次数与输入大小相等,在每次迭代中,我们创建一个StringBuilder对象

到目前为止还不错

。。。因此,我们正在创建与输入大小成比例的StringBuilder对象

是的,这也是真的。但是您没有将这些创建的对象从一个迭代保留到另一个迭代。它们逐渐消失了

事实上,编译器可能检测范围仅限于循环体的对象,并优化内存使用情况,使其始终处于使用的位置(可以是代码中小型对象(如
c
)的寄存器)

总之,如果编译器工作正常,那么您的算法是O(1)


如果在每次迭代中,将
c
str
放在一个列表中,情况会有所不同。

我将回顾此算法中的错误设计决策,并提出更好的建议

现有答案很好地回答了复杂性类问题,但不要指出问题中的错误:您的空间复杂性为O(N),因为您复制了整个输入(减去一个字符)

如果您的循环保存在每次迭代的临时副本上,那么空间复杂度将与时间复杂度相匹配:O(N*M),其中M是包含重复项的最短前缀的长度。(如果没有重复项,则为
M=N

鸽子洞原则确保了M字符的最大值。(或对于代码点,
input.length()>Character.MAX\u code\u POINT
,即1114111)

如果您的大部分输入是ASCII码,那么M最多将接近80。实际上,大多数语言不使用许多不同的代码点,即使范围不是从0开始。我认为非字母语言可能有几千个字形。但无论如何,关键是,检查字符串早期部分的重复是有用的,而不是在第一个字符恰好是唯一的情况下扫描整个潜在的巨大字符串

剧透:到目前为止,在剧集中添加角色是找到重复的最好方法。O(M)时间和空间,具有低恒定因子开销


除了性能,请记住Java
char
s是utf16,但是。对于Java来说,两个世界中最糟糕的都是它,这真的很不幸:ASCII的空间使用率是utf8的两倍,但仍然需要处理多元素“字符”。在设计Java时,16位足以容纳任何Unicode字符,因此utf16确实避免了utf8等多字节字符编码的困难。宽字符流行了一段时间,也许现在仍然在Windows、IDK上。Unix/POSIX/Internet协议在utf8上几乎实现了标准化

它看起来像是和你在一起

循环i=0..N并在(i)处执行
codepoint可能必须在每次迭代中从字符串的开头进行扫描,以找到代理项对。聪明的JVM可能会注意到冗余和优化,但我不会指望它


原算法分析 这种基本设计,即在每个字符上循环,并检查所有其他字符,很有意义,也很容易想到。有很多冗余的工作(当我们已经知道
a!=b
时,检查
a==c
b==c
),所以它将是O(N2)(参见下面的diff算法),但是我们可以用比您的版本少得多的恒定开销来实现它

  • 循环原始字符串的字符。没有什么问题,在
    i
    位置获取字符非常便宜

  • 制作包含所有cha的输入字符串的临时副本
    int val = str.charAt(i);
    
    int cp = str.codePointAt(pos);
    pos+=Character.charCount(cp);
    
       final int fastlen = 256;
       int i=0;
       while (++i < fastlen) {
           char c = p.charAt(i);
           if (p.lastIndexOf(c, fastlen) != i) return true;
             // maybe lastIndexOf(c, i + fastlen)?  We're going to repeat work anyway, so what's a little more?
       }
       // i == fastlen if we haven't returned yet
       for ( ; i < N ; i++ ){
           char c = p.charAt(i);
           if (p.lastIndexOf(c, fastlen) != -1 ||
               p.indexOf(c, i + 1) != -1 )
               return true;
       }
    
     for (final char c : myarray) { // loop over chars
         // add c to a HashSet<char>.  If it was already present, return true
     }
     return false;