难点算法题目个人收藏(持续更新)

基础常见题目集(可面试用)

二叉树

二叉树先序遍历

【数据格式]
  • class TreeNode {
          int val;
          TreeNode left;
          TreeNode right;
          TreeNode() {}
          TreeNode(int val) { this.val = val; }
          TreeNode(int val, TreeNode left, TreeNode right) {
              this.val = val;
              this.left = left;
              this.right = right;
          }
    }
    
【代码】
  • // 递归写法
    void preorderRecursive(TreeNode root, List<Integer> result){
       if(root == null) return;
       result.add(root.val);
       preorderRecursive(root.left, result);
       preorderRecursive(root.right, result);
    }
    // 非递归写法(栈)
    List<Integer> preorderIterative(TreeNode root) {
       ArrayList<Integer> list = new ArrayList<>();
       if (root == null) return;
       ArrayDeque<TreeNode> stack = new ArrayDeque<>();
       stack.push(root); // 使用push方法将元素压入栈顶
       while (!stack.isEmpty()) {
           TreeNode node = stack.pop(); // 弹出栈顶元素
           list.add(node.val);
           
           // 先添加右子节点,再添加左子节点
           if (node.right != null) stack.push(node.right);
           if (node.left != null) stack.push(node.left);
       }
       return result;
    }
    

二叉树中序遍历

  • 外层循环条件 curr != null || !stack.isEmpty()
    • 为什么需要这个条件:确保遍历完所有节点。
      • 当 curr != null 时,表示还有右子树需要处理;
      • 当 !stack.isEmpty() 时,表示还有父节点或左兄弟节点需要处理。
    • 循环结束条件:当 curr == null 且栈为空时,说明所有节点都已处理完毕。
【代码】
  • // 非递归写法(栈)
    void inorderRecursive (TreeNode root, List<Integer> result){
      if(root == null) return;
       inorderRecursive(root.left, result);
       result.add(root.val);
       inorderRecursive(root.right, result);
    }
    // 递归写法
    List<Integer> inorderIterative(TreeNode root) {
       List<Integer> result = new ArrayList<>();
       Deque<TreeNode> stack = new ArrayDeque<>();  // 使用栈来模拟递归调用栈
       TreeNode curr = root;  // 当前节点指针,从根节点开始
    
       // 外层循环:只有当栈为空且当前节点为空时,遍历才结束
       // curr == null,表示已经到达最左叶子节点的左子节点(null)。
       while (curr != null || !stack.isEmpty()) {
           // 【步骤1】遍历左子树:将当前节点及其所有左子节点压入栈
           while (curr != null) {
               stack.push(curr);     // 先访问当前节点
               curr = curr.left;     // 然后向左走,直到最左叶子节点
           }
    
           // 【步骤2】访问根节点:弹出栈顶元素并访问
           curr = stack.pop();       // 弹出栈顶节点,它是当前子树的根节点
           result.add(curr.val);     // 访问该节点(添加到结果列表)
    
           // 【步骤3】转向右子树:处理完左子树和根节点后,转向右子树
           // 当处理完一个节点的左子树和该节点本身后,转向该节点的右子树。
           curr = curr.right;        // 移动到右子节点,下次循环会处理右子树
       }
       return result;
    }
    

二叉树后序遍历

  • 实际用另一个栈将前序遍历的结果倒转过来
【代码】
  • // 递归实现
    private void postorderHelper(TreeNode node, List<Integer> result) {
        if (node == null) return;
        // 先遍历左子树
        postorderHelper(node.left, result);
        // 再遍历右子树
        postorderHelper(node.right, result);
        // 最后访问根节点
        result.add(node.val);
    }
    
    // 迭代实现
    public List<Integer> postorderIterative(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) return result;
    
        Deque<TreeNode> stack1 = new ArrayDeque<>();
        Deque<TreeNode> stack2 = new ArrayDeque<>();
    
        // 初始时根节点入栈1
        stack1.push(root);
    
        // 这个循环用于按照根-右-左的顺序将节点压入栈2
        while (!stack1.isEmpty()) {
            TreeNode current = stack1.pop();
            stack2.push(current);
    
            // 先压左子节点,再压右子节点,确保右子节点先被处理
            if (current.left != null) stack1.push(current.left);
            if (current.right != null) stack1.push(current.right);
        }
    
        // 此时栈2中的节点顺序是左-右-根,依次弹出即为后序遍历
        while (!stack2.isEmpty()) {
            result.add(stack2.pop().val);
        }
    
        return result;
    }
    

