0

0

优化括号平衡检测:理解与修复Java自定义栈的错误用法

霞舞

霞舞

发布时间:2025-10-11 13:50:29

|

994人浏览过

|

来源于php中文网

原创

优化括号平衡检测:理解与修复Java自定义栈的错误用法

本文针对使用自定义实现括号平衡检测时常见的逻辑错误进行深入分析与修复。文章详细阐述了原始代码在弹出操作中因循环条件不当导致的问题,并提供了一种基于`while`循环的改进方案,确保了栈操作的正确性及平衡性判断的准确性,同时兼容了在不导入任何库的限制下使用自定义栈的场景。

引言:括号平衡检测及其挑战

括号平衡检测是计算机科学中一个经典的算法问题,广泛应用于编译器、代码编辑器等领域,用于验证表达式或代码块的语法正确性。其核心思想是确保每个开括号(如 (, [, {)都有一个对应的闭括号(如 ), ], }),并且它们的嵌套顺序是正确的。栈(Stack)作为一种“后进先出”(LIFO)的数据结构,天然适合解决这类问题。

在某些特定场景下,我们可能被要求使用自定义的栈实现,而非Java标准库中的 java.util.Stack。这通常发生在教学环境或对内存、性能有极致要求的嵌入式系统中。在这种情况下,开发者需要自行实现 Node 类来构建链式栈。

问题分析:自定义栈的错误使用

原始代码尝试通过两个自定义栈来检测括号平衡。其基本思路是将所有开括号 ( 压入 stack1,将所有闭括号 ) 压入 stack2。随后,计划通过一个循环将两个栈中的元素逐一弹出,如果最终两个栈都为空,则认为表达式是平衡的。

以下是原始 isBalanced 方法的关键部分:

立即学习Java免费学习笔记(深入)”;

  static boolean isBalanced(String expr){
      // base case: length of the expression must be even
        if (expr == null || expr.length() % 2 == 1) {
            return false;
        }

        Stack stack1 = new Stack();
        Stack stack2 = new Stack();

        // traverse the input expression and push characters
        for (int i = 0; i< expr.length(); i++){ 
            if (expr.charAt(i) == '(') {
                stack1.push(expr.charAt(i));
            } 
            if (expr.charAt(i) == ')') {
               stack2.push(expr.charAt(i));
            }
        }

        // Problematic popping loop
        for(int i = 0; i< expr.length(); i++) {
            stack1.pop(); // May cause issues if stack1 is empty
            stack2.pop(); // May cause issues if stack2 is empty
        }

        return (stack1.isEmpty() && stack2.isEmpty()) ; 
    }

错误根源:不当的弹出循环条件

上述代码中的第二个 for 循环是导致问题的主要原因:

for(int i = 0; i< expr.length(); i++) {
    stack1.pop();
    stack2.pop();
}

这个循环的迭代次数是 expr.length(),即表达式的总长度。然而,stack1 和 stack2 中实际存储的元素数量通常远小于 expr.length(),它们只分别存储了开括号和闭括号。例如,对于输入 ((())),expr.length() 为 6。stack1 和 stack2 各自只包含 3 个元素。当 for 循环执行到第四次迭代时,stack1 和 stack2 都已经为空,但循环仍会继续尝试调用 pop() 方法。由于自定义 Stack 类中的 pop() 方法在栈为空时会打印 "Trying to pop when stack is empty" 并返回 null,这就会导致控制台输出多条错误信息。

尽管最终 stack1.isEmpty() && stack2.isEmpty() 可能会返回 true(因为它们确实被清空了),但这并非通过正确的逻辑实现的,并且伴随着不必要的错误提示。这种方法实际上只是检查了开括号和闭括号的数量是否相等,而不是严格意义上的结构平衡(即 ( 必须在其对应的 ) 之前出现)。但考虑到原始代码的设计意图和问题描述,我们主要关注如何修正其弹出逻辑以实现其特定的“平衡”定义。

解决方案:基于while循环的精确弹出

为了正确地从两个栈中弹出元素,我们应该在两个栈都还有元素时才进行弹出操作。一旦其中任何一个栈变空,就应该停止弹出,因为这意味着其中一类括号已经用尽。

修正后的 isBalanced 方法如下所示:

public class BalancedParenthesesChecker {

  // Stack class (provided by user, for context)
  static class Node {
      Object info;
      Node next;

