剑指offer刷题(栈、堆、 队列、 图) Stack & Queue Heap Hash Table 图

005-用两个栈实现队列

题目描述

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 push 和 pop ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

题解

用两个栈来实现队列(先进先出)的功能

  • stack1:实现push 操作
    - 直接添加到stack1中

  • stack2:实现pop操作
    1. stack1和Stack2 均而空,返回 -1;
    2.若 stack2 为空(有上面条件知stack1不为空)
    - 遍历stack1,将stack1中的元素添加到stack2中
    3.只要stack2不为空,弹出stack2栈顶的元素;

代码

import java.util.Stack;
public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    //push() 是从队列的头部插入元素
    public void push(int node) {
        stack1.add(node);
    }
    // pop() 是从队列的尾部取出元素
    public int pop() {
        if(stack1.isEmpty() && stack2.isEmpty()){
            return -1;
        }
        if(stack2.isEmpty()){
            while(!stack1.isEmpty()){
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();
    }
}

复杂度

由于问题特殊,以下分析仅满足添加 N 个元素并删除 N 个元素,即栈初始和结束状态下都为空的情况。

  • 时间复杂度: push函数为 O(1);pop()函数在N次队首元素删除操作中总共需完成N个元素的倒序。

  • 空间复杂度O(N):最差情况下,栈1和2共保存N个元素。

020-包含min函数的栈

题目描述

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.min();   --> 返回 -2. 

题解

知识点

  • dd & push

共同点:addpush都可以向stack中添加元素。

不同点:

`add`是继承自Vector的方法,且返回值类型是boolean.

`push`是Stack自身的方法,返回值类型是参数类类型。
  • peek & pop

共同点:peek,pop都是返回栈顶元素。

不同点:

peek()函数返回栈顶的元素,但不弹出该栈顶元素。
pop()函数返回栈顶的元素,并且将该栈顶元素出栈。

函数设计

  • push(x) 函数: 重点为保持栈 B 的元素是 非严格降序 的。
  1. 将 x 压入栈 A(即 A.add(x) );

  2. 若 ① 栈 B 为空 或 ② x 小于等于 栈 B 的栈顶元素,则将 x 压入栈 B (即 B.add(x) )。

  • pop() 函数: 重点为保持栈 A,B 的 元素一致性 。
  1. 执行栈 A 出栈(即 A.pop() ),将出栈元素记为 y ;
  2. 若 y等于栈 B 的栈顶元素,则执行栈 B 出栈(即 B.pop() )。
  • top() 函数: 直接返回栈 A 的栈顶元素即可,即返回 A.peek() 。

  • min() 函数: 直接返回栈 B 的栈顶元素即可,即返回 B.peek() 。

代码

class MinStack {
    Stack<Integer> stack;//c存储数据
    Stack<Integer> stackMin;//存储最小值
    /** initialize your data structure here. */
    public MinStack() {
      stack = new Stack<>();
      stackMin = new Stack<>(); 
    }
    
    public void push(int x) {
        stack.push(x);//将x压入存储栈stack中;
        //当stackMin为空或者当前x小于栈顶元素(x为最小值);将x压入栈stackMIn中;
        if(stackMin.isEmpty() ||x <= stackMin.peek()) stackMin.push(x);
    }
    
    public void pop() {
    //stack.pop()实现了出栈操作;
    //stackMin的栈顶元素 == stack的栈顶元素,说明最小值是最后进栈的,也需要出栈;
        if(stack.pop().equals(stackMin.peek())) stackMin.pop();
    }
    
    public int top() {
        return stack.peek();
    }
    
    public int min() {
        return stackMin.peek();
    }
}

复杂度

  • 时间复杂度 O(1): push(), pop(), top(), min() 四个函数的时间复杂度均为常数级别。
  • 空间复杂度 O(N) : 当共有 N 个待入栈元素时,辅助栈 B 最差情况下存储 N个元素,使用 O(N) 额外空间。

021-栈的压入、弹出序列

题目描述

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

示例 1:

输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:

push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1

示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false

解释:1 不能在 2 之前弹出。

提示:

  • 0 <= pushed.length == popped.length <= 1000
  • 0 <= pushed[i], popped[i] < 1000
  • pushed 是 popped 的排列。

题解

辅助栈 stack ,模拟 压入 / 弹出操作的排列。根据是否模拟成功,即可得到结果。

  • 入栈操作: 按照压栈序列的顺序执行。
  • 出栈操作: 每次入栈后,循环判断 “栈顶元素 == 弹出序列的当前元素” 是否成立,将符合弹出序列顺序的栈顶元素全部弹出。

代码

import java.util.ArrayList;
import java.util.Stack;
public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        Stack<Integer> stack = new Stack<Integer>();
        //用于标识弹出序列的位置
        int popIndex = 0;
        for(int i = 0; i< pushA.length;i++){
            stack.push(pushA[i]);
            //如果栈不为空,且栈顶元素等于弹出序列
            while(!stack.empty() && stack.peek() == popA[popIndex]){
                //出栈
                stack.pop();
                //弹出序列向后一位
                popIndex++;
            }
        }
        return stack.empty();
    }
}