二叉树层序遍历【广度优先搜索 (BFS)】

  • BFS 是一种遍历或搜索树或图的算法,它从根节点开始,逐层地访问节点,先访问距离根节点最近的节点。这种搜索方式使用队列来实现,确保节点按层次顺序被访问。
[应用场景】
  • 最短路径问题(无权图)
  • 网络爬虫
  • 社交网络中查找最近的联系人
  • 游戏中的地图遍历
[具体实现】
  • 使用队列 (Queue) 作为核心数据结构
  • 按照从上到下、从左到右的顺序访问节点
  • 每个节点仅入队和出队一次,时间复杂度 O (n)
【代码】
  • ArrayDeque 可以用作队列(FIFO)或栈(LIFO):
    • 队列模式:offer() 入队尾,poll() 出队头(对应 LinkedList 的 offer() 和 poll())
    • 栈模式: push()入栈顶,pop() 出栈顶
  • public List<Integer> levelOrderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) return result;
        
        ArrayDeque<TreeNode> queue = new ArrayDeque<>();
        queue.offer(root); // 入队尾
        
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll(); // 出队头
            result.add(node.val);
            
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
        
        return result;
    }
    

二叉树【深度优先搜索(DFS)】

  • DFS 是一种遍历或搜索树 / 图的算法,其特点是沿着树的深度遍历节点,尽可能深地搜索树的分支。当节点 v 的所在边都己被探寻过,搜索将回溯到发现节点 v 的那条边的起始节点。这种搜索方式使用递归或栈来实现。
  1. 深度优先搜索(DFS, Depth-First Search)在二叉树中的实现方式主要有两种:
    (1)递归实现(最常见)

    (2)非递归实现(使用栈模拟) DFS 实际对应的是我们熟悉的:

    • 前序遍历(根 → 左 → 右)
    • 中序遍历(左 → 根 → 右)
    • 后序遍历(左 → 右 → 根)
【应用场景】
  • 拓扑排序
  • 寻找连通分量
  • 解谜游戏(如迷宫)
  • 检测图中的环
【示例】
  • ✅ 举个典型 DFS 回溯场景(查找所有从根到叶子的路径)
    void dfsPaths(TreeNode root, List<Integer> path, List<List<Integer>> result) {     
    	if (root == null) return;
    	
        path.add(root.val);
              
        if (root.left == null && root.right == null) {
        	result.add(new ArrayList<>(path)); // 到达叶子节点
        } else {         
    	    dfsPaths(root.left, path, result);         
    	    dfsPaths(root.right, path, result);     
        }      
        path.remove(path.size() - 1); // 回溯 
    }
    

树形结构数据的构建与递归打印

[题目描述]

给定一组具有父子关系的节点数据,要求:

  1. 构建树形结构:将这些节点按照父子关系组织成一个树形结构(或多棵树森林结构)
  2. 递归打印树形结构:以缩进格式打印树的层次结构,子节点需要比父节点缩进一级

[输入数据格式]

