Skip to content

数据结构设计高频题


setAll 功能的哈希表

题目链接

https://www.nowcoder.com/practice/7c4559f138e74ceb9ba57d76fd169967

使用 O(1)的操作将哈希表中的 value 值全部设置为 newValue,不能遍历哈希表

思路分析

使用两个变量:setAllValue、setAllTime,加

用一个 setAllTime 变量记录 setAll 的时间,每一个元素都有一个对应的操作时间,如果操作时间小于 setAllTime,则说明改元素会被设置成 setAllValue,返回 setAllValue 即可,否则返回原来的值

代码实现

java
import java.io.*;
import java.util.*;

public class Main {

    // key,[value,time],time 表示操作次数
    public static HashMap<Integer, int[]> map = new HashMap<>();
    public static int setAllValue;
    public static int setAllTime;
    // 记录每一个元素的操作时间
    public static int cnt;

    public static void put(int k, int v) {
        // 如果 key 存在就更新 value,否则新增
        if (map.containsKey(k)) {
            int[] value = map.get(k);
            value[0] = v;
            value[1] = cnt++;
        } else {
            map.put(k, new int[]{v, cnt++});
        }
    }

    public static void setAll(int v) {
        setAllValue = v;
        setAllTime = cnt++;
    }

    public static int get(int k) {
        // 判断是否存在
        if (!map.containsKey(k)) {
            return -1;
        }
        // 存在,需要判断是返回 value 还是 setAllValue
        int[] value = map.get(k);
        if (value[1] > setAllTime) {
            return value[0];
        } else {
            return setAllValue;
        }
    }

    // 定义变量接收输入
    public static int n, opt, a, b;

    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        while (in.nextToken() != StreamTokenizer.TT_EOF) {
            // 在每一轮循环中的得出一个答案,先放到缓冲区
            // 所以每次循环开始先要重置一下变量的值
            map.clear();
            setAllValue = 0;
            setAllTime = -1;
            cnt = 0;

            n = (int) in.nval;

            for (int i = 0; i < n; i++) {
                in.nextToken();
                opt = (int) in.nval;

                // 若 opt = 1,put 操作
                // 若 opt = 2,get 操作
                // 若 opt = 3,setAll 操作
                if (opt == 1) {
                    in.nextToken();
                    a = (int) in.nval;
                    in.nextToken();
                    b = (int) in.nval;
                    put(a, b);
                } else if (opt == 2) {
                    in.nextToken();
                    a = (int) in.nval;
                    out.println(get(a));
                } else {
                    in.nextToken();
                    a = (int) in.nval;
                    setAll(a);
                }
            }
        }
        out.flush();
        out.close();
        br.close();
    }
}

LRU 缓存

题目链接

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

什么是 LRU

指定一个 capacity,存储的元素个数上限为 capacity,以 capacity = 3 举例

每一个元素是 key - value 的键值对结构,同时每个元素对应一个操作时间

只要对某个元素操作,则当前元素的操作时间就是最晚的,同时更新其余两个元素的操作时间

如果已经满了,还需要添加元素,则移除操作时间最早的元素

若添加的元素的 key 在缓存中已经存在,则会更新 value,同时更新三个元素的操作时间

思路分析

实现结构:哈希表(map 结构:key - 节点地址) + 双向链表

定义头尾指针,头节点指向操作时间最早的节点,尾节点指向操作时间最晚的节点

当对节点操作时,哈希表中做相应的更新,同时调整双向链表,维持元素操作的相对时序


代码实现

java
class LRUCache {

    class DoubleNode {
        // 每一个元素是 key - value 的结构
        public int key;
        public int val;
        public DoubleNode last;
        public DoubleNode next;