      Node(Object info, Node next){
          this.info=info;
          this.next=next;
      }    
  }

  static class Stack {
      private Node top;

      public Stack() {
          top = null;
      }

      public boolean isEmpty(){
          return (top ==null);
      }

      public void push(Object newItem){
          top = new Node(newItem,  top);
      }

      public Object pop(){
          if (isEmpty()){
              System.out.println(
               "Trying to pop when stack is empty");
              return null;
          }else{
              Node temp = top;
              top = top.next;
              return temp.info;
          }
      }

      void popAll(){
          top = null;
      }

      public Object peek(){
          if (isEmpty()){
             System.out.println(
              "Trying to peek when stack is empty");
             return null;
          }else{  
             return top.info;
          }
      }
  }

  /**
   * 检查给定表达式中的括号是否“平衡”。
   * 此方法根据原始问题对“平衡”的定义进行修复:
   * 仅检查开括号 '(' 的数量是否与闭括号 ')' 的数量相等。
   * 它不检查括号的嵌套顺序。
   *
   * @param expr 待检查的字符串表达式。
   * @return 如果开括号和闭括号数量相等且表达式长度为偶数,则返回 true;否则返回 false。
   */
  static boolean isBalanced(String expr){
      // 基本情况:表达式为空或长度为奇数,则不可能平衡
        if (expr == null || expr.length() % 2 == 1) {
            return false;
        }

        Stack stack1 = new Stack(); // 存储开括号 '('
        Stack stack2 = new Stack(); // 存储闭括号 ')'

        // 遍历表达式,将开括号和闭括号分别压入不同的栈
        for (int i = 0; i< expr.length(); i++){ 
            if (expr.charAt(i) == '(') {
                stack1.push(expr.charAt(i));
            } 
            if (expr.charAt(i) == ')') {
               stack2.push(expr.charAt(i));
            }
        }

        // 改进后的弹出循环:只有当两个栈都有元素时才进行弹出操作
        // 这样可以避免在栈为空时继续尝试弹出
        while (!stack1.isEmpty() && !stack2.isEmpty()) {
            stack1.pop();
            stack2.pop();
        }

        // 最终判断:如果两个栈都为空,则认为表达式是平衡的
        // 这意味着开括号和闭括号的数量相等
        return (stack1.isEmpty() && stack2.isEmpty()) ; 
    }

    public static void main(String[] args) {
        System.out.println("Testing ((())): " + isBalanced("((()))")); // Expected: true
        System.out.println("Testing ()): " + isBalanced("(())"));   // Expected: true
        System.out.println("Testing )(: " + isBalanced(")("));     // Expected: true (based on this specific definition)
        System.out.println("Testing (: " + isBalanced("("));       // Expected: false (odd length)
        System.out.println("Testing ): " + isBalanced(")"));       // Expected: false (odd length)
        System.out.println("Testing (()): " + isBalanced("(()"));    // Expected: false (odd length)
        System.out.println("Testing empty string: " + isBalanced("")); // Expected: true (length 0, both stacks empty)
    }
}

改进逻辑说明:

  1. 收集括号: 第一个 for 循环保持不变,它负责将表达式中的所有开括号 ( 压入 stack1,所有闭括号 ) 压入 stack2。
  2. 精确弹出: 关键的改变在于第二个循环。我们使用 while (!stack1.isEmpty() && !stack2.isEmpty()) 作为循环条件。这意味着只有当 stack1 和 stack2 都包含元素时,循环才会继续执行。每次迭代,两个栈各弹出一个元素。
  3. 避免空栈操作: 一旦其中一个栈变为空,循环就会终止。这有效地避免了在空栈上调用 pop() 方法,从而消除了 Trying to pop when stack is empty 的错误输出。
  4. 最终判断: 循环结束后,如果 stack1 和 stack2 都为空,则说明开括号和闭括号的数量是相等的,返回 true。否则,如果其中一个栈仍有剩余元素(表示数量不匹配),则返回 false。

自定义栈与节点类概览

为了完整性,这里再次展示自定义的 Stack 和 Node 类的结构。它们是实现上述逻辑的基础,且严格遵循了不导入任何标准库的限制。

Node 类:

public class Node {
    Object info; // 存储节点信息
    Node next;   // 指向下一个节点的引用

    Node(Object info, Node next){
        this.info=info;
        this.next=next;
    }    
}

