跳至主要內容

hashtable

HeChuangJun约 5361 字大约 18 分钟

概念

  • 哈希表是使用哈希的技术提供从键到值的映射的数据结构,我们将其称为键值对,键必须是唯一的,但值可以重复

  • 哈希表中可以放置的键值对可以是任意类型,不仅仅是字符串和数字,还可以是对象!但是,键需要是可哈希的,

  • 使用场景:判断一个元素是否出现集合里,出现次数

  • 哈希函数 H(x) 是将键“x”映射到固定范围内的整数的函数。可以为任意对象(例如字符串、列表、元组、多数据对象等)定义哈希函数。

  • 哈希冲突:如果 H(x)=H(y),则对象 x 和 y可能相等,但如果 H(x)≠ H(y),则 x 和 y 肯定不相等。与其直接比较 x 和 y,更聪明的方法是先比较它们的哈希值,只有当哈希值匹配时,我们才需要明确比较 x 和
    例如比较2个大文件的内容是否相等,可以用文件的哈希函数比较,避免打开文件。文件的哈希函数比哈希表使用的哈希函数更复杂,对于文件,我们使用所谓的加密哈希函数,也称为校验和。

  • 哈希函数 H(x) 必须是确定性的。如果 H(x)=y,则 H(x) 必须始终产生 y,而不会产生其他值。

  • 解决hash冲突的方法:链地址法通过维护一个数据结构(通常是链接列表)来保存散列到特定值的所有不同值,从而解决散列冲突问题。
    注意:用于缓存散列到特定值的项目的数据结构不限于链接列表,一些实现使用以下一种或多种混合:数组、二叉树、自平衡树等。

  • HT数据太多,如何保持 o(1) 插入和查找时间复杂度?创建一个具有更大容量的新 HT,并重新散列旧 HT 内的所有项目,并将它们分散到新 HT 的不同位置。

  • 算法复杂度
    Operation Average Worst
    Insertion 0(1)* o(n)
    Removal 0(1)* o(n)
    Search 0(1)* o(n)

链地址法代码实现

/**
 * An implementation of a hash-table using separate chaining with a linked list.
 *
 * @author William Fiset, william.alexandre.fiset@gmail.com
 */
package com.williamfiset.datastructures.hashtable;

import java.util.*;

class Entry<K, V> {

  int hash;
  K key;
  V value;

  public Entry(K key, V value) {
    this.key = key;
    this.value = value;
    this.hash = key.hashCode();
  }

  // We are not overriding the Object equals method
  // No casting is required with this method.
  public boolean equals(Entry<K, V> other) {
    if (hash != other.hash) return false;
    return key.equals(other.key);
  }

  @Override
  public String toString() {
    return key + " => " + value;
  }
}

@SuppressWarnings("unchecked")
public class HashTableSeparateChaining<K, V> implements Iterable<K> {

  private static final int DEFAULT_CAPACITY = 3;
  private static final double DEFAULT_LOAD_FACTOR = 0.75;

  private double maxLoadFactor;
  private int capacity, threshold, size = 0;
  private LinkedList<Entry<K, V>>[] table;