        public DoubleNode(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    class DoubleList {
        // 只定义头尾指针
        // <头指针> 指向操作时间 <最早> 的节点
        // <尾指针> 指向操作时间 <最晚> 的节点
        private DoubleNode head;
        private DoubleNode tail;

        public DoubleList() {
            head = null;
            tail = null;
        }

        public void addNode(DoubleNode newNode) {
            if (newNode == null) {
                return;
            }
            // 添加的节点是第一个节点
            if (head == null) {
                head = newNode;
                tail = newNode;
            } else {
                // 挂接新节点,同时调整两个指针
                tail.next = newNode;
                newNode.last = tail;
                // 尾节点来到新的尾巴,继续挂接下一个节点
                tail = newNode;
            }
        }

        // 删除操作时间最早的节点,即删除头节点
        public DoubleNode removeHead() {
            if (head == null) {
                return null;
            }
            DoubleNode ans = head;
            // 只有一个节点
            if (head == tail) {
                head = null;
                tail = null;
            } else {
                // head 来到头节点的下一个节点
                head = ans.next;
                // 删除头节点,处理两个指针
                ans.next = null;
                head.last = null;
            }
            return ans;
        }

        // 对元素操作后,需要调整时序
        public void moveNodeToTail(DoubleNode node) {
            // 只有一个节点,无需调整
            if (tail == node) {
                return;
            }

            // 如果头节点,调整头指针
            if (head == node) {
                head = node.next;
                head.last = null;
            } else {
                // 节点是中间节点,将该节点从链表中分离

                // 调整 lastNode 的 next 指针
                node.last.next = node.next;

                // 调整 nextNode 的 last 指针
                node.next.last = node.last;
            }

            // 调整指针,挂接节点
            tail.next = node;
            node.last = tail;
            node.next = node;
            tail = node;
        }
    }

    private HashMap<Integer, DoubleNode> keyNodeMap;
    private DoubleList nodeList;
    private final int capacity;

    public LRUCache(int n) {
        keyNodeMap = new HashMap<>();
        nodeList = new DoubleList();
        capacity = n;
    }

    public int get(int key) {
        if (keyNodeMap.containsKey(key)) {
            DoubleNode ans = keyNodeMap.get(key);
            nodeList.moveNodeToTail(ans);
            return ans.val;
        }
        // 不存在返回 -1
        return -1;
    }

    public void put(int key, int value) {
        // 如果 key 存在就更新 value,否则就新增 key
        if (keyNodeMap.containsKey(key)) {
            DoubleNode node = keyNodeMap.get(key);
            node.val = value;
            nodeList.moveNodeToTail(node);
        } else {
            // 新增之前先判断是否满容量
            // 满容量的情况下要新增,移除头节点
            if (keyNodeMap.size() == capacity) {
                // nodeList.removeHead() 返回头节点
                keyNodeMap.remove(nodeList.removeHead().key);
            }
            DoubleNode newNode = new DoubleNode(key, value);
            keyNodeMap.put(key, newNode);
            nodeList.addNode(newNode);
        }
    }
}

/**
 * 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);
 */

O(1) 增删和获取随机元素-1

题目链接

https://leetcode.cn/problems/insert-delete-getrandom-o1/

思路分析

采用 map 结构,key 存储元素值,vval 存储元素下标

定义一个动态数组,存储元素

remove 删除元素,如果在 random 的时候恰好随机到被删除的元素,这就会产生问题

如果 remove 元素,拿动态数组的最后一个元素填补空缺,同时更新 map 中元素对应的下标

代码实现

java
class RandomizedSet {

    public HashMap<Integer, Integer> map;

    public ArrayList<Integer> arr;

    public RandomizedSet() {
        map = new HashMap<>();
        arr = new ArrayList<>();
    }

    public boolean insert(int val) {
        if (map.containsKey(val)) {
            return false;
        }
        map.put(val, arr.size());
        arr.add(val);
        return true;
    }

    public boolean remove(int val) {
        if (!map.containsKey(val)) {
            return false;
        }
        // 删除一个数产生的空缺用最后一个数填补
        int valIndex = map.get(val);
        int endValue = arr.get(arr.size() - 1);
        map.put(endValue, valIndex);
        arr.set(valIndex, endValue);
        map.remove(val);
        arr.remove(arr.size() - 1);
        return true;
    }