每个节点包含三个属性:

  • id:节点唯一标识(整数)
  • parentId:父节点ID(根节点的parentId0
  • name:节点名称(字符串)

[示例输入]

List<Node> list = Arrays.asList(
    new Node(1, 0, "AA"),  // 根节点1
    new Node(2, 1, "BB"),  // AA的子节点
    new Node(3, 1, "CC"),  // AA的子节点
    new Node(4, 3, "DD"),  // CC的子节点
    new Node(5, 3, "EE"),  // CC的子节点
    new Node(6, 0, "FF"),  // 根节点2
    new Node(7, 6, "GG"),  // FF的子节点
    new Node(8, 0, "HH")   // 根节点3
);

输出要求
按树形层次缩进打印节点名称,例如:

AA
 BB
 CC
  DD
  EE
FF
 GG
HH

[程序实现要求]

  • 使用HashMap存储父子关系映射(Key为parentId,Value为子节点列表)

  • 通过递归方法遍历并打印树形结构,递归时传递缩进级别(level)

  • 根节点的parentId为0,需从parentId=0开始遍历

[考察知识点]

  • 树形结构的构建与遍历

[递归算法的应用]

  • HashMap的使用(快速查找子节点列表)

  • 字符串操作(缩进控制)

【代码】

	package com.bc.domain;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

import static com.alibaba.fastjson.JSONValidator.Type.Array;

/**
 * @ClassName:TestDemo
 * @Author: Yjt
 * @Date: 2025/5/26 10:08
 * @Description: 必须描述类做什么事情, 实现什么功能
 */
public class TestDemo {
    static class Node {
        private int id;
        private int parentId;
        private String name;

        public Node(int id, int parentId, String name) {
            this.id = id;
            this.parentId = parentId;
            this.name = name;
        }

        public int getId() {
            return id;
        }

        public int getParentId() {
            return parentId;
        }

        public String getName() {
            return name;
        }
    }

    public static void main(String[] args) {
        List<Node> list = Arrays.asList(
                new Node(1,0,"AA"),
                new Node(2,1,"BB"),
                new Node(3,1,"CC"),
                new Node(4,3,"DD"),
                new Node(5,3,"EE"),
                new Node(6,0,"FF"),
                new Node(7,6,"GG"),
                new Node(8,0,"HH")
        );
        HashMap<Integer, List<Node>> fuziMap = new HashMap<>();
        for (Node node : list) {
            fuziMap.putIfAbsent(node.getParentId(), new ArrayList<>());
            fuziMap.get(node.getParentId()).add(node);
        }
        printf(fuziMap,0,0);
    }

    public static void printf(HashMap<Integer, List<Node>> fuziMap,int parentId,int level) {
        List<Node> childrenList = fuziMap.get(parentId);
        if(childrenList == null) return;
        for (Node child : childrenList) {
            String string = new String(new char[level]).replace("\0", " ");
            System.out.println(string + child.getName());
            printf(fuziMap,child.getId(),level+1);
        }
    }
}


LRU 缓存机制(双链表+HashMap)

  1. 题目

    • 请你设计并实现一个最近最少使用(LRU)缓存机制。
    • 它应该支持以下操作:get(key) 和 put(key, value)。
    • 当缓存容量达到上限时,它应该删除最近最少使用的那个元素。
  2. 解释说明:

    • 用 HashMap + 双向链表 实现:
      • HashMap 用于 O(1) 时间定位 key;
      • 双向链表维护访问顺序,最近访问放头部,最久未访问在尾部;
      • 每次 get() 和 put() 都把对应节点移到链表头部;
      • 超过容量时,移除尾部元素(最久未使用)。
  3. 通俗易懂类比:

    • 想象你是图书管理员:
      • 图书馆最多放 5 本书;
      • 你会把最近看的书放在最前面;
      • 如果空间满了,你就把最久没看的书扔掉,腾出位置。
  4. 面试答题模板(背下来):

    “LRU 缓存一般用 HashMap + 双向链表实现:HashMap 保证 O(1) 查找,双向链表维护访问顺序。每次访问或写入都把节点移动到链表头部;如果超过容量则删除尾部节点。”

【代码】

public class TestDemo {
    /**
     *  例如:书架上的书本,每次使用完后放入第一个位置,依次类推。 如果放不下(超过最大长度),那么取下最后一个书本。
     *       map表示    【书本名称,书本(名称和内容))】    这里的顺序可能是乱的,和链表并不是对应的
     *      双链表表示   【书本之间的关系,书名称的索引 】    这里的顺序是实际的,统一用这个位置来作为根据。
     *
     */

    private final int capacity;                      // 最大长度
    private final Map<Integer, Node> map;           // 存放链表的各个节点信息,key为节点的索引
    private final DoubleNode doubleNode;           // 双链表


    TestDemo(int capacity){
        this.capacity = capacity;
        this.map = new HashMap<>();
        this.doubleNode = new DoubleNode();
    }

    // 获取(使用)指定的节点,使用过之后,添加到第一个位置
    public int get(int key){
        if(!map.containsKey(key)) return -1;

        Node node = map.get(key);
        // 使用过了,那么就要放入第一个位置
        // 先将节点从这个位置取下来,然后放入头节点之后(第一个位置)
        doubleNode.moveToHead(node);

        return node.value;  // 返回当前使用的节点的内容
    }

    // 拿了一个新节点,插入到链表中。
    public void put(int key, int value){
        Node newNode = new Node(key, value);

        if(map.containsKey(key)){
            // 更改新的节点内容(节点值)
            newNode.value = value;
            // 插入第一个位置
            doubleNode.moveToHead(newNode);
        }else{
            // 先判断map中位置是不是满了
            if(map.size() >= capacity){
                // 如果满了,那么删除掉最后一个节点元素,再插入到头节点。
                // 先从再从链表中消除位置,然会要删除的元素. 再将map中的清除,
                Node deleteEndNode = doubleNode.removeEnd();
                map.remove(deleteEndNode);
            }

            // 插入第一个位置
            doubleNode.addToHead(newNode);     // 是新的节点插入

            // 这里插入的位置是最后一个。但是判断位置的时候,是判断链表的位置,并不是map的位置。
            // 所以map的位置和链表的位置并不是相符的。
            map.put(key,newNode);
        }

    }


  static class Node{
      int key,value;
      Node pre,next;

      Node(int key, int value){
          this.key = key;
          this.value = value;
      }
  }
  
  static class DoubleNode{
      Node head;
      Node end;

      DoubleNode(){
          // 初始化头、尾节点
          head = new Node(0, 0);
          end = new Node(0, 0);

          head.next = end;
          end.pre = head;
      }

      public void moveToHead(Node node){
          // 插入元素到表头
          remove(node);   // 先删除元素
          addToHead(node); // 插入表头
      }

      public Node removeEnd(){
          // 删除表尾元素,返回删除的元素
          Node last = end.pre;
          if(last == head) return null;       // 无元素
          remove(last);
          return last;

      }

      public void addToHead(Node node){
          // 插入表头
          node.pre = head;
          node.next = head.next;
          head.next.pre = node;
          head.next = node;
      }

      public void remove(Node node){
          node.pre.next = node.next;
          node.next.pre = node.pre;
      }

  }

}

Kmp算法(两个字符串包含匹配)

【解题思路】KMP 算法的核心原理

暴力字符串匹配的问题:

当 haystack[i] ≠ needle[j] 时,只能将 i 回退到 i+1,从头开始匹配,有大量重复比较。

KMP的核心优化:

利用模式串自身的结构特征,当匹配失败时,避免回退 i,而是让 j 回退到合适的位置,跳过无效比较。

【代码】

public class KMPMatch {
    public static int strStr(String haystack, String needle) {
        if (needle.isEmpty()) return 0;
        int[] next = new int[needle.length()];
        buildNext(needle, next);  // 正确构造 next 数组
        int j = 0;

        for (int i = 0; i < haystack.length(); i++) {
            while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
                j = next[j - 1];
            }
            if (haystack.charAt(i) == needle.charAt(j)) {
                j++;
            }
            if (j == needle.length()) {
                return i - j + 1;   // 匹配成功,返回第一个元素的下标
            }
        }

        return -1;  // 未匹配成功
    }

    public static void buildNext(String pattern, int[] next) {
        int j = 0;
        next[0] = 0;
        for (int i = 1; i < pattern.length(); i++) {
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j)) {
                j = next[j - 1];  // 回退
            }
            if (pattern.charAt(i) == pattern.charAt(j)) {
                j++;
            }
            next[i] = j;
        }
    }

    public static void main(String[] args) {
        String haystack = "aabaabaaf", needle = "aabaaf";
        int i = strStr(haystack, needle);
        System.out.println(i);  // 应该输出 3
    }
}

