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();
}
}