二叉树
1. 遍历 (Traversal)
-
前序遍历 (Preorder Traversal): 先访问根节点,再访问左子树,最后访问右子树。
void preorderTraversal(Node root) { if (root == null) return; System.out.print(root.value + " "); preorderTraversal(root.left); preorderTraversal(root.right); }
-
中序遍历 (Inorder Traversal): 先访问左子树,再访问根节点,最后访问右子树。
void inorderTraversal(Node root) { if (root == null) return; inorderTraversal(root.left); System.out.print(root.value + " "); inorderTraversal(root.right); }
-
后序遍历 (Postorder Traversal): 先访问左子树,再访问右子树,最后访问根节点。
void postorderTraversal(Node root) { if (root == null) return; postorderTraversal(root.left); postorderTraversal(root.right); System.out.print(root.value + " "); }
-
层序遍历 (Level Order Traversal): 按层级顺序从上到下、从左到右访问节点,通常使用队列实现。
void levelOrderTraversal(Node root) { if (root == null) return; Queue<Node> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) { Node current = queue.poll(); System.out.print(current.value + " "); if (current.left != null) queue.add(current.left); if (current.right != null) queue.add(current.right); } }
2. 插入节点 (Insertion)
- 向二叉树中插入节点的操作,通常涉及递归地找到合适的空位置,然后将新节点插入其中。
Node insert(Node root, int value) { if (root == null) { return new Node(value); } if (value < root.value) { root.left = insert(root.left, value); } else { root.right = insert(root.right, value); } return root; }
3. 查找节点 (Search)
- 在二叉树中查找一个特定值,通常也使用递归或迭代的方法。
boolean search(Node root, int value) { if (root == null) { return false; } if (root.value == value) { return true; } else if (value < root.value) { return search(root.left, value); } else { return search(root.right, value); } }
4. 删除节点 (Deletion)
- 删除一个节点后,可能需要调整二叉树以保持其性质。删除操作通常要处理三种情况:
- 删除的节点没有子节点。
- 删除的节点有一个子节点。
- 删除的节点有两个子节点(通常需要找到右子树中的最小值节点替代)。
Node delete(Node root, int value) { if (root == null) return null; if (value < root.value) { root.left = delete(root.left, value); } else if (value > root.value) { root.right = delete(root.right, value); } else { if (root.left == null) return root.right; if (root.right == null) return root.left; Node minNode = findMin(root.right); root.value = minNode.value; root.right = delete(root.right, minNode.value); } return root; } Node findMin(Node root) { while (root.left != null) { root = root.left; } return root; }
5. 计算高度 (Height Calculation)
- 计算二叉树的高度,即从根节点到最远叶子节点的最长路径的节点数。 java
复制代码
int height(Node root) { if (root == null) return 0; int leftHeight = height(root.left); int rightHeight = height(root.right); return Math.max(leftHeight, rightHeight) + 1; }
6. 判断是否是二叉搜索树 (Check if a Tree is a Binary Search Tree)
例子:
假设你有一个树形结构,level
表示当前节点的层级:
markdown
复制代码
NodeValue
在这个例子中," ".repeat(4)
生成了 4 个空格,然后与 "NodeValue"
连接,最终输出了带有 4 个空格缩进的节点值。
java
复制代码
int level = 2; System.out.println(" ".repeat(level * 2) + "NodeValue");
输出将是:
- 验证一棵树是否符合二叉搜索树的性质(即对于每个节点,其左子树的所有节点值都小于该节点值,右子树的所有节点值都大于该节点值)。
-
boolean isBST(Node root, Integer min, Integer max) { if (root == null) return true; if (min != null && root.value <= min) return false; if (max != null && root.value >= max) return false; return isBST(root.left, min, root.value) && isBST(root.right, root.value, max); }
import java.util.Stack; class Node { int value; Node left, right; Node(int value) { this.value = value; left = right = null; } } public class BinaryTree { // 检查二叉树是否为 BST 的中序遍历方法 boolean isBST(Node root) { Stack<Node> stack = new Stack<>(); Node current = root; Integer prev = null; while (current != null || !stack.isEmpty()) { // 走到左子树的最下方 while (current != null) { stack.push(current); current = current.left; } current = stack.pop(); // 如果当前节点值小于等于前一个节点值,则不满足 BST 条件 if (prev != null && current.value <= prev) { return false; } // 更新 prev 为当前节点值 prev = current.value; // 处理右子树 current = current.right; } return true; } public static void main(String[] args) { BinaryTree tree = new BinaryTree(); Node root = new Node(10); root.left = new Node(5); root.right = new Node(20); root.left.left = new Node(3); root.left.right = new Node(7); if (tree.isBST(root)) { System.out.println("这棵树是二叉搜索树"); } else { System.out.println("这棵树不是二叉搜索树"); } } }
在 Java 中,
" ".repeat(level * 2)
是用来生成一个包含多个空格的字符串。这个方法的作用是重复特定数量的空格,并在输出中形成缩进效果。下面是详细解释:详细解释:
-
" "
:- 这是一个包含单个空格字符的字符串。
-
repeat(int count)
:repeat(int count)
是String
类的一个方法,它将当前字符串重复count
次,并返回一个新的字符串。- Java 11 及以上版本引入了这个方法。
- 如果
count
是 0,则返回一个空字符串。
-
level * 2
:level
是一个整数,表示树的当前层次(或深度)。level * 2
表示将level
乘以 2,得到重复空格的次数。- 这个倍数(2)可以调整,决定了每层缩进的程度。
-
组合:
- 当
level
为 1 时," ".repeat(level * 2)
生成" "
(两个空格)。 - 当
level
为 2 时," ".repeat(level * 2)
生成" "
(四个空格)。 - 依此类推,随着
level
增加,生成的空格数量也增加,这会使打印出来的树结构更清晰地表现出层次。
- 当
- 根节点
level = 0
,没有缩进。 - 第一层子节点
level = 1
,缩进两个空格。 - 第二层子节点
level = 2
,缩进四个空格。
7. 求树中节点的个数 (Count Nodes)
- 计算二叉树中所有节点的数量。
int countNodes(Node root) { if (root == null) return 0; return 1 + countNodes(root.left) + countNodes(root.right); }
8. 求树中叶子节点的个数 (Count Leaf Nodes)
- 计算二叉树中所有叶子节点的数量。
int countLeafNodes(Node root) { if (root == null) return 0; if (root.left == null && root.right == null) return 1; return countLeafNodes(root.left) + countLeafNodes(root.right); }
这些是与二叉树操作相关的一些基本方法,涵盖了遍历、插入、查找、删除、计算高度、验证二叉搜索树、以及统计节点数等常见操作。在实际应用中,可以根据具体需求进行组合和扩展。
目标一:进行中序遍历
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;
}
}
// structure class definition with inorderTraversal method
public class Solution{
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
TreeNode cur = null;
while (root != null) {
if (root.left != null) {
//保证当前节点进入root左侧二叉树,然后一直向右走到尽头
cur = root.left;
while (cur.right != null && cur.right != root) {
cur = cur.right;
}
//让当前指针cur的右指针指向root即记录当前中间的节点
if (cur.right == null) {
cur.right = root;
root = root.left;
}
//左子树遍历完成需要断开链接
else {
list.add(root.val);
cur.right = null;
root = root.right;
}
}
//如果没有左孩子,就访问右孩子
else {
list.add(root.val);
root = root.right;
}
}
return list;
}
public static void main(String[] args) {
// Construct a sample binary tree
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(7);
// Create an instance of Structure
Solution structure = new Solution();
// Perform inorder traversal
List<Integer> result = structure.inorderTraversal(root);
// Print the result
System.out.println("Inorder Traversal: " + result);
}
}
Inorder Traversal: [4, 2, 5, 1, 6, 3, 7]
Process finished with exit code 0
public class Solution{
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
TreeNode cur = null;
Deque<TreeNode> stk = new LinkedList<>();
while (root!=null || !stk.isEmpty()){
while (root!=null){
stk.push(root);
root = root.left;
}
root = stk.pop();
list.add(root.val);
root = root.right;
//这里有一点设计的很巧妙,就是如果在左子树的最后一个节点,由于没有子节点,while循环不会执行,root会重新接受一个新弹出的上级元素并开始寻找右子节点
}
return list;
}
}
先序遍历
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
// 定义树节点
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public class Solution {
// 前序遍历方法
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>(); // 存储遍历结果
if (root == null) {
return res; // 如果根节点为空,直接返回空列表
}
Deque<TreeNode> stack = new LinkedList<>(); // 创建栈来帮助遍历
TreeNode node = root; // 从根节点开始遍历
while (!stack.isEmpty() || node != null) { // 当栈不为空或当前节点不为空时
while (node != null) { // 处理当前节点及其左子树
res.add(node.val); // 访问当前节点,添加到结果列表中
stack.push(node); // 将当前节点压入栈中
node = node.left; // 继续遍历左子树
}
node = stack.pop(); // 从栈中弹出一个节点(左子树遍历完成)
node = node.right; // 继续遍历右子树
}
return res; // 返回最终的前序遍历结果
}
}
1
/ \
2 3
/ \
4 5
- 初始状态:`node = 1`, `stack = []`, `res = []`
- `node = 1`, `stack = [1]`, `res = [1]`
- `node = 2`, `stack = [1, 2]`, `res = [1, 2]`
- `node = 4`, `stack = [1, 2, 4]`, `res = [1, 2, 4]`
- `node = null`, 弹出 `4`, `node = null`
- 弹出 `2`, `node = 5`
- `node = null`, `stack = [1, 5]`, `res = [1, 2, 4, 5]`
- `node = null`, 弹出 `5`, `node = null`
- 弹出 `1`, `node = 3`
- `node = null`, `stack = []`, `res = [1, 2, 4, 5, 3]`
最终结果是 `[1, 2, 4, 5, 3]`。
力扣 3174 是个讨厌的字符串,看看相关方法吧,这里不想放
20, 有效括号的代码:这里主要是学习建立指定值的HashMap
class Solution {
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1)
{
return false;
}
Map<Character,Character> parirs = new HashMap<Character,Character>(){{
put(')','(');
put(']','[');
put('}','{');
}};
Deque<Character> stack = new LinkedList<Character>();
for(int i = 0; i<n ; i++){
char ch = s.charAt(i);
if(parirs.containsKey(ch)){
if(stack.isEmpty() || stack.peek() != parirs.get(ch)){
return false;
}
stack.pop();
}else{
stack.push(ch);
}
}
return stack.isEmpty();
}
}
在Java中,Deque
(双端队列)和Stack
(栈)都可以用于实现栈的功能,但它们在设计、使用和推荐的场景上有所不同。以下是它们的主要区别:
1. 设计意图和历史
- Stack:
Stack
是Java中最早引入的栈类,继承自Vector
。它是一个同步的类,也就是说,它在多线程环境中是线程安全的。但是,由于Stack
继承了Vector
,它带有Vector
的一些不必要的方法,比如insertElementAt()
,这与栈的基本操作无关。这使得Stack
被认为是一种设计不良的选择。 - Deque:
Deque
接口是在Java 1.6中引入的,代表了一个双端队列,可以用于栈的实现。Deque
接口比Stack
更灵活,可以作为栈(LIFO)或队列(FIFO)使用。常见的实现类如ArrayDeque
和LinkedList
。
2. 推荐使用
- Stack:由于
Stack
类继承自Vector
,它的同步性带来了一些不必要的开销。在现代Java编程中,Stack
通常被认为是过时的,不推荐在新代码中使用。 - Deque:
Deque
接口是实现栈功能的推荐选择。尤其是ArrayDeque
,它比Stack
性能更高,也没有同步开销。在单线程环境中使用Deque
的实现类来实现栈功能是更好的选择。
3. 操作方法
- Stack:
push(E item)
:将元素压入栈顶。pop()
:移除并返回栈顶的元素。peek()
:查看栈顶的元素,但不移除。
- Deque:
push(E item)
:将元素压入栈顶。pop()
:移除并返回栈顶的元素。peek()
:查看栈顶的元素,但不移除。addFirst(E e)
/removeFirst()
:也可以用这些方法来模拟栈操作。
虽然两者的方法名称略有不同,但Deque
的实现类(如ArrayDeque
)提供的功能与Stack
基本一致,并且在性能和设计上更优。
4. 性能
- Stack:由于是同步的,性能可能会比非同步的
Deque
慢,尤其是在不需要线程安全的情况下。 - Deque:通常情况下,
ArrayDeque
的性能要优于Stack
,因为它是非同步的,并且没有Vector
所带来的额外开销。
结论:
在现代Java开发中,使用Deque
(特别是ArrayDeque
)来实现栈的功能是推荐的做法,而不是使用过时的Stack
类。
Map<Character,Character> parirs = new HashMap<Character,Character>(){{
put(')','(');
put(']','[');
put('}','{');
}};这里的put方法里面内容可以写成这样吗put('(',')');
在你给出的代码中,put(')', '(');
的意思是将右括号 )
作为键,左括号 (
作为值存储在 Map
中。这个 Map
的用法是将闭合括号(右括号)映射到对应的开括号(左括号),通常用来验证字符串中括号是否匹配。
如果你写成 put('(', ')');
,则会将左括号 (
作为键,右括号 )
作为值,这样的映射关系就反过来了。
区别在于使用场景:
-
put(')', '(');
:- 适用于你想通过右括号查找对应的左括号的场景。
- 例如,用于检查括号匹配时,弹出栈顶元素并对比当前的右括号,看它是否与预期的左括号匹配。
-
put('(', ')');
:- 适用于你想通过左括号查找对应的右括号的场景。
- 例如,在某些括号匹配算法中,你可能需要根据当前左括号找到预期的右括号。
总结:
你可以写成 put('(', ')');
,但这取决于你要实现的逻辑。如果你想通过左括号来查找右括号,这样写是合理的;如果你想通过右括号来查找左括号,那就应该用 put(')', '(');
。
队列相关方法与函数
在算法题中,队列是一种非常常用的数据结构,主要用于广度优先搜索(BFS)、滑动窗口、任务调度等场景。以下是一些常用的队列相关方法和它们的典型应用:
1. 基本操作
offer(E e)
/add(E e)
: 将元素添加到队列尾部。offer()
在添加失败时返回false
,而add()
在失败时会抛出异常。- 用途:将新元素加入到处理队列中。
poll()
: 移除并返回队列头部的元素,如果队列为空,则返回null
。- 用途:处理或访问队列头部元素,同时将其从队列中移除。
remove()
: 移除并返回队列头部的元素,如果队列为空,则抛出异常。- 用途:和
poll()
类似,但在队列为空时会抛出异常。
- 用途:和
peek()
: 查看队列头部的元素但不移除它,如果队列为空,则返回null
。- 用途:查看即将处理的元素,而不移除它。
element()
: 查看队列头部的元素但不移除它,如果队列为空,则抛出异常。- 用途:和
peek()
类似,但在队列为空时会抛出异常。
- 用途:和
2. 双端队列操作
addFirst(E e)
: 在队列的头部添加元素。addLast(E e)
: 在队列的尾部添加元素(等同于add(E e)
)。removeFirst()
: 移除并返回队列头部的元素。removeLast()
: 移除并返回队列尾部的元素。peekFirst()
: 查看但不移除队列头部的元素。peekLast()
: 查看但不移除队列尾部的元素。pollFirst()
: 移除并返回队列头部的元素,如果队列为空,则返回null
。pollLast()
: 移除并返回队列尾部的元素,如果队列为空,则返回null
。
双端队列(Deque
)的这些操作在处理滑动窗口问题时非常有用,例如求最大值、最小值等。
3. 常见应用场景
-
广度优先搜索(BFS):
- 使用队列来实现层次遍历或最短路径搜索。
- 常用方法:
offer(E e)
,poll()
,peek()
。
-
滑动窗口问题:
- 例如,滑动窗口最大值问题。
- 常用方法:
addLast(E e)
,pollFirst()
,peekFirst()
,配合双端队列Deque
使用。
-
循环队列:
- 用来处理循环任务,模拟环形结构。
- 常用方法:
offer(E e)
,poll()
,peek()
。
-
任务调度:
- 使用队列管理任务执行的顺序,尤其是在处理依赖关系或资源调度问题时。
- 常用方法:
offer(E e)
,poll()
,peek()
。
4. 具体实现类
LinkedList
: 实现了Queue
和Deque
接口,适合需要频繁插入和删除操作的场景。ArrayDeque
: 比LinkedList
更高效的实现,特别适合用于栈和队列操作。PriorityQueue
: 基于优先级的队列,通常用于贪心算法、Dijkstra 算法等场景。
5. 线程安全的队列
ConcurrentLinkedQueue
: 适用于多线程环境的无界非阻塞队列。BlockingQueue
: 提供阻塞操作的队列接口,适用于生产者-消费者模式。
常用的实现:
ArrayBlockingQueue
LinkedBlockingQueue
PriorityBlockingQueue
DelayQueue
这些方法和场景涵盖了大多数常见的算法题中的队列使用情况。了解这些操作的具体应用有助于更好地解题和优化代码。
字符串相关
Java 中的 String
类是不可变的(即一旦创建,内容无法改变),并且是最常用的类之一。以下是 String
类的一些常用方法及其典型用途:
1. 字符串创建与初始化
String s = "Hello";
: 直接赋值创建字符串。String s = new String("Hello");
: 使用构造函数创建字符串。
2. 字符串长度
length()
: 返回字符串的长度。 java复制代码
String s = "Hello"; int len = s.length(); // len = 5
3. 字符串比较
equals(Object obj)
: 比较字符串的内容是否相等(区分大小写)。s1.equals(s2);
equalsIgnoreCase(String anotherString)
: 比较字符串的内容是否相等(不区分大小写)。s1.equalsIgnoreCase(s2);
compareTo(String anotherString)
: 按字典顺序比较两个字符串。s1.compareTo(s2); // 返回负数、零或正数
compareToIgnoreCase(String str)
: 按字典顺序比较两个字符串(不区分大小写)。s1.compareToIgnoreCase(s2);
4. 查找与定位
charAt(int index)
: 返回指定索引处的字符。char c = s.charAt(1); // c = 'e'
indexOf(String str)
: 查找子字符串首次出现的位置。int index = s.indexOf("lo"); // index = 3
lastIndexOf(String str)
: 查找子字符串最后一次出现的位置。int index = s.lastIndexOf("l"); // index = 3
contains(CharSequence s)
: 判断字符串是否包含某子字符串。boolean contains = s.contains("He"); // contains = true
5. 字符串替换
replace(char oldChar, char newChar)
: 替换所有出现的指定字符。String replaced = s.replace('l', 'p'); // replaced = "Heppo"
replace(CharSequence target, CharSequence replacement)
: 替换所有出现的指定字符串。String replaced = s.replace("He", "We"); // replaced = "Wello"
replaceAll(String regex, String replacement)
: 使用正则表达式替换。String replaced = s.replaceAll("l+", "p"); // replaced = "Hepo"
replaceFirst(String regex, String replacement)
: 使用正则表达式替换首个匹配。String replaced = s.replaceFirst("l+", "p"); // replaced = "Hepo"
6. 字符串分割
split(String regex)
: 按指定正则表达式分割字符串,返回一个数组。String[] parts = s.split("l"); // parts = ["He", "", "o"]
split(String regex, int limit)
: 按指定正则表达式分割字符串,并限制结果数组的长度。String[] parts = s.split("l", 2); // parts = ["He", "lo"]
7. 字符串截取
substring(int beginIndex)
: 返回从指定索引开始到字符串末尾的子字符串。String sub = s.substring(2); // sub = "llo"
substring(int beginIndex, int endIndex)
: 返回从beginIndex
开始到endIndex
(不包括)之间的子字符串。String sub = s.substring(1, 4); // sub = "ell"
8. 字符串转换
toLowerCase()
: 将字符串转换为小写。String lower = s.toLowerCase(); // lower = "hello"
toUpperCase()
: 将字符串转换为大写。String upper = s.toUpperCase(); // upper = "HELLO"
trim()
: 去除字符串首尾的空白字符。String trimmed = s.trim(); // trimmed = "Hello"(如果 s 包含前后空格)
valueOf(int i)
: 将其他数据类型转换为字符串。String str = String.valueOf(123); // str = "123"
9. 字符串拼接
concat(String str)
: 拼接两个字符串。String concatenated = s.concat(" World"); // concatenated = "Hello World"
+
运算符: 直接使用+
拼接字符串。String concatenated = s + " World"; // concatenated = "Hello World"
10. 字符串判断
isEmpty()
: 判断字符串是否为空(长度为0)。boolean empty = s.isEmpty(); // empty = false
isBlank()
: 判断字符串是否为空或仅包含空白字符(Java 11+)。boolean blank = s.isBlank(); // blank = false
startsWith(String prefix)
: 判断字符串是否以指定前缀开头。boolean starts = s.startsWith("He"); // starts = true
endsWith(String suffix)
: 判断字符串是否以指定后缀结尾。boolean ends = s.endsWith("lo"); // ends = true
11. 格式化字符串
String.format(String format, Object... args)
: 返回格式化后的字符串。 java复制代码
String formatted = String.format("Hello %s, your age is %d", "John", 25); // formatted = "Hello John, your age is 25"
12. 字符串内容判断
matches(String regex)
: 判断字符串是否匹配正则表达式。 java复制代码
boolean match = s.matches("[A-Za-z]+"); // match = true
13. 字符数组操作
toCharArray()
: 将字符串转换为字符数组。 java复制代码
char[] chars = s.toCharArray(); // chars = ['H', 'e', 'l', 'l', 'o']
getBytes()
: 将字符串转换为字节数组。 java复制代码
byte[] bytes = s.getBytes(); // 根据平台的默认字符编码转换
-
char
与字符串的区别 -
char
是单个字符:char
类型表示单个字符,例如'A'
、'3'
、'@'
。它只能存储一个字符,而不是一组字符。
-
字符串是多个字符的组合:
- 字符串通常是由多个字符组成的字符序列。字符串可以由多个
char
组成,通常用双引号括起来,例如"Hello"
、"12345"
、"@!$"
。 - 在 C 语言中,字符串通常是由字符数组(例如
char str[] = "Hello";
)或指向字符的指针(例如char *str = "Hello";
)表示。
- 字符串通常是由多个字符组成的字符序列。字符串可以由多个
. - 力扣(LeetCode)
public class Solution {
public int numIslands(char[][] grid) {
// 将 char[][] 转换为 int[][]
int[][] intGrid = new int[grid.length][grid[0].length];
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
intGrid[r][c] = grid[r][c] - '0'; // '1' 转换为 1, '0' 转换为 0
}
}
// 调用原有的 numIslands 方法
return numIslands(intGrid);
}
private int numIslands(int[][] grid) {
int islandCount = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 1) {
dfs(grid, r, c);
islandCount++;
}
}
}
return islandCount;
}
private void dfs(int[][] grid, int r, int c) {
if (!inArea(grid, r, c)) {
return;
}
if (grid[r][c] != 1) {
return;
}
grid[r][c] = 2;
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
private boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
}