算法练习-哈希表(一)

算法练习-哈希表

1 哈希表介绍

1.1 哈希表的由来

假设又100个学生,如何根据id快速查找学生信息?

方案一:将所有的学生都存在一个数组中,查找id时遍历数组,时间复杂度:O(n)

方案二:将数组中的数据进行排序,查找时使用二分查找,时间复杂度:O(logn)

方案三:将数组下标与id一一映射 , 时间复杂度:O(1)

private int maxId = 1000; // 假设最大id为1000
private Student[] s = new Student[maxId + 1];

public int insert(Student student) {
    
    
    s[student.id] = student;
}

public Student find(int id) {
    
    
    return s[id];
}

但是如果id的范围很大,还是进行一一映射,空间开销就特别的大

所以可以对id进行hash处理:id 对学生的数量取余,结果为存到数组的位置

但是会产生一个问题:可能会出现多个学生取余结果相同的问题,这种情况需要解决哈希冲突

1.2 哈希冲突的解决方法

1.2.1 链表法

对于结果相同的数据,通过链表进行存储

请添加图片描述

1.2.1 开放寻址法

  • 线性探测

产生冲突之后,依次查看其后的下一个位置,如果发现空位置插入新元素

  • 二次探测

二次探测法在探测下一个槽位时,不是简单地加1,而是按照某个二次方程的规律计算下一个槽位的位置

next = (h + i^2) % table_size

i表示探测的次数,table_size表示哈希表的大小,%表示取余运算。二次探测法每次探测时,会根据当前探测的次数i计算下一个槽位的位置,如果该槽位已经被占用,则继续探测下一个槽位,直到找到一个空闲槽位为止。

  • 双重哈希

在哈希表中使用了两个哈希函数,通过第一次哈希和第二次哈希的结果来寻找空闲槽位,解决哈希冲突

h1 = hash1(key)
h2 = hash2(key)
i = 0
while table[(h1 + i * h2) % table_size] is not None:
    i += 1
next = (h1 + i * h2) % table_size

hash1和hash2是两个不同的哈希函数,table_size是哈希表的大小,i表示探测的次数。双重哈希首先使用hash1计算出关键字的哈希值h1,然后使用hash2计算出一个增量值h2。接着,双重哈希从h1开始探测槽位,每次探测跨越h2个槽位,直到找到一个空闲槽位为止。

1.3 动态扩容

不知道有多少数据会插入到哈希表的情况下,只能给哈希表预先设置一个起始大小

哈希表的性能会随着装入数据的个数而降低

当哈希表的性能下降到一定程度之后,就会触发扩容

判断性能下降到一定程度:装载因子 = 数据个数 / 槽的个数

对于开放寻址法解决哈希冲突:装载因子要小于1

对于链表发解决哈希冲突:装载因子可以大于1

1.3.1 动态扩容的实现

类似数组的扩容,申请一个更大的哈希表,将原哈希表中的数搬移到新的哈希表中

对于比较大的哈希表,重新计算哈希值和搬移比较耗时

1.3.2 避免集中扩容

为了解决扩容耗时过多的问题,可以将扩容操作穿插在多次插入操作的过程中,分批完成

也就是,新的哈希表创建之后,不立即插入数

每次插入新数据的时候,顺便将一个旧数据插入到新的哈希表中

2 例题

2.1 两数之和

链接:https://leetcode.cn/problems/two-sum

2.1.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]

提示:

2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案

2.1.2 题解

class Solution {
    
    
    public int[] twoSum(int[] sums, int target) {
    
    
        int n = sums.length;
        // k为数字 v为下标
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n ; i++) {
    
    
            map.put(sums[i], i);
        }
        
        for (int i = 0; i < n; i++) {
    
    
            if (map.containsKey(target - sums[i])) {
    
    
                int value = map.get(target - sums[i]);
                if (value != i) {
    
    
                    return new int[]{
    
    i, value};
                }
            }
        }
                return new int[0];
    }
}

2.2 三数之和

链接:https://leetcode.cn/problems/3sum

2.2.1 题目

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

3 <= nums.length <= 3000
-105 <= nums[i] <= 105

2.2.2 题解