  public HashTableSeparateChaining() {
    this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR);
  }

  public HashTableSeparateChaining(int capacity) {
    this(capacity, DEFAULT_LOAD_FACTOR);
  }

  // Designated constructor
  public HashTableSeparateChaining(int capacity, double maxLoadFactor) {
    if (capacity < 0) throw new IllegalArgumentException("Illegal capacity");
    if (maxLoadFactor <= 0 || Double.isNaN(maxLoadFactor) || Double.isInfinite(maxLoadFactor))
      throw new IllegalArgumentException("Illegal maxLoadFactor");
    this.maxLoadFactor = maxLoadFactor;
    this.capacity = Math.max(DEFAULT_CAPACITY, capacity);
    threshold = (int) (this.capacity * maxLoadFactor);
    table = new LinkedList[this.capacity];
  }

  // Returns the number of elements currently inside the hash-table
  public int size() {
    return size;
  }

  // Returns true/false depending on whether the hash-table is empty
  public boolean isEmpty() {
    return size == 0;
  }

  // Converts a hash value to an index. Essentially, this strips the
  // negative sign and places the hash value in the domain [0, capacity)
  private int normalizeIndex(int keyHash) {
    return (keyHash & 0x7FFFFFFF) % capacity;
  }

  // Clears all the contents of the hash-table
  public void clear() {
    Arrays.fill(table, null);
    size = 0;
  }

  public boolean containsKey(K key) {
    return hasKey(key);
  }

  // Returns true/false depending on whether a key is in the hash table
  public boolean hasKey(K key) {
    int bucketIndex = normalizeIndex(key.hashCode());
    return bucketSeekEntry(bucketIndex, key) != null;
  }

  // Insert, put and add all place a value in the hash-table
  public V put(K key, V value) {
    return insert(key, value);
  }

  public V add(K key, V value) {
    return insert(key, value);
  }

  public V insert(K key, V value) {

    if (key == null) throw new IllegalArgumentException("Null key");
    Entry<K, V> newEntry = new Entry<>(key, value);
    int bucketIndex = normalizeIndex(newEntry.hash);
    return bucketInsertEntry(bucketIndex, newEntry);
  }

  // Gets a key's values from the map and returns the value.
  // NOTE: returns null if the value is null AND also returns
  // null if the key does not exists, so watch out..
  public V get(K key) {

    if (key == null) return null;
    int bucketIndex = normalizeIndex(key.hashCode());
    Entry<K, V> entry = bucketSeekEntry(bucketIndex, key);
    if (entry != null) return entry.value;
    return null;
  }

  // Removes a key from the map and returns the value.
  // NOTE: returns null if the value is null AND also returns
  // null if the key does not exists.
  public V remove(K key) {

    if (key == null) return null;
    int bucketIndex = normalizeIndex(key.hashCode());
    return bucketRemoveEntry(bucketIndex, key);
  }

  // Removes an entry from a given bucket if it exists
  private V bucketRemoveEntry(int bucketIndex, K key) {

    Entry<K, V> entry = bucketSeekEntry(bucketIndex, key);
    if (entry != null) {
      LinkedList<Entry<K, V>> links = table[bucketIndex];
      links.remove(entry);
      --size;
      return entry.value;
    } else return null;
  }

  // Inserts an entry in a given bucket only if the entry does not already
  // exist in the given bucket, but if it does then update the entry value
  private V bucketInsertEntry(int bucketIndex, Entry<K, V> entry) {

    LinkedList<Entry<K, V>> bucket = table[bucketIndex];
    if (bucket == null) table[bucketIndex] = bucket = new LinkedList<>();

    Entry<K, V> existentEntry = bucketSeekEntry(bucketIndex, entry.key);
    if (existentEntry == null) {
      bucket.add(entry);
      if (++size > threshold) resizeTable();
      return null; // Use null to indicate that there was no previous entry
    } else {
      V oldVal = existentEntry.value;
      existentEntry.value = entry.value;
      return oldVal;
    }
  }

  // Finds and returns a particular entry in a given bucket if it exists, returns null otherwise
  private Entry<K, V> bucketSeekEntry(int bucketIndex, K key) {

    if (key == null) return null;
    LinkedList<Entry<K, V>> bucket = table[bucketIndex];
    if (bucket == null) return null;
    for (Entry<K, V> entry : bucket) if (entry.key.equals(key)) return entry;
    return null;
  }

  // Resizes the internal table holding buckets of entries
  private void resizeTable() {

    capacity *= 2;
    threshold = (int) (capacity * maxLoadFactor);

    LinkedList<Entry<K, V>>[] newTable = new LinkedList[capacity];

    for (int i = 0; i < table.length; i++) {
      if (table[i] != null) {

        for (Entry<K, V> entry : table[i]) {
          int bucketIndex = normalizeIndex(entry.hash);
          LinkedList<Entry<K, V>> bucket = newTable[bucketIndex];
          if (bucket == null) newTable[bucketIndex] = bucket = new LinkedList<>();
          bucket.add(entry);
        }

        // Avoid memory leak. Help the GC
        table[i].clear();
        table[i] = null;
      }
    }

    table = newTable;
  }

  // Returns the list of keys found within the hash table
  public List<K> keys() {

    List<K> keys = new ArrayList<>(size());
    for (LinkedList<Entry<K, V>> bucket : table)
      if (bucket != null) for (Entry<K, V> entry : bucket) keys.add(entry.key);
    return keys;
  }

  // Returns the list of values found within the hash table
  public List<V> values() {

    List<V> values = new ArrayList<>(size());
    for (LinkedList<Entry<K, V>> bucket : table)
      if (bucket != null) for (Entry<K, V> entry : bucket) values.add(entry.value);
    return values;
  }

  // Return an iterator to iterate over all the keys in this map
  @Override
  public java.util.Iterator<K> iterator() {
    final int elementCount = size();
    return new java.util.Iterator<K>() {

      int bucketIndex = 0;
      java.util.Iterator<Entry<K, V>> bucketIter = (table[0] == null) ? null : table[0].iterator();

      @Override
      public boolean hasNext() {

        // An item was added or removed while iterating
        if (elementCount != size) throw new java.util.ConcurrentModificationException();

        // No iterator or the current iterator is empty
        if (bucketIter == null || !bucketIter.hasNext()) {

          // Search next buckets until a valid iterator is found
          while (++bucketIndex < capacity) {
            if (table[bucketIndex] != null) {

              // Make sure this iterator actually has elements -_-
              java.util.Iterator<Entry<K, V>> nextIter = table[bucketIndex].iterator();
              if (nextIter.hasNext()) {
                bucketIter = nextIter;
                break;
              }
            }
          }
        }
        return bucketIndex < capacity;
      }

      @Override
      public K next() {
        return bucketIter.next().key;
      }

      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }

  // Returns a string representation of this hash table
  @Override
  public String toString() {

    StringBuilder sb = new StringBuilder();
    sb.append("{");
    for (int i = 0; i < capacity; i++) {
      if (table[i] == null) continue;
      for (Entry<K, V> entry : table[i]) sb.append(entry + ", ");
    }
    sb.append("}");
    return sb.toString();
  }
}

