哈希表相关内容

基础知识

	哈希表最大的优点是高效,在哈希表中插入、删除或查找一个元素都只需要O(1)的时间,在java中哈希表有两个对应的类型,即HashSet和HashMap。

HashSet常用函数

序号函数函数功能
1add在HashSet中添加一个元素
2contains判断HashSet中是否包含一个元素
3remove从Hashset中删除一个元素
4size返回HashSet中元素的个数

HashMap常用函数

序号函数函数功能
1containsKey判断HashMap中是否包含某个键
2get如果键存在,则返回对应的值,否则返回null
3getOrDefault如果键存在,则返回对应的值,否则返回输入的默认值
4put如果键不存在,则添加一组键值到值的映射,否则修改键对应的值
5putIfAbsent当键不存在时添加一组键到值的映射
6remove删除某个键
7replace修改某个键对应的值
8size返回HashMap中键到值的映射个数

哈希表的设计

	哈希表可以用数组和链表结合进行实现,链表解决哈希冲突问题,并且为了效率要保证链表尽可能短,所以哈希表容量一般不会填充满就会扩容,并且
链表过长时,会将其转为TreeNode,用来提高效率。

例题解答

1、插入、删除和随机访问都是O(1)的容器

题目
	设计一个数据结构,使如下三个操作的时间复杂度都是O(1)。
	* insert(value): 如果数据集中不包含一个数值,则把它添加到数据集中。
	* remove(value): 如果数据集中包含一个数值,则把它删除。
	* getRandom(): 随机返回数据集中的一个数值,要求数据集中每个数字被返回的概率都相通。
分析
	要求插入、删除复杂度都是O(1),所以可以用哈希表来解决。
示例代码
public class RandomizedSet{
	Map<Integer,Integer> numToLocation;
	List<Integer> nums;
	public RandomizedSet(){
		numToLocation = new HashMap<>();
		nums = new ArrayList<>();
	}
	public boolean insert(int val){
		if(numToLocation.containsKey(val)){
			return false;
		}
		numToLocation.put(val,nums.size());
		nums.add(val);
		return true;
	}
	public boolean remove(int val){
		if(!numToLocation.containsKey(val)){
			return false;
		}
		int index = numToLocation.get(val);
		numToLocation.put(nums.get(nums.size() -1),index);
		numToLocation.remove(val);
		nums.set(index,nums.get(nums.size() -1));
		nums.remove(nums.size() - 1);
		return true;
	}
	public int getRandom(){
		Random random = new Random();
		int r = random.nextInt(nums.size());
		return nums.get(r);
	}
}

2、最近最少使用缓存

题目
	请设计实现一个最近最少使用(LRU)缓存,要求如下两个操作的时间复杂度都是O(1)。
	* get(key):如果缓存中存在键key,则返回它对应的值;否则返回-1。
	* put(key,value):如果缓存中之前包含键key,则它的值设为value;否则添加键key及对应的值value。在添加一个键时,如果缓存容量已经满了,则
	在添加新键之前删除最近最少使用的键(缓存中最长时间没有被使用过的元素)。
分析
	根据题的要求,可知只用哈希表满足不了要求,需要使用哈希表和双向链表结合来解决。
示例代码
//双向链表
class ListNode{
	public int key;
	public int value;
	public ListNode prev;
	public ListNode next;
	public ListNode(int key, int value){
		this.key = key;
		this.value = value;
	}
}

