代码随想录 | 栈与队列部分

目录

栈与队列理论基础

问题思考 

1. 栈的实现

2. 队列的实现

3. 设计理念对比

总结

详解栈与队列的接口与实现

1. Queue 和 Deque(接口层级)

2. ArrayDeque 和 LinkedList(实现类)

3. LinkedList 额外实现了 List

总结关系图

选择建议

 4. ArrayDeque和LinkedList方法详解

栈的操作与 Deque 的对应关系

size和length方法详解 

232.用栈实现队列

代码实现 

225. 用队列实现栈

 代码实现

 注意语法

20. 有效的括号

思路 

 代码实现

1047. 删除字符串中的所有相邻重复项

 代码实现

注意语法

150. 逆波兰表达式求值

代码实现

注意语法

239. 滑动窗口最大值

 思路

代码实现

347.前 K 个高频元素

思路

代码实现

注意语法

1. 堆的特性

2. Java 中的 PriorityQueue

3. 自定义对象的堆

4. 常见应用场景

5. 代码解析

6. 哈希表算法:map.entrySet()


栈与队列理论基础

队列是先进先出,栈是先进后出。

栈与队列理论1

问题思考 

那么我这里再列出四个关于栈的问题,大家可以思考一下。

  1. C++中stack 是容器么?
  2. 我们使用的stack是属于哪个版本的STL(标准模板库)?
  3. 我们使用的STL中stack是如何实现的?
  4. stack 提供迭代器来遍历stack空间么?

 以下是针对Java语言,对于这几个问题的解释:

1. Java中的 Stack 是集合类吗?

  • java.util.Stack 是 Java 集合框架(Java Collections Framework)的一部分,但它继承自 Vector 类(一种线程安全的动态数组)。
  • 但注意:官方已不推荐直接使用 Stack,而是建议用 Deque 接口的实现类(如 ArrayDeque)来模拟栈操作。

2. Java 的 Stack 类属于集合框架的哪个版本?

  • Stack 是 Java 1.0 引入的早期集合类,属于原始集合框架的一部分。
  • 替代方案:从 Java 1.6 开始,官方推荐使用 Deque 接口的 push()pop() 方法实现栈(例如 new ArrayDeque<>()),因为 Deque 的设计更现代且性能更好。

3. Java 中 Stack 类是如何实现的?推荐使用的栈实现是什么?

  • Stack 的实现:基于 Vector(动态数组),所有操作通过数组的末尾操作(如 push() 对应 addElement())。

  • 推荐实现:使用 Deque 接口的 ArrayDeque(基于可扩容数组)或 LinkedList(基于双向链表)。示例:

    Deque<Integer> stack = new ArrayDeque<>();  // 推荐
    Deque<Integer> stack = new LinkedList<>();  // 可选,但性能略低