开放地址法代码实现

哈希表 (HT) 的目标是构建从键到值的映射
键必须是可哈希的,我们需要一个将键转换为整数的哈希函数。
我们使用在键集上定义的哈希函数来索引数组(哈希表)
哈希函数并不完美,因此有时两个键 k、k2(k1 ≠ k2)哈希为相同的值。当发生这种情况时,我们就会遇到哈希冲突(即 H(k1)= H(k2))
开放寻址是解决此问题的一种方法

当使用开放寻址作为冲突解决技术时,键值对存储在表(数组)本身中,而不是像单独链接中的数据结构
这意味着我们需要非常关注哈希表的大小以及表中当前有多少元素。
加载因子 =表中的项目/表的大小

当我们想要将一个键值对 (k,v) 插入到哈希表中时,我们会对键进行哈希处理,并获得此键值对所属的原始位置,即 H(k)。
如果键哈希到的位置已被占用,我们会通过根据探测序列 P(x) 偏移当前位置来尝试哈希表中的另一个位置,我们一直这样做,直到找到一个未占用的位置。
你可以想出无数个探测序列,这里列举几个:
线性探测:P(x)= ax + b,其中 a、b 为常数
二次探测:P(x)= ax2 + bx+c,其中 a、b、c 为常数
双重哈希:P(k,x)=xHz(k),其中 Hz(k) 是二次哈希函数
伪随机数生成器:P(k,x)=x
RNG(H(k),x),其中 RNG 是以 H(k) 为种子的随机数生成器函数