Stack 类:

public class Stack{
    private Node top; // 栈顶指针

    public Stack() {
        top = null; // 构造函数,初始化空栈
    }

    public boolean isEmpty(){
        return (top ==null); // 判断栈是否为空
    }

    public void push(Object newItem){
        top = new Node(newItem,  top); // 压栈操作:新元素成为栈顶
    }

    public Object pop(){
        if (isEmpty()){
            System.out.println(
             "Trying to pop when stack is empty"); // 空栈弹出错误提示
            return null;
        }else{
            Node temp = top; // 暂存当前栈顶
            top = top.next;  // 栈顶指向下一个节点
            return temp.info; // 返回原栈顶元素
        }
    }

    void popAll(){
        top = null; // 清空栈
    }

    public Object peek(){
        if (isEmpty()){
           System.out.println(
            "Trying to peek when stack is empty"); // 空栈窥视错误提示
           return null;
        }else{  
           return top.info; // 返回栈顶元素,不移除
        }
    }
}

这些自定义类提供了栈的基本功能,并且在 pop 和 peek 方法中包含了对空栈操作的错误提示,这对于调试非常有帮助。

注意事项与最佳实践

  1. 明确“平衡”定义: 本教程所修复的 isBalanced 方法,其“平衡”的定义是:表达式中开括号 ( 的数量与闭括号 ) 的数量相等,且表达式总长度为偶数。它不检查括号的嵌套顺序。例如,)( 在此定义下会被认为是平衡的(因为一个 ( 和一个 ) 数量相等)。如果需要实现严格的结构平衡检测(例如 ([{}]) 应该平衡,而 ([)] 不平衡),通常会采用单栈匹配法:遇到开括号压栈,遇到闭括号则检查栈顶是否为对应的开括号并弹出,最终栈为空则平衡。
  2. 栈操作的边界条件: 在进行 pop() 或 peek() 操作之前,始终应该检查栈是否为空 (isEmpty())。这是避免运行时错误的关键。自定义栈的 pop 方法中已经包含了此检查,并提供了友好的错误信息。
  3. 循环条件的精确性: 避免使用固定次数的循环来处理动态数据结构(如栈)的元素。应根据数据结构的实际状态(如 !isEmpty())来控制循环,确保操作的合法性。
  4. 代码可读性 良好的变量命名(如 stack1 和 stack2)和注释有助于理解代码的意图和逻辑。

总结

通过对 isBalanced 方法中弹出循环逻辑的修正,我们成功解决了自定义栈在处理括号平衡问题时出现的 Trying to pop when stack is empty 错误。核心改进在于将固定次数的 for 循环替换为基于栈实际状态的 while 循环,确保了只有在栈非空时才执行弹出操作。这个案例强调了在操作数据结构时,精确控制循环条件和处理边界情况的重要性,尤其是在使用自定义数据结构且无法依赖标准库的健壮性时。理解并正确应用这些原则,是编写高质量、健壮代码的基础。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

254

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

1109

2024.03.01

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

107

2023.09.25

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

550

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

30

2025.12.22

深入理解算法:高效算法与数据结构专题
深入理解算法:高效算法与数据结构专题

本专题专注于算法与数据结构的核心概念,适合想深入理解并提升编程能力的开发者。专题内容包括常见数据结构的实现与应用,如数组、链表、栈、队列、哈希表、树、图等;以及高效的排序算法、搜索算法、动态规划等经典算法。通过详细的讲解与复杂度分析,帮助开发者不仅能熟练运用这些基础知识,还能在实际编程中优化性能,提高代码的执行效率。本专题适合准备面试的开发者,也适合希望提高算法思维的编程爱好者。

45

2026.01.06

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

447

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

606

2023.08.10

TypeScript类型系统进阶与大型前端项目实践
TypeScript类型系统进阶与大型前端项目实践

本专题围绕 TypeScript 在大型前端项目中的应用展开,深入讲解类型系统设计与工程化开发方法。内容包括泛型与高级类型、类型推断机制、声明文件编写、模块化结构设计以及代码规范管理。通过真实项目案例分析,帮助开发者构建类型安全、结构清晰、易维护的前端工程体系,提高团队协作效率与代码质量。

49

2026.03.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.4万人学习

C# 教程
C# 教程

共94课时 | 11.3万人学习

Java 教程
Java 教程

共578课时 | 82.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号