滑动窗口最大值(Sliding Window Maximum)

【题目描述】

  • 给定一个整数数组 nums 和一个正整数 k(窗口大小),该算法使用 单调队列(Monotonic Queue) 高效计算所有长度为 k 的滑动窗口中的最大值,并返回这些最大值的数组。

  • 关键思想

    1. 单调递减队列:维护一个双端队列(Deque),存储数组元素的索引,并保证队列中的元素按从大到小排列。
    2. 窗口范围管理:在遍历数组时,移除超出当前窗口范围的元素(队头)和比当前元素小的元素(队尾)。
    3. 记录最大值:每个窗口的最大值始终位于队头。
  • 时间复杂度

    • O(n),每个元素最多入队和出队一次。
  • 空间复杂度

    • O(k),双端队列最多存储 k 个元素。
  • 输入输出示例:

    // 示例1:
    nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3  // 输入
    
    [3, 3, 5, 5, 6, 7]      // 输出
    
    // 示例2:
    nums = [9, 8, 7, 6, 5], k = 2      // 输入
    [9, 8, 7, 6]      // 输出
    
    

【代码】

  • public static void main(String[] args) {
        int[] nums = {1, 15, 2, 13, 23, 2};
        int k = 3;
        int[] ints = maxSlidingWindow(nums, k);
        for (int anInt : ints) {
            System.out.println(anInt);
        }
    	// 输出:15,15,23,23
    }
    
    public static int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0) return new int[0];
    
        int n = nums.length;
        int[] result = new int[n - k + 1]; // 最终结果
        Deque<Integer> deque = new ArrayDeque<>(); // 存下标
    
        for (int i = 0; i < n; i++) { // 4 23
            // 1️⃣ 删除窗口外的下标
            // 保持窗口内的元素最多只能有k条
            if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {  // -3+1
                deque.pollFirst(); // 从头删
            }
    
            // 2️⃣ 删除比当前值小的所有下标(保持队列单调递减)
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast(); // 从尾删
            }
    
            // 3️⃣ 加入当前下标
            deque.offerLast(i);  // 1,  3
    
            // 4️⃣ 只要窗口长度够了,队头就是最大值
            if (i >= k - 1) {   // k - 1 = 2
                result[i - k + 1] = nums[deque.peekFirst()];
                // result[0-1]   1-15
            }
        }
    
        return result;
    }
    

