面试鸭----集合篇

337 阅读7分钟

写在前面

本文章所有面试题均由程序员鱼皮开发的面试鸭网站提供,仅针对免费题目做笔记用于个人学习,如果侵权或因其他原因不允许发布该文章,请联系我进行删除

说说Java中HashMap原理

回答重点

HashMap 是基于哈希表的数据结构,用于存储键值对(key-value )。其核心是将键的哈希值映射到数组索引位置,通过数组+链表(在 Java 8 及之后是数组 +链表+红黑树)来处理哈希冲突。

Hashmap 使用键的 hashcod()方法计算哈希值,并通过 indexfor, 方法确定元素在数组中的存储位置。哈希值是经过一定扰动处理的,防止哈希值分布不均匀,从而减少冲突。

Hashmap 的默认初始容量为 16,负载因子为 0.75。也就是说,当存储的元素数量超过 16x0.75=12个时, Hashmap 会触发扩容操作,容量x2并重新分配元素位置。这种扩容是比较耗时的操作,频繁扩容会影响性能。

扩展知识

HashMap的红黑树优化

从Java8开始,为了优化当多个元素映射到同一个哈希桶(即发生哈希冲突)时的查找性能,当链表长度超过8时,链表会转变为红黑树。红黑树是一种自平衡二叉搜索树,能够将最坏情况下的査找复杂度从 O(n) 降低到 O(log n)。 如果树中元素的数量低于 6,红黑树会转换回链表,以减少不必要的树操作开销。

image.png

误用 hashcode()和 equals()会导致 HashMap中的元素无法正常查找或插入。

image.png

哈希冲突链表法

image.png

jdk1.7之前的HashMap

image.png

jdk1.7的链表头插法:产生环

image.png

image.png

jdk1.8的HashMap

jdk1.8的尾插法

image.png

image.png

Java 中的 hashCode 和 equals 方法之间有什么关系

回答重点

image.png

扩展知识

image.png

image.png

image.png

image.png

实际上就是就算你重写equals但没有重写hashcode,对于像HashSet这样不允许有重复键的集合,复用了HashMap的hashcode方法,因此在比较hashcode时直接判定为不同对象了,就算name相同也被判定为不同了(即set里出现了重复的元素)

使用HashMap时,有哪些提升性能的技巧

回答重点

image.png

扩展知识

image.png

什么是哈希碰撞,怎么解决

回答重点

image.png

扩展知识

拉链法其实就是HashMap原理里使用的,再现哈希法就是多使用一个哈希函数,所以就不再赘述

image.png

Java的CopyOnWriteArrayList和Collections.synchronizedList有什么区别,分别有什么优缺点

回答重点

image.png

Java中有哪些常见的集合类

回答重点

image.png

image.png

扩展知识

什么是LinkedHashMap

重点回答

image.png

扩展知识

image.png

使用HashMap实现LRU:

image.png

image.png

具体就是在put方法将数据插入后会执行一个afterNodeInsertion,这个方法通过if判断分支来决定是否要删除一个最久未使用的节点,而这个if判断中有个条件就是removeEldestEntry,默认是返回false,所以可以根据实际需要决定返回的布尔类型,比如当HashMap中的数据超过规定的缓存大小是,就返回true,从而走入if判断,删除一个最久未使用的节点(最头部节点),而如果之前设置了accessOrder为true,说明如果一个节点被访问使用过,则会将该节点移动到HashMap的尾部(最头部节点就是最久未使用的)

HashMap实现LRU的代码:重写一下removeEldestEntry即可

    private static final class LRUCache<K, V> extends LinkedHashMap<K, V> {
        private final int maxCacheSize;

        LRUCache(int initialCapacity, int maxCacheSize) {
            super(initialCapacity, 0.75F, true);
            this.maxCacheSize = maxCacheSize;
        }

        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return this.size() > this.maxCacheSize;
        }
    }

手写LRU算法: 这里以尾部为最久未使用,头部为最近使用

public class LRUCache<K, V> {
    // 定义一个内部类 Node,代表缓存中的每个节点
    class Node<K, V> {
        K key;   // 节点的键
        V value; // 节点的值
        Node<K, V> prev, next; // 指向前一个节点和后一个节点的指针

        // 无参构造函数
        public Node() {}

        // 带参数的构造函数,用于初始化节点
        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    // 缓存的最大容量
    private int capacity;
    // 用于快速查找节点的哈希表
    private HashMap<K, Node> map;
    // 双向链表的头尾节点,这两个节点是哨兵节点,不会存储实际数据
    private Node<K, V> head;
    private Node<K, V> tail;

    // 构造函数,初始化缓存
    public LRUCache(int capacity) {
        this.capacity = capacity;
        map = new HashMap<>(capacity);
        head = new Node<>(); // 初始化头节点
        tail = new Node<>(); // 初始化尾节点
        head.next = tail; // 头节点的下一个节点指向尾节点
        tail.prev = head; // 尾节点的前一个节点指向头节点
    }

