【程序员面试金典】数组与字符串

来源:互联网 发布:女朋友生气的原因 知乎 编辑:程序博客网 时间:2024/05/01 01:05

        今天开始看《程序员面试金典》这本书,这本书里面的算法题还是挺好的,能学到很多新的思想,解决问题的方式。我会针对里面的内容,总结下算法题的解题方法和代码的实现,有些题我会加上自己的思考,提供不止一种解法以飨读者。大家如果有新的解题方法或者新的想法,欢迎给我留言,互相交流,共同进步!

        下面先看看第一部分,数组和字符串中的算法题:

1.问题一

       实现一个算法,确定一个字符串的所有字符是否全部都不同。假设不允许使用额外的数据结构,又该如何处理?

        我们假设字符集为ASCII。因为ASCII有256个字符,如果给定的字符串中的字符数大于256,那肯定有重复的,所以可以首先做个判断。对于这道题,看到第一眼可能会想到,从第一个字符开始逐个向后比较即可(我第一反应就是这样想的……凡人的特点),这种算法的时间复杂度为O(N2),空间复杂度为O(1),这种算法无疑是最差的,但是也列一下代码吧……

private static boolean isAllDifferent1(String str) {if(str.length() > 256) return false;for(int i = 0; i < str.length(); i++) {for(int j = i+1; j < str.length(); j++) {if(str.charAt(i) == str.charAt(j))return false;}}return true;}
        我们来看看第二种算法,这种算法需要一个boolean型数组,索引值i对应的标记值表示该字符串是否含有ASCII码中第i个字符。若这个字符第二次出现,立马返回false,这种方法比较巧妙,但是需要额外的数据结构和存储空间,如boolean型数组。时间复杂度为O(N),空间复杂度为O(1)。下面我们看看这种算法的具体实现:
