JAVA的各种变量中,开发用的最多的是字符串,字符串的相关操作比较重要。有关字符串的算法题也较多,IT笔试面试中对字符串是肯定会有所考察的。

下面来说一下前段时间看到的字符串反转操作相关算法题,题中有些问题值得深思,对此给出了自己的看法,如果有什么疑问或者问题请留言。



1. StringBulider 实现

public String reverse(String str) {
            if((null== str) || (str.length()  <=1)) {
                return str;
            }
            StringBuffer result =new StringBuffer(str);
            for(int i =0; i < (str.length() /2); i++) {
                int swapIndex = str.length() -1- i;
                char swap = result.charAt(swapIndex);
                result.setCharAt(swapIndex, result.charAt(i));
                result.setCharAt(i, swap);
            }
            return result.toString();
        }


2. char数组实现

public String reverse(String str) {
          if((null== str) || (str.length() <=1)) {
              Return str;
          }
          char[] chars = str.toCharArray();
          int right = chars.length -1;
          for(int left =0; left < right; left++) {
              Char swap = chars[left];
              chars[left] = chars[right];
              chars[right--] = swap;
          }
          return new String(chars);
      }


3. SringBuffer方法

public String reverse(String str) {
          if((null== str) || (str.length() <=1)) {
              Return str;
          }
          StringBuffer reverse =new StringBuffer(str.length());
          for(int i = str.length() -1; i >=0; i--) {
            reverse.append(str.charAt(i));
          }
          return reverse.toString();
      }
  }



4.用相关接口来实现

//<StringBuffer 实现了此接口>
  public interface Reverser {
    public String reverse(String str);
}

/*
Java中,最好的实现就是用JDK中StringBuffer的反转方法,它不仅速度快,
效率高,而且还知道如何处理unicode代理对(surrogate pairs)。
其实现方法实际上是实现了Reverse接口的。
*/
public class JdkReverser implements Reverser {
     public String reverse(String str) {
           if((null== str) || (str.length() <=1)) {
                Return str;
            }
        return new StringBuffer(str).reverse().toString();
        }
  }


5.递归(Recursion) 实现

public String reverse(String str) {
        if((null== str) || (str.length()  <=1)) {
               return str;
          }
     return reverse(str.substring(1)) + str.charAt(0);
}


实际上见得最多的应该就是3,4,5了。毕竟这三个可以从效率,以及概念等方面去考察,也能反映出一个人的基础是否扎实。


6.在此提出几个关于递归的问题。

① 递归方案的效率?


② 什么叫(Tail)递归?


③ +”操作的效率如何?和“+=”比较呢?


④ 关于为什么String都是不可变的(至少在大多时候)?


⑤ 反转“Stephan”时,有多少个字符串对象创建?

简单分析:
①. 递归调用实际上是函数自己在调用自己,而函数的调用开销是很大的,系统要为每次函数调用分配存储空间,并将调用点压栈予以记录。而在函数调用结束后,还要释放空间,弹栈恢复断点。所以说,函数调用不仅浪费空间,还浪费时间。相比于递归来说,迭代只需要进行n次迭代,递归的效率是比较低的。需要注意的是迭代并不适合所有的场合。

②. 如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。

当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。

尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。

尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去。

尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,深层函数所面对的不是越来越简单的问题,而是越来越复杂的问题——因为参数里带有前面若干步的运算路径。对于阶乘而言,越深并不意味着越复杂。

传统递归越深,距离目标越近;尾递归越深,距离起点越远。

③在所有字符串拼接中,+ 和+=是效率最低的(通常情况下是这样认为的)。一般2种方式的效率是差不多的。

" += " 操作不改变原数据类型,这里会用到new 来创建一个temp临时字符串变量来存储中间值,用原字符串+temp之后在赋给原串,相对于+来说,效率较低。

下面给出相关的其他字符串拼接分析:

在字符串数量确定的情况下,String.Concat的性能要高于StringBuilder而且其实字符串数量越多,差距越明显。<ref 字符串性能分析>

一般情况下, StringBulid > StringBuffer > concat > +

   并不是所有的String字符串操作都会比StringBuffer慢,在某些特殊的情况下,String字符串的拼接会被JVM解析成StringBuilder对象拼接,在这种情况下String的速度比StringBuffer的速度快。如:

     String name = ”I  ” + ”am ” + ”chenssy ” ;

     StringBuffer name = new StringBuffer(”I ”).append(” am ”).append(” chenssy ”);

对于“+”,它每次拼接都会创建一个StringBuilder对象,并且还要调用toString()方法将其转换为字符串,这样性能就大大降低了。

对于concat,每次的concat操作都会创建一个String对象,这样就会让concat的速度慢下来。

String:在字符串不经常变化的场景中可以使用String类,如:常量的声明、少量的变量运算等。

     StringBuffer:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。

     StringBuilder:在频繁进行字符串的运算(拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,如SQL语句的拼装、JSON封装等(貌似这两个我也是使用|StringBuffer)。

String的成员变量是private final的,也就是初始化之后不可改变。通常我们是不能改变String的。

     为什么要用大多情况下呢?这里是说,如果用反射,可以反射出String对象中的私有属性value,进而改变通过获得的value引用改变数组的结构。从而通过value来改变String。具体见<Java中的String为什么是不可变的?>

⑤可以从下表中结合递归来分析,

7


n


6


an


5


han


4


phan


3


ephan


2


tephan


1


Stephan

首先进栈操作,顺序如表中顺序,因为是递归,所以第一次是最长的,取首字符之后,通过new来创建子串,然后依次压入栈中,最后栈顶为n。

然后才开始输出,取每个字串的首字符,如表中第2列中标色字符所示。所以是创建了7-1 = 6个对象,即new了6次。

下面给出尾递归的一种实现

public class TestTailReverse {
    public static void main(String[] args) {
        String string = "Stephan";
        int len = string.length();
        System.out.println(reverse(string, "",len));
    }
    public static String reverse(String str, String des,int len) {
        des = des + str.charAt(str.length() - 1);
        return len == 1 ? des : reverse(
                str.substring(0, len - 1), des,--len);
    }
}