经典模拟数据结构题目

LeetCode经典数据结构设计题目

剑指 Offer 09. 用两个栈实现队列

代码思路

  • 栈,先进后出,意味着局部进来的顺序与弹出顺序是逆序
    • 需要将顺序转换为正序,通过另一个栈作为中转,那么逆序的逆序输出时就是正序
    • 那么bStack就是队列的输出顺序
      • 当bStack不为空时,直接输出
      • 当bStack为空时,将aStack的所有数据都弹出,压入bStack,逆序完成
  • 队列,先进先出,意味着进来的顺序与弹出顺序一致
155. 最小栈class CQueue {
    Stack<Integer> aStack;
    Stack<Integer> bStack;
    public CQueue() {
        this.aStack = new Stack<>();
        this.bStack = new Stack<>();
    }
    
    public void appendTail(int value) {
        aStack.add(value);
    }
    
    public int deleteHead() {
        if(!bStack.isEmpty()) return bStack.pop();
        if(aStack.isEmpty()) return -1;
        while(!aStack.isEmpty()) bStack.add(aStack.pop());
        return bStack.pop();
    }
}
155. 最小栈

代码思路:

  • 保留一个最小值min,且获取min的时间复杂度是O(1),那么可以考虑添加一个辅助栈,记录栈中当前最小值,辅助栈不严格递减即可
    • 当添加一个元素val,维护辅助栈
      • 若栈为空或者val不大于栈顶元素,将当前元素压入
    • 当移除一个元素top,判断是否移除了最小值min
      • 若移除了最小值min,minStack弹出min
class MinStack {
    Stack<Integer> common;
    Stack<Integer> minStack;
    public MinStack() {
        common = new Stack<>();
        minStack = new Stack<>();
    }
    
    public void push(int x) {
        if(minStack.isEmpty() || minStack.peek()>=x) minStack.add(x);
        common.add(x);
    }
    
    public void pop() {
        if(common.isEmpty()) return;
        int top = common.pop();
        if(top==minStack.peek())
        {
            minStack.pop();
        }
    }
    
    public int top() {
        if(common.isEmpty()) return -1;
        return common.peek();
    }
    
    public int min() {
        if(minStack.isEmpty()) return -1;
        return minStack.peek();
    }
}
面试题59 - II. 队列的最大值

代码思路: 单调队列,简单说就是新来的比早来的还 ‘强’,那么‘旧人’走(单调队列像资本家,就是很残酷)

  • 单调队列分为单调递增和单调递减队列,其实就是压入元素时,处理方法不同,以单调递增队列maxQ为例

    • 当add新增一个元素value

      • 当maxQ为空,压入队列

      • 从队尾开始,将比value小的移除队列,value才能入队(所以单调队列采用双端队列实现)

    • 当正常队列移除元素时,判断是否移除了最大值(maxQ的队头元素)

      • 若移除最大值,maxQ队头出队即可
class MaxQueue {

    LinkedList<Integer> queue;
    LinkedList<Integer> maxQ;
    public MaxQueue() {
        this.queue = new LinkedList<>();
        this.maxQ = new LinkedList<>();
    }
    
    public int max_value() {
        if(maxQ.isEmpty()) return -1;
        return maxQ.peekFirst();
    }
    
    public void push_back(int value) {
        while(!maxQ.isEmpty() && value>maxQ.peekLast()) maxQ.removeLast();
        maxQ.addLast(value);
        queue.addLast(value);
    }
    
    public int pop_front() {
        if(queue.isEmpty()) return -1;
        int top = queue.removeFirst();
        if(top==max_value()) maxQ.removeFirst();
        return top;
    }
}
剑指 Offer 41. 数据流中的中位数

代码思路:

  • 中位数的含义是将数据分为大小两堆,小堆比中位数小,大堆比中位数大
    • 也有相同和偶数为数据存在,只从一般性出发
  • 重点是如何划分为大小两堆
    • 数据计算中位数最多需要小堆(left)的最大值或者大堆(right)的最小值,可以采用堆(数据结构),来做到数据操作O(logN)
  • 数据如何存放到两个堆中,能满足数据小大分流,从小堆(left)获取最大值,从大堆(right)获取最小值?
    • 小堆(left)采用 大根堆(堆顶最大,任意非叶子节点,满足nums[i]>nums[i*2+1] && nums[i]>nums[2*i+2])
    • 大堆(right)采用 小根堆 (堆顶最小,任意非叶子节点,满足nums[i]<nums[i*2+1] && nums[i]<nums[2*i+2])
    • 每次数据加入时的策略
      • 从大根堆,弹出小堆(left)中的最大值,压入大堆[right]
      • 从小根堆,弹出大堆(right)中的最小值,压入小堆[left]
    • 如何选择压入策略
      • 维护两堆数量不能超过一,才能求取中位数
      • 选择一个数据存放多的堆A
        • 当堆数量相同时,压入A,如果是奇数个数据,A的最值就是中位数
        • 当堆数量不相同时,压入数量少的堆B