复杂度

  • 时间复杂度 O(N) : 其中 N 为列表 pushed 的长度;每个元素最多入栈与出栈一次,即最多共 2N 次出入栈操作。
  • 空间复杂度 O(N) : 辅助栈 stack 最多同时存储 N 个元素。

044-翻转单词顺序列(栈)

题目描述

loser a am I 翻转后为 I am a loser

题解

双指针

substring(start, stop) :方法用于提取字符串中介于两个指定下标之间的字符,方法返回的子串包括 start 处的字符,但不包括 stop 处的字符。
append() :将指定的字符串追加到此字符序列
trim():删除收尾的空格
单引号引的数据 是char类型的——> 单引号只能引一个字符(表示单个字符)
双引号引的数据 是String类型的——> 而双引号可以引0个及其以上(引用字符串)

代码

class Solution{
      public String ReverseSentence(String str) {
            StringBuffer res = new StringBuffer();
            str = str.trim();
            int i = str.length -1, j = i;
            while(i > 0){
                  // 搜索首个空格,charAt(i) 返回指定位置(i)的字符;
                  while(i > = 0 && str.charAt(i) != ' ') i--;
                  res.append(str.subString(i + 1, j + 1) + " ");
                  
                  //// 跳过单词间空格
                  while(i >= 0 && str.charAt(i) == ' ') i--;
                  j = i;
            }

            // 转化为字符串并返回
            return res.toString().trim;
      }
}

复杂度

  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

括号序列

给出一个仅包含字符'(',')','{','}','['和']',的字符串,判断给出的字符串是否是合法的括号序列
括号必须以正确的顺序关闭,"()"和"()[]{}"都是合法的括号序列,但"(]"和"([)]"不合法。

示例1
输入

""
返回值
false
示例2
输入

"{}()"
返回值
true

题解:

利用栈的性质,

  • 遍历到"(" "{" "[" 时:
    • push "(" "{" "["进栈
  • 遍历到 "(" "{" "[" 时:
    • 与 栈中 pop() 出的元素比较
    • 不相等返回 false;
public class Solution {
  public boolean solution(String s){
    Stack<Character> stack = new Stack<Character>();
    
    for(char c : s.toCharArray()) {
        if(c == '('){
          stack.push(')');
        }else if(c == '{'){
          stack.push('}');
        }else if(c == '['){
          stack.push(']');
        }else {
          if(stack.isEmpty() || stack.pop() != c){
            return false;
        }
      } 
      return true; 
  }
}

064-滑动窗口的最大值(双端队列)

题目描述

题解

代码

复杂度

Heap

029-最小的K个数

题目描述

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。

示例1

输入
[4,5,1,6,2,7,3,8],4

返回值
[1,2,3,4]

题解

法一:大根堆(前 K 小) / 小根堆(前 K 大),Java中有现成的 PriorityQueue.

  • 默认是小根堆,实现大根堆需要重写一下比较器。
    PriorityQueue<Integer> queue = new PriorityQueue<>((o1,o2) -> o2-o1);

剑指offer刷题(栈、堆、 队列、 图)
Stack & Queue
Heap
Hash Table
图

代码

import java.util.ArrayList;
import java.util.PriorityQueue;

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
      if(input.length < k || k < 1) return new ArrayList<>();
       
      //默认是小根堆,实现大根堆需要重写一下比较器
      PriorityQueue<Integer> queue = new PriorityQueue<>((o1,o2) -> o2-o1);
      
      //遍历数组
      for(int num : input){
          //近小顶堆填满
          if(queue.size() < k){
              queue.add(num);
          }
           
            //小顶堆的堆顶为最大值,若遍历的数小于堆顶,将堆顶元素替换
          if(queue.size() >= k && queue.peek() > num){
          queue.poll();
          queue.add(num);
        }
      }