class LRUCache{
	//声明一个头节点和尾节点方面操作
	private ListNode head;
	private ListNode tail;
	//声明一个哈希缓存用于降低操作时间复杂度
	private Map<Integer,ListNode> map;
	int capacity;
	public LRUCache(int cap){
		//在构造方法中初始化容器与节点并将双向链表链接起来
		map = new HashMap<>();
		head = new ListNode(-1,-1);
		tail = new ListNode(-1,-1);
		head.next = tail;
		tail.prev = head;
		capacity = cap;
	}
	//根据key获取value值
	public int get(int key){
		//从map缓存中获取
		ListNode node = map.get(key);
		//如果缓存中没有,则返回-1
		if(node == null){
			return -1;
		}
		//缓存中有,则将获取到的节点移动到最后面
		moveToTail(node,node.value);
		//并返回节点的值
		return node.value;
	}
	public void put(int key,int value){
		//如果缓存中有,则将node移动到链表末尾
		if(map.containsKey(key)){
			moveToTail(map.get(key),value);
		}else{
			// 如果缓存中没有,并且缓存已经放满
			if(map.size() == capacity){
				//则删除头节点,因为最近使用的都放在末尾节点,头节点为最近最少使用的
				ListNode toBeDeleted = head.next;
				deleteNode(toBeDeleted);
				//并且删除该节点的环境
				map.remove(toBeDeleted.key);
			}
			//根据key和value创建新的node节点,并加入到链表尾部
			ListNode node = new ListNode(key,value);
			insertToTail(node);
			//并加入到缓存中
			map.put(key,node);
		}
	}
	//移动node节点到尾部的操作,先删除当前node节点,然后将修改当前node节点的value值,然后将node插入的尾节点。
	private void moveToTail(ListNode node,int newValue){
		deleteNode(node);
		node.value = newValue;
		insertToTail(node);
	}
	//删除节点,将node节点的前一个节点的下一个指针指向node节点的下一个节点,然后将node节点的下一个节点的上一个指针指向node节点的上一个节点
	private void deleteNode(ListNode node){
		node.prev.next = node.next;
		node.next.prev = node.prev;
	}
	//将node节点插入尾部,将尾部的前一个节点的下一个指针指向node节点,将node节点的向前指针指向尾节点的上一个节点,node节点的下一个指针指向尾节点,尾节点的前一个指针指向node
	private void insertToTail(ListNode node){
		tail.prev.next = node;
		node.prev = tail.prev;
		node.next = tail;
		tail.prev = node;
	}
}

3、有效的变位词

题目
	给定两个字符串s和t,请判断它们是不是一组变位词。在一组变位词中,它们中的字符及每个字符出现的次数都相同,但字符的顺序部能相同,例如,
"anagram"和"nagaram" 就是一组变位词。
分析
	首先两个字符串长度需要一样,然后可以是用哈希表存放s的每一个字符和出现的次数,然后再遍历t,判断哈希表中是否存在该字符,并且次数需要大于
0,如果不存在或等于0则直接返回false,如果存在,将哈希表中存在的字符次数减去1,直到遍历完t字符串都没有返回false,那么就是一组变位词。
示例代码
public boolean isAnagram(String str1,String str2){
	if(str1.length() != str2.length()){
		return false;
	}
	Map<Character,Integer> counts = new HashMap<>();
	for(char ch : str1.toCharArray()){
		counts.put(ch,counts.getOrDefault(ch,0)+1);
	}
	for(char ch : str2.toCharArray()){
		if(!counts.containsKey(ch) || counts.get(ch) == 0){
			return false;
		}
		counts.put(ch,counts.get(ch) -1);
	}
	return true;
}

4、变位词组

题目
	给定一组单词,请将它们按照变位词分组。例如,输入一组单词["eat","tea","tan","ate","nat","bat"],这组单词可以分成3组,分别是["eat",
"tea","ate"],["tan","nat"]和["bat"]。假设单词中只包含英文字母。
分析
	因为是变位词,所以如果对字符串的字符数组进行排序,那么是变位词的字符数组排序后生成的字符串应该是相同的。
示例代码
    public List<List<String>> groupAnagrams(String[] strs){
        Map<String,List<String>> groups = new HashMap<>();
        for(String str : strs){
            char[] chars =  str.toCharArray();
            Arrays.sort(chars);
            String temp = new String(chars);
            groups.putIfAbsent(temp,new LinkedList<>());
            groups.get(temp).add(str);
        }
        return new LinkedList<>(groups.values());
    }

5、外星语言是否排序

题目
	有一门外星语言,它的字母表刚好包含所有的英文小写字母,只是字母表的顺序不同,给定一组单词和字母表顺序,请判断这些单词是否按照字母表的顺
序排序。例如,输入一组单词["offer","is","coming"],以及字母表顺序"zyxwvutsrqponmlkjihgfedcba",由于字母'o'在字母表中位于'i'的前面,因此
单词"offer"排在"is"的前面;同样,由于字母'i'在字母表中位于'c'的前面,因此单词"is"排在"coming"的前面。因此,这一组单词是按照字母表顺序排序的,应输出true。
分析
	由于字母表的顺序由一个输入的字符串决定,在确定单词排序的顺序时,它们的每个字母在该字母表中的顺序至关重要。为了方便查找每个字符的顺序,