    // 获取缓存中指定键的值
    public V get(K key) {
        Node<K, V> node = map.get(key); // 从哈希表中查找节点
        if (node == null) {
            return null; // 如果找不到,返回 null
        }
        moveNodeToHead(node); // 将找到的节点移到链表头部
        return node.value; // 返回节点的值
    }

    // 将节点插入到链表头部
    private void addToHead(Node<K, V> newNode) {
        newNode.prev = head; // 新节点的前驱设为头节点
        newNode.next = head.next; // 新节点的后继设为原头节点的后继
        head.next.prev = newNode; // 原头节点后继的前驱设为新节点
        head.next = newNode; // 头节点的后继设为新节点
    }

    // 将节点移动到链表头部
    private void moveNodeToHead(Node<K, V> node) {
        removeNode(node); // 先将节点从当前位置移除
        addToHead(node); // 再将节点插入到链表头部
    }

    // 从链表中移除指定节点
    private void removeNode(Node<K, V> node) {
        node.prev.next = node.next; // 节点的前驱的后继设为节点的后继
        node.next.prev = node.prev; // 节点的后继的前驱设为节点的前驱
    }

    // 移除链表尾部的节点
    private void removeTailNode() {
        removeNode(tail.prev); // 移除尾节点的前一个节点
    }

    // 向缓存中添加或更新键值对
    public void put(K key, V value) {
        Node<K, V> node = map.get(key); // 查找是否已存在该键的节点
        if (node == null) { // 如果不存在
            if (map.size() >= capacity) { // 如果缓存已满
                map.remove(tail.prev.key); // 从哈希表中移除最近最少使用的节点
                removeTailNode(); // 从链表中移除最近最少使用的节点
            }
            Node<K, V> newNode = new Node<>(key, value); // 创建新节点
            map.put(key, newNode); // 将新节点添加到哈希表中
            addToHead(newNode); // 将新节点插入到链表头部
        } else { // 如果节点已存在
            node.value = value; // 更新节点的值
            moveNodeToHead(node); // 将节点移动到链表头部
        }
    }

    // 主方法,用于测试 LRUCache
    public static void main(String[] args) {
        LRUCache<Integer, Integer> lruCache = new LRUCache<>(3); // 创建一个最大容量为 3 的缓存
        lruCache.put(1, 1); // 添加键值对 (1, 1)
        lruCache.put(2, 2); // 添加键值对 (2, 2)
        lruCache.put(3, 3); // 添加键值对 (3, 3)
        lruCache.get(1); // 访问键 1,使其成为最近最常使用的
        lruCache.put(4, 4); // 添加键值对 (4, 4),此时缓存已满,键 2 成为最近最少使用的,将被移除
        System.out.println(lruCache); // 打印缓存的状态,这里假设已经重写了 toString 方法
    }
}

什么是TreeMap

重点回答

image.png

使用示例

import java.util.Comparator;
import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        // 使用自然顺序
        TreeMap<Integer, String> treeMap = new TreeMap<>();
        treeMap.put(3, "Three");
        treeMap.put(1, "One");
        treeMap.put(2, "Two");
        System.out.println("TreeMap: " + treeMap);

        // 使用自定义比较器
        TreeMap<Integer, String> reverseTreeMap = new TreeMap<>(Comparator.reverseOrder());
        reverseTreeMap.put(3, "Three");
        reverseTreeMap.put(1, "One");
        reverseTreeMap.put(2, "Two");
        System.out.println("Reverse TreeMap: " + reverseTreeMap);


        // 这里都是指键的大小范围
        // 获取子映射
        System.out.println("SubMap (1, 3): " + treeMap.subMap(1, 3));

        // 获取前缀映射
        System.out.println("HeadMap (2): " + treeMap.headMap(2));

        // 获取后缀映射
        System.out.println("TailMap (2): " + treeMap.tailMap(2));
    }
}

扩展知识

红黑树介绍

image.png

image.png

数组和链表在Java中的区别

回答重点

image.png

image.png

扩展知识

image.png

Java中的List接口有哪些实现类

回答重点

image.png

扩展知识

扩展Vector:因为有同步,所以性能开销大 image.png

image.png

Java中的ArrayList和LinkedList有什么区别

回答重点

image.png

扩展知识

image.png

ArrayList的扩容机制

回答重点

image.png

扩展知识

image.png

image.png

image.png

HashMap和HashTable有什么区别

回答重点

image.png

扩展知识

image.png

ConcurrentHashMap在1.7和1.8有什么区别

回答重点

image.png

扩展知识

image.png

image.png

其实就是每个segment在执行put方法时,由于是加锁的,一个segment被访问时其他线程无法访问该segment,又因为有6个segment,所以最多允许同时操作6个segment

image.png

Node 是 ConcurrentHashMap 中用来存储键值对的基本数据结构。每个 Node 包含一个键、一个值以及指向下一个 Node 的引用。这种链表形式允许在同一个哈希桶内存储多个键值对。 由于每个 Node 都可以独立加锁,而不是像以前那样对整个 Segment 加锁,所以减少了锁竞争的机会。即使某个 Node 正在被修改,其他 Node 仍然可以被其他线程访问和修改,提高了并发性。

image.png

image.png

TODO 看不懂啊看不懂┭┮﹏┭┮