题目一:
在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1. 如输入“abaccdeff”,则输出“b”。
最直观的做法就是从头开始扫描这个字符串中的每个字符,当访问到某个字符的时候,拿这个字符和其他字符进行比较,如果后面没有重复的字符,那么这个字符就出现一次。如果字符串有n个字符,则每个字符可能与后面的O(n)个字符相比较,时间复杂度最大为O(n^2)。
第二种思路,对于char 的字符来说,总共有256种不同的字符,因此只需统计每一个字符在字符串中出现的次数就可以了,这样的话,使用一个哈希表就能解决这个问题。
定义哈希表的键值是字符,而值是该字符出现的次数。同时从头开始扫描字符串两次。第一次扫描字符串时,每扫描到一个字符,就在哈希表找到对应的项,把相应的值加1,这样,就能快速的找到第一个只出现一次的字符了。
第一次扫描时,在哈希表中更新第一个字符出现的次数的时间是O(1)。如果字符串长度为n,那么第一次扫描的时间复杂度是O(n)。第二次扫描时,同样在O(1)时间内能读出一个字符出现的次数,所以时间复杂度仍然是O(n)。这样算起来,总的时间复杂度是O(n)。同时,只需要一个包含256个字符的辅助数组就能解决,大小是1kb,由于这个数组的大小是一个常数,因此可以认为这种算法的空间复杂度为O(1)。
综上可以得到下列代码:
class Solution{
public int FirstNotRepeatingChar(String str) {
if (str == null || str.Length <= 0)
return -1;
int index = 0;
//长度定义为256是因为可能存在256个不同的字符,把每个字符对应其相应的ASCII码下标
int tableSize = 256;
//定义一个长度为tableSize这么大的数组用于储存字符于字符串中出现的个数
int[] hashTable = new int[tableSize];
for (int i = 0; i < str.Length; i++) {
hashTable[str[i]]++;
}
for (int i = 0; i < str.Length; i++){
if (hashTable[str[i]] == 1){
index = i;
return str.IndexOf(str[i]);
}
}
return -1;
}
或:
// 将ASCII作为数组下标,存储出现的个数,相当于实现了一个简单的哈希表,也可以直接用map
public static char firstRepeatchar2(String str) {
int maxNum = 256;
char target = str.charAt(0);
int[] arr = new int[maxNum];
for (int i = 0; i < str.length(); i++)
arr[(int)(str.charAt(i))]++;
for (int i = 0; i < str.length(); i++) {
if (arr[(int)(str.charAt(i))] == 1){
target = str.charAt(i);
break;
}
}
return target;
}
}
前面的例子中,之所以把哈希表的大小定义为256是因为字符(char)是8bit的类型,总共只有256个字符。但实际上,有的时候字符不只是256个,比如汉字就有几千个。如果题目要求考虑汉字,那么前面的算法是不是有问题?如果有,则怎么解决?
汉字编码(GB2312-80)采用区位码表示汉字。区位码分94个区,每个区94个位,构成94*94个单元表格。“区号”和“位号”各占一字节,所以一个汉字占2个字节。比如:“啊”的区号是16,位号是01 。
但是博主在VS上打印的时候发现,从19968到35268都是汉字,且之后还有不少数量的汉字在相隔数万个字符后出现,所以在这里博主有点不知所措,对于如果在汉字字符串中找到第一个出现的汉字,这里无法使用前面的哈希表来做,如果用一个类来保存,那么查找的时候无法直接查找下标,那么就相当于是在那当前的汉字去与所有出现的汉字进行排查,所以对于优化查找汉字的算法,博主在这里说声抱歉,后期遇到相应的问题找到答案后会更新博客,完善提出的问题,有想法的小伙伴也可以在评论区提出,博主会不定时的查看评论并于小伙伴们探讨的。
扩展题
1、定义一个函数,输入两个字符串,从第一个字符串中删除在第二个字符串中出现过的所有字符。例如:从第一个字符串“We are student。”中删除在第二个字符串“aeiou”中出现的字符,得到的结果是“W r stdnts。”。
public String GetNewString(String originalString,String matchString) {
int[] match = new int[256];
for(int i=0; i<matchString.length(); i++) {
match[(int)matchString.charAt(i)] = match[(int)matchString.charAt(i)]+ 1;
}
StringBuilder ret = new StringBuilder("");
for(int i=0; i<originalString.length(); i++) {
if(match[(int)matchString.charAt(i)] == 0) {
ret.append(originalString.charAt(i));
}
}
return ret.toString();
}
2、定义一个函数,删除字符串中所有重复出现的字符。例如:输入“google”,删除重复的字符之后的结果是“gole”。
public String DeleteRepeatChar(String str) {
int[] match = new int[256];
for(int i=0; i<str.length(); i++) {
match[(int)str.charAt(i)] = match[(int)str.charAt(i)]+ 1;
}
StringBuilder ret = new StringBuilder("");
for(int i=0; i<str.length(); i++) {
if(match[(int)str.charAt(i)] < 2) {
ret.append(str.charAt(i));
}
}
return ret.toString();
}
也可以创建一个布尔类型的数组作为一个简单的哈希表。数组中的元素的意义是其下标看作ASCII码后对应的字母在字符串中是否已经出现,我们先把数组中所有的元素都设为false。
以“google”为例,当扫描到第一个g时,g的ASCII码是103,那么我们把数组中下标为103的元素的值设为true,之后再查找时就知道,g在之前已经出现过了。
这样只需要O(1)的时间就能判断字符是否在前面已经出现,如果字符串的长度是n的话,那么总的时间复杂度是O(n)。
3、在英语中,如果两个单词中出现的字母相同,并且每个字母出现的次数也相同,那么这两个单词互为变位词(Anagram)。例如:silent与listen、evil与live等互为变位词。请完成一个函数,判断输入的两个字符是不是互为变位词。
思路:
创建一个用数组实现的简单哈希表,用来统计字符串每个字符出现的次数。当扫描到第一个字符串中的每个字符时,为哈希表对应的项加1,当扫描第二个字符串中的每个字符时,为哈希表对应的项减1。如果扫描完第二个字符串后,哈希表中所有的值都是0,那么这两个字符串就是互为变位词。
小结:如果需要判断多个字符是不是在某个字符串里出现过或者统计多个字符在某个字符串中出现的次数,那么我们可以考虑基于数组创建一个简单的哈希表,这样可以用很小的空间消耗换来时间效率的提升。
题目二:
字符流中只出现一次的字符。
实现一个函数,用来找出字符流中第一个只出现一次的字符。例如当从字符流中只读出前两个字符“go”,第一个只出现一次的字符是“g”,当从该字符流中读出前六个字符“google”,第一个只出现一次的字符就是“l”。
字符只能一个接着一个从字符流里读出来。因此可以定一一个数据容器来保存字符在字符流中的位置。当一个字符第一次从字符流中读出来的时候,把它在字符流中的位置保存到数据容器中,当这个字符再次从字符流中读出来时,那么它就不是只出现一次的字符了,也就可以被忽略掉,这时把它在数据容器里保存的值更新成一个特殊的值(如负数值)。
为了尽可能高效的解决这个问题,需要在O(1)时间内往数据容器里插入一个字符,以及更新一个字符对应的值。受第一题的影响,这个数据容器可以使用哈希表来实现。用字符的ASCII码作为哈希表的键值,而把字符对应的位置作为哈希表的值。
如何理解字符流?与第一题做过的在一个字符串(1<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置有什么区别?
以前的题目可以对str进行操作,现在不存在这么一个str。
按照以前的做法,可以做到知道哪些字符出现一次,但不能确定哪个是第一个。
思路:用一个量记录字符出现的前后顺序
参考代码如下:
public class FirstCharacterInStream {
private int index;
private int[] occurence;
public FirstCharacterInStream() { //在构造函数中初始化成员变量
index=0;
occurence = new int[256];
for(int i=0;i<256;i++) {
occurence[i]=-1;
}
}
public void insert(char ch) {
if(occurence[(int)ch]==-1) {
occurence[(int)ch]=index; //第一次出现
}else if(occurence[(int)ch]>=0) {
occurence[(int)ch]=-2; //已经出现过了
}
index++;
}
public char getFirst() {
int minIndex=Integer.MAX_VALUE; //最大的integer
char ch='#';
for(int i=0;i<256;i++) {
if(occurence[i]>=0 && occurence[i]<minIndex) {
ch = (char) i;
minIndex=occurence[i];
}
}
return ch;
}
}
收获
1.对于数据流、字符流等,需要定义数据容器来保存记录。
流和串的区别:
1)串:字符串已经保存下来了,能够读取遍历,因此在字符串中第一个只出现一次的字符中,只需要存下每个字符出现的个数,然后直接在字符串中遍历;
2)流:字符流没有存下来,无法进行遍历,因此在本题中,只能在数据容器哈希表中遍历,而且哈希表中存放的是对应字符的位置,而不是个数。
2.记得会用构造函数来初始化参数;
3.Integer.MAX_VALUE=2^31-1,是32位操作系统(4字节)中最大的符号型整型常量。
4.分清楚:字符与ASCII码的转化,以及 字符形式的数字转和整型数字间的转化
public static void main(String[] args) {
//字符转化为ASCII码
char ch_a = 'a';
int code_a = (int)ch_a; // =ASCII码97
//ASCII码转化为字符
char copyCh_a = (char) code_a; // =ASCII码97对应的字符'a'
//字符形式数字转化为整型
char c1 = '2';
int n1 = c1-'0'; //=2, 由'2'和'1'的ASCII码相减得到
//数字转化为字符形式
char copyC1 = (char)(n1+'0'); //='2' ,由'0'的ASCII码加2得到'2'的ASCII码
System.out.println(5);
}