# HashTable

## 底层机制

set是简化版hashtable，而hashtable的本质其实是array（或者说Python里的list），因为list的本质就是array index和value的映射。所以关键问题是把key转换成index。一般分成3步：

1. 通过key和hash function计算它的hash\_number=hash(key) 这一步虽然可能有collision但是可能性比较低
2. index=hash\_number%N, N是array size。 这一步有很大概率有collision，取决于N的大小
3. 如果有hash collision，两种方法来解决（正文）； 如果没有，list\[index]=value

## Dictionary 使用场景

TF-IDF (Term Frequency- Inverse Document Frequency) 统计词频时，可以用list来统计频率，但是使用list的缺点是每次更新频率的时间复杂度都是O(n), 只能遍历一遍、找到词、给词频+1； 随着单词变多，这个过程就变得低效了。

在Python中，可以使用dictionary的结构来实现高效的“更新词频“的需求。

```python
def words_to_frequencies (words):
    myDict={}
    for word in words:
        if word in myDict:
            myDict[word]+=1
        else:
            myDict[word]=1
    return myDict
```

Time Complexity: O(n)

Space Complexity: O(1) 如果我们不算return值的话，那没有额外空间的消耗

### Example: 查找高频

如果要找到频率最高的word，分两步1. 遍历，找到最大的值； 2. 找到list，返回

```python
def most_common_words (freqs):
    best = max(freqs.values())
    words = [
        key                                        #代表元 expression    
        for key, val in freqs.items()              #for循环
        if val==best                               #判断条件
        ]
    return (words, best)   
```

List comprehension的写法：\[expression for elem in collection if condition]

相类似的字典也有dictionary comprehension，只需要把\[] 换成{}

{key\_expression: value\_expression for value in collection if condition}

### Example：合并大小写的频次

mydic = {'a': 10, 'b': 6, 'A': , 'B': }

```python
new_mydic={
    k.lower(): mydict.get(k.lower(), 0) + mydic.get(k.upper(),0)
    for k in mydic.keys()
}

#output: {'a': 17, 'b': 34, 'z': 3}
```

### get():

Python 字典(Dictionary) get() 函数返回指定key的value，如果值不在字典中返回默认值。

#### 语法

```
dict.get(key, default=None)
```

#### 参数

* key -- 字典中要查找的键。
* default -- 如果指定键的值不存在时，返回该默认值。

#### 返回值

返回指定键的值，如果值不在字典中返回默认值None。

#### 实例

```
dict = {'Name': 'Zara', 'Age': 27}

print "Value : %s" %  dict.get('Age')
print "Value : %s" %  dict.get('Sex', "Never")
```

以上实例输出结果为：

```
Value : 27
Value : Never
```

### [Example: Top K freq words](https://app.laicode.io/app/problem/67)

第一步：先把词频算出来&#x20;

第二步：sorting，按照单词的频率排序，而且是频率加负号，按负的频率排序，频率越大顺序越小。 或者也可以用key = lambda kv: kv\[1], reverse=True&#x20;

sorted的syntax:   sorted(iterable, key=key, reverse=reverse)

再次注意，dictionary没有顺序！所以不能直接输出前x个。

```python
class Solution(object):
  def topKFrequent(self, combo, k):
    """
    input: string[] combo, int k
    return: string[]
    """
    # write your solution here

    mydict={}
    for char in combo:
      if char in mydict:
        mydict[char]+=1
      else:
        mydict[char]=1

    sorted_tuple=sorted(mydict.items(), key= lambda kv: -kv[1] )

    return [x[0] for x in sorted_tuple][:k]
```

前面部分是O(n), sort部分都是O(nlogn), 最后一步还是O(n), 最终就是O(nlogn)

这里输出的是dict\_keys

如果使用collections这个package，然后把items转成list 可以通过。

```python
class Solution(object):
  def topKFrequent(self, combo, k):
    """
    input: string[] combo, int k
    return: string[]
    """
    # write your solution here
    import collections
    import heapq
    counts = collections.Counter(combo)
    items = list(counts.items())
    items.sort(key=lambda item:(-item[1],item[0]))
    return [item[0] for item in items[0:k]]
```

另外还可以用heap做这道题，也就是把整理好的dict一个个push到max heap或者min heap里。因为Python内置函数的heap是一个min heap，如果想用它做max heap，在每个元素push进去的时候对value取负号。然后在有元素可pop并且pop出来的元素数量<=k的前提下一个个pop元素, append到result上，然后反过来输出。

{% tabs %}
{% tab title="Max Heap" %}

```python
class Solution(object):
  def topKFrequent(self, combo, k):
    """
    input: string[] combo, int k
    return: string[]
    """
    # write your solution here
    # max heap
    mydict={}
    for char in combo:
      if char in mydict:
        mydict[char]+=1
      else:
        mydict[char]=1
      
    import heapq  
    freq=[]
    for char in mydict.keys():
      heapq.heappush(freq, (-mydict[char], char))
    
    topk, i =[],0
    while i < k and len(freq)>=1:
        topk.append(heapq.heappop(freq)[1])
        i+=1
    return topk
```

{% endtab %}

{% tab title="Min Heap" %}

```python
class Solution(object):
  def topKFrequent(self, combo, k):
    """
    input: string[] combo, int k
    return: string[]
    """
    # write your solution here
    # max heap

    import collections
    import heapq

    count=collections.Counter(combo)
    heap=[]
    
    for key in count.keys():
      heapq.heappush(heap, (count[key], key))
      if len(heap)>k:
        heapq.heappop(heap)
    
    res=[]
    while len(res)<=k and len(heap)>=1: 
      res.append(heapq.heappop(heap)[1])
    
    return res[::-1]
```

{% endtab %}
{% endtabs %}

### Example: Palindromic Testing

给一个单词，测试它能否被写成回文数的形式。能满足回文的条件，那就必须满足除了中间的数可能一个外，其余的都是偶数个；换句话说，奇数的字母只有1个，其余都是偶数个。所以我们需要建立字母到频率的字典，计算频率的个数。

```python
def is_palindromic(word):
    freq = {}
    for i in range(len(word)):
        if word[i] in freq:
            freq[word[i]]+=1
        else:
            freq[word[i]]=1
    odd_cnt = 0
    for key in freq.key():
        if freq[key]%2==1:
            odd_cnt += 1
            if odd_cnt > 1:
                return False
    return True 
```

Time Complexity: O(n)

Space Complexity: O(C) C是distinct chars in string&#x20;

### [Example: Subarray Sum to Target II](https://app.laicode.io/app/problem/571)

Given an array nums and a target value k, find the total number of subarrays that sums to k.

Nums = \[1,6,5,2,3,4,0] k=7, return 4\
prefixS = \[0,1,7,12,14,17,21,21]\
prefixS\[j]-prefixS\[i]==target <--->prefixS\[j]-target==prefixS\[i]

O(n) time, O(C) space, c is the number of distinct values

```python
class Solution(object):
  def numOfSubarraySumToK(self, nums, k):
    """
    input: int[] nums, int k
    return: int
    """
    # write your solution here
    sums, count = 0,0
    mydict={}
    for num in nums:
      if sums not in mydict:
        mydict[sums]=0
      mydict[sums]+=1 #注意这句和下句的先后顺序
      sums+=num
      if sums-k in mydict:
        count+=mydict[sums-k]
    return count
```

### Example: Nearest repeated entries in an array

重复的单词最近的距离 key是单词，value是位置，距离只需要减一下、更新为距离。

```python
def nearest_repeat (arr):
    word_ind = {}
    dist = float('inf')
    for i in range(len(arr)):
        if arr[i] in word_ind:
            dist = min(dist, i -word_ind[arr[i]])
        word_ind[arr[i]] = i
    return dist
```

Time Complexity: O(n)

Space Complexity: O(C) C是distinct entries in arr

### [Example: 2 sum unsorted, no dup](https://app.laicode.io/app/problem/180)

return index, O(n) time and O(n) space

```python
def 2sum(nums, target):
    if len(nums)<=1:
        return False
    dict={}
    for i in range (nums):
        if nums[i] in dict:
            return (dict[num[i]], i)
        else:
            dict[target-nums[i]]=i
    return False
```

### Example: Nearest repeated entries in an array

### Example: Longest Contained Range

brute-force: sort, O(nlogn), search, O(n)

set: “中心开花，左右出发“，以任何一个元素出发，往两边扩展

1. 先放进集合里
2. pop 任取元素，左右扩展