private static boolean isAllDifferent2(String str) {if(str.length() > 256) return false;boolean[] char_set = new boolean[256];//创建一个boolean型数组for(int i = 0; i < str.length(); i++) {//获得str中第i个字符在ASCII码中的位置。//charAt(i)表示str中第i个字符,然后赋给int变量会转换成在ASCII码中的位置int val = str.charAt(i);if(char_set[val]) //如果数组中对应的该位置为true,表示已经出现过该字符return false;char_set[val] = true;//否则,将数组中对应的该位置置为true,表示出现了该字符}return true;}
        接下来看看第三种方法,这种方法也是效率最高的,而且不需要额外的数据结构。该方法的原理与方法二类似,但是它是使用位向量来解决的,可以将占用空间减少到方法二的1/8,因为只需要一个int变量(32位)。而且移位运算的效率很高。我们看看具体的实现代码:
private static boolean isAllDifferent3(String str) {if(str.length() > 256) return false;int checker = 0;for(int i = 0; i < str.length(); i++) {int val = str.charAt(i);//找到str中第i个字符在ASCII码中的位置valif((checker & (1 << val)) > 0) {//将1左移val位,再与checher做&运算return false; //如果此位已经有1,说明已经出现过该字符,结果就>0,返回false}checker |= (1 << val);//如果&结果为0说明此位是0,没有出现过该字符,将此位置1}return true;}
        若允许修改字符串,那么还可以在O(NlogN)时间里对字符串进行排序,然后先行检查其中有无相邻字符完全相同的情况,不过,值得注意的是,很多排序算法会占用额外的空间。

2.问题二

       给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。

        根据题目要求,组成这两个字符串的字符应该是一模一样的才对。那么既然这样,我们有两种方法可以处理。

        第一种方法:排序字符串。思路:先对两个字符串进行排序,如果排序结果相同,肯定符合要求,否则不符合要求。这里先将字符串转化为字符数组,然后利用Arrays.sort方法对字符数组进行排序,该方法对基本类型用的是快速排序,对对象类型用的是归并排序,时间复杂度为O(NlogN)。该算法在某种程度上可能不算最优,但是它清晰易懂,实践角度来看,可能是解决该问题的上佳之选。下面看一下代码实现:

private static boolean permutation1(String s, String t) {if(s.length() != t.length()) //如果两者长度不同,肯定不满足条件return false;return sort(s).equals(sort(t));//判断排序后两者是否一样}private static String sort(String s) {char[] array = s.toCharArray();java.util.Arrays.sort(array);return new String(array);}
        第二种方法:检查字符串中各字符数是否相同。换句话说,如果字符串是两个单词,那么组成这个单词应该一模一样,即各字符数应该相同。该算法的时间复杂度为O(N)。下面看一下代码实现:

private static boolean permutation2(String s, String t) {if(s.length() != t.length()) return false;int[] letters = new int[256];//假设为ASCII码char[] s_array = s.toCharArray();//先把s转换成数组for(char c : s_array) {letters[c]++; //对s中出现的每个字符及其数量进行统计}for(int i = 0; i < t.length(); i++) {int c = (int) t.charAt(i);//找到t中每个字符的位置if(--letters[c] < 0) { //只要将letter数组中该位置存储的值减1,如果小于0,说明t中该字符的数量多于sreturn false;}}return true;}
        第二种方法很巧妙,它的思想跟问题一中使用的本质上是一样的,只不过这里数组中存储的是int型(字符数量),问题一种数组存储的是boolean型变量(是或非)。

3.问题三

       请编写一个方法,将字符串中的空格全部替换为“%20”。假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实长度。

       这题不是很难,可以实现的方法比较多,这里主要列举两个,并附上一个比较“耍牛氓”的方法……

       第一种方法:使用StringBuffer。思路:遍历原始string中的每个字符,如果不是' ',就添加到StringBuffer中,如果是' ',就往StringBuffer中添加“%20”。该算法比较简单,代码实现如下:

private static String replaceSpace1(String iniString, int length) {    StringBuffer bf = new StringBuffer();    for(int i = 0; i < length ; i ++) {        if(iniString.charAt(i) == ' ') {            bf.append("%20");        } else {            bf.append(iniString.charAt(i));        }    }    return bf.toString();}
        第二种方法:使用数组。思路如下:

        1. 先遍历原来的string,计算出共有多少个' ',然后算出转变后string的长度newLength;

        2. 用newLength初始化一个新的字符数组array,将原string中的字符先放到array中;

        3.从尾部开始遍历原来的string,如果不是' ',就将array中相应的位置字符往尾部存,两个数组下标同时减小;如果是' ',在array中目前的位置向前依次添加0,2,%

        具体代码如下:

private static String replaceSpace2(String iniString, int length) {    int spaceCount = 0; //记录空格数    for(int i = 0; i < length; i ++) {        if(iniString.charAt(i) == ' ')            spaceCount++;    }    int newLength = length + spaceCount * 2; //计算转变之后的数组大小    char[] array = new char[newLength]; //新建一个新的数组    for(int i = 0; i < length; i ++) {    array[i] = iniString.charAt(i); //先将原来的数组原封不动移到新的数组中去    }     int originalIndex = length - 1;    int newIndex = newLength - 1; //两个数组都从尾部开始走    while(originalIndex >= 0 && newIndex > originalIndex) { //新数组的下标必须必旧数组要大,因为新数组中会添加字符    if(array[originalIndex] == ' ') { //如果旧数组当前位置为' '    array[newIndex--] = '0'; //则向新数组对应位置依次向前加上0,2,%    array[newIndex--] = '2';    array[newIndex--] = '%';    } else {    array[newIndex--] = array[originalIndex]; //否则直接在新数组的位置添加旧数组的值    }    --originalIndex;    }    return new String(array);}
        第三种方法比较耍牛氓,如果可以使用String现有的API的话……那我就不客气啦……

private static String replaceSpace3(String iniString, int length) {return iniString.replaceAll(" ", "%20");}

        额……一句代码就可以了……

4.问题四

       利用字符重复出现的次数,编写一个方法,实现基本的字符串压缩功能。比如:字符串aabccccaaa会变成a2b1c4a3。若压缩后的字符串没有变短,则返回原先的字符串

        这道题也不是很难,这里给出两种解决方法:

        第一种方法:使用StringBuffer。思路:从字符串第二项开始,与前一项进行比较,如果相同继续往下走,如果不同,将前面出现的字符和次数依次放到StringBuffer中。该算法时间复杂度和空间复杂度均为O(N)。代码如下:

private static String compressSameChar1(String str) {StringBuffer bf = new StringBuffer();char last = str.charAt(0);int count = 1; //记录出现的次数for(int i = 1; i < str.length(); i ++ ) {if(str.charAt(i) == last) {count++;} else {bf.append(last);bf.append(count);last = str.charAt(i);count = 1;}}//把最后一组重复字符加进StringBuffer中bf.append(last);bf.append(count);if(bf.length() >= str.length())return str;return bf.toString();}
        第二种方法:使用数组也可以高效的实现,只是代码量稍微大一点,不过思想和使用StringBuffer一样。思路:首先检查压缩后的字符串的长度,然后用该长度初始化一个字符数组,在该数组中存入题目要求的结果。该算法的时间和空间复杂度均为O(N)。代码实现如下:

private static String compressSameChar2(String str) {int size = countCompression(str);//计算压缩后字符的长度if(size >= str.length())return str;char[] array = new char[size];int index = 0;char last = str.charAt(0);int count = 1;for(int i = 1; i < str.length(); i ++) {if(str.charAt(i) == last) {count++;} else {index = setChar(array, last, index, count);//存入字符和出现的次数,返回下一个存储的索引位置last = str.charAt(i);count = 1;}}//最后一组重复字符串index = setChar(array, last, index, count);if(array.length >= str.length()) return str;return String.valueOf(array);}private static int setChar(char[] array, char last, int index, int count) {array[index++] = last;//将数目转换成字符串,再转换成字符数组char[] cnt = String.valueOf(count).toCharArray();//从最大的数字到最小的,复制字符for(char c : cnt) {array[index++] = c;}return index;}private static int countCompression(String str) {if(str == null | str.isEmpty()) return 0;char last = str.charAt(0);int size = 0;int count = 1;for(int i = 1; i < str.length(); i ++) {if(str.charAt(i) == last) {count++;} else {last = str.charAt(i);size += 1 + String.valueOf(count).length(); //1个字符+字符出现次数count = 1;}}size += 1 + String.valueOf(count).length();return size;}

5.问题五

       给定一幅由N*N矩阵表示的图像,其中每个元素的大小为4字节,编写一个方法,将图像旋转90度,要求不能占用额外的内存空间。

        这道题要复杂点,矩阵即二维数组,要将矩阵旋转90度,最简单做法就是一层一层的进行旋转,将上边的移到右边,右边移到下边,下边移到左边,左边移到上边。那么该如何交换这四条边是算法的核心。一种做法就是把上边复制到一个数组中,然后将左边移到上边,下边移到左边,等等。但是这需要O(N)的存储空间。如果不能额外消耗存储空间,我们可以按索引一个一个进行交换,交换的过程还是类似,从最外面一层开始逐渐向里在每一层上执行上述交换。该算法的时间复杂度为O(N2),但这已经是最优的做法了,因为任何算法都需要访问所有的N*N个元素。下面是算法的实现:

private static int[][] rotate(int[][] matrix, int length) {for(int layer = 0; layer < length / 2; ++layer) {int first = layer;int last = length - 1 - layer;for(int i = first; i < last; ++i) {//横着i时表示列,竖着时i表示行int offset = i - first; //offset表示偏移量int top = matrix[first][i]; //先保存上边matrix[first][i] = matrix[last-offset][first];//左到上matrix[last-offset][first] = matrix[last][last-offset]; //下到左matrix[last][last-offset] = matrix[i][last];//右到下matrix[i][last] = top; //上到右}}return matrix;}
       这个算法的代码看起来有点绕,但是只要仔细阅读一下,画个示意图,还是很容易理解的。

6.问题六

       编写一个算法,若M*N矩阵中某个元素为0,则将其所在的行与列清零。

        拿到这个题目可能有个误区,以为很简单,直接遍历整个矩阵,只要发现0元素,就将其所在的行与列清零。这就进入了陷阱……因为在读取被清零的行或列时,读到的全是0,于是又开始对行列清零……于是就完了……

        正确的做法是:新建一个矩阵标记零元素的位置,然后在第二次遍历矩阵时将零元素所在的行和列清零。这种算法的空间复杂度为O(MN)。但是这不是一个非常好的算法,因为完全不需要占用O(MN)个空间,因为我们不需要准确的知道具体哪一行哪一列元素为0,只要知道哪一行,就可以把整行清空,列也是。下面是这种算法的实现,用了两个数组分别标记包含零元素的所有行和列。

private static int[][] setZeros(int[][] matrix) {boolean[] row = new boolean[matrix.length]; //matrix.length表示行数boolean[] column = new boolean[matrix[0].length];//matrix[0].length表示列数//第一次for循环,找到所有0元素所在的行和列for(int i = 0; i < matrix.length; i++) {for(int j = 0; j < matrix[0].length; j++) {if(matrix[i][j] == 0) {row[i] = true;column[j] = true;}}}//第二次for循环删除所有0元素所在的行和列for(int i = 0; i < matrix.length; i++) {for(int j = 0; j < matrix[0].length; j++) {if(row[i] || column[j]) { //两个条件满足一个即可将该位置的元素清零matrix[i][j] = 0;}}}return matrix;}
        为了提高空间利用率,我们也可以用位向量代替boolean型数组,道理是一样的,但是位向量的效率和空间利用率更高。看下面代码:
private static int[][] setZeros2(int[][] matrix) {int row = 0, column = 0;for(int i = 0; i < matrix.length; i++) {for(int j = 0; j < matrix[0].length; j++) {if(matrix[i][j] == 0) {row |= 1 << i; //相应的位置1column |= 1 << j;}}}for(int i = 0; i < matrix.length; i++) {for(int j = 0; j < matrix[0].length; j++) {if((row & (1 << i)) > 0 || (column & (1 << j)) > 0) {matrix[i][j] = 0;}}}return matrix;}
        相信大家应该能看的出来,只要能用boolean型数组存储标记的,基本都可以用位向量来解决,熟练使用位向量不仅很巧妙,而且效率会更高。

        数组和字符串部分的算法题就分析到这,如有问题指出,欢迎留言指正,针对上面几道算法题,如果大家有新的思路,欢迎提出,互相交流,共同进步~

_____________________________________________________________________________________________________________________________________________________

-----乐于分享,共同进步!

-----更多文章请看:http://blog.csdn.net/eson_15
4 0
原创粉丝点击