给定一个 n × n 的二维矩阵 matrix 表示一个图像

【题目描述】

  • 给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

  • 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转

【解题思路】

  • 转置矩阵:将矩阵的行和列互换,即matrix[i][j]matrix[j][i]交换。

  • 翻转每一行:将每一行的元素进行反转,即 matrix[i][j]matrix[i][n-1-j]交换。
    关键变量:

【代码】

  • public static void rotateT(int[][] matrix) {
            int n = matrix.length;
            // 除了中间斜着的一排,两边元素分别调换(将矩阵的行和列互换)
            for (int i = 0; i < n; i++) {
                for (int j = i; j < n; j++) {
                    int temp = matrix[j][i];
                    matrix[j][i] = matrix[i][j];
                    matrix[i][j] = temp;
                }
            }
            for(int i = 0; i < n; i++){
                // 每一行反转
                int left = 0, right = n - 1;
                while (left <= right){
                    int temp = matrix[i][left];
                    matrix[i][left] = matrix[i][right];
                    matrix[i][right] = temp;
                    left++;
                    right--;
                }
            }
        }
    

【代码解释】

  1. 转置矩阵:

    • 使用双重循环遍历矩阵,交换 matrix[i][j] 和 matrix[j][i]。

    • 注意内层循环从 i 开始,避免重复交换已经处理过的元素。

  2. 翻转每一行:

    • 对每一行进行遍历,交换 matrix[i][j] 和 matrix[i][n-1-j]。

    • 只需遍历到行的中间位置即可完成翻转。

中缀转后缀表达式(逆波兰表达式)(Infix to Postfix Expression)

【题目描述】

  • 给定一个字符串形式的中缀表达式,包含数字、字母、运算符 +, -, *,/以及括号(),请将其转换为对应的后缀表达式(也称逆波兰表达式,Reverse Polish Notation, RPN)。

举例

中缀表达式后缀表达式
"a+b*c""abc*+"
"a+b*(c-d)-e" "abcd-*+e-"

