基础知识
哈希表最大的优点是高效,在哈希表中插入、删除或查找一个元素都只需要O(1)的时间,在java中哈希表有两个对应的类型,即HashSet和HashMap。
HashSet常用函数
序号 函数 函数功能 1 add 在HashSet中添加一个元素 2 contains 判断HashSet中是否包含一个元素 3 remove 从Hashset中删除一个元素 4 size 返回HashSet中元素的个数
HashMap常用函数
序号 函数 函数功能 1 containsKey 判断HashMap中是否包含某个键 2 get 如果键存在,则返回对应的值,否则返回null 3 getOrDefault 如果键存在,则返回对应的值,否则返回输入的默认值 4 put 如果键不存在,则添加一组键值到值的映射,否则修改键对应的值 5 putIfAbsent 当键不存在时添加一组键到值的映射 6 remove 删除某个键 7 replace 修改某个键对应的值 8 size 返回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;
}
public int get ( int key) {
ListNode node = map. get ( key) ;
if ( node == null ) {
return - 1 ;
}
moveToTail ( node, node. value) ;
return node. value;
}
public void put ( int key, int value) {
if ( map. containsKey ( key) ) {
moveToTail ( map. get ( key) , value) ;
} else {
if ( map. size ( ) == capacity) {
ListNode toBeDeleted = head. next;
deleteNode ( toBeDeleted) ;
map. remove ( toBeDeleted. key) ;
}
ListNode node = new ListNode ( key, value) ;
insertToTail ( node) ;
map. put ( key, node) ;
}
}
private void moveToTail ( ListNode node, int newValue) {
deleteNode ( node) ;
node. value = newValue;
insertToTail ( node) ;
}
private void deleteNode ( ListNode node) {
node. prev. next = node. next;
node. next. prev = node. prev;
}
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 ++ ) {
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) ;
if ( order[ ch1 - 'a' ] < order[ ch2 - 'a' ] ) {
return true ;
}
if ( order[ ch1 - 'a' ] > order[ ch2 - 'a' ] ) {
return false ;
}
}
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) {
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 ] ) ;
if ( minuteFlags[ min] ) {
return 0 ;
}
minuteFlags[ min] = true ;
}
return helper ( minuteFlags) ;
}
private int helper ( boolean [ ] minuteFlags) {
int minDiff = minuteFlags. length - 1 ;
int prev = - 1 ;
int first = minuteFlags. length - 1 ;
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) ;
}
}
minDiff = Math . min ( first + minuteFlags. length - last, minDiff) ;
return minDiff;
}
总结
哈希表的时间效率很高,添加、删除、查找操作的时间复杂度都是O(1)。哈希表一般由链表的数组构成,HashMap在链表长度大于等于7时会将链表转成树结构
来保证效率,jdk代码如下: