项目地址:https://github.com/SpecialYy/Sword-Means-Offer
题目
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符”go”时,第一个只出现一次的字符是”g”。当从该字符流中读出前六个字符“google”时,第一个只出现一次的字符是”l”。
输出描述:
如果当前字符流没有存在出现一次的字符,返回#字符。
解析
预备知识
看到统计字符相关的题目,可以首选哈希表的思想,也就是说字符作为key,出现次数作为value,这样我们就可以牺牲空间换来O(1)的访问。一般适用于键值较少的情况,否则可能会超出内存限制。
因为哈希表的底层结构其实就是数组,键值对的key通过hash函数计算的结果在数组索引范围内进行寻址,也就是说通过哈希函数可以把key转化为数字,且数字范围为数组的大小。
又因为字符的ascii码就是整型数字且唯一表示字符,因此我们可以直接把字符直接作为数组索引,那么出现的次数就是数组中元素值。
思路一
有了预备知识对哈希表原理的解释,我们可以利用字符的特性,使用数组作为我们简单map容器。
我们申请一个2的15次方大的数组(因为Java中char用2个字节实现,其他语言的话可以自行选择)来作为map对字符的出现次数进行统计,然后申请一个字符串缓冲器StringBuilder来记录插入的字符,为了防止缓冲器容量爆炸,我们在插入时判断如果该字符已经出现过,就没必要放入了,因为我们要输出的第一个不重复的字符。
在我们对StringBuilder遍历求出现1次的字符的时候,可以顺便删除已经重复的字符,避免下次不必要的查找,类似的优化可以体现在并查集中查找祖先的过程。
//-------------方法一-------------
//Java中采用双字节存储char类型,最多有2的15次方个
static int MAX = 1 << 15;
//记录字符出现的次数
int[] visited = new int[MAX];
//保持插入字符的顺序
StringBuilder sb = new StringBuilder();
public void Insert(char ch) {
if(visited[ch] == 0) {
sb.append(ch);
}
visited[ch]++;
}
public char FirstAppearingOnce() {
for(int i = 0; i < sb.length(); i++) {
if(visited[sb.charAt(i)] == 1) {
return sb.charAt(i);
}else if(visited[sb.charAt(i)] > 1){
sb.delete(0, 1);
//注意此处i要后退一下,因为删除操作会涉及数组左移
//也就是原本i+1的元素会左移到i位置上
//迭代器中安全删除也是同样的处理
i -= 1;
}
}
return '#';
}
思路二
思路一中我们利用额外的字符串缓冲器来记录字符出现顺序,能不能只利用map数组就能解决问题呢?
在剑指Offer一书中给出了这样的答案,采用的是多状态和一个全局的顺序索引index来优化的。
书中规定未访问的元素在map中对应的value为-1,当访问一个元素,如果该元素对应的value为-1,表示这是该元素第一次出现,value记为出现次序index。
如果该元素的值不为-1,表明之前出现过,那么我们把它对应的value设置为-2。
在查找第一个不重复的字符时,我们遍历一遍map数组,找出其中value值表示次序的且次序最小的那个字符。
static int MAX = 1 << 15;
int[] count = new int[MAX];
int index = 0;
//初始块代码,Java特性,表明对象初始化时会执行
{
Arrays.fill(count, -1);
}
//--------------方法二--------------
public void Insert2(char ch) {
if(count[ch] == -1) {
count[ch] = (index++);
}else if(count[ch] >= 0) {
count[ch] = -2;
}
}
public char FirstAppearingOnce2() {
char ch = '#';
int minIndex = Integer.MAX_VALUE;
for(int i = 0; i < MAX; i++) {
if(count[i] >= 0 && count[i] < minIndex) {
ch = (char) i;
minIndex = count[i];
}
}
return ch;
}
总结
字符类的关于次数问题,可以采用空间换时间的做法。