4. Java 的 Stack 类或推荐的栈实现是否提供迭代器?使用时需要注意什么?

  • Stack:由于继承自 Vector,支持迭代器(iterator() 方法),但遍历栈会破坏 LIFO 语义(栈本应仅通过顶部操作)。

  • Deque 实现(如 ArrayDeque:也提供迭代器,但同样不推荐遍历。示例:

    Deque<Integer> stack = new ArrayDeque<>();
    stack.push(1);
    stack.push(2);
    
    // 可以遍历,但违背栈的设计原则
    for (int num : stack) {
      System.out.println(num);  // 输出顺序为 2, 1(栈顶到栈底)
    }
  • 关键原则:栈是后进先出(LIFO)结构,应仅通过 push()pop() 操作。使用迭代器遍历栈虽然可行,但不符合栈的设计意图。

总结对比(C++ vs Java)

问题C++ 答案Java 答案
栈是否属于容器/集合类是(容器适配器)是(但推荐用 Deque 替代 Stack
实现版本STL 的特定版本(如 C++11)Stack(Java 1.0)→ Deque(Java 1.6+)
底层实现默认基于deque,可指定其他容器Stack 基于 Vector,推荐 ArrayDeque 基于数组
是否支持迭代器否(栈不允许遍历)是(但不推荐使用)

附加建议

  • 避免直接使用 Stack:因其同步开销(继承自 Vector)和设计过时。
  • 优先选择 ArrayDeque:非线程安全、高性能,符合现代 Java 编程规范。


1. 栈的实现

  • 传统实现 – java.util.Stack

    Java 提供的 Stack 类继承自 Vector,这意味着它内部实际上是基于动态数组实现的。Stack 类提供了常用的操作,例如:

    • push(E item):将元素压入栈顶
    • pop():从栈顶弹出元素
    • peek():查看栈顶元素
    • empty():判断栈是否为空

    不过,由于 Stack 继承自 Vector,它默认就拥有迭代器接口,可以对内部元素进行遍历,这与理论上栈应只暴露 LIFO 操作的思想有所出入。

  • 现代替代 – 使用 Deque 接口

    为了避免 Stack 类暴露出不必要的遍历功能,Java 推荐使用 Deque 接口来模拟栈。常见的实现有 ArrayDequeLinkedList。例如:

    Deque<Integer> stack = new ArrayDeque<>();
    stack.push(1);
    stack.push(2);
    System.out.println(stack.pop());  // 输出 2
    

    使用 Deque 的好处在于,它既能实现栈的 LIFO 逻辑,又可以根据需要隐藏或暴露迭代功能(取决于你如何设计接口)。


2. 队列的实现

  • Queue 接口及其实现

    Java 中的队列通过 Queue 接口来定义,常见的实现有 LinkedListArrayDeque。队列遵循先进先出(FIFO)原则,提供的常用操作包括:

    • offer(E e):将元素添加到队列尾部
    • poll():从队列头部取出并删除元素
    • peek():仅查看队列头部的元素

    例如,使用 LinkedList 实现队列:

    Queue<Integer> queue = new LinkedList<>();
    queue.offer(1);
    queue.offer(2);
    System.out.println(queue.poll());  // 输出 1
    
  • 底层结构与适配器思想

    与 STL 中容器适配器(例如 stack 和 queue 默认使用 deque 实现)类似,Java 的队列实现也依赖于底层容器(如链表或数组),但它们并没有刻意隐藏迭代器接口。实际上,许多队列实现都允许你遍历所有元素,因为它们本身就是 Collection 的一部分。


3. 设计理念对比

  • STL 的容器适配器

    在 C++ STL 中,stack 和 queue 被设计为容器适配器,其核心思想是仅提供特定的接口(如 push、pop 等),并通过封装一个底层容器(如 deque、vector、list)来完成实际的数据存储。这样做的目的是严格限制用户对数据结构的访问,只暴露特定操作,而不允许随意遍历容器内容。

  • Java 的实现方式

    Java 的 Collections Framework 更多依赖于接口与继承的方式。例如,Stack 类直接继承自 Vector,暴露了全部的迭代能力;而使用 Deque 实现的栈或队列,虽然可以只使用 push/pop 或 offer/poll 操作,但它们依然继承自 Collection 接口,允许遍历。也就是说,在 Java 中,严格隐藏遍历接口的思想没有像 STL 那样明确,但通过选择合适的使用方式(例如只调用栈或队列的相关方法)依然可以达到类似的逻辑效果。


总结

    • Java 传统的 Stack 类基于 Vector 实现,但由于继承了 Vector,它暴露了遍历等不必要的接口。
    • 为了更符合严格的 LIFO 操作,推荐使用 Deque(如 ArrayDeque)来实现栈,这样既能完成 push/pop 操作,也能根据需要控制是否允许遍历。
  • 队列
    • Java 通过 Queue 接口及其实现(如 LinkedListArrayDeque)来实现 FIFO 结构。
    • 队列同样依赖于底层容器,但通常不会刻意隐藏遍历功能,因为它们本身作为 Collection 的一部分,允许对所有元素进行迭代。

详解栈与队列的接口与实现

1. Queue 和 Deque(接口层级)

  • Queue(队列)Deque(双端队列)都是 Java 集合框架(Java Collections Framework, JCF) 的接口。
  • Queue 代表 先进先出(FIFO) 结构,通常用于任务调度、消息队列等。
  • Deque 继承自 Queue,支持 两端插入和删除,可以作为 双端队列(双向 FIFO)栈(LIFO) 使用。

2. ArrayDeque 和 LinkedList(实现类)

  • ArrayDequeLinkedList 都是 Deque 接口的实现类。
  • ArrayDeque 使用 动态数组 作为底层存储结构,适用于需要高效的队列和栈操作。
  • LinkedList 使用 双向链表 作为底层存储结构,适用于频繁的插入和删除操作。

3. LinkedList 额外实现了 List

  • LinkedList 不仅仅是一个队列(Queue/Deque),它还实现了 List 接口,因此可以按索引访问元素,适用于链表操作。
  • ArrayDeque 仅实现 Deque,不能像 LinkedList 那样通过索引访问元素。

总结关系图

  • QueueDeque 的父接口。
  • Deque 扩展了 Queue,支持双端操作。
  • LinkedListArrayDeque 都实现了 Deque,但它们的底层数据结构不同。
  • LinkedList 额外实现了 List,支持按索引访问。

选择建议

  • ArrayDeque
    • 适用于栈(LIFO)队列(FIFO) 操作,比 LinkedList 更快(避免链表的指针操作)。
    • 适合频繁访问两端的情况,如 push()pop()offerFirst()pollLast()
  • LinkedList
    • 适用于需要按索引访问的链表操作,但比 ArrayDeque 慢(需要遍历链表)。
    • 适合插入和删除频繁不经常遍历的情况。
  • Queue
    • 适用于标准队列场景,如任务调度、消息队列等。
  • Deque
    • 适用于双端插入和删除的场景,如双端队列、栈等。

👉 如果只是需要队列或栈功能,推荐使用 ArrayDeque,它通常比 LinkedList 更高效!

 4. ArrayDeque和LinkedList方法详解

ArrayDeque 和 LinkedList 是 Java 中实现双端队列(Deque)的类,支持从队列的两端(队首和队尾)高效地添加、删除和访问元素。

方法类型队首(头部)方法队尾(尾部)方法关键行为说明
添加元素addFirst(E e)addLast(E e)直接插入,失败抛异常     无返回值(void
offerFirst(E e)offerLast(E e)建议用此方法,插入成功返回 true,失败返回 false
移除元素removeFirst()removeLast()直接删除,队列为空时抛异常 返回被删除的元素
pollFirst()pollLast()安全删除,队列为空时返回 null
访问元素getFirst()getLast()队列为空时抛异常
peekFirst()peekLast()队列为空时返回 null

空队列处理

  • peekFirst()peekLast() 在队列为空时返回 null
  • getFirst()getLast() 在队列为空时抛出异常。

队列容量

  • ArrayDeque 是动态扩容的,没有固定容量限制。
  • addFirst()addLast() 在队列已满时会抛出异常,而 offerFirst()offerLast() 会返回 false

性能

  • ArrayDeque 的所有操作(添加、删除、访问)都是 O(1) 时间复杂度。

 但注意!

 如果你在 Deque(如 ArrayDequeLinkedList)中直接使用 pushpoppeek 方法,那么你就是在实现 栈(Stack) 的操作。这是因为 Deque 接口支持栈的所有操作,并且 pushpoppeek 方法默认操作的是 栈顶元素(即双端队列的队首元素)。

栈的操作与 Deque 的对应关系

栈操作Deque 对应方法功能描述
pushpush(E e)将元素压入栈顶(即添加到队首)。
poppop()弹出栈顶元素(即移除并返回队首元素)。
peekpeek()查看栈顶元素(即返回队首元素,但不移除)。
isEmptyisEmpty()检查栈是否为空。

size和length方法详解 

在 Java 中,集合(Collection)类(如 ArrayListLinkedList)使用 size() 方法获取元素数量,而数组(Array)使用 length 属性获取长度

类型方法/属性示例
数组length 属性int len = arr.length;
集合类size() 方法int size = list.size();
  • 数组是固定长度的
    • 数组在创建时确定长度(如 int[] arr = new int[5]),length 表示其容量(固定不变)。
    • 例如:arr.length 始终是 5,即使数组中只有 3 个元素被赋值。
  • 集合是动态扩容的
    • 集合类的元素数量(size())会随着增删操作动态变化。
    • 例如:list.add(10) 后,list.size() 会增加 1,但底层数组可能已自动扩容。
  • 语义不同
    • length静态属性,反映数组的容量(物理长度)。
    • size()动态方法,反映集合当前存储的元素数量(逻辑长度)。
  • 面向对象特性
    • 数组是 Java 中的基础数据结构,直接通过属性 length 访问。
    • 集合是对象,通过方法 size() 提供更灵活的操作(可能涉及内部计算)。


232.用栈实现队列

力扣题目链接

栈的基本操作! | LeetCode:232.用栈实现队列_哔哩哔哩_bilibili

代码实现 

class MyQueue {

    Stack<Integer> stackIn;
    Stack<Integer> stackOut;
    
    void move(){
        // move 方法只在 stackOut 为空时才需要执行
        if (stackOut.isEmpty()) { 
            while(!stackIn.empty()){
                stackOut.push(stackIn.pop());
            }
        }
    }

    public MyQueue() {
        stackIn = new Stack<>();
        stackOut = new Stack<>(); 
    }
    
    public void push(int x) {
        stackIn.push(x);
    }
    
    public int pop() {
        move();
        return stackOut.pop();
    }
    
    public int peek() {
        move();
        return stackOut.peek();
    }
    
    public boolean empty() {
        return stackIn.empty() && stackOut.empty();
    }
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */

 注意:move 方法只在 stackOut 为空时才需要执行;因为stackOut不为空时,最早进入队列的元素一定在stackOut中!

225. 用队列实现栈

力扣题目链接

队列的基本操作! | LeetCode:225. 用队列实现栈_哔哩哔哩_bilibili

 代码实现

class MyStack {
    
    Queue<Integer> queue1;
    Queue<Integer> queue2;

    public MyStack() {
        queue1 = new ArrayDeque<>();
        queue2 = new ArrayDeque<>();
    }
    
    public void push(int x) {
        queue1.offer(x);
    }
    
    public int pop() {
        keepOne();
        return queue1.poll();
    }
    
    public int top() {
        keepOne();
        return queue1.peek();
    }
    
    public boolean empty() {
        return queue1.isEmpty() && queue2.isEmpty();
    }

    void keepOne(){
        if(queue1.isEmpty()){
            while(!queue2.isEmpty()){
                queue1.offer(queue2.poll());
            }
        }
        while(queue1.size()>1){
            queue2.offer(queue1.poll());
        }
    }
}

 注意语法

void keepOne(){
        if(queue1.size() == 0){
            queue1 = queue2;
            queue2.clear();
        }
        while(queue1.size()>1){
            queue2.offer(queue1.poll());
        }
    }

 我们观察一下以上代码哪里错了?

关键问题出现在keepOne方法中,当queue1为空时,错误地将queue2赋值给queue1并立即清空queue2,导致后续操作出现空指针。正确的做法是将queue2的元素逐个转移到queue1,而不是直接赋值引用。

 也就是根据java语法,对于不同数组之间的赋值,实际上是使其指向相同的指针!!!

20. 有效的括号

 力扣题目链接

栈的拿手好戏!| LeetCode:20. 有效的括号_哔哩哔哩_bilibili

思路 

思路比较简单,看代码注释即可。(自己想的方法复杂度还是稍微有点高了...)

简单写一下:

1. 碰到左括号,就把相应的右括号入栈;

2. 如果是右括号判断是否和栈顶元素匹配时,一定要先考虑栈里是否有元素!

3. 最后判断栈中元素是否均匹配完成。

 代码实现

class Solution {
    public boolean isValid(String s) {
        Deque<Character> deque = new ArrayDeque<>();
        for(int i=0;i<s.length();i++){
            char ch = s.charAt(i);
            //碰到左括号,就把相应的右括号入栈
            if (ch == '(') {
                deque.push(')');
            }else if (ch == '{') {
                deque.push('}');
            }else if (ch == '[') {
                deque.push(']');
            }
            //如果是右括号判断是否和栈顶元素匹配
            else if (deque.isEmpty()){
                return false;
            }
            else if (ch == deque.peek()){ // 一定要先考虑栈里是否有元素!
                deque.pop();
            }else{
                return false;
            }
        }
        //最后判断栈中元素是否均匹配完成
        return deque.isEmpty();
    }
}

1047. 删除字符串中的所有相邻重复项

力扣题目链接

栈的好戏还要继续!| LeetCode:1047. 删除字符串中的所有相邻重复项_哔哩哔哩_bilibili

 代码实现

class Solution {
    public String removeDuplicates(String s) {
        Deque<Character> deque = new ArrayDeque<>();
        for(int i=0; i<s.length(); i++){
            if(!deque.isEmpty() && s.charAt(i) == deque.peek()){
                deque.pop();
            }else{
                deque.push(s.charAt(i));
            }
        }
        // 转化为字符串,且注意顺序!!!
        String str = "";
        while (!deque.isEmpty()) {
            str = deque.pop() + str;
        }
        return str;
    }
}

转化为字符串的时候注意顺序!!!

或者拿字符串直接作为栈,省去了栈还要转为字符串的操作:

class Solution {
    public String removeDuplicates(String s) {
        // 将 res 当做栈
        // 也可以用 StringBuilder 来修改字符串,速度更快
        // StringBuilder res = new StringBuilder();
        StringBuffer res = new StringBuffer();
        // top为 res 的长度
        int top = -1;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            // 当 top >= 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top--
            if (top >= 0 && res.charAt(top) == c) {
                res.deleteCharAt(top);
                top--;
            // 否则,将该字符 入栈,同时top++
            } else {
                res.append(c);
                top++;
            }
        }
        return res.toString();
    }
}

拓展:双指针

class Solution {
    public String removeDuplicates(String s) {
        char[] ch = s.toCharArray();
        int fast = 0;
        int slow = 0;
        while(fast < s.length()){
            // 直接用fast指针覆盖slow指针的值
            ch[slow] = ch[fast];
            // 遇到前后相同值的,就跳过,即slow指针后退一步,下次循环就可以直接被覆盖掉了
            if(slow > 0 && ch[slow] == ch[slow - 1]){
                slow--;
            }else{
                slow++;
            }
            fast++;
        }
        return new String(ch,0,slow);
    }
}

注意语法

在 Java 中,StringBuilderStringBuffer 是用于处理可变字符串的两个类。它们的核心功能相似,但关键区别在于线程安全性性能。以下是详细对比:

1. 线程安全性

StringBufferStringBuilder
线程安全:所有公共方法都使用 synchronized 关键字修饰,保证多线程环境下的同步操作。非线程安全:方法没有同步,适用于单线程环境。
适合多线程共享数据的场景(如并发修改字符串)。适合单线程或不需要同步的场景(性能更高)。

2. 性能

StringBufferStringBuilder
由于同步机制(synchronized),在多线程频繁操作时,性能较低。没有同步开销,性能比 StringBuffer 高约 10%~15%
适用于需要线程安全的场景。适用于单线程或需要高性能的场景。

3. 继承关系

两者均继承自 AbstractStringBuilder,提供相似的方法(如 append(), insert(), reverse() 等),但 StringBuffer 通过同步方法实现线程安全。

public final class StringBuffer extends AbstractStringBuilder
    implements Serializable, CharSequence {
    // 所有方法用 synchronized 修饰
}

public final class StringBuilder extends AbstractStringBuilder
    implements Serializable, CharSequence {
    // 无同步修饰
}

总结

特性StringBuilderStringBuffer
线程安全❌ 不支持✅ 支持
性能✅ 更高❌ 较低(因同步开销)
适用场景单线程环境多线程环境

选择建议:优先使用 StringBuilder(单线程场景下性能更好),仅在需要线程安全时选择 StringBuffer

150. 逆波兰表达式求值

力扣题目链接

栈的最后表演! | LeetCode:150. 逆波兰表达式求值_哔哩哔哩_bilibili

代码实现

class Solution {
    public int evalRPN(String[] tokens) {
        Deque<Integer> deque = new ArrayDeque<>(); // 要放整型
        for(int i=0;i<tokens.length;i++){
            String s = tokens[i];
            if(s.equals("+")){
                deque.push(deque.pop() + deque.pop());
            }else if(s.equals("-")){
                deque.push(- deque.pop() + deque.pop());
            }else if(s.equals("*")){
                deque.push(deque.pop() * deque.pop());
            }else if(s.equals("/")){
                int temp1 = deque.pop();
                int temp2 = deque.pop();
                deque.push(temp2 / temp1);
            }else{
                deque.push(Integer.valueOf(s));
            }
        }
        return deque.pop();
    }
}

注意语法

1. Integer.valueOf(s) 是 Java 中用于将字符串 s 转换为 Integer 对象的方法。它是 Integer 类的一个静态方法,常用于将字符串形式的数字转换为整数对象。

String s = "123";
Integer num = Integer.valueOf(s);
System.out.println(num); // 输出 123

2. 查看以下代码错误原因:

if(s == new String('+'))

错误原因:

new String('+') 试图通过 char 类型参数创建字符串,但 String 类没有这样的构造函数。Java 中可用的 String 构造函数包括:

  • String(String original):接受另一个字符串。
  • String(char[] value):接受字符数组

char 转换为 String:若一定要从 char 生成字符串,可以用以下方法:

// 方法 1: 使用 String.valueOf()
String operator = String.valueOf('+');

// 方法 2: 使用 Character.toString()
String operator = Character.toString('+');

// 方法 3: 通过字符数组构造
String operator = new String(new char[]{'+'});

3. 字符串的比较

如果只是想比较字符串 s 是否为 "+",直接使用字符串字面量即可:

if (s.equals("+")) 

 注意:字符串比较必须用 equals()

Java 中 == 比较的是对象引用,而非内容。例如:

String s1 = new String("+");
String s2 = "+";

System.out.println(s1 == s2);          // false(引用不同)
System.out.println(s1.equals(s2));     // true(内容相同)

 4. 同时,一定要注意单双引号!!!

特性'+'"+"
类型char(基本类型)String(对象)
比较方式==(直接比较值)equals()(比较内容)
适用场景单个字符操作(如 ASCII 计算)字符串处理(如文本解析、拼接)
单引号包裹,表示单个字符双引号包裹,表示字符串(可能包含多个字符)

5. 注意除法逻辑:

deque.push(1 / deque.pop() * deque.pop()); // 会导致逻辑错误。

例如,对于 13 和 5

  • 先弹出 5,然后计算 1 / 5,结果是 0(因为 1 / 5 是整数除法,结果为 0)。

  • 再弹出 13,计算 0 * 13,结果是 0

239. 滑动窗口最大值

力扣题目链接

单调队列正式登场!| LeetCode:239. 滑动窗口最大值_哔哩哔哩_bilibili

 思路

我先对于暴力求解进行了两步优化:

1. 使用队列结构,更好的实现窗口滑动;

2. 对于被移除的元素不是最大值的情况,判断新加入的元素和原来最大值之间的max,从而减少遍历全部窗口内元素的次数。

该代码如下:

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        Queue<Integer> queue = new LinkedList<>();
        int[] res = new int[nums.length-k+1];
        for(int i = 0; i<k; i++){
            queue.offer(nums[i]);
        }
        boolean maxIn = false;
        for(int i = 0;i<=nums.length-k; i++){
            if(maxIn && i<nums.length-k){
                res[i] = max(res[i-1],nums[i+k-1]);
            }else{
                res[i] = findMax(queue);
            }
            if(queue.poll() != res[i]){
                maxIn = true;
            }else{
                maxIn = false;
            }
            if(i<nums.length-k){
                queue.offer(nums[i+k]);
            }
        }
        return res;
    }
    public int findMax(Queue<Integer> queue){
        int max = queue.peek();
        for(int nums : queue){
            if(nums > max){
                max = nums;
            }
        }
        return max;
    }

    public int max(int a, int b){
        return (a > b) ? a : b;
    }
}

但很遗憾的是,我的代码依旧超时了,通过了46/51个测试用例。

实际上,即使这样优化,我的代码的时间复杂度最高情况依旧是O(n * k)

所以我们不得不学习一下先进的优化算法了!!!

实现一个最大值永远在出口处的单调队列: 也就是使用deque双端队列实现该算法!

  • pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作
  • push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

代码实现

   注意:一定要判断队列是否非空!

(并且 这种情况一定是单调队列,否则去掉第一个值之后需要遍历才能找到max值)

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> deque = new ArrayDeque<>();
        int[] res = new int[nums.length-k+1];
        // 将初始序列读入
        for(int i = 0; i<k; i++){
            while (!deque.isEmpty() && deque.peekLast()<nums[i]){
                deque.pollLast();
            }
            deque.offerLast(nums[i]);
        }
        res[0] = deque.peekFirst();
        // 滑动窗口
        for(int i = 1;i<=nums.length-k; i++){
            // 移除元素逻辑
            if (!deque.isEmpty() && nums[i-1] == deque.peekFirst()){
                deque.pollFirst();
            }
            // 加入元素逻辑
            while (!deque.isEmpty() && deque.peekLast()<nums[i+k-1]){
                deque.pollLast();
            }
            deque.offerLast(nums[i+k-1]);
            res[i] = deque.peekFirst();
        }
        return res;
    }
}