```python
def longest_contained_range(arr):
    unprocessed = set(arr)
    maxlen = 0
    while unprocessed:
        elem = unprocessed.pop()
        lower = elem - 1
        while lower in unprocesssed:
            unprocessed.remove(lower)
            lower = lower-1
        upper = elem + 1
        while upper in unprocessed:
            unprocessed.remove(upper)
            upper = upper+1
        maxlen = max(maxlen, upper-lower-1)
    return maxlen
```

时间 O(n)

空间 O(C)

### list vs. dictionary&#x20;

![](https://cdn.mathpix.com/snip/images/y9O3fSu8KAqCz-ToOpNBDbA38tZIL1SJ-Ci2qFFeSVY.original.fullsize.png)

![](https://cdn.mathpix.com/snip/images/3azTsxI4SoO1qqsyI08qpJ-39AS2_DOFSPIFLplyEgw.original.fullsize.png)

list存储的是单独的元素，而字典存储的是对应关系（mapping），或者叫它映射。我们叫它Key-value pair，键值-存储值。

对任何一个数据结构，都需要知道 创建、增、删、查、改

### 创建

```python
my_dict = {}
grades = {'Ana': 'B', 'John': 'A+', 'Denise': 'A', 'Katy': 'A'}
```

### 查找

字典没有顺序，只能按key查找，所以必须是存在在字典里的元素，不然直接run下行就会出现key error

```python
grades['John']
```

还有一个函数，get 如果找不到，可以给None&#x20;

```python
print (grades.get ('Bob'))
```

判断key在不在字典里，就可以用lookup的操作，

```python
'John' in grades  #return False
'Daniel' in grades #return True 
```

### 增改

加一个新的entry、改已有的syntax，都可以很简单的

```python
grades['John'] = 'A'
grades['Ana'] = 'B+'
```

### 删除

```python
del grades['Ana'] #从字典里取这个key，然后删了
grades.pop('Ana')  #随机pop出一个Ana（如果有两个Ana）
```

### 拿到key的集合

python2 用grades.keys() return的是\[‘A’, 'A', 'A+', 'B'] （value的list）

python3 的return是 （value的view）

view都不会占额外的空间，就像是range和xrange的区别

当然也可以用list(dict.keys()) ，占空间呗

### 拿到value的集合

类似👆

### requirements for keys

1. Key必须unique，不能重复；第二次的一样的值会把第一个覆盖掉
2. Key必须是immutable的类型 所以X={\[1]:0}不对，list unhashable

像Y={(1,2):10}就行，str也行

## Dictionary Implementation

字典，hash的实现里，不需要去查找每个元素了，而是有一系列的buckets。每次有一个key，先算hashfunction（key），通过这个值，来得到bucket的序号。hashfunc可以接收任何变量，最后它能计算出一个integer，下面的code中，array\_size是一个常数，也就是bucket size. 用算好的hash来%array\_size, 得到index，意味着最后要把它放到index的桶里去找。

```python
hash = hashfunc(key)
index = hash % array_size
```

### Hash Collision&#x20;

假设两个key，x和y，本身x!=y, 但是hash(x)==hash(y), 就有了collision，因为它们被映射到了同一个bucket里，所以在第二行一定一样。

此外，因为array\_size是一个定值，即使1不一样了，到2还是有可能一样，那还是会collision。

&#x20;如果有perfect hashing, array\_size又是无穷大（这在实际情况中是不存在的），就不会有冲突。

那么 现实中如何解决hash collision？

策略一： open addressing&#x20;

![](https://cdn.mathpix.com/snip/images/pUAj2XwLpyYfoIPIcnZD3vFEnzo_1c3bZEwDIRy7QiU.original.fullsize.png)

如果算出来的数值被占了，发生了hash collision，就占用它最近的没有被占用的单元。

策略二：separate chaining&#x20;

![](https://cdn.mathpix.com/snip/images/k3-3lDVqKUvdqMWLZKpb6wVyjXe5-suCvXR3ZPL23c4.original.fullsize.png)

在每一个bucket里，就做成链表，把下一个人直接占在它后面 worst case就是，如果bucket=1(概率极低), 那么就变成了singly linked list，那么查找的时间复杂度又是O(n)了 不过，on average 还是可以O(1)

Time Complexity of different operations on dictionary&#x20;

|Operation | Avg | Worst |&#x20;

Search O(1) O(n)&#x20;

Add O(1) O(n)&#x20;

Delete O(1) O(n)&#x20;

Update O(1) O(n)

## Set

有时我们并不关心value，只关心key本身，这个时候dict就退化成了set，集合。集合就是只包含了key的hashtable。需要注意的是，集合本身可变mutable，但是里面包含的元素不变。所以增删查改咯～

### Add

```python
x = {"a","b","c","d"}
x.add("e") #one element

x.update({"e", "f"}) #multiple elements
```

### remove&#x20;

如果remove的元素不在，使用x.remove会报keyerror； 但是使用x.discard("a")不会报

如果从中随意删除任何一个(因为set没有顺序，所以真的不知道删了哪个... )，x.pop()； 如果是清空，x.clear() ；

### union

下面是集合操作的取并集 这个x和y是用set（）把list变成集合

x = set(\["Postcard",  "Radio", "Telegram"])

y = set(\[ "Radio", "Television"])

print x.union(y)&#x20;

### 更多

![](https://cdn.mathpix.com/snip/images/77rv6-zY5MX29CrX7WnJlZ2CnYKgkN2eZtkMrfn-hUU.original.fullsize.png)

set也有set comprehension

{expression for value in collection if condition}

```python
squared = {x**2 for x in [1,1,2]}
```

## Recap

1. One to one mapping&#x20;
2. \<key, value> pair, key map to value
3. Key, no duplicates&#x20;
4. Value, allow duplicates&#x20;
5. hash\_set is a set {1,3}, it only contains keys&#x20;

In python, hash\_set is set; hash\_table is dictionary&#x20;

## 题目

### find one missing from unsorted array

1. use a set, iterate over the input array                                    Average O(n) Worst O(n^2)
2. for each number from 1 to n: find if it's in the set               Average O(n) Worst O(n^2)

#### 优化 use a boolean\[]

因为连续，可以放进物理连续的boolean array里\
Step 1: for each element in the input array (value=i), put it into the seen\[i]=true       O(n)\
Step 2: for each element from 1...n: check if seen\[i]==true                                            O(n)

#### 数学

等差数列求和，减去sum of input array，剩下的就是output        Space O(1)\
but it may overflow

#### bit operation XOR

* 交换律 a XOR b = b XOR a
* 结合律 a XOR (b XOR c) = (a XOR b) XOR c
* 0是单元 0 XOR a = a
* a XOR a =0

![](/files/-LyldrON1aQHDC6O_Fra)

```python
class Solution(object):
  def missing(self, array):
    """
    input: int[] array
    return: int
    """
    # write your solution here
    n = len(array)+1
    xor = 0
    for i in range(1, n+1):
      xor ^= i
    for num in array:
      xor ^= num
    
    return xor
```

### find common numbers between 2 sorted arrays a\[M], b\[N]

> 需要确认 如果有重复元素怎么办

Assume没有重复 或者 有重复元素只输出一次

#### Hashset

比较大小，把小的放进hashset

Step 1: Insert all elements from smaller array a into the hash set

Step 2: For each element in array b, check if it's in the HashSet or not

Time = O(M+N) average, worst O(M^2+N\*M)\
Space = O(M)

#### 谁小移谁

不相等就谁小移谁，相等就分别往前

Time = O(M+N)\
Space = O(1)

```python
class Solution(object):
  def common(self, A, B):
    """
    input: int[] A, int[] B
    return: Integer[]
    """
    # write your solution here
    i, j = 0, 0
    result = []
    while (i<len(A) and j<len(B)):
      if (A[i]==B[j]):
        result.append(A[i])
        i+=1
        j+=1
      elif A[i]<B[j]:
        i+=1
      else:
        j+=1
    return result
```

#### 如果M<<\<N Binary Search

For each element in b\[N], run binary search M times.&#x20;

Time = O(MlogN)\
Space = O(1)

### 2 sum sorted

因为是sorted，所以可以用2 pointers的方法，利用增加和减小的单调性：一个往前移一个往后移；如果现在的和比target小，移动i往后；如果比现在的target大，移动j往前。

```python
def two_sum(list, target):
    i,j = 0, len(list)-1
    while i<j:
        if list[i]+list[j] < target:
            i += 1
        elif list[i]+list[j] > target:
            j -= 1
        elif list[i]+list[j] == target:
            return True
    return False
    
# Time O(n)
# Space O(1)
```

### 2 sum unsorted

利用set的性质，经过的数字都存下来；用target的数字和当前经过的数字相减，如果得到的结果在之前的set里，就找到了，return true。否则，false。

```python
def two_sum(list, target):
    hashset = set()
    for key in list:
        if (target-key) in hashset:
            return True
        else:
            hashset.add(key)
    return False
    
# Time O(n)
# Space O(n)
```

### [2 sum with duplicates](https://app.laicode.io/app/problem/181)

\[10, 2, 2, 13, 2], target 4&#x20;

* 如果题目要output多少对，用一个dict，存count： {10:1, 2:2, ...}
* 如果题目要output哪些index的组合，dict里是list of index: {10:(0), 2:(1,2), ...}

```python
def two_sum_duplicate(arr, sum):
    dic = {}
    count = 0
    for key in arr:
        if sum-key in dic:
            count+=dic[sum-key]
        if key in dic:
            dic[key]+=1
        else:
            dic[key]=1
    return count

# Time O(n)
# Space O(n)
```

### 3 sum unsorted

方法一：走n遍的2 sum。Time O( $$n^2$$ ) Space O(n)

方法二：先排序，反正已经O( $$n^2$$ )了，就算先排序也不会影响这个时间复杂度，那就排序吧！可以让空间复杂都变成O(1)。

```python
def three_sum (array, target):
    if array is None or len(array)==0:
        return False
    
    array.sort()
    for i in range(len(array)-2):
        start = i + 1
        end = len(array)-1
        while start<end:
            tmp_sum = array[start] + array[end] + array[i]
            if tmp_sum == target:
                return True
            elif tmp_sum < target:
                start += 1
            else:
                end -= 1
        return False
        
# Time O(n2)
# Space O(1)
```

### 4 sum unsorted

方法一：走n遍的3 sum。O( $$n^3$$ )

```python
for i in range(len(array)-3):
  for j in range(i+1, len(array)-2):
    #2sum  
  
# Time O(n3)
# Space O(1)
```

方法二：变成2个2sum，如何找到pair of 2 sum, 和是target。\
1\. 先配对  O( $$n^2$$ )\
2\. sort一遍或者hashset  O( $$n^2$$ )\
3\. 在配对的时候要检查它是否是用过的 O(1)

```python
class twoSumPair:
    def __init__(self, index1, index2):
        self.index1 = index1
        self.index2 = index2
        self.number1 = array[index1]
        self.number2 = array[index2]
        self.sum = self.number1 + self.number2

    pairlist=[twoSumPair(0,1), twoSumPair(0,2), twoSumPair(0,3), twoSumPair(0,4),
    twoSumPair(1,2),twoSumPair(1,3)...]
```

### k sum

在array里选k个数字组成sum=target。DFS，k个数字的subset

### 2 difference sorted

2 pointers，错一位同向而行。因为如果是相向而行，目前的值大于target，那么移动i或者j都行，所以到底移动哪个？ 两个都尝试时间复杂度就会变高。同向而行，如果当前值大于target，移动i向前；如果当前值小于target，移动j向前。

```python
def two_difference(array,target):
    i,j = 0, 1
    while j<len(array):
        if array[j]-array[i] < target:
            j += 1
        elif array[j]-array[i] > target:
            i += 1
        elif:
            return True
    return False
    
# Time O(n)
# Space O(1)
```

### 2 difference unsorted

1. 创建set
2. loop array，检查set里的元素是否等于array\[i]-target或者array\[i]+target

```python
def two_sum(list, target):
    hashset = set()
    for key in list:
        if (key-target) in hashset or (target+key) in hashset:
            return True
        else:
            hashset.add(key)
    return False
    
# Time O(n)
# Space O(n)
```

### Longest sublist without duplicate values.&#x20;

1 2 3 1 4 3 longest is 2 3 1 4

```python
def longest_sub_list(list):
    hashset=set()
    longest=0
    slow=fast=0
    while fast<len(list):
        while list[fast] in hashset:
            hashset.remove(list[slow])
            slow+=1
        hashset.add(list[fast])
        longest = max(longest, fast-slow+1)
        fast+=1
    return longest
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://louisazhou.gitbook.io/notes/chapter2/hashtable.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
