哈希表和字符串经典算法
算法准备
数据结构复习
HashTable
- 线程安全,操作函数使用synchronized关键字修饰,保证线程安全;
- 初始默认容量 11,拓展因子 0.75;最大值,为Integer.MAX_VALUE - 8;
- 扩容为oldsize<<1 +1;
- 根据key获取index的方式是:(hash&0x7FFFFFFF)%tab.length;
- put(key,value),value不允许为空,key也不允许(key.hashcode会抛出异常);
为什么key获取index的方法是(hash&0x7FFFFFFF)%tab.length?
因为这样可以使负数也变成正数,使其在数组的范围之内。
0x7FFFFFFF = 0111 ....1111 除了符号位都是1
与上一个负数,那么符号位肯定是0,就变成正数了。
HashMap
- 线程不安全
- 默认容量 16,容量一定是2的幂数(采用对原本容量减一,在与上其无符号右移1, 2, 4 ,8,16向与,在加一得策略);最大容量1<<30;默认拓展因子,0.75;
- 相对于HashTable,增加树化机制,防止链表过长。所以设计树节点,树节点是普通节点大小的两倍;链表变成树的阈值为8,树退化为链表的阈值为6;这里重点关注一个值MIN_TREEIFY_CAPACITY,它的意义是防止map扩容和map链表转树节点冲突。只有表中tab的length长度》=它时,才允许链表转树,否则全部采用扩容。
- 求key得hash(),采用key.hashCode() ^ key.hasCode() >>> 16, 使keyhash得高16与低16异或,更加均匀,不容易碰撞。
- 根据key获取tab index得方式是: tab.length -1 & hash(key),就是4中的计算方式,既保证了得到得值在tab数组得length范围内,又保证了hash均匀,使节点尽可能减少碰撞。
- put(key,value)都允许null值。
算法实战
LeetCode 409.最长回文串
思路:
- 首先理解最长回文的意思,就是左边和右边看一样的。
- 重要点是能够想象到如果一个字符是偶数个它一定是回文串,如果一个字符是奇数个那么它个数减一一定是回文,且最后的结果一定只包括一个奇数个数的字符。
- 那么思路就是,统计每个字符的个数,偶数字符直接计数,奇数字符减一计数。最后单独加上是否存在奇数或者偶数的flag;
代码
public int longestPalindrome(String s) {
//128位计数数组,包括了a-zA-Z
int []countA = new int[128];
//是否存在奇数字符
int flag = 0;
//结果
int res = 0;
//计数
for(int i =0; i<s.length();i++){
countA[s.charAt(i)]++;
}
//统计
for(int i = 0; i<countA.length;i++){
int val = countA[i];
if(isO(val)){
res+=val;
}else{
res+=val-1;
flag =1;
}
}
return res+flag;
}
public boolean isO(int val){
return val%2==0?true:false;
}
LeetCode 290. 单词规律
思路:
- 既然是匹配那么就是有一定的一一对应规律,pattern的字符对应s中的一个单词。
- 首先,如果s.split()之后数组的长度如果与pattern的长度不想等,那么直接返回true。
- 既然要记录一一对应,我们就可以利用hashMap来记录,这个关系。还需要一个数据结构,记录pattern字符是否出现,因为如果这个word在hashMap中没出现,但是在数据结构中已经出现,直接返回false;
- 如果s中的一个单词,不存在map中,并且对应的pattern未使用,则放入map。如果s在map中,比较map中的pattern 和对应的pattern是否相等,相等继续循环,不想等return false;
代码:
public boolean wordPattern(String pattern, String s) {
String[] sA = s.split(" ");
int np = pattern.length();
int ns = sA.length;
if(np!=ns) return false;
//记录pattern和s的关系
Map<String,Character> valM = new HashMap<String,Character>();
//记录patter是否用过
int[] used = new int[128];
for(int i = 0; i<np;i++){
String word = sA[i];
Character ps = pattern.charAt(i);
if(valM.get(word)==null){
if(used[ps]!=0){
return false;
}else{
used[ps]++;
}
valM.put(word,ps);
}else{
if(!valM.get(word).equals(ps)) return false;
}
}
return true;
}
LeetCode 49 字母异位词分组
思路:
- 其实就是统计相同字母组合的单词,也就是将打乱顺序的字母组合排序,那么就可以得到相同的组合。
- 利用hashMap,key是单词排序后的string. value是排序之前的string的集合list.
- 遍历字符串数组,排序,判断是否在map中,在:取出list, 将原字符串添加进去,不在,创建list添加
代码:
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List<String>> val = new HashMap<String,List<String>>();
List<List<String>> res = new ArrayList<List<String>>();
for(String str:strs){
char[] temp = str.toCharArray();
Arrays.sort(temp);
String sortS = new String(temp);
if(val.containsKey(sortS)){
List<String> list = val.get(sortS);
list.add(str);
}else{
List<String> list2 = new ArrayList<String>();
list2.add(str);
val.put(sortS,list2);
}
}
for(List list:val.values()){
res.add(list);
}
return res;
}
LettCode 3 无重复字符的最长子串
思路:
-
既然要找不重复的最长字串,这里面我们可以提取出什么信息呢?
涉及到了重复,那么我们肯定需要一个记录字符是否出现的数据结构;最长字串那么一定是连续的,连续的就是需要顺序的遍历整个字符串,不能跳跃。
-
最容易想到的办法:肯定是找到所有子字符串,然后比较找到最长的。但是复杂度为n方,并且有很多多余的字串
-
其实这道题是非常经典的一道题,它的解决方案称之为滑动窗口。什么是滑动窗口?窗口就是代表一段视野,映射到java就是一段距离,两个点表示一段距离,也就是两个指针。我们根据不同的情况,来改变两个指针那么广义上看就是这个窗口一直在滑动。
代码:
public int lengthOfLongestSubstring(String s) {
//记录字符是否出现
int used[] = new int[128];
// 第一个指针
int begin = 0;
//第二个指针
int end = 0;
//最大长度
int res = 0;
//遍历第二个指针,知道它到字符串末尾
while(end < s.length()){
//如果之前遍历没遇到该字符
if(used[s.charAt(end)]==0){
//记录该字符出现了
used[s.charAt(end)]++;
//第二个指针右移一位
end++;
}else
//如果出现了重复字符
{ //记录最长字符长度,因为只有这个时候才能达到最大长度
res = Math.max(res,end -begin);
//移动第一个指针,因为我们要找到是哪个字符重复了
//第一个指针,直到相同的字符停下
while(s.charAt(begin)!=s.charAt(end) && begin < end){
//指针没忘前移动一个,说明我们窗口不包含这个字符了
//那就需要把这个用过的标记清除
used[s.charAt(begin)]--;
//第一个指针往右移动
begin++;
}
//注意这里的操作,这里这两个指针都指到了同一个字符
//我们需要把这个窗口整体往后面移动一位,那么就保持了只含有唯一字符
// abcbd---- abcbd-----》abcbd
| | | |
begin++;
end++;
}
}
res = Math.max(res,end -begin);
return res;
}
LeetCode 187 重复的DNA序列
思路:
- 遍历目标对象,从头到尾记录长度为10的字串
- 通过hashMap判断字串是不是已经遍历过了:如果未遍历,放入map中。如果遍历了,value +1
- 遍历目标对象之后,遍历map,取出value >1 的key,加入结果集合
public List<String> findRepeatedDnaSequences(String s) {
//记录字串是否出现的次数
Map<String,Integer> entries = new HashMap<String,Integer>();
//结果集合
List<String> res = new ArrayList<String>();
//遍历目标字符串
for(int i =0; i<s.length(); i++){
//遍历到最后一个长度为10的字串为止
if((10+i) > s.length()) break;
//获取长度为10的字串
String val = s.substring(i,10+i);
//检查是否出现过
if(entries.get(val)== null){
entries.put(val,1);
}else{
//出现,记录出现的次数
Integer time = entries.get(val);
entries.put(val,time+1);
}
}
//遍历map,生成结果集
for(Map.Entry entry:entries.entrySet()){
if((Integer)entry.getValue() > 1){
res.add((String)entry.getKey());
}
}
return res;
}
LeetCode 76 最小覆盖子串
思路:
- 滑动窗口经典算法,实现方式:双指针。主要关注点,左右指针的边界、移动规则。
- 先移动右指针,边界(不能大于遍历字符串的总长度);当遍历的字串覆盖了目标字串时候,先记录这个时候字串的结果,因为这里肯定满足条件(比较现在的结果,和right-left+1的长度)。然后,这时候移动左指针,因为我们想让找到的字串最小,那么既然找到了一个字串,那么我们就应该缩小字串。左指针边界,小于right并且小于length,并且要保证遍历的字串长度小于目标字串(这个是保证我们移动右指针的触发条件)。
- 左指针向右移动只到窗口不包括目标字串,继续重复2.直到右边界越界
代码:
public String minWindow(String s, String t) {
int[] tVal = new int[256];
int[] sVal = new int[256];
String res = "";
//记录t中出现的字符
for(int i = 0; i<t.length();i++){
tVal[t.charAt(i)]++;
}
int left = findNextNum(0,s,tVal);
if(left == s.length()) return "";
int right= left;
//记录是否已经遍历了一个字串
int count = 0;
while(right < s.length()){
char temp = s.charAt(right);
if(sVal[temp] < tVal[temp]){
count ++;
}
sVal[temp]++;
//找到了一个字串,记录长度,并且尝试移动左指针缩小串口
while(left < s.length() && count == t.length()){
if(res == "" || res.length() > right-left +1){
res = s.substring(left,right+1);
}
char LeftChar = s.charAt(left);
//判断是不是count是否需要减少,如果s中的数目大于t中的数目,说明不需要,因为这时候即使少了一个字符,还是满足字串,如果是小于等于,说明我们要跳出left指针的移动,要遍历right去找新的字符来补充字串
if(sVal[LeftChar] <= tVal[LeftChar]) count--;
sVal[LeftChar]--;
left = findNextNum(left+1,s,tVal);
}
right = findNextNum(right+1,s,tVal);
}
return res;
}
//辅助函数,找到下一个目标字符
public int findNextNum(int start, String s, int Vt[]){
while(start < s.length()){
char v = s.charAt(start);
if(Vt[v] != 0) return start;
start++;
}
return start;
}