347.前 K 个高频元素

力扣题目链接

优先级队列正式登场!大顶堆、小顶堆该怎么用?| LeetCode:347.前 K 个高频元素_哔哩哔哩_bilibili

思路

  1. 要统计元素出现频率
  2. 对频率排序
  3. 找出前K个高频元素

堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。

所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。

本题我们就要使用优先级队列来对部分频率进行排序。

为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。

此时要思考一下,是使用小顶堆呢,还是大顶堆?

有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。

那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。

而且使用大顶堆就要把所有元素都进行排序(也可行),那能不能只排序k个元素呢

所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

代码实现

class Solution {
    //解法1:基于大顶堆实现
    public int[] topKFrequent1(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>(); //key为数组元素值,val为对应出现次数
        for (int num : nums) {
            map.put(num, map.getOrDefault(num,0) + 1);
        }
        //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
        //出现次数按从队头到队尾的顺序是从大到小排,出现次数最多的在队头(相当于大顶堆)
        PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair2[1] - pair1[1]);
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {//大顶堆需要对所有元素进行排序
            pq.add(new int[]{entry.getKey(), entry.getValue()});
        }
        int[] ans = new int[k];
        for (int i = 0; i < k; i++) { //依次从队头弹出k个,就是出现频率前k高的元素
            ans[i] = pq.poll()[0];
        }
        return ans;
    }
    //解法2:基于小顶堆实现
    public int[] topKFrequent2(int[] nums, int k) {
        Map<Integer,Integer> map = new HashMap<>(); //key为数组元素值,val为对应出现次数
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数
        //出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆)
        PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) { //小顶堆只需要维持k个元素有序
            if (pq.size() < k) { //小顶堆元素个数小于k个时直接加
                pq.add(new int[]{entry.getKey(), entry.getValue()});
            } else {
                if (entry.getValue() > pq.peek()[1]) { //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个)
                    pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了
                    pq.add(new int[]{entry.getKey(), entry.getValue()});
                }
            }
        }
        int[] ans = new int[k];
        for (int i = k - 1; i >= 0; i--) { //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多
            ans[i] = pq.poll()[0];
        }
        return ans;
    }
}

