每日一题《剑指offer》字符串篇之字符流中第一个不重复的字符
字符流中第一个不重复的字符
难度:中等
描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 "go" 时,第一个只出现一次的字符是 "g" 。当从该字符流中读出前六个字符 “google" 时,第一个只出现一次的字符是"l"。
数据范围
数据范围:字符串长度满足 1≤n≤1000 ,字符串中出现的字符一定在 ASCII 码内。
进阶:空间复杂度O(n) ,时间复杂度O(n)
举例
解题思路
方法一:哈希表+字符串;又是一个找到是否重复的问题。我们还是可以用哈希表来记录各个字符出现的次数,根据这样只要是字符串最前面且哈希表中次数为1的字符就是我们要找的。
具体做法:
- step 1:准备一个字符串来记录输入的字符流,用哈希表统计每个字符的次数,二者都是全局变量。
- step 2:在Insert函数中对输入的字符,加到字符串最后,然后统计出现次数。
- step 3:在FirstAppearingOnce函数遍历该字符串,对于每个字符查找哈希表,返回第一个计数为1的字符,如果遍历完字符串以后都没,则返回#。
方法二:哈希表+队列;除了使用字符串记录字符流,还可以用队列记录字符流,每次插入的时候,只需要将第一次出现的字符加入到队列中,然后正常计数。
查找第一个不重复出现的字符的时候,从队首开始查询哈希表,如果出现次数为1,则返回该字符,如果不为1,则从队首将其弹出,因为反正后续也不可能是这个已经重复的字符了。
具体做法:
- step 1:准备一个队列来记录输入的字符流,用哈希表统计每个字符的次数,二者都是全局变量。
- step 2:在Insert函数中对输入的字符,加到队列最后,然后统计出现次数。
- step 3:在FirstAppearingOnce函数中,不断检查队首元素直到队列为空,队首出现次数为1次,就返回,队首出现次数不为1就弹出。
实现代码(java)
方法一:
import java.util.*; public class Solution { private StringBuilder s = new StringBuilder(); private HashMap<Character, Integer> mp = new HashMap<>(); //Insert one char from stringstream public void Insert(char ch) { //插入字符 s.append(ch); //哈希表记录字符出现次数 mp.put(ch, mp.getOrDefault(ch, 0) + 1); } //return the first appearence once char in current stringstream public char FirstAppearingOnce() { //遍历字符串 for(int i = 0; i < s.length(); i++) //找到第一个出现次数为1的 if(mp.get(s.charAt(i)) == 1) return s.charAt(i); //没有找到 return '#'; } }
方法二:
import java.util.*; public class Solution { private Queue<Character> q = new LinkedList<>(); private HashMap<Character, Integer> mp = new HashMap<>(); //Insert one char from stringstream public void Insert(char ch) { //插入字符 if(!mp.containsKey(ch)) q.offer(ch); //哈希表记录字符出现次数 mp.put(ch, mp.getOrDefault(ch, 0) + 1); } //return the first appearence once char in current stringstream public char FirstAppearingOnce() { while(!q.isEmpty()){ //第一个不重复的字符 if(mp.get(q.peek()) == 1) return q.peek(); //弹出前面的已经重复的字符 else q.poll(); } //都重复了 return '#'; } }
学习完本题的思路你可以解决如下题目: JZ39. 数组中出现次数超过一半的数字
数组中出现次数超过一半的数字
难度:中等
描述
给一个长度为 n 的数组,数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例如输入一个长度为9的数组[1,2,3,2,2,2,5,4,2]。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。
数据范围
n≤50000,数组中元素的值 0≤val≤10000
要求:空间复杂度:O(1),时间复杂度O(n)
举例
解题思路
方法一:主要分三步:
- 判断给定的array长度是否为零,为零则没有这样符合条件的数字,直接return 0
- 我们创建一个hashmap,然后遍历这个array,hashmap的key是array里的不同数字,value是这些数字出现在array里的次数
- 我们遍历hashmap,检测value(即当前数字出现的次数)是否大于数组array长度的一半,如果有这个数字,我们return回key(即当前遍历到的数字),如果我们走完了整个hashmap还没有发现这样一个数字,我们需要return 0
实现代码(java)
方法一:
import java.util.*; public class Solution { public int MoreThanHalfNum_Solution(int [] array) { if(array.length == 0){ return 0; } int len = array.length; int threshold = len/2; Map<Integer,Integer> map = new HashMap<>(); for(int i = 0; i < len; i++){ if(!map.keySet().contains(array[i])){ map.put(array[i],1); }else{ map.put(array[i],map.get(array[i])+1); } } for(Integer key: map.keySet()){ if(map.get(key) > threshold){ return key; } } return 0; } }
数组中只出现一次的两个数字
难度:中等
描述
一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。
数据范围
数组长度 2≤n≤1000,数组中每个数的大小 0
要求:空间复杂度 O(1)O(1),时间复杂度 O(n)O(n)
举例
解题思路
用异或^可解此题。
但是首先要知道一个知识点,a^b^a = a^a^b = b^a^a =b,这个知识点也就是本题的简单版本:如果数组中除了某一个数字,其他数字都出现了两次,找出该数字。思路就是遍历数组,对每一个数字都求异或,最后得到的值就是要找的数字。
有了该知识点的储备,再来看看本题。本题是要找两个数字a和b,那我们把该数组分成两个数组,其中a和一部分出现两次的数字在一块儿,b和另一部分出现两次的数字在一块儿,那这两个数组不是就变成了上面讲的那个简单版本中的数组吗?然后再分别对这两个数组求异或,就可以找到这两个数字了。
举例:[a,2,2,3,3,b]。把该数组分成[a,2,2]和[3,3,b],再对这两个数组求异或,便能得到a和b。
问题:怎么把a和b区分开来?
可以利用二进制来区分。先对整个数组求异或得到c,根据上面的知识,可以知道c其实就是a^b=c。那么对于c,假如c二进制的第一位是1,其实就代表a二进制的第一位是1(或0),b二进制的第一位是0(或1),总而言之如果第一位的c等于1,那么a和b在第一位肯定不相等。
所以我们就可以想到利用二进制的第一位(有可能是第二位,第三位 。。。因为上面是假设的c第一位是1)为1来区分两个数组,第一位为1的是数组一,第一位为0的是数组二。这样就相当于把a和b给区分开来了。
a和b区分开以后,剩下的就简单了,判断数组中其他数字的二进制的第一位是否为1,是的话就分到数组一,为0就分到数组二。最后对数组一和数组二分别进行异或,得到的就是a和b。
有个地方没有讲清楚,利用二进制的第一位为1来区分两个数组,如果第一位不是1,那就判断第二位,第三位,一直到找到为1的地方。假设一直找到第n位才为1,那就判断数组中的其他数字的二进制的第n位是否为1,做&运算即可判断。
实现代码(java)
import java.util.*; public class Solution { /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param array int整型一维数组 * @return int整型一维数组 */ public int[] FindNumsAppearOnce (int[] array) { /** * 异或的运算方法是一个二进制运算: * 1^1=0 * 0^0=0 * 1^0=1 * 0^1=1 */ int res = 0; //对整个数组求异或的结果 for (int i = 0; i < array.length; i++) { res ^= array[i]; } int temp = 1; //判断异或结果的二进制第一位是否为1,为1则直接跳过该循环 while ((temp & res) == 0) { //为0则继续往前找,一直到找到为1的二进制位 temp <<= 1; } int lastNumIs1 = 0; int lastNumIs0 = 0; for (int j = 0; j < array.length; j++) { //如果该数字二进制的第某位为0,则分到数组一 if ((array[j] & temp) == 0) { lastNumIs1 ^= array[j]; } else { //如果该数字二进制的第某位为1,则分到数组二 lastNumIs0 ^= array[j]; } } return lastNumIs1 < lastNumIs0 ? new int[]{lastNumIs1,lastNumIs0} : new int[]{lastNumIs0,lastNumIs1}; } }