数据结构与算法-哈希表
哈希算法
public class HashTable {
// 定义一个内部类 Entry,用于存储键值对及其哈希码
static class Entry {
int hash; // 哈希码
Object key; // 键
Object value; // 值
Entry next; // 指向下一个节点的指针,用于处理哈希冲突
public Entry(int hash, Object key, Object value) {
this.hash = hash;
this.key = key;
this.value = value;
}
}
// 存储 Entry 对象的数组,即哈希表的实际存储结构
Entry[] table = new Entry[16];
// 当前哈希表中存储的元素数量
int size = 0;
// 负载因子,默认值为 0.75
float loadFactor = 0.75f;
// 扩容阈值,当 size 超过该值时触发扩容操作
int threshold = (int) (loadFactor * table.length);
/**
* 根据哈希码和键获取对应的值。
*
* @param hash 哈希码
* @param key 键
* @return 对应的值,如果找不到则返回 null
*/
public Object get(int hash, Object key) {
int idx = hash & (table.length - 1); // 计算索引位置
if (table[idx] == null) {
return null; // 如果该位置为空,则返回 null
}
Entry p = table[idx];
while (p != null) { // 遍历链表
if (p.key.equals(key)) {
return p.value; // 找到匹配的键,返回对应的值
}
p = p.next; // 继续遍历链表中的下一个节点
}
return null; // 如果没有找到匹配的键,返回 null
}
/**
* 将键值对插入哈希表中。
*
* @param hash 哈希码
* @param key 键
* @param value 值
*/
public void put(int hash, Object key, Object value) {
int idx = hash & (table.length - 1); // 计算索引位置
if (table[idx] == null) {
table[idx] = new Entry(hash, key, value); // 如果该位置为空,直接插入新节点
} else {
Entry p = table[idx];
while (true) {
if (p.key.equals(key)) {
p.value = value; // 如果找到相同的键,更新其值
return;
}
if (p.next == null) {
break; // 如果到达链表末尾,跳出循环
}
p = p.next; // 继续遍历链表中的下一个节点
}
p.next = new Entry(hash, key, value); // 在链表末尾插入新节点
}
size++; // 更新元素数量
if (size > threshold) {
resize(); // 如果超过阈值,进行扩容操作
}
}
/**
* 扩容操作,将哈希表的容量扩大一倍,并重新分配所有元素。
*/
private void resize() {
Entry[] newTable = new Entry[table.length << 1]; // 创建新的数组,长度为原来的两倍
for (int i = 0; i < table.length; i++) {
Entry p = table[i];
if (p != null) {
Entry a = null;
Entry b = null;
Entry aHead = null;
Entry bHead = null;
// 遍历当前桶中的链表,并根据新的哈希值重新分配到两个不同的链表中
while (p != null) {
if ((p.hash & table.length) == 0) {
if (a != null) {
a.next = p;
} else {
aHead = p;
}
a = p;
} else {
if (b != null) {
b.next = p;
} else {
bHead = p;
}
b = p;
}
p = p.next;
}
// 将重新分配后的链表插入到新的哈希表中
if (a != null) {
a.next = null;
newTable[i] = aHead;
}
if (b != null) {
b.next = null;
newTable[i + table.length] = bHead;
}
}
}
table = newTable; // 更新哈希表为新的数组
threshold = (int) (loadFactor * table.length); // 更新扩容阈值
}
/**
* 根据哈希码和键删除对应的键值对。
*
* @param hash 哈希码
* @param key 键
* @return 被删除的值,如果找不到则返回 null
*/
public Object remove(int hash, Object key) {
int idx = hash & (table.length - 1); // 计算索引位置
if (table[idx] == null) {
return null; // 如果该位置为空,则返回 null
}
Entry p = table[idx];
Entry prev = null; // 用于记录前一个节点
while (p != null) {
if (p.key.equals(key)) {
if (prev == null) {
table[idx] = p.next; // 如果要删除的是第一个节点,直接更新头节点
} else {
prev.next = p.next; // 否则更新前一个节点的 next 指针
}
size--; // 更新元素数量
return p.value; // 返回被删除的值
}
prev = p; // 更新前一个节点
p = p.next; // 继续遍历链表中的下一个节点
}
return null; // 如果没有找到匹配的键,返回 null
}
}
两数之和-Leetcode 1
给定一个整数数组
nums
和一个整数目标值target
,请你在该数组中找出 和为目标值target
的那 两个 整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。示例 2:
输入:nums = [3,2,4], target = 6 输出:[1,2]示例 3:
输入:nums = [3,3], target = 6 输出:[0,1]
public int[] twoSum(int[] nums, int target) {
HashMap<Integer, Integer> map=new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) {
int k=target-nums[i];
if(map.containsKey(k)) {
return new int[] {map.get(k),i};
}
map.put(nums[i], i);
}
return null;
}
无重复字符的最长字串-Leetcode 3
给定一个字符串
s
,请你找出其中不含有重复字符的 最长子串
的长度。示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是"abc"
,所以其长度为 3。示例 2:
输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是"b"
,所以其长度为 1。示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是"wke"
,所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke"
是一个子序列,不是子串。
方法一:
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> map=new HashMap<Character, Integer>();
int begin=0;
int maxLength=0;
for (int end = 0; end < s.length(); end++) {
char ch=s.charAt(end);
if(map.containsKey(ch)) {
begin=Math.max(begin, map.get(ch)+1);
map.put(ch, end);
}else{
map.put(ch, end);
}
maxLength=Math.max(maxLength, end-begin+1);
}
return maxLength;
}
方法二:
public int lengthOfLongestSubstring(String s) {
int[] map=new int[128];
Arrays.fill(map, -1);
int begin = 0;
int maxLength = 0;
for (int end = 0; end < s.length(); end++) {
char ch = s.charAt(end);
if(map[ch]!=-1) {
begin=Math.max(begin, map[ch]+1);
map[ch]=end;
}else {
map[ch]=end;
}
maxLength = Math.max(maxLength, end - begin + 1);
}
return maxLength;
}
字母异位词分组-Leetcode 49
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例 1:
输入: strs =["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]示例 2:
输入: strs =[""]
输出: [[""]]示例 3:
输入: strs =["a"]
输出: [["a"]]
方法一:
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<String, List<String>> map=new HashMap<String, List<String>>();
for(String str :strs) {
char[] chars=str.toCharArray();
Arrays.sort(chars);
String key=new String(chars);
List<String> strings=map.computeIfAbsent(key, k -> new ArrayList<>());
strings.add(str);
}
return new ArrayList<>(map.values());
}
方法二:
static class ArrayKey {
int[] key = new int[26];
public ArrayKey(String str) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
key[ch - 'a']++;
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ArrayKey arrayKey = (ArrayKey) o;
return Arrays.equals(key, arrayKey.key);
}
@Override
public int hashCode() {
return Arrays.hashCode(key);
}
}
public List<List<String>> groupAnagrams(String[] strs) {
HashMap<ArrayKey, List<String>> map = new HashMap<>();
for (String str : strs) {
List<String> strings = map.computeIfAbsent(new ArrayKey(str), k -> new ArrayList<>());
strings.add(str);
}
return new ArrayList<>(map.values());
}
找出出现一次的数字-Leetcode 136
给你一个 非空 整数数组
nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
示例 1 :
输入:nums = [2,2,1]
输出:1
示例 2 :
输入:nums = [4,1,2,1,2]
输出:4
示例 3 :
输入:nums = [1]
输出:1
方法一:
public int singleNumber(int[] nums) {
HashSet<Integer> set = new HashSet<>();
for (int num : nums) {
if (!set.add(num)) {
set.remove(num);
}
}
return set.toArray(new Integer[0])[0];
}
方法二:
public int singleNumber(int[] nums) {
int num = nums[0];
for (int i = 1; i < nums.length; i++) {
num = num ^ nums[i];
}
return num;
}
判断字母异位词-Leetcode 242
给定两个字符串
s
和t
,编写一个函数来判断t
是否是s
的字母异位词
。示例 1:
输入: s = "anagram", t = "nagaram" 输出: true示例 2:
输入: s = "rat", t = "car" 输出: false
public boolean isAnagram(String s, String t) {
return Arrays.equals(getKey(s), getKey(t));
}
private static int[] getKey(String s) {
int[] array = new int[26];
char[] chars = s.toCharArray();
for (char ch : chars) {
array[ch - 97]++;
}
return array;
}
第一个不重复字符-Leetcode 387
给定一个字符串
s
,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回-1
。示例 1:
输入: s = "leetcode" 输出: 0示例 2:
输入: s = "loveleetcode" 输出: 2示例 3:
输入: s = "aabb" 输出: -1
public int firstUniqChar(String s) {
int[] array=new int[26];
char[] chars=s.toCharArray();
for(char ch:chars) {
array[ch-97]++;
}
for (int i = 0; i < chars.length; i++) {
char ch=chars[i];
if(array[ch-97]==1) {
return i;
}
}
return -1;
}
出现次数最多的单词-Leetcode 819
给你一个字符串
paragraph
和一个表示禁用词的字符串数组banned
,返回出现频率最高的非禁用词。题目数据 保证 至少存在一个非禁用词,且答案 唯一 。
paragraph
中的单词 不区分大小写 ,答案应以 小写 形式返回。示例 1:
输入:paragraph = "Bob hit a ball, the hit BALL flew far after it was hit.", banned = ["hit"] 输出:"ball" 解释: "hit" 出现了 3 次,但它是禁用词。 "ball" 出现了两次(没有其他单词出现这么多次),因此它是段落中出现频率最高的非禁用词。 请注意,段落中的单词不区分大小写, 标点符号会被忽略(即使它们紧挨着单词,如 "ball,"), 并且尽管 "hit" 出现的次数更多,但它不能作为答案,因为它是禁用词。示例 2:
输入:paragraph = "a.", banned = [] 输出:"a"
public String mostCommonWord(String paragraph, String[] banned) {
Set<String> banSet=Set.of(banned);
String[] split=paragraph.toLowerCase().split("[^A-Za-z]+");
HashMap<String, Integer> map=new HashMap<String, Integer>();
for(String key:split) {
if(banSet.contains(key)) {
continue;
}
map.compute(key, (k,v)-> v==null?1:v+1);
}
Integer max=0;
String maxkey=null;
for(Map.Entry<String, Integer> e:map.entrySet()) {
Integer value=e.getValue();
if(value>max) {
max=value;
maxkey=e.getKey();
}
}
return maxkey;
}