class Solution {
    
    
    public List<List<Integer>> threeSum(int[] nums) {
    
    
        Arrays.sort(nums);
        List<List<Integer>> result = new ArrayList<>();
        int n = nums.length;
        HashMap<Integer, Integer> hashMap = new HashMap<>();
        for (int i = 0; i < n; i++) {
    
    
            hashMap.put(nums[i], i);
        }
        for (int i = 0; i < n; i++) {
    
    
            if (i != 0 && nums[i] == nums[i - 1]) continue;
            for (int j = i + 1; j < n; j++) {
    
    
                if (j != i + 1 && nums[j] == nums[j - 1]) continue;
                int target = -1 * (nums[i] + nums[j]);
                if (!hashMap.containsKey(target)) continue;
                int k = hashMap.get(target);
                if (k > j) {
    
    
                    List<Integer> result0 = new ArrayList<>();
                    result0.add(nums[i]);
                    result0.add(nums[j]);
                    result0.add(nums[k]);
                    result.add(result0);
                }
            }
            
        }
        return result;
    }
}

2.3 移除两个集合中的相同元素

2.3.1 题目

给定a1和a2两个数组,将a1中出现在a2中的数字去掉

2.3.2 题解

public int removeDup(int[] a1, int[] a2) {
    
    
    HashSet<Integer> set = new HashSet<>();
    int n1 = a1.length;
    int n2 = a2.length;
    for (int i = 0; i < n2; ++i) {
    
    
        set.add(a2[i]);
    }
    int k = 0;
    for (int i = 0; i < n1; i++) {
    
    
        if (!set.contains(a1[i])) {
    
    
            a1[k] = a1[i];
            k++;
        }
    }
    return k;
}

2.4 LRU缓存

链接:https://leetcode.cn/problems/lru-cache

2.4.1 题目

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:
LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。

示例:

输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]

解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是 {1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回 1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4

提示:

1 <= capacity <= 3000
0 <= key <= 10000
0 <= value <= 105
最多调用 2 * 105 次 get 和 put

2.4.2题解

 class LRUCache {
    
    
    
    private class DLinkedNode {
    
    
        public int key;
        public int value;
        public DLinkedNode prev;
        public DLinkedNode next;
        public DLinkedNode(int key, int value) {
    
    
            this.key = key;
            this.value = value;
        }
    }

    private Map<Integer, DLinkedNode> hashtable = new HashMap<>();
    private int size;
    private int capacity;
    private DLinkedNode head;
    private DLinkedNode tail;

    public LRUCache(int capacity) {
    
    
        this.size = 0;
        this.capacity = capacity;
        this.head = new DLinkedNode(-1, -1);
        this.tail = new DLinkedNode(-1, -1);
        this.head.next = tail;
        this.head.prev = null;
        this.tail.prev = head;
        this.tail.next = null;
    }
    
    public int get(int key) {
    
    
        if (size == 0) return 0-1;
        DLinkedNode node = hashtable.get(key);
        if (node == null) return -1;
        removeNode(node);
        addNodeAtHead(node);
        return node.value;
    }

    public void remove(int key) {
    
    
        DLinkedNode node = hashtable.get(key);
        if (node != null) {
    
    
            removeNode(node);
            hashtable.remove(key);
            size--;
            return;
        }
    }

    public void removeNode(DLinkedNode node) {
    
    
        node.next.prev = node.prev;
        node.prev.next = node.next;
        
    }

    public void addNodeAtHead(DLinkedNode node) {
    
    
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
        node.prev = head;
    }
    
    public void put(int key, int value) {
    
    
        DLinkedNode node = hashtable.get(key);
        if (node != null) {
    
    
            node.value = value;
            removeNode(node);
            addNodeAtHead(node);
            return;
        }
        if (size == capacity) {
    
    
            hashtable.remove(tail.prev.key);
            removeNode(tail.prev);
            size--;
        }
        DLinkedNode newNode = new DLinkedNode(key, value);
        addNodeAtHead(newNode);
        hashtable.put(key, newNode);
        size++;
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

猜你喜欢

转载自blog.csdn.net/weixin_62759952/article/details/129531772