501. 二叉搜索树中的众数
题目链接:
501. 二叉搜索树中的众数 - 力扣(LeetCode)
题目:
解题思路:
递归,中序遍历(将搜索树结构变成有序数组进行处理),涉设置一个全局集合变量和全局计数器和最大数值计数器,当遍历到的元素值与之前相等就count++,若不等就去判断是否大于之前的元素,还是等于之前的元素,若等于之前元素的数量,就直接加入集合就行,如大于之前元素的数量,先把集合清空,再将当前元素加入集合中,最后遍历一遍将集合变成数即可
代码:
/*** Definition for a binary tree node.* public 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;* }* }*//*
*Stack Integer ArrayList String StringBuffer peek
*Collections imports LinkedList offer return
*empty polls offerLast pollFirst isEmpty
*List Deque append length HashMap null
*return
*/
class Solution {List<Integer> res;int count;int maxCount;TreeNode pre;public int[] findMode(TreeNode root) {count=0;maxCount=0;res=new ArrayList<>();find(root);if(pre!=null){check();}int [] arr=new int[res.size()];for(int i=0;i<arr.length;i++){arr[i]=res.get(i);}return arr;}public void find(TreeNode root){if(root==null) return ;find(root.left);if(pre==null||pre.val!=root.val){//这里用或那pre.val还能使用吗if(pre!=null){check();}count=1;pre=root;}else{count++;}find(root.right);}public void check(){if(count==maxCount){res.add(pre.val);}else if(count>maxCount){res.clear();res.add(pre.val);maxCount=count;}}
}
深入解析二叉搜索树众数查找算法:逻辑与实现细节
在二叉树相关算法中,寻找众数(出现次数最多的元素)是一个经典问题。尤其对于二叉搜索树(BST),我们可以利用其特殊性质设计出高效的解决方案。本文将详细解析一段基于中序遍历的 BST 众数查找代码,探讨其设计思路、实现细节及注意事项。
算法背景与思路
二叉搜索树的重要特性是:中序遍历结果为有序序列。这一特性让我们可以将 BST 视为一个有序数组,相同的元素会连续出现,这为统计元素出现次数提供了极大便利。
我们的核心思路是:
通过中序遍历遍历整个 BST,获得有序序列
实时统计当前元素的出现次数
维护一个最大值计数器,记录已出现的最大频次
根据当前元素频次与最大频次的关系,更新结果集
这种方法无需额外空间存储整个序列,只需 O (1) 的额外空间(不包括结果存储),时间复杂度为 O (n)(n 为节点总数)。
代码结构解析
我们先来看完整代码结构,以便建立整体认知:
java
运行
/**
* Definition for a binary tree node.
* public 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;
* }
* }
*/
class Solution {
List<Integer> res; // 存储众数结果
int count; // 当前元素出现次数计数器
int maxCount; // 最大出现次数记录
TreeNode pre; // 前一个节点的引用
public int[] findMode(TreeNode root) {
// 初始化变量
count = 0;
maxCount = 0;
res = new ArrayList<>();
// 执行查找逻辑
find(root);
// 处理最后一个节点
if(pre != null){
check();
}
// 转换结果为数组并返回
int [] arr = new int[res.size()];
for(int i = 0; i < arr.length; i++){
arr[i] = res.get(i);
}
return arr;
}
// 中序遍历核心逻辑
public void find(TreeNode root){
if(root == null) return ;
find(root.left); // 左
// 中:处理当前节点
if(pre == null || pre.val != root.val){
if(pre != null){
check();
}
count = 1;
pre = root;
}else{
count++;
}
find(root.right); // 右
}
// 检查并更新结果集
public void check(){
if(count == maxCount){
res.add(pre.val);
}else if(count > maxCount){
res.clear();
res.add(pre.val);
maxCount = count;
}
}
}
代码主要包含三个部分:
findMode():主方法,负责初始化、触发遍历和结果转换
find():递归中序遍历方法,实现核心计数逻辑
check():根据计数结果更新众数集合
核心变量解析
让我们详细了解几个关键变量的作用:
res:List<Integer>类型,用于存储所有众数。由于可能存在多个频次相同的众数,需要用列表存储。
count:当前元素的出现次数计数器。每当遇到相同元素时递增,遇到新元素时重置为 1。
maxCount:记录遍历过程中遇到的最大频次。用于判断当前元素是否有资格成为众数。
pre:指向 "前一个节点" 的引用,用于与当前节点比较值是否相同,是实现连续计数的关键。
这些变量被定义为成员变量而非局部变量,避免了递归过程中的参数传递开销,同时保证了状态在递归调用间的连续性。
关键逻辑详解
1. 中序遍历的实现
java
运行
public void find(TreeNode root){
if(root == null) return ;
find(root.left); // 左子树遍历
// 处理当前节点(中序位置)
// ...
find(root.right); // 右子树遍历
}
这是典型的递归式中序遍历实现,遵循 "左 - 中 - 右" 的访问顺序。正是这种顺序保证了我们按从小到大的顺序访问 BST 中的元素。
2. 元素计数逻辑
计数逻辑是整个算法的核心,位于中序遍历的 "中" 位置:
java
运行
if(pre == null || pre.val != root.val){
if(pre != null){
check();
}
count = 1;
pre = root;
}else{
count++;
}
这段代码的作用是:判断当前节点与前一个节点是否相同,以决定是开始新计数还是延续当前计数。
关于 "或" 操作的疑问解析
有读者可能会问:if(pre == null || pre.val != root.val)中使用了 "或" 操作,当pre == null为真时,pre.val不会被执行,这是为什么?
这是 Java 的短路逻辑特性:当使用||(逻辑或)时,如果第一个条件为true,则不会执行第二个条件。因此当pre为null时,pre.val不会被访问,也就不会出现空指针异常。
这种写法简洁地处理了两种情况:
pre == null:表示这是遍历的第一个节点
pre.val != root.val:表示遇到了新的元素
计数逻辑的执行流程
当遇到新元素(或第一个元素)时:
如果不是第一个元素(pre != null),则检查上一个元素的计数情况(调用check())
重置计数器count = 1
更新pre指向当前节点
当遇到相同元素时:
计数器count递增(count++)
不更新pre(仍指向前一个相同元素)
3. 结果集更新逻辑
check()方法负责根据当前计数更新结果集:
java
运行
public void check(){
if(count == maxCount){
// 当前元素频次与最大频次相同,加入结果集
res.add(pre.val);
}else if(count > maxCount){
// 当前元素频次更高,清空原有结果,更新最大频次
res.clear();
res.add(pre.val);
maxCount = count;
}
}
这个方法的逻辑很清晰:
当当前元素频次等于最大频次:加入结果集
当当前元素频次大于最大频次:清空原有结果(因为它们不再是众数),添加当前元素,更新最大频次
4. 最后一个元素的处理
在findMode()主方法中,有一段特殊处理:
java
运行
// 处理最后一个节点
if(pre != null){
check();
}
为什么需要这段代码?因为在find()方法中,我们只在遇到新元素时才会检查上一个元素。而遍历的最后一个元素没有 "下一个元素" 来触发检查,所以需要在遍历结束后专门处理。
这是一个容易被忽略的边界情况,缺少这段代码会导致最后一组元素的计数无法被正确统计。
5. 结果转换
最后,我们需要将List<Integer>转换为题目要求的int[]类型:
java
运行
int [] arr = new int[res.size()];
for(int i = 0; i < arr.length; i++){
arr[i] = res.get(i);
}
return arr;
这是 Java 集合与基本类型数组之间的常规转换操作。
算法执行流程示例
为了更好地理解算法执行过程,我们用一个简单的 BST 示例来模拟:
假设 BST 结构如下(中序遍历结果:1, 2, 2, 3, 3, 3):
plaintext
2
/ \
1 3
/
3
/
3
执行流程:
遍历到 1(第一个节点):
pre 为 null,count=1,pre 指向 1
遍历到 2:
pre.val (1) != 2,调用 check () 检查 1(count=1)
maxCount 为 0,因此 res 添加 1,maxCount=1
count 重置为 1,pre 指向 2
遍历到 2(第二个 2):
与 pre.val 相同,count=2
遍历到 3(第一个 3):
pre.val (2) != 3,调用 check () 检查 2(count=2)
2 > maxCount (1),清空 res,添加 2,maxCount=2
count 重置为 1,pre 指向 3
遍历到 3(第二个 3):
与 pre.val 相同,count=2
遍历到 3(第三个 3):
与 pre.val 相同,count=3
遍历结束,调用 check () 检查最后一个元素 3(count=3):
3 > maxCount (2),清空 res,添加 3,maxCount=3
最终结果:[3],正确反映了 3 是出现次数最多的元素。
算法优化点与特性
空间效率:算法只使用了常数级别的额外空间(不包括结果存储),优于使用哈希表计数的 O (n) 空间复杂度方法。
时间效率:只需一次遍历(O (n) 时间)即可完成统计,无需二次遍历。
实时更新:在遍历过程中实时更新结果集,避免了存储整个序列的需求。
边界处理:专门处理了第一个元素和最后一个元素的特殊情况,确保计数完整。
可能的改进方向
虽然当前算法已经很高效,但仍有一些可改进之处:
非递归实现:对于极深的树,递归可能导致栈溢出,可以改为迭代式中序遍历。
提前终止:如果已知树中存在超过一半的元素是同一个值(绝对众数),可以添加提前终止逻辑。
泛型支持:可以扩展为支持任意可比类型的 BST,而不仅仅是整数。
总结
本文详细解析了基于中序遍历的 BST 众数查找算法,该算法巧妙利用了 BST 中序遍历的有序性,实现了高效的众数查找。核心思想是通过一次遍历实时统计元素出现次数,并根据计数结果动态更新众数集合。
算法的关键在于:
正确维护前一个节点的引用以判断元素是否连续
在元素切换时及时检查上一个元素的计数情况
专门处理最后一个元素以避免遗漏
理解这个算法不仅能帮助我们解决众数查找问题,更能加深对 BST 特性和中序遍历应用的理解,为解决其他树相关问题提供思路。
总结:
本文详细解析了二叉搜索树(BST)中众数查找的高效算法。该算法利用BST中序遍历的有序特性,通过一次遍历实时统计元素出现次数:使用count记录当前元素频次,maxCount跟踪最大频次,pre节点比较相邻元素值。当遇到不同值时调用check()方法更新结果集,需特别注意处理首尾节点的边界情况。该方案仅需O(n)时间复杂度和O(1)额外空间(不含结果存储),相比哈希表方法更节省空间。关键点包括中序遍历的有序性处理、计数逻辑的精确控制以及结果集的动态更新,为树结构统计问题提供了典型解决思路。