大多数随机选择的模 N 探测序列将产生比表大小更短的循环。当您尝试插入键值对并且循环上的所有存储桶都被占用时,这会出现问题,因为您将陷入无限循环!
问:如何处理产生比表大小更短的循环的探测函数?
答:一般来说,共识是我们不处理这个问题,而是通过将探测函数的范围限制为产生正好长度为 N 的循环的函数来完全避免它*
*有一些具有特殊属性的例外,可以产生更短的循环。

线性探测、二次探测和双重哈希等技术都存在导致循环的问题,这就是为什么这些方法使用的探测函数是特定的,
请注意,开放寻址对所使用的哈希函数和探测函数非常敏感:如果您使用分离链接作为冲突解决方法,您不必(过多)担心这一点

问:P(x)=ax 中常数 a负载因子 的哪些值会产生模 N 总数的完整循环?
答:当 a 和 N 互质时,即两个数字的最大公约数 (GcD) 等于一。因此,当 Gcp(a,N)= 1 时,探测函数 P(x) 能够生成一个完整循环,并且我们总能找到一个空桶!因此选择互为质数的数字作为模避免死循环,或者P(x) 的一个常见选择是 P(x)= 1X因为 GcD(N,1)=1,无论 N(表大小) 的选择如何

Probing function:P(x)=5X
Fixed table size:N=12
Max load factor:a=0.35
Threshold before resize=N*=4

OP 是一种探测方法,它根据二次公式进行探测,具体来说:
P(x)= ax2 + bx +c,其中 a、b、c 为常数,且 a ≠(否则我们进行线性探测)(注意:常数 c 已过时,您知道为什么吗?)
但是,正如我们之前所看到的,并非所有二次函数都是可行的,因为它们无法产生 N 阶循环。我们需要某种方法来处理这个问题。

问:那么我们如何选择一个可以使用的探测函数?
答:有很多方法,但最流行的三种方法是:
1)让 P(x)=x2,保持表大小为素数>3,并保持a小于等于二分之一
2)让 P(x)=(x^2 + x)/2,保持表大小为 2 的幂
3)让 P(x)=(-1^x)*x2,保持表大小为素数 N,其中 N≡3 mod 4

双重hash DH 是一种根据另一个哈希函数的常数倍数进行探测的探测方法,具体为:
P(k,x)= x*H(k),其中 H(k) 是
第二个哈希函数
Hz(k) 必须散列与 H(k) 相同类型的key.比如都是字符串
注意:请注意,加倍散列会减少为线性探测(除了常数直到运行时才知道)

避免死循环,首先哈希表的大小必须是质数,然后令δ=H2(k)mod N 当δ=0就会死循环,我们需要强制设置为δ=1
通常,选择用于组成 Hz(k) 的哈希函数是从称为通用哈希函数的哈希函数池中挑选出来的,这些哈希函数通常对一种基本数据类型进行操作。

开放地址删除连续的线性探测中间数据时,将该数据设置为“墓碑”,查询时跳过墓碑,避免找不到数据
0:我的 HT 中有很多墓碑,我该如何清除它们?
答:墓碑在 HT 中算作已填充的槽位,因此它们会增加负载因子,并在调整表大小时被移除。此外,插入新的 (k,v) 对时,您可以用新的键值对替换带有墓碑的存储桶

/**
 * An implementation of a hash-table using open addressing with quadratic probing as a collision
 * resolution method.
 *
 * <p>In this implementation we are using the following probing function: H(k, x) = h(k) + f(x) mod
 * 2^n
 *
 * <p>Where h(k) is the hash for the given key, f(x) = (x + x^2) / 2 and n is a natural number. We
 * are using this probing function because it is guaranteed to find an empty cell (i.e it generates
 * all the numbers in the range [0, 2^n) without repetition for the first 2^n numbers).
 *
 * @author William Fiset, william.alexandre.fiset@gmail.com
 */