class MedianFinder {
    PriorityQueue<Integer> min;
    PriorityQueue<Integer> max;
    public MedianFinder() {
        this.min = new PriorityQueue<>((a,b)->b-a);
        this.max = new PriorityQueue<>((a,b)->a-b);
    }
    
    public void addNum(int num) {
        if(min.size()==max.size())
        {
            max.add(num);
            min.add(max.poll());
        }else{
            min.add(num);
            max.add(min.poll());
        }
    }
    
    public double findMedian() {
        if(min.size()>max.size())
        {
            return min.peek()/1.0;
        }
        return (min.peek()+max.peek())/2.0;
    }
}

也可以自建一个堆(JAVA 堆的简单实现)

class MedianFinder {
    Heap min;
    Heap max;
    public MedianFinder() {
        this.min = new Heap(50000,(a,b)->b-a);// 大堆
        this.max = new Heap(50000,(a,b)->a-b);// 小堆
    }
    public void addNum(int num) {
        if(min.size()==max.size())
        {
            
            max.add(num);
            min.add(max.poll());
        }else{
            min.add(num);
            max.add(min.poll());
        }
    }
    
    public double findMedian() {
        if(min.size()>max.size())
        {
            return min.peek()/1.0;
        }
        return (min.peek()+max.peek())/2.0;
    }
}
class Heap {  // 堆
    private int[] nums;
    private int size;
    private int last;
    private BiFunction<Integer,Integer,Integer> cmp;
    public Heap(int size, BiFunction<Integer,Integer,Integer> cmp) {
        this.nums = new int[size];
        this.size = size;
        this.last = -1;
        this.cmp = cmp;
    }
    // add
    public void add(int value)
    {
        if(size()>=size) return;
        ++last;
        nums[last] = value;
        up(last);
    }
    void up(int up)
    {
        while(up>0)
        {
            int parent = (up-1)/2;
            if(parent<0 || cmp.apply(nums[parent],nums[up])<=0)
            {
                break;
            }
            swap(up,parent);
            up = parent;
        }
    }
    void swap(int l,int r)
    {
        int t = nums[l];
        nums[l] = nums[r];
        nums[r] = t;
    }
    // poll
    Integer poll()
    {
        if(size()<=0) return null;
        int res = nums[0];
        nums[0] = nums[last--];
        if(size()<=0) return res;
        down(0);
        return res;
    }
    void down(int down)
    {
        while(down<=last)
        {
            int left = 2*down+1;
            if((left+1)<=last && cmp.apply(nums[left+1],nums[left])<0) left++;
            if(left>last || cmp.apply(nums[left],nums[down])>=0) break;
            swap(left,down);
            down = left;
        }
    }
    //size
    int size()
    {
        return last+1;
    }
    //peek
    Integer peek()
    {
        if(size()<=0) return null;
        return nums[0];
    }
}
146. LRU 缓存

代码思路:

  • 这道题主要考察的是双向链表的操作,以及如何将双向链表的操作从O(N)降至O(1)

  • 考虑一个问题,双向链表能不能直接实现缓存?可以,但是定位节点的复杂度是O(N)

    • 将数据存储在双向链表中(并且依据访问时间排序),通过hashMap来定位辅助操作(定位)
  • 如何降低双向链表的操作时间复杂度?

    • 将链表中的数据交由HashMap来管理,每次操作,通过hashMap定位具体节点,将O(N)–>O(1)
  • 具体操作

    • get

      • 从map中获取节点node
        • 若不存在,直接返回
      • 将node移动到双向链表的头部,表示该节点最近访问过(队尾元素表示最久不访问的节点)
    • put

      • 从map中获取节点node
        • node不存在,则为新增操作
          • 新建node
          • 交由map管理
          • 将node添加到队头
        • node存在,则为更新操作
          • 更新node
          • 将node移动到队头
            • 删除node
            • 添加node
class LRUCache {
    int capacity;
    DNode head,tail;
    Map<Integer,DNode> dic;
    int size;

    public LRUCache(int capacity) {
        dic = new HashMap<>(capacity);
        this.capacity = capacity;
        head = new DNode();
        tail = new DNode();
        head.next = tail;
        tail.pre = head;
        this.size = 0;
    }

    public int get(int key) {
        DNode node = dic.get(key);
        if(node==null)
        {
            return -1;
        }
        moveToHead(node);
        return node.value;
    }

    private void moveToHead(DNode node) {
        removeNode(node);
        addToHead(node);
    }

    private void removeNode(DNode node) {
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    public void put(int key, int value) {
        DNode node = dic.get(key);
        if(node==null)
        {
            node = new DNode(key,value);
            dic.put(key,node);
            addToHead(node);
            this.size++;
        }else{
            node.value = value;
            moveToHead(node);
        }
        if(size>capacity)
        {
            DNode last = removeLast();
            dic.remove(last.key);
            size--;
        }
    }

    private DNode removeLast() {
        DNode pre = this.tail.pre;
        removeNode(pre);
        return pre;
    }

    private void addToHead(DNode node) {
        node.pre = head;
        node.next = head.next;
        head.next.pre = node;
        head.next = node;
    }

    class DNode
    {
        int key;
        int value;
        DNode pre;
        DNode next;

        public DNode(int key, int value) {
            this.key = key;
            this.value = value;
        }
        public DNode() {
        }

        public DNode(int key, int value, DNode pre, DNode next) {
            this.key = key;
            this.value = value;
            this.pre = pre;
            this.next = next;
        }
    }
}
707. 设计链表

代码思路:

  • 链表实现问题
    • 双向链表,维护一个头指针和尾指针
    • 每个节点维护,前后指针
class MyLinkedList {
    Node head,tail;
    int size;
    public MyLinkedList() {
        this.head = new Node();
        this.tail = new Node();
        head.next = tail;
        tail.pre = head;
    }
    
    public int get(int index) {
        if(index>=size) return -1;
        Node cur = head;
        for(int i=0;i<=index;i++)
        {
            cur = cur.next;
        }
        return cur.val;
    }
    
    public void addAtHead(int val) {
        Node node = new Node(val);
        node.pre = head;
        node.next = head.next;
        head.next = node;
        node.next.pre = node;
        size++;
    }
    
    public void addAtTail(int val) {
        Node node = new Node(val);
        Node last =  tail.pre;
        last.next = node;
        tail.pre = node;
        node.pre = last;
        node.next = tail;
        size++;
    }    
    public void addAtIndex(int index, int val) {
        Node pre = head;
        if(index<0)
        {
            addAtHead(val);
            return;
        }else if(index==size)
        {
            addAtTail(val);
            return;
        }else if(index>size)
        {
            return;
        }
        Node node = new Node(val);
        for(int i=0;i<index;i++)
        {
            pre = pre.next;
        }
        node.pre = pre;
        node.next = pre.next;
        pre.next = node;
        node.next.pre = node;
        size++;
    }
    
    public void deleteAtIndex(int index) {
        if(index<0 || index>=size) return;
        Node pre = head;
        for(int i=0;i<index;i++)
        {
            pre = pre.next;
        }
        pre.next = pre.next.next;
        pre.next.pre = pre;
        size--;
    }
    class Node
    {
        int val;
        Node next;
        Node pre;
        public Node(){}
        public Node(int val)
        {
            this.val = val;
        }
        public Node(int val,Node next,Node pre)
        {
            this.val = val;
            this.next= next;
            this.pre = pre;
        }
    }
}
380. O(1) 时间插入、删除和获取随机元素

代码思路

  • 随机插入、随机删除的数据结构常见的有set、hashMap
    • 但是set和hashMap并不支持获取随机元素
  • 获取随机元素可以使用数组来实现,但是数组随机插入和删除时间复杂度O(N)
    • 通过hashMap来记录数组值和下标的映射,将元素查找O(N)–>O(1)
  • 具体操作
    • 插入value
      • 若hashMap存在value,不插入
      • 否则
        • 有效数组加一,并加入value
        • hashMap记录value的下标映射
    • 删除value
      • 通过hashMap获取下标,将该位置替换成数组最后一个元素,数组有效长度idx减一
        • hashMap移除value
        • hashMap重新记录最后一个值的下标位置
          • 特殊情况,删除的是最后一个元素,hashMap不记录元素移动
    • 查看
      • 通过hashMap获取下标,查看元素
class RandomizedSet {
    int[] array;
    // 数组有效个数
    int size;
    Random random = new Random();
    Map<Integer,Integer> position;
    public RandomizedSet() {
        array = new int[200001];
        size = 0;
        position = new HashMap<>(200001);
    }
    
    public boolean insert(int val) {
        if(position.containsKey(val)) return false;
        position.put(val,size);
        array[size++] = val;
        return true;
    }
    
    public boolean remove(int val) {
        if(!position.containsKey(val)) return false;
        // 判断是不是删除最后一个
        int index = position.get(val);
        array[index] = array[size-1];
        if(index!=size-1)
        {
            position.put(array[index],index);
        }
        position.remove(val);
        size--;

        return true;
    }
    
    public int getRandom() {
        return array[random.nextInt(size)];
    }
}
933. 最近的请求次数

代码思路

  • 这道题其实是一个过期时间的变形,过期时间是指某个数据从开始到移除的时间,是向后看 1s–>3s
  • 而这道题,是向后看(看过去)
    • 在ping后,可以确定过去的3000ms的数据是有效的,可以被计算的
  • 具体实现
    • 由于ping的时间是递增的,所以先入先出的数据状态,采用队列来实现
      • 每次元素t入队,将保护[t-3000,t]的数据不被清理,那么队头小于t-3000的数据就要出队,最后队列个数就是在[t-3000,t]区间的ping个数
class RecentCounter {
    Deque<Integer> queue;
    public RecentCounter() {
        this.queue = new LinkedList<>();
    }
    
    public int ping(int t) {
        queue.addLast(t);
        while(!queue.isEmpty() && queue.peek()<t-3000)
        {
            queue.pollFirst();
        }
        return queue.size();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值