【解题思路】

  1. 初始化一个空的输出缓冲区(用于存储后缀表达式)。
  2. 初始化一个操作符栈。
  3. 从左到右遍历输入表达式的每一个字符:
    • 如果是操作数(数字或字母) :直接加入输出缓冲区。
    • 如果是左括号 ‘(’ :压入操作符栈。
    • 如果是右括号 ‘)’ :
      • 弹出操作符栈中的元素并加入输出缓冲区,直到遇到左括号为止
      • 左括号只弹出不加入输出。
    • 如果是运算符(+、-、*、/) :
      • 将栈顶优先级大于等于当前运算符的运算符依次弹出加入输出,直到栈顶优先级低于当前运算符或栈为空
      • 当前运算符压入栈。
    • 表达式遍历结束后,将操作符栈中剩余所有运算符依次弹出并加入输出缓冲区。

【代码】

  • public static boolean isOperation(char ch) {
       return ch == '+' || ch == '-' || ch == '*' || ch == '/';
    }
    
    public static int getPrecedence(char op) {
       switch (op) {
           case '*':
           case '/':
               return 2;
           case '+':
           case '-':
               return 1;
           default:
               return 0;
       }
    }
    
    // main
    public static String calculate(String s) {
       StringBuffer buffer = new StringBuffer();
       ArrayDeque<Character> stack = new ArrayDeque<>();
    
       for (int i = 0; i < s.length(); i++) {
           char ch = s.charAt(i);
    
           if (ch == ' ') continue;
    
           if (Character.isLetterOrDigit(ch)) {
               buffer.append(ch);
           } else if (ch == '(') {
               stack.push(ch);
           } else if (ch == ')') {
               while (!stack.isEmpty() && stack.peek() != '(') {
                   buffer.append(stack.pop());
               }
               stack.pop(); // 弹出 '('
           } else if (isOperation(ch)) {
               while (!stack.isEmpty() && getPrecedence(stack.peek()) >= getPrecedence(ch)) {
                   buffer.append(stack.pop());
               }
               stack.push(ch);
           }
       }
    
       while (!stack.isEmpty()) {
           buffer.append(stack.pop());
       }
    
       return buffer.toString();
    }
    

逆波兰表达式求值(Evaluate Reverse Polish Notation)

[题目描述]

给定一个字符串形式的后缀表达式 (也称为逆波兰表达式 ),请计算其对应的数值结果。该表达式由数字和运算符 +, -, *, / 组成,使用空格分隔每个元素。

示例

输入: ["2", "1", "+", "3", "*"]
输出: 9
解释: (2 + 1) * 3 = 9

输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: 4 + (13 / 5) = 4 + 2 = 6

输入: ["10", "6", "9", "3", "+", "-11", "*", "/", "-"]
输出: 17

[解题思路]

  • 后缀表达式的特点是:

    • 每个运算符作用于它前面最近的两个操作数。
    • 因此我们可以用栈来保存操作数,遇到运算符时弹出两个数进行计算,再将结果压入栈中。
  • 算法步骤如下:

    1. 初始化一个空栈 stack。
    2. 遍历表达式的每一个元素:
    3. 如果当前元素是数字,则将其转换为整数并压入栈中。
    4. 如果当前元素是运算符:
      • 弹出栈顶的两个元素作为操作数。
      • 注意顺序:先弹出的是第二个操作数 ,后弹出的是第一个操作数
      • 根据运算符执行相应的加减乘除操作。
      • 将计算结果重新压入栈中。
      • 表达式遍历结束后,栈中应只剩下一个元素,即最终结果。
    	public static int evaluatePostfix(String expr) {
            ArrayDeque<Integer> stack = new ArrayDeque<>();	
    
            for (int i = 0; i < expr.length(); i++) {
                char ch = expr.charAt(i);
    
                if (ch == ' ') continue; // 跳过空格
    
                if (Character.isDigit(ch)) {
                    // 如果是数字,压栈
                    stack.push(Integer.parseInt(String.valueOf(ch)));
                } else {
                    // 如果是运算符
                    if (stack.size() < 2) {
                        throw new IllegalArgumentException("表达式不合法:操作数不足");
                    }
    
                    int second = stack.pop();
                    int first = stack.pop();
                    int result;
    
                    switch (ch) {
                        case '+':
                            result = first + second;
                            break;
                        case '-':
                            result = first - second;
                            break;
                        case '*':
                            result = first * second;
                            break;
                        case '/':
                            if (second == 0) {
                                throw new ArithmeticException("除数不能为零");
                            }
                            result = first / second;
                            break;
                        default:
                            throw new IllegalArgumentException("不支持的运算符: " + ch);
                    }
    
                    stack.push(result);
                }
            }
    
            if (stack.size() != 1) {
                throw new IllegalArgumentException("表达式不合法");
            }
    
            return stack.pop();
        }
    