    public int getRandom() {
        return arr.get((int) (Math.random() * arr.size()));
    }
}
/**
 * Your RandomizedSet object will be instantiated and called as such:
 * RandomizedSet obj = new RandomizedSet();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

O(1) 增删和获取随机元素-2

💡 Tip

区别于上题,本题允许有重复元素,getRandom()从当前的多个元素集合中返回一个随机元素。每个元素被返回的概率与集合中包含的相同值的数量线性相关

题目链接

https://leetcode.cn/problems/insert-delete-getrandom-o1-duplicates-allowed/

思路分析

跟上题思路类似,由于允许重复元素存在,map 中的 value 变为 set 结构,存储多个重复元素的下标

代码实现

java
class RandomizedCollection {

    private HashMap<Integer, HashSet<Integer>> map;

    private ArrayList<Integer> arr;

    public RandomizedCollection() {
        map = new HashMap<>();
        arr = new ArrayList<>();
    }

    // 第一次加入返回 true,否则返回 false
    public boolean insert(int val) {
        arr.add(val);
        HashSet<Integer> set = map.getOrDefault(val, new HashSet<Integer>());
        // 记录元素的下标
        set.add(arr.size() - 1);
        map.put(val, set);
        return set.size() == 1;
    }

    public boolean remove(int val) {
        if (!map.containsKey(val)) {
            return false;
        }
        HashSet<Integer> valIndexSet = map.get(val);
        int valAnyIndex = valIndexSet.iterator().next();
        int endValue = arr.get(arr.size() - 1);
        // 如果删除的是最后一个元素,直接删除,无需填补
        if (val == endValue) {
            valIndexSet.remove(arr.size() - 1);
        } else {
            // 用最后一个数去填补删除元素产生的空缺
            HashSet<Integer> endValueSet = map.get(endValue);
            endValueSet.add(valAnyIndex);
            arr.set(valAnyIndex, endValue);
            endValueSet.remove(arr.size() - 1);
            valIndexSet.remove(valAnyIndex);
        }
        arr.remove(arr.size() - 1);
        if (valIndexSet.isEmpty()) {
            map.remove(val);
        }
        return true;
    }

    public int getRandom() {
        return arr.get((int) (Math.random() * arr.size()));
    }
}

/**
 * Your RandomizedCollection object will be instantiated and called as such:
 * RandomizedCollection obj = new RandomizedCollection();
 * boolean param_1 = obj.insert(val);
 * boolean param_2 = obj.remove(val);
 * int param_3 = obj.getRandom();
 */

获取数据流的中位数

题目链接

https://leetcode.cn/problems/find-median-from-data-stream/

思路分析

求中位数的前提是有序

可以利用有序的特点,运用两个堆实现,维护中间值

左边较小的部分放在大顶堆中,右边较大的部分放在小顶堆中,那两个堆的堆顶元素就是中间值

只要两个堆的大小差值不大于 2,就无需调整堆

如果总元素个数是奇数,则中位数就是大顶堆的堆顶元素

如果总元素个数是偶数,则中位数是两个堆的堆顶元素的平均值

代码实现

java
class MedianFinder {

    // 较大的元素放在小根堆
    private PriorityQueue<Integer> minHeap;

    // 较小的元素放在大根堆
    private PriorityQueue<Integer> maxHeap;

    public MedianFinder() {
        minHeap = new PriorityQueue<>((a, b) -> a - b);
        maxHeap = new PriorityQueue<>((a, b) -> b - a);
    }

    public void addNum(int num) {
        if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
            maxHeap.add(num);
        } else {
            minHeap.add(num);
        }
        balance();
    }

    public double findMedian() {
        // 元素个数是偶数个
        if (maxHeap.size() == minHeap.size()) {
            return (double) (maxHeap.peek() + minHeap.peek()) / 2;
        } else {
            // 元素个数是奇数个
            return maxHeap.size() > minHeap.size() ? maxHeap.peek() : minHeap.peek();
        }
    }

    public void balance() {
        if (Math.abs(maxHeap.size() - minHeap.size()) == 2) {
            if (maxHeap.size() > minHeap.size()) {
                // 大根堆弹出一个进小根堆
                minHeap.add(maxHeap.poll());
            } else {
                // 小根堆弹出一个进大根堆
                maxHeap.add(minHeap.poll());
            }
        }
    }
}

/**
 * Your MedianFinder object will be instantiated and called as such:
 * MedianFinder obj = new MedianFinder();
 * obj.addNum(num);
 * double param_2 = obj.findMedian();
 */

最大频率栈

题目链接

https://leetcode.cn/problems/maximum-frequency-stack

思路分析


代码实现

java
class FreqStack {

    // 出现的最大次数
    private int topTimes;

    // 每层节点
    private HashMap<Integer, ArrayList<Integer>> cntValues = new HashMap<>();

    // 每一个数出现了几次(词频表)
    private HashMap<Integer, Integer> valueTimes = new HashMap<>();

    public void push(int val) {
        // 更新自己的词频
        valueTimes.put(val, valueTimes.getOrDefault(val, 0) + 1);
        // 当前元素的最大词频,后续可能会更新
        int curTopTimes = valueTimes.get(val);
        // 如果当前词频对应的层么米有链表,建出来
        if (!cntValues.containsKey(curTopTimes)) {
            cntValues.put(curTopTimes, new ArrayList<>());
        }
        // 拿到当前词频对应层的链表,添加元素
        ArrayList<Integer> curTimeValues = cntValues.get(curTopTimes);
        curTimeValues.add(val);
        // 更新最大词频
        topTimes = Math.max(topTimes, curTopTimes);
    }

