zoukankan      html  css  js  c++  java
  • 剑指offer刷题(栈、堆、 队列、 图)

    Stack & Queue

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

    代码

    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)最差情况
  • 相关阅读:
    L3-1 二叉搜索树的结构 (30 分)
    L3-2 森森快递 (30 分)(贪心+线段树/分块)
    三分(凸函数)
    (三分入门)(凹函数)
    Print Article(斜率DP入门+单调队列)
    PTA 逆散列问题 (30 分)(贪心)
    二叉树遍历相关
    7-5 堆中的路径 (25 分)
    Grouping ZOJ
    D
  • 原文地址:https://www.cnblogs.com/hugoyu/p/13979535.html
Copyright © 2011-2022 走看看