注意语法

在 Java 中,大小顶堆(最大堆和最小堆)是一种基于完全二叉树的数据结构,常用于实现优先队列。堆的特点是:

  • 最大堆:父节点的值大于或等于其子节点的值。
  • 最小堆:父节点的值小于或等于其子节点的值。

  Java 中的 PriorityQueue 类实现了堆的功能,默认是最小堆;

但可以通过自定义比较器实现最大堆。

1. 堆的特性

  • 堆的性质
    • 堆是一个完全二叉树。
    • 最大堆的根节点是最大值,最小堆的根节点是最小值。
  • 时间复杂度
    • 插入元素:O(log n)
    • 删除堆顶元素:O(log n)
    • 获取堆顶元素:O(1)

2. Java 中的 PriorityQueue

PriorityQueue 是 Java 提供的优先队列实现,底层基于堆。默认是最小堆,但可以通过自定义比较器实现最大堆。

默认最小堆

PriorityQueue<Integer> minHeap = new PriorityQueue<>();

自定义最大堆

PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
// 或者
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

3. 自定义对象的堆

如果堆中存储的是自定义对象,需要实现 Comparable 接口或提供 Comparator

示例:自定义对象的最小堆

import java.util.PriorityQueue;

class Person implements Comparable<Person> {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return this.age - other.age; // 按年龄升序(最小堆)
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class CustomMinHeapExample {
    public static void main(String[] args) {
        // 创建一个最小堆
        PriorityQueue<Person> minHeap = new PriorityQueue<>();

        // 添加元素
        minHeap.offer(new Person("Alice", 25));
        minHeap.offer(new Person("Bob", 20));
        minHeap.offer(new Person("Charlie", 30));

        // 获取堆顶元素(年龄最小)
        System.out.println("堆顶元素: " + minHeap.peek()); // 输出: Bob (20)

        // 弹出堆顶元素
        System.out.println("弹出元素: " + minHeap.poll()); // 输出: Bob (20)

        // 剩余元素
        System.out.println("剩余元素: " + minHeap); // 输出: [Alice (25), Charlie (30)]
    }
}

示例:自定义对象的最大堆

import java.util.PriorityQueue;
import java.util.Comparator;

class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

public class CustomMaxHeapExample {
    public static void main(String[] args) {
        // 创建一个最大堆
        PriorityQueue<Person> maxHeap = new PriorityQueue<>((a, b) -> b.age - a.age);

        // 添加元素
        maxHeap.offer(new Person("Alice", 25));
        maxHeap.offer(new Person("Bob", 20));
        maxHeap.offer(new Person("Charlie", 30));

        // 获取堆顶元素(年龄最大)
        System.out.println("堆顶元素: " + maxHeap.peek()); // 输出: Charlie (30)

        // 弹出堆顶元素
        System.out.println("弹出元素: " + maxHeap.poll()); // 输出: Charlie (30)

        // 剩余元素
        System.out.println("剩余元素: " + maxHeap); // 输出: [Alice (25), Bob (20)]
    }
}

4. 常见应用场景

