树就是一种类似现实生活中的树的数据结构(倒置的树)。任何一颗非空树只有一个根节点。
树的定义:树是⼀种数据结构,它是由n(n≥1)个有限节点组成⼀个具有层次关系的集合。把它叫做“树”是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,⽽叶朝下的。
一棵树具有以下特点:
下图就是一颗树,并且是一颗二叉树。

如上图所示,通过上面这张图说明一下树中的常用概念:
关于树的深度和高度的定义可以看 stackoverflow 上的这个问题:What is the difference between tree depth and height? 。
二叉树的存储主要分为 链式存储 和 顺序存储 两种:
和链表类似,二叉树的链式存储依靠指针将各个节点串联起来,不需要连续的存储空间。
每个节点包括三个属性:
可是 JAVA 没有指针啊!
那就直接引用对象呗(别问我对象哪里找)

顺序存储就是利用数组进行存储,数组中的每一个位置仅存储节点的 data,不存储左右子节点的指针,子节点的索引通过数组下标完成。根结点的序号为 1,对于每个节点 Node,假设它存储在数组中下标为 i 的位置,那么它的左子节点就存储在 2i 的位置,它的右子节点存储在下标为 2i+1 的位置。
一棵完全二叉树的数组顺序存储如下图所示:

大家可以试着填写一下存储如下二叉树的数组,比较一下和完全二叉树的顺序存储有何区别:

可以看到,如果我们要存储的二叉树不是完全二叉树,在数组中就会出现空隙,导致内存利用率降低

二叉树的先序遍历,就是先输出根结点,再遍历左子树,最后遍历右子树,遍历左子树和右子树的时候,同样遵循先序遍历的规则,也就是说,我们可以递归实现先序遍历。
代码如下:
public void preOrder(TreeNode root){
if(root == null){
return;
}
system.out.println(root.data);
preOrder(root.left);
preOrder(root.right);
}

二叉树的中序遍历,就是先递归中序遍历左子树,再输出根结点的值,再递归中序遍历右子树,大家可以想象成一巴掌把树压扁,父结点被拍到了左子节点和右子节点的中间,如下图所示:

代码如下:
public void inOrder(TreeNode root){
if(root == null){
return;
}
inOrder(root.left);
system.out.println(root.data);
inOrder(root.right);
}

二叉树的后序遍历,就是先递归后序遍历左子树,再递归后序遍历右子树,最后输出根结点的值
代码如下:
public void postOrder(TreeNode root){
if(root == null){
return;
}
postOrder(root.left);
postOrder(root.right);
system.out.println(root.data);
}
层序遍历属于迭代遍历,也比较简单