弗洛伊德判圈算法(Floyd’s Cycle-Finding Algorithm)

【算法描述】

  • 这是由计算机科学家 Robert W. Floyd 提出的一种用于检测循环结构的算法,特别适用于:

    • 链表中是否存在环
    • 检测伪随机数生成器中的循环
    • 数学中的周期函数问题等
  • 在链表问题中,它通过使用两个不同速度的指针(一个一次走一步,一个一次走两步),来判断是否进入了一个循环。

【解题思路】(链表中是否存在环)

  1. 定义两个指针:
    • slow:每次移动 1 步
    • fast:每次移动 2 步
  2. 如果链表中存在环,这两个指针终将相遇
  3. 如果链表没有环,那么 fast 指针最终会遇到 null,结束遍历
  • 为什么你一开始觉得“从同一位置出发不会相遇”?
    • 这是因为在你脑海中,可能想象的是一个非环结构 ,比如直线跑步比赛:
      - 慢人:A → B → C → D…
      - 快人:A → C → E…
    • 在这种情况下,快人确实会越跑越远,永远不会追上慢人。
    • 但这是在没有环 的情况下才成立的逻辑

【代码】

  • public static class ListNode{
        private ListNode next;
        private int val;
    
        public ListNode(int x){
            this.val = x;
            this.next = null;
        }
    }
    
    public static boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }
    
        ListNode slow = head;
        ListNode fast = head.next;
    	
    	// 注意,这里以快的指针来判断是否到了终点
        while (fast != null && fast.next != null) {
            if (slow == fast) {
                return true; // 相遇,说明有环
            }
            slow = slow.next;
            fast = fast.next.next;  // 每次走两步
        }
    
        return false;
    }
    
    

查找算法

二分查找(有序)

  • public static void main(String[] args) {
        // 二分查找示例
        int[] arr = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
    
        int left = 0, right = arr.length - 1;
        int target = 11;
        int mid = 0;
        boolean found = false;
        while (left <= right) {
            mid = left + (right - left) / 2;
            if(arr[mid] < target) {
                left = mid + 1;
            }else if(arr[mid]  > target) {
                right = mid - 1;
            }
            else {
                System.out.println("找到了目标元素"+ mid);
                found = true;
                break;
            }
        }
        if (!found) {
            System.out.println("未找到目标元素");
        }
    }
    

排序算法

冒泡排序(稳定)O(n²)

原理

  • 冒泡排序通过重复地遍历要排序的列表,比较相邻元素并交换它们的位置来完成排序。每一轮遍历将最大的元素"冒泡"到列表的末尾。

复杂度

  • 时间复杂度:

    • 最好情况:O(n) (已经有序)

    • 平均和最坏情况:O(n²)

  • 空间复杂度:O(1) (原地排序)

    public static void main(String[] args) {
        int[] data = {64, 34, 25, 12, 22, 11, 90};
        bubbleSort(data);
        for (int datum : data) {
            System.out.printf(datum + ",");
        }
    }
    public static void bubbleSort(int[] arr) {
        int left = 0, right = arr.length - 1;
        int temp = 0;
        int j = 0;
        for(int i = 0; i < arr.length; i++){
        // 这里要减1,因为每次都有一个元素已经排好序。
            for(j = 1; j < arr.length - i; j++){
                if(arr[j] < arr[j - 1]){
                    temp = arr[j];
                    arr[j] = arr[j - 1];
                    arr[j - 1] = temp;
                }
            }
        }
    }
    

选择排序(不稳定)O(n²)

原理

  • 选择排序每次从未排序部分选择最小(或最大)的元素,放到已排序部分的末尾。