    public int pop() {
        // 弹出词频最大的元素
        ArrayList<Integer> topTimeValues = cntValues.get(topTimes);
        // 同一层链表,从右往左移除元素
        // 返回移除的数字,目的是后续更新词频表
        int ans = topTimeValues.remove(topTimeValues.size() - 1);
        // 如果元素没了,清除当前层的链表
        if (topTimeValues.size() == 0) {
            cntValues.remove(topTimes);
            topTimes--;
        }
        // 更新移除元素对应的词频
        int times = valueTimes.get(ans);
        // 要移除的数字只剩下依次,再移除就没了
        if (times == 1) {
            valueTimes.remove(ans);
        } else {
            valueTimes.put(ans, times - 1);
        }
        return ans;
    }
}

/**
 * Your FreqStack object will be instantiated and called as such:
 * FreqStack obj = new FreqStack();
 * obj.push(val);
 * int param_2 = obj.pop();
 */

全 O(1)的数据结构

题目链接

https://leetcode.cn/problems/all-oone-data-structure

思路分析


代码实现

java
class AllOne {

    // 定义双向链表节点(桶)
    class Bucket {
        // 词频
        public int cnt;
        // 该词频对应的字符串
        public HashSet<String> set;
        public Bucket last;
        public Bucket next;

        public Bucket(String s, int n) {
            set = new HashSet<>();
            set.add(s);
            cnt = n;
        }
    }

    // 在 cur 桶(节点)的后面插入 pos 桶(节点)
    public void insert(Bucket cur, Bucket pos) {
        // 先处理后面,再处理前面
        pos.next = cur.next;
        cur.next.last = pos;

        cur.next = pos;
        pos.last = cur;
    }

    // 移除 cur 桶(节点)
    public void remove(Bucket cur) {
        cur.last.next = cur.next;
        cur.next.last = cur.last;
    }

    // 头桶指针
    Bucket head;

    // 尾桶指针
    Bucket tail;

    HashMap<String, Bucket> map;

    // 初始化,建立首尾两个桶,不存储字符串
    public AllOne() {
        head = new Bucket("", 0);
        tail = new Bucket("", Integer.MAX_VALUE);
        head.next = tail;
        tail.last = head;
        map = new HashMap<>();
    }

    // 整体思路
    // 如果链表 有 对应的词频,就以最新的词频为准,移动字符串
    // 如果链表中 没有 对应的词频,就创建新的桶,加入双向链表
    public void inc(String key) {
        // 字符串第一次进来,放到词频为 1 的桶中
        if (!map.containsKey(key)) {
            // 词频为 1 的桶存在,直接放入
            if (head.next.cnt == 1) {
                map.put(key, head.next);
                head.next.set.add(key);
            } else {
                // 词频为 1 的桶不存在,创建桶并插入到链表中
                Bucket bucket = new Bucket(key, 1);
                map.put(key, bucket);
                // 在 head 桶的后面插入 bucket 桶
                insert(head, bucket);
            }
        } else {
            Bucket bucket = map.get(key);
            // 需要插入 key,看是否存在词频为 bucket.cnt + 1 的桶
            if (bucket.next.cnt == bucket.cnt + 1) {
                map.put(key, bucket.next);
                bucket.next.set.add(key);
            } else {
                Bucket newBucket = new Bucket(key, bucket.cnt + 1);
                map.put(key, newBucket);
                insert(bucket, newBucket);
            }
            // 字符串移动了到了新的桶,更新原来的桶
            bucket.set.remove(key);
            // 如果桶空了,把桶移除
            if (bucket.set.isEmpty()) {
                // 前后节点重连,移除 bucket 桶
                remove(bucket);
            }
        }
    }

    public void dec(String key) {
        Bucket bucket = map.get(key);
        // 字符串的词频只有 1,删除就没了
        if (bucket.cnt == 1) {
            map.remove(key);
        } else {
            // 有对应词频的桶就移动字符串,否则新建
            if (bucket.last.cnt == bucket.cnt - 1) {
                map.put(key, bucket.last);
                bucket.last.set.add(key);
            } else {
                Bucket newBucket = new Bucket(key, bucket.cnt - 1);
                map.put(key, newBucket);
                insert(bucket.last, newBucket);
            }
        }
        // 字符串移动了到了新的桶,更新原来的桶
        bucket.set.remove(key);
        // 如果桶空了,把桶移除
        if (bucket.set.isEmpty()) {
            remove(bucket);
        }
    }

    public String getMaxKey() {
        return tail.last.set.iterator().next();
    }

    public String getMinKey() {
        return head.next.set.iterator().next();
    }
}

/**
 * Your AllOne object will be instantiated and called as such:
 * AllOne obj = new AllOne();
 * obj.inc(key);
 * obj.dec(key);
 * String param_3 = obj.getMaxKey();
 * String param_4 = obj.getMinKey();
 */