本文为博主原创文章,转载请注明出处: http://blog.csdn.net/eson_15/article/details/51174168
今天开始看《程序员面试金典》这本书,这本书里面的算法题还是挺好的,能学到很多新的思想,解决问题的方式。我会针对里面的内容,总结下算法题的解题方法和代码的实现,有些题我会加上自己的思考,提供不止一种解法以飨读者。大家在阅读的时候如果有新的解题方法或者新的想法,欢迎给我留言,互相交流,共同进步!
下面先看看第一部分,数组和字符串中的算法题:
实现一个算法,确定一个字符串的所有字符是否全部都不同。假设不允许使用额外的数据结构,又该如何处理?
我们假设字符集为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码中的位置val if((checker & (1 << val)) > 0) {//将1左移val位,再与checher做&运算 return false; //如果此位已经有1,说明已经出现过该字符,结果就>0,返回false } checker |= (1 << val);//如果&结果为0说明此位是0,没有出现过该字符,将此位置1 } return true; }若允许修改字符串,那么还可以在O(NlogN)时间里对字符串进行排序,然后先行检查其中有无相邻字符完全相同的情况,不过,值得注意的是,很多排序算法会占用额外的空间。
给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。
根据题目要求,组成这两个字符串的字符应该是一模一样的才对。那么既然这样,我们有两种方法可以处理。
第一种方法:排序字符串。思路:先对两个字符串进行排序,如果排序结果相同,肯定符合要求,否则不符合要求。这里先将字符串转化为字符数组,然后利用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中该字符的数量多于s return false; } } return true; }第二种方法很巧妙,它的思想跟问题一中使用的本质上是一样的,只不过这里数组中存储的是int型(字符数量),问题一种数组存储的是boolean型变量(是或非)。
请编写一个方法,将字符串中的空格全部替换为“%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"); }
额……一句代码就可以了……
这道题也不是很难,这里给出两种解决方法:
第一种方法:使用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; }
这道题要复杂点,矩阵即二维数组,要将矩阵旋转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; }这个算法的代码看起来有点绕,但是只要仔细阅读一下,画个示意图,还是很容易理解的。
拿到这个题目可能有个误区,以为很简单,直接遍历整个矩阵,只要发现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; //相应的位置1 column |= 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型数组存储标记的,基本都可以用位向量来解决,熟练使用位向量不仅很巧妙,而且效率会更高。
数组和字符串部分的算法题就分析到这,如有问题指出,欢迎留言指正,针对上面几道算法题,如果大家有新的思路,欢迎提出,互相交流,共同进步~
_____________________________________________________________________________________________________________________________________________________
-----乐于分享,共同进步!