package com.williamfiset.datastructures.hashtable;

@SuppressWarnings("unchecked")
public class HashTableQuadraticProbing<K, V> extends HashTableOpenAddressingBase<K, V> {

  public HashTableQuadraticProbing() {
    super();
  }

  public HashTableQuadraticProbing(int capacity) {
    super(capacity);
  }

  // Designated constructor
  public HashTableQuadraticProbing(int capacity, double loadFactor) {
    super(capacity, loadFactor);
  }

  // Given a number this method finds the next
  // power of two above this value.
  private static int nextPowerOfTwo(int n) {
    return Integer.highestOneBit(n) << 1;
  }

  // No setup required for quadratic probing.
  @Override
  protected void setupProbing(K key) {}

  @Override
  protected int probe(int x) {
    // Quadratic probing function (x^2+x)/2
    return (x * x + x) >> 1;
  }

  // Increase the capacity of the hashtable to the next power of two.
  @Override
  protected void increaseCapacity() {
    capacity = nextPowerOfTwo(capacity);
  }

  // Adjust the capacity of the hashtable to be a power of two.
  @Override
  protected void adjustCapacity() {
    int pow2 = Integer.highestOneBit(capacity);
    if (capacity == pow2) return;
    increaseCapacity();
  }
}

242有效的字母异位词

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
说明: 你可以假设字符串只包含小写字母。

时间复杂度O(m+n) 
空间复杂度O(1)
class Solution {
    public boolean isAnagram(String s, String t) {
      int[] record = new int[26];
      for (int i = 0; i < s.length(); i++) {
        // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
        record[s.charAt(i) - 'a']++;
      }
      for (int i = 0; i < t.length(); i++) {
        record[t.charAt(i) - 'a']--;
      }
      for (int count: record) {
        // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
        if (count != 0) {
            return false;
        }
      }
      return true;// record数组所有元素都为零0,说明字符串s和t是字母异位词
    }
}

383赎金信

给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
注意:
你可以假设两个字符串均只含有小写字母。
canConstruct("a", "b") -> false
canConstruct("aa", "ab") -> false
canConstruct("aa", "aab") -> true

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        // shortcut
        if (ransomNote.length() > magazine.length()) {
            return false;
        }
        // 定义一个哈希映射数组
        int[] record = new int[26];

        // 遍历
        for(char c : magazine.toCharArray()){
            record[c - 'a'] += 1;
        }

        for(char c : ransomNote.toCharArray()){
            record[c - 'a'] -= 1;
        }
        
        // 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符
        for(int i : record){
            if(i < 0){
                return false;
            }
        }

        return true;
    }
}

349两个数组的交集

给定两个数组 nums1 和 nums2 ,返回 它们的交集。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
            return new int[0];
        }
        Set<Integer> set1 = new HashSet<>();
        Set<Integer> resSet = new HashSet<>();
        //遍历数组1
        for (int i : nums1) {
            set1.add(i);
        }
        //遍历数组2的过程中判断哈希表中是否存在该元素
        for (int i : nums2) {
            if (set1.contains(i)) {
                resSet.add(i);
            }
        }
      
        //方法1:将结果集合转为数组

        return resSet.stream().mapToInt(x -> x).toArray();
        
        //方法2:另外申请一个数组存放setRes中的元素,最后返回数组
        int[] arr = new int[resSet.size()];
        int j = 0;
        for(int i : resSet){
            arr[j++] = i;
        }
        
        return arr;
    }
}

202快乐数

编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:输入:19 输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

class Solution {
    public boolean isHappy(int n) {
        Set<Integer> record = new HashSet<>();
        while (n != 1 && !record.contains(n)) {
            record.add(n);
            n = getNextNumber(n);
        }
        return n == 1;
    }

    private int getNextNumber(int n) {
        int res = 0;
        while (n > 0) {
            int temp = n % 10;
            res += temp * temp;
            n = n / 10;
        }
        return res;
    }
}

