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

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

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

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码中的位置val
		if((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中该字符的数量多于s
			return 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; //相应的位置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型数组存储标记的,基本都可以用位向量来解决,熟练使用位向量不仅很巧妙,而且效率会更高。

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

       

        欢迎大家关注我的公众号:“武哥聊编程”,一个有温度的公众号~

        关注回复:资源,可以领取海量优质视频资料
        程序员私房菜

_____________________________________________________________________________________________________________________________________________________

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

-----更多文章请看:http://blog.csdn.net/eson_15

你可能感兴趣的:(●,结构算法,------【Java算法题】,数据结构和算法)