      return new ArrayList<>(queue);
    }
}

复杂度

  • 时间复杂度: O(NlogK)
  • 空间复杂度: O(K)

Hash Table

034-第一个只出现一次的字符

题目描述

题解

代码

复杂度

065-矩阵中的路径(BFS)

题目描述

题解

代码

复杂度

066-机器人的运动范围(DFS)

题目描述

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:
输入:m = 2, n = 3, k = 1
输出:3

示例 2:
输入:m = 3, n = 1, k = 0
输出:1

提示:

  • 1 <= n,m <= 100
  • 0 <= k <= 20

题解

1. DFS方法
这里统计的是能走多少个格子,所以统计肯定是不能有重复的,题中说了,机器人是可以沿着上下左右四个方向走的。但你想一下,任何一个格子你从任何一个方向进来(比如从上面进来),那么他只能往其他3个方向走,因为如果在往回走就重复了。但实际上我们只要沿着两个方向走就可以了,一个是右边,一个是下边,也就是上面图中红色的箭头。我们来看下代码

2. BFS方法
DFS是沿着一个方向一直往下走,有一种不撞南墙不回头的感觉,直到不满足条件才会回头。而BFS就显得有点博爱了,他不是一条道走下去,他会把离他最近的都访问一遍,访问完之后才开始访问第二近的……,一直这样下去,所以最好的一种数据结构就是使用队列,因为队列是先进先出,离他最近的访问完之后加入到队列中,最先入队的也是最先出队的。如下图,DFS就是沿着一条道走下去,然后再走其他的道……。BFS就是图中先访问圈内的部分,然后再把圈放大继续访问……。

代码

DFS

public int movingCount(int m, int n, int k) {
    //临时变量visited记录格子是否被访问过
    boolean[][] visited = new boolean[m][n];
    return dfs(0, 0, m, n, k, visited);
}

public int dfs(int i, int j, int m, int n, int k, boolean[][] visited) {
    //i >= m || j >= n是边界条件的判断,k < sum(i, j)判断当前格子坐标是否
    // 满足条件,visited[i][j]判断这个格子是否被访问过
    if (i >= m || j >= n || k < sum(i, j) || visited[i][j])
        return 0;
    //标注这个格子被访问过
    visited[i][j] = true;
    //沿着当前格子的右边和下边继续访问
    return 1 + dfs(i + 1, j, m, n, k, visited) + dfs(i, j + 1, m, n, k, visited);
}

//计算两个坐标数字的和
private int sum(int i, int j) {
    int sum = 0;
    while (i != 0) {
        sum += i % 10;
        i /= 10;
    }
    while (j != 0) {
        sum += j % 10;
        j /= 10;
    }
    return sum;
}

BFS

public int movingCount(int m, int n, int k) {
    //临时变量visited记录格子是否被访问过
    boolean[][] visited = new boolean[m][n];
    int res = 0;
    //创建一个队列,保存的是访问到的格子坐标,是个二维数组
    Queue<int[]> queue = new LinkedList<>();
    //从左上角坐标[0,0]点开始访问,add方法表示把坐标
    // 点加入到队列的队尾
    queue.add(new int[]{0, 0});
    while (queue.size() > 0) {
        //这里的poll()函数表示的是移除队列头部元素,因为队列
        // 是先进先出,从尾部添加,从头部移除
        int[] x = queue.poll();
        int i = x[0], j = x[1];
        //i >= m || j >= n是边界条件的判断,k < sum(i, j)判断当前格子坐标是否
        // 满足条件,visited[i][j]判断这个格子是否被访问过
        if (i >= m || j >= n || k < sum(i, j) || visited[i][j])
            continue;
        //标注这个格子被访问过
        visited[i][j] = true;
        res++;
        //把当前格子右边格子的坐标加入到队列中
        queue.add(new int[]{i + 1, j});
        //把当前格子下边格子的坐标加入到队列中
        queue.add(new int[]{i, j + 1});
    }
    return res;
}

//计算两个坐标数字的和
private int sum(int i, int j) {
    int sum = 0;
    while (i != 0) {
        sum += i % 10;
        i /= 10;
    }
    while (j != 0) {
        sum += j % 10;
        j /= 10;
    }
    return sum;
}

复杂度

  • 时间复杂度:O(MN)最差情况
  • 空间复杂苏:O(MN)最差情况