  • Top K 问题:使用最小堆或最大堆快速找到前 K 个最大或最小的元素。
  • 排序:堆排序。
  • 任务调度:优先处理优先级高的任务。
  • Dijkstra 算法:优先队列用于选择最短路径。

5. 代码解析

PriorityQueue<int[]> pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]);
  • PriorityQueue<int[]>
    • 优先队列中存储的元素是 int[] 类型(整数数组)。
    • 例如:[1, 2][3, 4] 等。
  • (pair1, pair2) -> pair1[1] - pair2[1]
    • 这是一个 Lambda 表达式,用于定义优先队列的排序规则。
    • pair1pair2 是队列中的两个元素(都是 int[] 类型)。
    • pair1[1]pair2[1] 分别表示这两个数组的第二个元素。
    • pair1[1] - pair2[1] 表示根据第二个元素的值进行升序排序

6. 哈希表算法:map.entrySet()

map.entrySet() 是 Java 中 Map 接口的一个方法,用于返回一个包含所有键值对(Map.Entry)的集合(Set。每个 Map.Entry 对象表示一个键值对,可以通过它访问键和值。

(1)方法签名:

Set<Map.Entry<K, V>> entrySet();
  • 返回值:一个 Set 集合,包含 Map 中的所有键值对(Map.Entry)。
  • 泛型
    • K:键的类型。
    • V:值的类型。

(2)Map.Entry 接口

Map.EntryMap 接口的内部接口,表示一个键值对。它提供了以下常用方法:

方法名功能描述
getKey()返回当前键值对的键。
getValue()返回当前键值对的值。
setValue(V value)设置当前键值对的值(注意:会修改原始 Map 中的值)。

(3)使用场景

  • 遍历 Map:通过 entrySet() 可以方便地遍历 Map 中的所有键值对。
  • 修改值:通过 Map.EntrysetValue() 方法可以直接修改 Map 中的值。
  • 获取键值对集合:将 Map 转换为 Set,便于进一步操作。

(4)代码示例:遍历 Map

import java.util.HashMap
import java.util.Map;
import java.util.Set;

public class EntrySetExample {
    public static void main(String[] args) {
        // 创建一个 Map
        Map<String, Integer> map = new HashMap<>();
        map.put("Alice", 25);
        map.put("Bob", 30);
        map.put("Charlie", 35);

        // 获取 entrySet
        Set<Map.Entry<String, Integer>> entries = map.entrySet();

        // 遍历 entrySet
        for (Map.Entry<String, Integer> entry : entries) {
            System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值