题目一:
在字符串中找出第一个只出现一次的字符。如输入“abaccdeff”,则输出“b”。
最直观的做法就是从头开始扫描这个字符串中的每个字符,当访问到某个字符的时候,拿这个字符和其他字符进行比较,如果后面没有重复的字符,那么这个字符就出现一次。如果字符串有n个字符,则每个字符可能与后面的O(n)个字符相比较,时间复杂度最大为O(n^2)。
对于这种思路,可以有下面代码:
class Solution
{
public int FirstNotRepeatingChar(string str)
{
//遍历字符串的每一个字符
for (int i = 0; i < str.Length; i++)
{
int j;
//拿该字符与后面的所有字符进行比较,如果找到相同的字符,那么就遍历下一个字符,如果
for (j = 0; j < str.Length; j++)
{
if (i != j && str[i] == str[j])
{
break;
}
}
if (j == str.Length)
{
return i;
}
}
//如果所有的字符都有重复的,那么就返回-1
return -1;
}
}
那么就有了第二种思路,对于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;
int tableSize = 256; //长度定义为256是因为可能存在256个不同的字符,把每个字符对应其相应的ASCII码下标
int[] hashTable = new int[tableSize]; //定义一个长度为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;
}
}
前面的例子中,之所以把哈希表的大小定义为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。”。
class Solution2
{
public string GetNewString(string originalString,string matchString)
{
StringBuilder sb=new StringBuilder();
int hashSize = 256;
int[] hashTable = new int[hashSize];
//初始化哈希表
for (int i = 0; i < hashSize; i++)
hashTable[i] = 0;
//统计匹配字符串中各字符的出现次数
for(int i=0;i<matchString.Length;i++)
{
hashTable[(int)matchString[i]]++;
}
//获取除在匹配字符串中其他的所有字符
for(int i = 0; i < originalString.Length; i++)
{
char ch = originalString[i];
if(hashTable[(int)ch]==0)
{
sb.Append(ch);
}
}
return sb.ToString();
}
}
为了解决这个问题,可以创建一个用数组实现的简单的哈希表来存第二个字符串。这样我们从头到尾扫描第一个字符串的每个字符时,用O(1)时间就能判断出该字符是不是在第二个字符串中。如果第一个字符串的长度是n,那么总的时间复杂度是O(n)。
2、定义一个函数,删除字符串中所有重复出现的字符。例如:输入“google”,删除重复的字符之后的结果是“gole”。
class Solution3
{
public string DeleteRepeatChar(string str)
{
StringBuilder sb = new StringBuilder();
//定义一个哈希数组用于判断字符是否已经出现
int hashSize = 256;
bool[] hashTable = new bool[hashSize];
for (int i = 0; i < hashSize; i++)
hashTable[i] = false;
for(int i=0;i<str.Length;i++)
{
char ch = str[i];
//如果这个字符没有出现过,那么它在哈希表里对应的值则为false
if (!hashTable[ch])
{
sb.Append(ch);
hashTable[ch] = true;
}
}
return sb.ToString();
}
}
这个问题中可以创建一个布尔类型的数组作为一个简单的哈希表。数组中的元素的意义是其下标看作ASCII码后对应的字母在字符串中是否已经出现,我们先把数组中所有的元素都设为false。
以“google”为例,当扫描到第一个g时,g的ASCII码是103,那么我们把数组中下标为103的元素的值设为true,之后再查找时就知道,g在之前已经出现过了。
这样只需要O(1)的时间就能判断字符是否在前面已经出现,如果字符串的长度是n的话,那么总的时间复杂度是O(n)。
3、在英语中,如果两个单词中出现的字母相同,并且每个字母出现的次数也相同,那么这两个单词互为变位词(Anagram)。例如:silent与listen、evil与live等互为变位词。请完成一个函数,判断输入的两个字符是不是互为变位词。
class Solution4
{
public bool IsChangePositionWord(string word1,string word2)
{
bool isChangePositionWord = true;
int hashSize = 256;
int[] hashTable = new int[hashSize];
for(int i=0;i<hashSize;i++)
{
hashTable[i] = 0;
}
for (int i = 0; i < word1.Length; i++)
{
hashTable[word1[i]]++;
}
for (int i = 0; i < word2.Length; i++)
{
hashTable[word2[i]]--;
}
for (int i = 0; i < hashSize; i++)
if (hashTable[i] != 0)
return false;
return isChangePositionWord;
}
}
创建一个用数组实现的简单哈希表,用来统计字符串每个字符出现的次数。当扫描到第一个字符串中的每个字符时,为哈希表对应的项加1,当扫描第二个字符串中的每个字符时,为哈希表对应的项减1。如果扫描完第二个字符串后,哈希表中所有的值都是0,那么这两个字符串就是互为变位词。
小结:如果需要判断多个字符是不是在某个字符串里出现过或者统计多个字符在某个字符串中出现的次数,那么我们可以考虑基于数组创建一个简单的哈希表,这样可以用很小的空间消耗换来时间效率的提升。
题目二:
字符流中只出现一次的字符。
实现一个函数,用来找出字符流中第一个只出现一次的字符。例如当从字符流中只读出前两个字符“go”,第一个只出现一次的字符是“g”,当从该字符流中读出前六个字符“google”,第一个只出现一次的字符就是“l”。
字符只能一个接着一个从字符流里读出来。因此可以定一一个数据容器来保存字符在字符流中的位置。当一个字符第一次从字符流中读出来的时候,把它在字符流中的位置保存到数据容器中,当这个字符再次从字符流中读出来时,那么它就不是只出现一次的字符了,也就可以被忽略掉,这时把它在数据容器里保存的值更新成一个特殊的值(如负数值)。
为了尽可能高效的解决这个问题,需要在O(1)时间内往数据容器里插入一个字符,以及更新一个字符对应的值。受第一题的影响,这个数据容器可以使用哈希表来实现。用字符的ASCII码作为哈希表的键值,而把字符对应的位置作为哈希表的值。
如何理解字符流?与第一题做过的在一个字符串(1<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置有什么区别?
以前的题目可以对str进行操作,现在不存在这么一个str。
按照以前的做法,可以做到知道哪些字符出现一次,但不能确定哪个是第一个。
思路:用一个量记录字符出现的前后顺序
参考代码如下:
class CharStatistics
{
private static int index = 1;
/// <summary>
/// hashTable[i]:字符在字符流中出现的位置
/// hashTable[i] = -1 : 表示这个字符在字符流中没有出现过
/// hashTable[i] = -2 : 表示这个字符在字符流中出现多次
/// hashTable[i] >= 0 : 表示这个字符出现的位置,越小表示出现越早
/// </summary>
private static int[] hashTable = new int[256];
public void Init()
{
for (int i = 0; i < 256; i++)
hashTable[i] = -1;
}
public void Insert(char ch)
{
//之前没有出现过,则将index放入对应的位置,记录出现顺序
if (hashTable[ch] == -1)
hashTable[ch] = index++;
//如果字符在之前已经出现过了,那么把该字符标记为-2
else if (hashTable[ch] >= 0)
hashTable[ch] = -2;
}
public char FirstAppearingOnce()
{
char ch = '\0';
for(int i=0;i<256;i++)
if(hashTable[i]>=0)
{
ch = (char)i;
break;
}
return ch;
}
}