基础常见题目集(可面试用)
二叉树
二叉树先序遍历
【数据格式]
-
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 的那条边的起始节点。这种搜索方式使用递归或栈
来实现。
-
深度优先搜索(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); // 回溯 }
树形结构数据的构建与递归打印
[题目描述]
给定一组具有父子关系的节点数据,要求:
- 构建树形结构:将这些节点按照父子关系组织成一个树形结构(或多棵树森林结构)
- 递归打印树形结构:以缩进格式打印树的层次结构,子节点需要比父节点缩进一级
[输入数据格式]
每个节点包含三个属性:
id
:节点唯一标识(整数)parentId
:父节点ID(根节点的parentId
为0
)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)
-
题目
- 请你设计并实现一个最近最少使用(LRU)缓存机制。
- 它应该支持以下操作:get(key) 和 put(key, value)。
- 当缓存容量达到上限时,它应该删除最近最少使用的那个元素。
-
解释说明:
- 用 HashMap + 双向链表 实现:
- HashMap 用于 O(1) 时间定位 key;
- 双向链表维护访问顺序,最近访问放头部,最久未访问在尾部;
- 每次 get() 和 put() 都把对应节点移到链表头部;
- 超过容量时,移除尾部元素(最久未使用)。
- 用 HashMap + 双向链表 实现:
-
通俗易懂类比:
- 想象你是图书管理员:
- 图书馆最多放 5 本书;
- 你会把最近看的书放在最前面;
- 如果空间满了,你就把最久没看的书扔掉,腾出位置。
- 想象你是图书管理员:
-
面试答题模板(背下来):
“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 回退到合适的位置,跳过无效比较。
- 👉 核心工具:next[] 数组
- next[i] 表示:模式串 needle[0…i] 中,前缀和后缀相等的最大长度。
- 主串匹配部分其实并不复杂,关键在于 j = next[j - 1] 的回退策略。
- 讲解KMP算法(理论视频)
- 求next数组代码篇
【代码】
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 的滑动窗口中的最大值,并返回这些最大值的数组。
-
关键思想
- 单调递减队列:维护一个双端队列(Deque),存储数组元素的索引,并保证队列中的元素按从大到小排列。
- 窗口范围管理:在遍历数组时,移除超出当前窗口范围的元素(队头)和比当前元素小的元素(队尾)。
- 记录最大值:每个窗口的最大值始终位于队头。
-
时间复杂度
- 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--; } } }
【代码解释】
-
转置矩阵:
-
使用双重循环遍历矩阵,交换 matrix[i][j] 和 matrix[j][i]。
-
注意内层循环从 i 开始,避免重复交换已经处理过的元素。
-
-
翻转每一行:
-
对每一行进行遍历,交换 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-" |
【解题思路】
- 初始化一个空的输出缓冲区(用于存储后缀表达式)。
- 初始化一个操作符栈。
- 从左到右遍历输入表达式的每一个字符:
- 如果是操作数(数字或字母) :直接加入输出缓冲区。
- 如果是左括号 ‘(’ :压入操作符栈。
- 如果是右括号 ‘)’ :
- 弹出操作符栈中的元素并加入输出缓冲区,直到遇到左括号为止。
- 左括号只弹出不加入输出。
- 如果是运算符(+、-、*、/) :
- 将栈顶优先级大于等于当前运算符的运算符依次弹出加入输出,直到栈顶优先级低于当前运算符或栈为空。
- 当前运算符压入栈。
- 表达式遍历结束后,将操作符栈中剩余所有运算符依次弹出并加入输出缓冲区。
【代码】
-
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
[解题思路]
-
后缀表达式的特点是:
- 每个运算符作用于它前面最近的两个操作数。
- 因此我们可以用栈来保存操作数,遇到运算符时弹出两个数进行计算,再将结果压入栈中。
-
算法步骤如下:
- 初始化一个空栈 stack。
- 遍历表达式的每一个元素:
- 如果当前元素是数字,则将其转换为整数并压入栈中。
- 如果当前元素是运算符:
- 弹出栈顶的两个元素作为操作数。
- 注意顺序:
先弹出的是第二个操作数 ,后弹出的是第一个操作数
。 - 根据运算符执行相应的加减乘除操作。
- 将计算结果重新压入栈中。
- 表达式遍历结束后,栈中应只剩下一个元素,即最终结果。
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 提出的一种用于检测循环结构的算法,特别适用于:
- 链表中是否存在环
- 检测伪随机数生成器中的循环
- 数学中的周期函数问题等
-
在链表问题中,
它通过使用两个不同速度的指针(一个一次走一步,一个一次走两步)
,来判断是否进入了一个循环。
【解题思路】(链表中是否存在环)
- 定义两个指针:
- slow:每次移动 1 步
- fast:每次移动 2 步
- 如果链表中存在环,这两个指针终将相遇
- 如果链表没有环,那么 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. 核心区别对比
特性 | HashSet | HashMap | ArrayList | LinkedList |
---|---|---|---|---|
接口实现 | Set | Map | List | List + Deque |
底层数据结构 | 哈希表(HashMap) | 数组+链表/红黑树 | 动态数组 | 双向链表 |
存储元素 | 单个对象 | 键值对 | 单个对象 | 单个对象 |
顺序特性 | 无序 | 无序 | 保持插入顺序 | 保持插入顺序 |
允许重复 | ❌ | 键不可重复 | ✔️ | ✔️ |
允许null | 允许1个null | 允许1个null键 | 允许多个null | 允许多个null |
线程安全 | 不安全 | 不安全 | 不安全 | 不安全 |
2. 时间复杂度对比
操作 | HashSet | HashMap | ArrayList | LinkedList |
---|---|---|---|---|
添加元素 | 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<>();
如何选择?
-
需要唯一性 → HashSet
-
需要键值映射 → HashMap
-
读多写少随机访问 → ArrayList
-
频繁增删首尾元素 → LinkedList
-
需要排序 → TreeSet/TreeMap
-
需要保持插入顺序 → LinkedHashSet/LinkedHashMap
-