454四数相加II

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
例如: 输入:
A = [ 1, 2]
B = [-2,-1]
C = [-1, 2]
D = [ 0, 2]
输出:2
解释:两个元组如下:
(0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0
(1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        int res = 0;
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        //统计两个数组中的元素之和,同时统计出现的次数,放入map
        for (int i : nums1) {
            for (int j : nums2) {
                int sum = i + j;
                map.put(sum, map.getOrDefault(sum, 0) + 1);
            }
        }
        //统计剩余的两个元素的和,在map中找是否存在相加为0的情况,同时记录次数
        for (int i : nums3) {
            for (int j : nums4) {
                res += map.getOrDefault(0 - i - j, 0);
            }
        }
        return res;
    }
}

t1两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

示例 输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。
空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
public int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
    for (int i = 0; i < nums.length; ++i) {
        if (hashtable.containsKey(target - nums[i])) {
            return new int[]{hashtable.get(target - nums[i]), i};
        }
        hashtable.put(nums[i], i);
    }
    return new int[0];
}

t49字母异位词分组

  • 给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
  • 示例 输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"],输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
时间复杂度O(nklogk),n、k是strs中的字符串的数量和最大长度。需要遍历n个字符串,
对于每个字符串,需要 O(klogk) 的时间进行排序以及 O(1) 的时间更新哈希表,总时间复杂度O(nklogk)
空间复杂度O(nk),n 、k是strs 中的字符串的数量和最大长度。需要用哈希表存储全部字符串
class Solution {
  public List<List<String>> groupAnagrams(String[] strs) {
    Map<String,List<String>> hashMap = new HashMap<String,List<String>>();
    for(String s: strs){
      char[] split = s.toCharArray();
      Arrays.sort(split);
      String newStr = new String(split);
      List<String> list = hashMap.getOrDefault(newStr,new ArrayList<String>());
      list.add(s);
      hashMap.put(newStr,list);
    }
    return new ArrayList<List<String>>(hashMap.values());
  }
}

t128最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n) 的算法解决此问题。
示例 1:输入:nums = [100,4,200,1,3,2] 输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

时间复杂度:O(n),其中 n 为数组的长度。具体分析已在上面正文中给出。
空间复杂度:O(n)。哈希表存储数组中所有的数需要 O(n) 的空间。
class Solution {
  public int longestConsecutive(int[] nums) {
    //去重
    Set<Integer> num_set = new HashSet<Integer>();
    for (int num : nums) {
        num_set.add(num);
    }

    int longestStreak = 0;

    for (int num : num_set) {
        if (!num_set.contains(num - 1)) {
            int currentNum = num;//如果集合中不包含 num - 1,意味着 num 是某个连续序列的起点。
            int currentStreak = 1;

            while (num_set.contains(currentNum + 1)) {
                currentNum += 1;
                currentStreak += 1;
            }
            //每次都更新序列的最大值
            longestStreak = Math.max(longestStreak, currentStreak);
        }
    }

    return longestStreak;
  }
}

public int longestConsecutive(int[] nums) {
    // key表示num,value表示num所在连续区间的长度
    Map<Integer, Integer> map = new HashMap<>();
    int ans = 0;
    for (int num : nums) {
        // 当map中不包含num,也就是num第一次出现
        if (!map.containsKey(num)) {
            // left为num-1所在连续区间的长度,更进一步理解为:左连续区间的长度
            int left = map.getOrDefault(num - 1, 0);
            // right为num+1所在连续区间的长度,更进一步理解为:右连续区间的长度
            int right = map.getOrDefault(num + 1, 0);
            // 当前连续区间的总长度
            int curLen = left + right + 1;
            ans = Math.max(ans, curLen);
            // 将num加入map中,表示已经遍历过该值。其对应的value可以为任意值。
            map.put(num, -1);
            // 更新当前连续区间左边界和右边界对应的区间长度
            map.put(num - left, curLen);
            map.put(num + right, curLen);
        }
    }
    return ans;
}