复杂度

  • 时间复杂度:O(n²) (所有情况)
  • 空间复杂度:O(1)
    public static void bubbleSort(int[] arr) {
        int temp = 0;
        int minIndex = 0;
        for (int i = 0; i < arr.length; i++) {
            // 这里每次要重置,取最后一个元素。
            minIndex = i;
            for(int j = i + 1; j < arr.length; j++){
                if(arr[minIndex] > arr[j]){
                    minIndex = j;
                }
            }
            temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }
    

java相关知识

反射和注解

通过注解 + 反射来模拟 @Controller + @RequestMapping

✅ 第一步:定义注解

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyController {
    String value() default "";
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRequestMapping {
    String path();
}

✅ 第二步:创建控制器类

@MyController("userController")
public class UserController {

    @MyRequestMapping(path = "/hello")
    public void sayHello() {
        System.out.println("Hello from UserController!");
    }

    @MyRequestMapping(path = "/bye")
    public void sayBye() {
        System.out.println("Goodbye from UserController!");
    }
}

✅ 第三步:模拟框架容器的启动和调用(主程序)

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

public class MiniMvcFramework {

    // 模拟路由映射表
    static Map<String, Method> routeMap = new HashMap<>();
    static Map<String, Object> instanceMap = new HashMap<>();

    public static void main(String[] args) throws Exception {
        // 模拟扫描到的类
        Class<?> clazz = UserController.class;

        // 识别是否是 Controller 类
        if (clazz.isAnnotationPresent(MyController.class)) {
            Object controllerInstance = clazz.getDeclaredConstructor().newInstance();

            instanceMap.put(clazz.getSimpleName(), controllerInstance);

            // 遍历所有方法,收集带注解的方法
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.isAnnotationPresent(MyRequestMapping.class)) {
                    String path = method.getAnnotation(MyRequestMapping.class).path();
                    routeMap.put(path, method);
                }
            }
        }

        // 模拟“请求”
        simulateRequest("/hello");
        simulateRequest("/bye");
        simulateRequest("/notExist");
    }

    // 模拟前端发起的请求
    public static void simulateRequest(String path) throws Exception {
        Method method = routeMap.get(path);
        if (method != null) {
            Object instance = instanceMap.get(method.getDeclaringClass().getSimpleName());
            method.invoke(instance);
        } else {
            System.out.println("404 Not Found: " + path);
        }
    }
}

✅ 输出结果

Hello from UserController!
Goodbye from UserController!
404 Not Found: /notExist

HashMap、HashSet、ArrayList、LinkedList 对比

1. 核心区别对比

特性HashSetHashMapArrayListLinkedList
接口实现SetMapListList + Deque
底层数据结构哈希表(HashMap)数组+链表/红黑树动态数组双向链表
存储元素单个对象键值对单个对象单个对象
顺序特性无序无序保持插入顺序保持插入顺序
允许重复键不可重复✔️✔️
允许null允许1个null允许1个null键允许多个null允许多个null
线程安全不安全不安全不安全不安全

2. 时间复杂度对比

操作HashSetHashMapArrayListLinkedList
添加元素O(1)O(1)O(1)*O(1)
删除元素O(1)O(1)O(n)O(1)
按索引访问不支持不支持O(1)O(n)
查找元素O(1)O(1)O(n)O(n)
内存占用中等中等较低较高

*注:ArrayList添加元素平均O(1),扩容时为O(n)

3. 典型使用场景

  • HashSet:去重操作/集合运算
Set<String> uniqueNames = new HashSet<>();
  • HashMap:键值对存储/快速查找
Map<Integer, String> idToName = new HashMap<>();
  • ArrayList:随机访问频繁/遍历操作多
List<String> highAccessList = new ArrayList<>();
  • LinkedList:频繁插入删除/实现队列/栈
Deque<String> queue = new LinkedList<>();
  1. 如何选择?
    • 需要唯一性 → HashSet

    • 需要键值映射 → HashMap

    • 读多写少随机访问 → ArrayList

    • 频繁增删首尾元素 → LinkedList

    • 需要排序 → TreeSet/TreeMap

    • 需要保持插入顺序 → LinkedHashSet/LinkedHashMap

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值