前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
// 前序遍历顺序:中-左-右,入栈顺序:中-右-左
public List preorderTraversal(TreeNode root) {
List result = new ArrayList();
if (root == null){
return result;
}
Stack stack = new Stack();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.right != null){
stack.push(node.right);
}
if (node.left != null){
stack.push(node.left);
}
}
return result;
}
刚刚在进行前序遍历迭代的过程中,其实有两个操作:
前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点,所以刚刚能写出相对简洁的代码
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
public List inorderTraversal(TreeNode root) {
List result = new ArrayList();
if (root == null){
return result;
}
Stack stack = new Stack();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()){
if (cur != null){
stack.push(cur);
cur = cur.left;
}else{
cur = stack.pop();
result.add(cur.val);
cur = cur.right;
}
}
return result;
}
后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了
// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
public List postorderTraversal(TreeNode root) {
List result = new ArrayList();
if (root == null){
return result;
}
Stack stack = new Stack();
stack.push(root);
while (!stack.isEmpty()){
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null){
stack.push(node.left);
}
if (node.right != null){
stack.push(node.right);
}
}
Collections.reverse(result);
return result;
}
二叉树(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。
二叉树 的分支通常被称作“左子树”或“右子树”。并且,二叉树 的分支具有左右次序,不能随意颠倒。
二叉树 的第 i 层至多拥有 2^(i-1) 个节点,深度为 k 的二叉树至多总共有 2^(k+1)-1 个节点(满二叉树的情况),至少有 2^(k) 个节点(关于节点的深度的定义国内争议比较多,我个人比较认可维基百科对节点深度的定义)。

⼆叉树在Java 中表示:
public class TreeLinkNode {
int val;
TreeLinkNode left = null;
TreeLinkNode right = null;
TreeLinkNode next = null;
TreeLinkNode(int val) {
this.val = val;
}
}
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是 满二叉树。也就是说,如果一个二叉树的层数为 K,且结点总数是(2^k) -1 ,则它就是 满二叉树。如下图所示:

除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则这个二叉树就是 完全二叉树 。
大家可以想象为一棵树从根结点开始扩展,扩展完左子节点才能开始扩展右子节点,每扩展完一层,才能继续扩展下一层。如下图所示:

完全二叉树有一个很好的性质:父结点和子节点的序号有着对应关系。
细心的小伙伴可能发现了,当根节点的值为 1 的情况下,若父结点的序号是 i,那么左子节点的序号就是 2i,右子节点的序号是 2i+1。这个性质使得完全二叉树利用数组存储时可以极大地节省空间,以及利用序号找到某个节点的父结点和子节点,后续二叉树的存储会详细介绍。
若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
下面这两棵树都是搜索树

平衡二叉树 是一棵二叉排序树,且具有以下性质:
平衡二叉树的常用实现方法有 红黑树、替罪羊树、加权平衡树、伸展树 等。
在给大家展示平衡二叉树之前,先给大家看一棵树:

你管这玩意儿叫树???
没错,这玩意儿还真叫树,只不过这棵树已经退化为一个链表了,我们管它叫 斜树。
如果这样,那我为啥不直接用链表呢?
谁说不是呢?
二叉树相比于链表,由于父子节点以及兄弟节点之间往往具有某种特殊的关系,这种关系使得我们在树中对数据进行搜索和修改时,相对于链表更加快捷便利。
但是,如果二叉树退化为一个链表了,那么那么树所具有的优秀性质就难以表现出来,效率也会大打折,为了避免这样的情况,我们希望每个做 “家长”(父结点) 的,都 一碗水端平,分给左儿子和分给右儿子的尽可能一样多,相差最多不超过一层,如下图所示:

查找元素
插入元素
删除元素
旋转操作
/**
* AVL树的Java实现
*/
public class AVLTree {
// 树节点定义
class Node {
int key; // 节点值
Node left; // 左子节点
Node right; // 右子节点
int height; // 节点高度
Node(int key) {
this.key = key;
this.height = 1; // 新节点高度初始为1
}
}
Node root; // 根节点
// 获取节点高度,空节点高度为0
private int height(Node node) {
if (node == null) {
return 0;
}
return node.height;
}
// 获取节点的平衡因子
private int getBalanceFactor(Node node) {
if (node == null) {
return 0;
}
return height(node.left) - height(node.right);
}
// 更新节点高度
private void updateHeight(Node node) {
if (node == null) {
return;
}
node.height = Math.max(height(node.left), height(node.right)) + 1;
}
// 右旋转(处理左左情况)
private Node rotateRight(Node y) {
Node x = y.left;
Node T2 = x.right;
// 执行旋转
x.right = y;
y.left = T2;
// 更新高度
updateHeight(y);
updateHeight(x);
return x; // 返回新的根节点
}
// 左旋转(处理右右情况)
private Node rotateLeft(Node x) {
Node y = x.right;
Node T2 = y.left;
// 执行旋转
y.left = x;
x.right = T2;
// 更新高度
updateHeight(x);
updateHeight(y);
return y; // 返回新的根节点
}
// 插入节点
public void insert(int key) {
root = insertNode(root, key);
}
private Node insertNode(Node node, int key) {
// 1. 执行标准BST插入
if (node == null) {
return new Node(key);
}
if (key node.key) {
node.right = insertNode(node.right, key);
} else {
// 相同键值不做处理,或根据需求更新节点
return node;
}
// 2. 更新节点高度
updateHeight(node);
// 3. 获取平衡因子
int balance = getBalanceFactor(node);
// 4. 如果节点失衡,进行旋转调整
// 左左情况 - 右旋
if (balance > 1 && getBalanceFactor(node.left) >= 0) {
return rotateRight(node);
}
// 右右情况 - 左旋
if (balance 1 && getBalanceFactor(node.left) 0) {
node.right = rotateRight(node.right);
return rotateLeft(node);
}
// 返回未变化的节点引用
return node;
}
// 查找最小值节点
private Node findMinNode(Node node) {
Node current = node;
while (current.left != null) {
current = current.left;
}
return current;
}
// 删除节点
public void delete(int key) {
root = deleteNode(root, key);
}
private Node deleteNode(Node root, int key) {
// 1. 执行标准BST删除
if (root == null) {
return root;
}
if (key root.key) {
root.right = deleteNode(root.right, key);
} else {
// 找到要删除的节点
// 情况1:叶子节点或只有一个子节点
if (root.left == null || root.right == null) {
Node temp = (root.left != null) ? root.left : root.right;
// 没有子节点
if (temp == null) {
root = null;
} else {
// 一个子节点
root = temp;
}
} else {
// 情况2:有两个子节点
// 找到右子树的最小节点(中序后继)
Node temp = findMinNode(root.right);
// 复制中序后继的值到当前节点
root.key = temp.key;
// 删除中序后继
root.right = deleteNode(root.right, temp.key);
}
}
// 如果树只有一个节点,删除后直接返回
if (root == null) {
return root;
}
// 2. 更新高度
updateHeight(root);
// 3. 获取平衡因子
int balance = getBalanceFactor(root);
// 4. 进行旋转操作保持平衡
// 左左情况
if (balance > 1 && getBalanceFactor(root.left) >= 0) {
return rotateRight(root);
}
// 左右情况
if (balance > 1 && getBalanceFactor(root.left) 0) {
root.right = rotateRight(root.right);
return rotateLeft(root);
}
return root;
}
// 查找节点
public boolean search(int key) {
return searchNode(root, key);
}
private boolean searchNode(Node root, int key) {
if (root == null) {
return false;
}
if (key == root.key) {
return true;
}
if (key
AVL树是最早被发明的自平衡二叉搜索树之一,适用于许多需要高效查找和维持数据有序性的场景。
比如内存管理器经常使用AVL树跟踪内存块的分配与释放。
在需要频繁执行范围查询的应用中,AVL树也比较适用,常用于实现区间查询功能。
二叉堆是一种特殊的完全二叉树,常用于实现优先队列。最小堆的每个节点的值都小于或等于其子节点的值,最大堆的每个节点的值都大于或等于其子节点的值。二叉堆支持高效的插入、删除最值和构建操作。
二叉堆是一种特殊的完全二叉树数据结构,它满足堆属性。完全二叉树是指除了最后一层外,其他层的节点都是满的,而最后一层的节点都靠左排列。二叉堆主要有两种类型:
二叉堆的这种特殊结构使得它可以高效地找到最大值或最小值,所以也常被用来实现优先队列。
二叉堆的核心特性如下:
插入元素(Insert)
删除顶部元素(Extract-Max/Min):移除并返回堆顶元素(最大/最小值)
上浮(Heapify-Up):将一个元素向上移动到合适位置的过程
下沉(Heapify-Down):将一个元素向下移动到合适位置的过程
public class MinHeap {
private int[] heap;
private int size;
private int capacity;
// 构造函数
public MinHeap(int capacity) {
this.capacity = capacity;
this.size = 0;
this.heap = new int[capacity];
}
// 获取父节点索引
private int parent(int i) {
return (i - 1) / 2;
}
// 获取左子节点索引
private int leftChild(int i) {
return 2 * i + 1;
}
// 获取右子节点索引
private int rightChild(int i) {
return 2 * i + 2;
}
// 交换两个节点
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
// 插入元素
public void insert(int key) {
if (size == capacity) {
System.out.println("堆已满,无法插入");
return;
}
// 先将新元素插入到堆的末尾
heap[size] = key;
int current = size;
size++;
// 上浮操作:将元素向上移动到合适位置
while (current > 0 && heap[current]
二叉堆广泛应用于各种算法和系统中:
Java 提供了 PriorityQueue 类,它基于二叉堆实现:
import java.util.PriorityQueue;
public class PriorityQueueExample {
public static void main(String[] args) {
// 默认是最小堆
PriorityQueue minHeap = new PriorityQueue();
// 添加元素
minHeap.add(5);
minHeap.add(3);
minHeap.add(8);
minHeap.add(1);
minHeap.add(10);
System.out.println("优先队列内容:" + minHeap);
System.out.println("最小元素:" + minHeap.peek());
System.out.println("提取最小元素:" + minHeap.poll());
System.out.println("提取后的优先队列:" + minHeap);
// 创建最大堆(通过自定义比较器)
PriorityQueue maxHeap = new PriorityQueue((a, b) -> b - a);
maxHeap.add(5);
maxHeap.add(3);
maxHeap.add(8);
maxHeap.add(1);
maxHeap.add(10);
System.out.println("最大堆内容:" + maxHeap);
System.out.println("最大元素:" + maxHeap.peek());
}
}
详情可以看:PriorityQueue
除了基本的二叉堆,还有几种重要的堆变种:
B树是一种自平衡的多路搜索树,它是二叉搜索树的扩展,专为磁盘或其他外部存储设备设计。B树的每个节点拥有更多的子节点,这使树的高度更低,减少访问磁盘的次数。
B树中的几个关键概念:
一个阶为m的B树满足以下性质:
B树核心特性:
搜索操作:搜索B树中的键与搜索二叉搜索树类似,但需要在每个节点中遍历多个键
插入操作
删除操作
数据库系统是B树最主要的应用领域。几乎所有主流关系数据库都使用B树或其变种来实现索引。数据库引擎通过B树索引可以快速定位到数据所在的页面,极大提升查询性能。例如,MySQL的InnoDB存储引擎使用B+树(B树的变种)来构建其索引结构。
文件系统也广泛采用B树来组织文件和目录。如NTFS、HFS+等文件系统都使用B树或其变种来管理文件分配表和目录结构,有效地支持大型存储系统中的文件检索。
时间序列数据库或地理信息系统中,经常需要检索特定范围内的数据点,B树的有序特性使这类操作变得高效。
键值存储系统如Redis、LevelDB等也借鉴了B树的设计理念。虽然它们可能使用了不同的变种或混合结构,但基本思想源自B树的高效查找和范围操作特性。
B+树是B树的一个重要变种,它在数据库系统中更为常用:
B* 树对B树进行了进一步优化:
B+树是一种平衡树数据结构,是B树的变种,被广泛应用于数据库索引和文件系统中。B+树保持数据有序,而且能够高效地进行查找、顺序访问、插入和删除操作。
B+树的主要组成部分包括:
B+树核心特性
查找操作
插入操作
删除操作
B+树在数据库系统和文件系统中得到了广泛应用。在数据库领域,几乎所有主流关系型数据库的索引结构都采用了B+树或其变种。MySQL的InnoDB存储引擎使用B+树作为其主要索引结构,通过将数据按主键顺序组织在叶子节点中,实现了高效的查询和范围扫描操作。
在文件系统中,B+树被用于管理文件的目录结构和索引,比如NTFS、ext4等现代文件系统。由于B+树能够高效地处理大量数据,同时保持较低的树高度,使文件系统能够快速定位和访问文件。
B+树还被广泛应用于地理信息系统(GIS)中的空间索引,快速查找特定地理区域内的对象。
Trie树,也称为前缀树或字典树,是一种树形数据结构,专门用于高效存储和检索字符串集合。Trie这个名字来源于"retrieval"(检索)一词,反映了它的主要用途。
在Trie树中,每个节点代表一个字符,从根节点到某一节点的路径上经过的字符连接起来,就是该节点对应的字符串。Trie树的关键特点是,所有拥有相同前缀的字符串,在树中共享这个前缀的存储空间。
Trie树核心特性
Trie树支持以下基本操作:
class Trie {
private TrieNode root;
// Trie树的节点结构
class TrieNode {
// 子节点,使用数组实现(假设只包含小写字母a-z)
private TrieNode[] children;
// 标记该节点是否为某个单词的结尾
private boolean isEndOfWord;
public TrieNode() {
children = new TrieNode[26]; // 26个英文字母
isEndOfWord = false;
}
}
/** 初始化Trie树 */
public Trie() {
root = new TrieNode();
}
/** 向Trie树中插入单词 */
public void insert(String word) {
TrieNode current = root;
for (int i = 0; i
树状数组(Binary Indexed Tree),也称为Fenwick Tree,是一种支持高效的前缀和计算和单点更新的数据结构。它的核心思想是利用二进制的性质来维护数据间的层级关系,从而在O(log n)的时间内完成查询和更新操作。
树状数组的关键概念是"父子关系",这种关系是通过二进制表示中的最低位1来确定的。对于任意一个节点i,它的父节点是i + (i & -i),它的子节点是i - (i & -i)。
树状数组通常使用一个一维数组表示,采用1-indexed(即从索引1开始存储有效数据)的方式:
public void update(int i, int delta) {
i = i + 1; // 转为1-based索引
while (i
public int query(int i) {
i = i + 1; // 转为1-based索引
int sum = 0;
while (i > 0) {
sum += bit[i];
i -= i & -i; // 移动到前一个节点
}
return sum;
}
时间复杂度:
树状数组在以下场景中特别有用:
通过差分数组技术,树状数组可以支持区间更新,但查询变为单点查询,这样就能在O(log n)时间内完成区间更新操作。
// 创建树状数组(假设已实现BinaryIndexedTree类)
BinaryIndexedTree bit = new BinaryIndexedTree(new int[]{0, 2, 1, 4, 3, 6, 5});
// 查询前缀和
System.out.println("query(2): " + bit.query(2)); // 索引0到2的和: 2+1+4=7
// 更新元素值
bit.update(1, 2); // 将索引1的元素增加2
System.out.println("query(2): " + bit.query(2)); // 现在索引0到2的和: 2+(1+2)+4=9
// 区间查询
System.out.println("rangeQuery(1, 3): " + bit.rangeQuery(1, 3)); // 索引1到3的和: (1+2)+4+3=10
线段树(Segment Tree)是一种高效的数据结构,专门用于解决区间查询和区间修改问题。与树状数组相比,线段树功能更加强大,可以支持更多种类的区间操作。
线段树的核心思想是通过分治法将一个区间划分为多个子区间,并用树的形式组织这些区间的信息。在这棵树中,每个节点代表一个区间,根节点代表整个数组区间,叶子节点代表单个元素。
核心概念解释:
线段树核心特性:
线段树的结构:
线段树是一棵完全二叉树,其中:
懒惰标记(Lazy Propagation):
懒惰标记是一种优化技术,用于延迟区间修改的传播。当一个节点的所有子节点都需要被修改时,我们不立即修改这些子节点,而是在节点上标记修改信息,只有在需要访问子节点时才将修改下推,提高区间修改的效率。
下面是线段树的基础实现(以区间和为例):
public class SegmentTree {
private int[] tree; // 存储线段树节点
private int[] lazy; // 懒惰标记
private int[] nums; // 原始数组的副本
private int n; // 原始数组长度
public SegmentTree(int[] array) {
n = array.length;
// 线段树数组大小一般为原数组大小的4倍
tree = new int[4 * n];
lazy = new int[4 * n];
nums = array.clone();
build(0, 0, n - 1);
}
// 构建线段树
private void build(int node, int start, int end) {
if (start == end) {
// 叶子节点,存储单个元素
tree[node] = nums[start];
return;
}
int mid = (start + end) / 2;
int leftNode = 2 * node + 1;
int rightNode = 2 * node + 2;
// 递归构建左右子树
build(leftNode, start, mid);
build(rightNode, mid + 1, end);
// 合并子节点的信息
tree[node] = tree[leftNode] + tree[rightNode];
}
// 单点修改
public void update(int index, int val) {
// 计算与原值的差值
int diff = val - nums[index];
nums[index] = val;
updateSingle(0, 0, n - 1, index, diff);
}
private void updateSingle(int node, int start, int end, int index, int diff) {
// 检查索引是否在当前节点范围内
if (index end) {
return;
}
// 更新当前节点的值
tree[node] += diff;
if (start != end) {
int mid = (start + end) / 2;
int leftNode = 2 * node + 1;
int rightNode = 2 * node + 2;
// 递归更新子节点
updateSingle(leftNode, start, mid, index, diff);
updateSingle(rightNode, mid + 1, end, index, diff);
}
}
// 区间查询
public int query(int left, int right) {
return queryRange(0, 0, n - 1, left, right);
}
private int queryRange(int node, int start, int end, int left, int right) {
// 如果当前节点的区间完全在查询区间外
if (right end) {
return 0;
}
// 如果当前节点的区间完全在查询区间内
if (left end) {
return;
}
// 如果当前节点的区间完全在修改区间内
if (left
线段树在许多实际问题中有广泛应用,特别是在需要同时支持区间查询和区间修改的情况下:
当区间范围非常大,但实际有值的点比较稀疏时,可以使用动态线段树(通常使用指针实现)来节省空间:
public class DynamicSegmentTree {
private class Node {
int val; // 节点值
int lazy; // 懒惰标记
Node left; // 左子节点
Node right; // 右子节点
int start; // 区间起点
int end; // 区间终点
Node(int start, int end) {
this.start = start;
this.end = end;
this.val = 0;
this.lazy = 0;
}
}
private Node root;
public DynamicSegmentTree(int start, int end) {
root = new Node(start, end);
}
// 区间更新
public void update(int left, int right, int val) {
update(root, left, right, val);
}
private void update(Node node, int left, int right, int val) {
// 如果区间完全在更新范围外
if (node.end right) {
return;
}
// 如果区间完全在更新范围内
if (node.start >= left && node.end right) {
return 0;
}
// 如果区间完全在查询范围内
if (node.start >= left && node.end
可持久化线段树是线段树的一种变体,它可以保存历史版本,允许查询任意历史状态:
public class PersistentSegmentTree {
private class Node {
int val; // 节点值
Node left; // 左子节点
Node right; // 右子节点
Node(int val) {
this.val = val;
this.left = null;
this.right = null;
}
Node(Node other) {
this.val = other.val;
this.left = other.left;
this.right = other.right;
}
}
private Node[] roots; // 存储历史版本的根节点
private int n; // 数组大小
private int versionCount; // 版本数量
public PersistentSegmentTree(int[] array, int maxVersions) {
n = array.length;
roots = new Node[maxVersions];
versionCount = 0;
// 构建初始版本
roots[versionCount++] = build(0, n - 1, array);
}
// 构建线段树
private Node build(int start, int end, int[] array) {
if (start == end) {
return new Node(array[start]);
}
int mid = (start + end) / 2;
Node node = new Node(0);
node.left = build(start, mid, array);
node.right = build(mid + 1, end, array);
node.val = node.left.val + node.right.val;
return node;
}
// 创建新版本并更新单个元素
public void update(int index, int val) {
roots[versionCount] = update(roots[versionCount - 1], 0, n - 1, index, val);
versionCount++;
}
private Node update(Node node, int start, int end, int index, int val) {
if (index end) {
return node;
}
// 创建新节点(路径复制)
Node newNode = new Node(node);
if (start == end) {
newNode.val = val;
return newNode;
}
int mid = (start + end) / 2;
if (index = versionCount) {
throw new IllegalArgumentException("版本不存在");
}
return query(roots[version], 0, n - 1, left, right);
}
private int query(Node node, int start, int end, int left, int right) {
if (right end) {
return 0;
}
if (left
本文来自在线网站:seven的菜鸟成长之路,作者:seven,转载请注明原文链接:www.seven97.top
登录查看全部
参与评论
手机查看
返回顶部