可以创建一个哈希表,哈希表的键为字母表的每个字母,而值为字母在字母表中的顺序。
	因为字母表中的字母数目是固定的26个,因此可以用一个长度为26的数组来模拟哈希表,数组的下标对应哈希表的键,而数组的值对应哈希表的值。
示例代码
public boolean isAlienSorted(String[] words, String order){
	//初始化一个数组模拟哈希表
	int[] orderArray = new int[order.length()];
	for(int i = 0; i < order.length(); i ++){
		//将字母表的字符减去'a'可以将字母模拟成数组下标,数组下标对应的值为字母表中字符的顺序
		orderArray[order.charAt(i) - 'a'] = i;
	}
	//再遍历字符串的字符
	for(int i = 0; i < words.length - 1; i ++){
		//将当前字符串与数组中后一位字符串的字符位置顺序进行比较
		if(!isSorted(words[i],words[i + 1],orderArray)){
			return false;
		}
	}
	return true;
}
private boolean isSorted(String word1,String word2,int[] order){
	int i = 0;
	for(; i < word1.length() && i < word2.length(); ++i){
		char ch1 = word1.charAt(i);
		char ch2 = word2.charAt(i);	
		//字符也减去'a'即可获取字符在哈希表中的顺序,如果当前的比下一个小,则说明顺序正确。
		if(order[ch1 - 'a'] < order[ch2 - 'a']){
			return true;
		}
		//如果当前字符顺序大于后面字符的顺序,则顺序不对,返回false
		if(order[ch1 - 'a'] > order[ch2 - 'a']){
			return false;
		}	
	}
	// 说明连个字符串字符一样,再判断字符长度是否一致,如果一致,则返回true
	return i == word1.length();
}

6、最小时间差

题目
	给定一组范围在00:00至23:59的时间,求任意两个时间之间的最小时间差,例如,输入时间组["23:50","23:59","00:00"],"23:59"和"00:00"之间只
有1分钟的间隔,是最小的时间差。
分析
	1、可以使用蛮力法,计算每一个时间的差值,将结果替换为最小值可得到结果。
	2、将数组排序后再计算,分析可知数组时间组是按分钟计算的,每天24小时的分钟数为1440分钟,可以用一个长度为1440的数组表示一天的时间,那么
00:00对应的数组下标为0,00:01对应的数组下标为1,23.59对应的数组下标为1439,如果时间数组中包含对应的时间,则将对应的数组设置为true,其它位置
的值都是false,00:00 -> 23:59这个时间特殊处理,这样可以大大提高效率。
示例代码
public int findMinDifference(List<String> timePoints){
	//如果时间组个数都大于每天的分钟数,则最小时间差为0
	if(timePoints.size() > 1440){
		return 0;
	}
	boolean[] minuteFlags = new boolean[1440];
	for(String time : timePoints){
		//将时间组的时间转换为分钟数
		String[] t = time.split(":");
		int min = Integer.parseInt(t[0]) * 60 + Integer.parseInt(t[1]);
		// 如果为true,则说明有两个一样的时间,则时间差为0
		if(minuteFlags[min]){
			return 0;
		}
		minuteFlags[min] = true;
	}
	// 根据辅助数组计算数组中true的最小距离
	return helper(minuteFlags);
}

private int helper(boolean[] minuteFlags){
	//初始化一个最小距离,然后利用Math.min函数比较换成更小的
	int minDiff = minuteFlags.length - 1;
	//前一个是ture的数组下标
	int prev = -1;
	//第一个是true的下标 用于处理特殊情况
	int first = minuteFlags.length - 1;
	//最后一个是true的下标
	int last = -1;
	for(int i = 0; i < minuteFlags.length; i ++){
		if(minuteFlags[i]){
			if(prev >= 0){
				minDiff = Math.min(i - prev,minDiff);
			}
			prev = i;
			first = Math.min(i,first);
			last = Math.max(i,last);
		}
	}
	//用于处理特殊情况,00:00 -> 23:59
	minDiff = Math.min(first + minuteFlags.length - last, minDiff);
	return minDiff;
}

总结

	哈希表的时间效率很高,添加、删除、查找操作的时间复杂度都是O(1)。哈希表一般由链表的数组构成,HashMap在链表长度大于等于7时会将链表转成树结构
来保证效率,jdk代码如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿文_ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值