每日一题——力扣20. 有效的括号(举一反三+思想解读+逐步优化)


一个认为一切根源都是“自己不够强”的INTJ

个人主页:用哲学编程-CSDN博客
专栏:每日一题——举一反三
Python编程学习
Python内置函数

Python-3.12.0文档解读

目录

我的写法

专业点评

时间复杂度分析

空间复杂度分析

代码优化建议

我要更强

优化方案1:使用动态数组

优化方案2:使用有限状态机

优化方案3:简化逻辑(不使用动态内存分配)

总结

哲学和编程思想

优化方案1:使用动态数组

1. 动态内存管理

2. 抽象

3. 扩展性

4. 资源管理

优化方案2:使用有限状态机

1. 状态化思维

2. 确定性

3. 简化逻辑

优化方案3:简化逻辑(不使用动态内存分配)

1. 预分配

2. 空间时间权衡

3. 极简主义

通用编程思想

举一反三

1. 动态内存管理

2. 抽象与封装

3. 状态化思维与有限状态机

4. 预分配与空间时间权衡

5. 高内聚低耦合

6. 资源管理

总结


https://leetcode.cn/problems/valid-parentheses/description/


我的写法

#include <stdlib.h>


// 检查括号字符串是否有效的函数
bool isValid(char* s) {
    // 定义栈的顶部索引和底部索引
    int top = 0, bottle = 0;
    // 定义栈的大小
    int size = 10000;
    // 动态分配一个整型数组作为栈
    int* stack = (int*)malloc(size * sizeof(int));
    // 定义字符变量ch,用于遍历字符串
    int ch = ' ';


    // 遍历输入字符串
    for (int i = 0; (ch = s[i]) != '\0'; i++) {
        // 打印当前字符(用于调试)
        printf("%c", ch);
        // 如果字符是左括号之一,则将其压入栈中
        if (ch == '(' || ch == '{' || ch == '[') {
            stack[top++] = ch;
        } else {
            // 如果栈为空但遇到了右括号,说明不匹配
            if (top == 0) {
                return false;
            } else {
                // 检查栈顶与当前右括号是否匹配
                if (stack[top - 1] == '(' && ch == ')') {
                    top--; // 匹配则弹出栈顶
                } else if (stack[top - 1] == '{' && ch == '}') {
                    top--; // 匹配则弹出栈顶
                } else if (stack[top - 1] == '[' && ch == ']') {
                    top--; // 匹配则弹出栈顶
                } else {
                    // 不匹配则返回false
                    return false;
                }
            }
        }
    }
    // 遍历结束后,栈应该为空,如果不为空则返回false
    if (top != bottle) {
        return false;
    } else {
        return true; // 栈为空则返回true,表示字符串括号匹配
    }
}


 

这段代码实现了检查括号字符串是否有效的功能。下面是专业的点评和复杂度分析:

专业点评

  1. 代码结构与逻辑
    • 代码结构清晰:函数 isValid 逻辑清晰,分为遍历字符串、检查括号类型、维护栈等部分。
    • 使用栈数据结构合适:对括号匹配的问题,使用栈来解决是非常合适的,符合括号匹配的后进先出(LIFO)特性。
  2. 可能存在的改进点
  • 内存管理:代码中使用了 malloc 动态分配栈空间,但在函数结束时没有释放分配的内存。如果 isValid 函数在大规模使用时会造成内存泄漏问题,应该使用 free(stack) 释放内存。
  • 栈溢出检查:没有对 top 的值进行合法性检查,当字符过多时可能会导致栈溢出。可以通过动态调整栈大小来解决这个问题。
  • 无用的变量:bottle 变量并没有实际用途,可以移除。
  • 打印语句:printf("%c", ch); 用于调试的打印语句应在生产代码中删除,避免不必要的输出开销。

时间复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 s 的长度。
  • 遍历字符串的每一个字符需要 O(n) 的时间。
  • 每个字符的入栈和出栈操作在均摊情况下都是 O(1) 的时间复杂度,因此总时间复杂度为 O(n)。

空间复杂度分析

  • 空间复杂度:O(n),其中 n 是字符串 s 的长度。
  • 最坏情况下,所有的字符都是左括号(例如字符串全是 ((((((),此时栈的大小会达到字符串的长度。
  • 因此,栈的空间复杂度为 O(n)。

代码优化建议

bool isValid(char* s) {
    int top = 0;
    int size = 10000;
    int* stack = (int*)malloc(size * sizeof(int));
    if (stack == NULL) {
        return false; // 内存分配失败时返回false
    }
    int ch = ' ';
    for (int i = 0; (ch = s[i]) != '\0'; i++) {
        if (ch == '(' || ch == '{' || ch == '[') {
            stack[top++] = ch;
        } else {
            if (top == 0) {
                free(stack); // 释放内存
                return false;
            } else {
                if ((stack[top - 1] == '(' && ch == ')') ||
                    (stack[top - 1] == '{' && ch == '}') ||
                    (stack[top - 1] == '[' && ch == ']')) {
                    top--;
                } else {
                    free(stack); // 释放内存
                    return false;
                }
            }
        }
    }
    bool isValid = (top == 0);
    free(stack); // 释放内存
    return isValid;
}
  1. 内存管理:
  2. 动态调整栈大小(可选):
  • 如果预期字符串长度会很长,可以考虑动态调整栈大小以避免栈溢出。

总结来看,这段代码在逻辑上是正确的,但在内存管理和栈溢出检查方面还有优化空间。时间复杂度为 O(n),空间复杂度为 O(n),在大多数情况下表现良好。


我要更强

在括号匹配问题上,时间复杂度已经是最优的 O(n),因为每个字符都需要检查一次。空间复杂度的优化可能性有限,因为在最坏情况下,所有字符都是左括号或有嵌套括号时,仍然需要 O(n) 的空间来存储这些括号。但是,我们可以考虑一些具体的优化方法:

  1. 使用动态数组(矢量):动态调整栈的大小,以节省空间。
  2. 使用有限状态机:对某些特定情况可以优化,但一般情况下仍然需要 O(n) 的空间。

以下是几个优化方案及其实现代码:

优化方案1:使用动态数组

动态数组可以根据需要调整大小来节省空间。

#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

// 动态数组结构
typedef struct {
    char* data;
    int capacity;
    int size;
} Stack;

// 初始化动态数组
Stack* createStack(int initialCapacity) {
    Stack* stack = (Stack*)malloc(sizeof(Stack));
    stack->data = (char*)malloc(initialCapacity * sizeof(char));
    stack->capacity = initialCapacity;
    stack->size = 0;
    return stack;
}

// 释放动态数组
void freeStack(Stack* stack) {
    free(stack->data);
    free(stack);
}

// 动态数组压入元素
void push(Stack* stack, char value) {
    // 如果容量不足,增加容量
    if (stack->size == stack->capacity) {
        stack->capacity *= 2;
        stack->data = (char*)realloc(stack->data, stack->capacity * sizeof(char));
    }
    stack->data[stack->size++] = value;
}

// 动态数组弹出元素
char pop(Stack* stack) {
    return stack->data[--stack->size];
}

// 检查栈是否为空
bool isEmpty(Stack* stack) {
    return stack->size == 0;
}

// 检查栈顶元素
char top(Stack* stack) {
    return stack->data[stack->size - 1];
}

// 检查括号是否有效
bool isValid(char* s) {
    Stack* stack = createStack(128); // 初始容量设置为128
    for (int i = 0; s[i] != '\0'; i++) {
        char ch = s[i];
        if (ch == '(' || ch == '{' || ch == '[') {
            push(stack, ch);
        } else {
            if (isEmpty(stack)) {
                freeStack(stack);
                return false;
            }
            char topElement = top(stack);
            if ((topElement == '(' && ch == ')') ||
                (topElement == '{' && ch == '}') ||
                (topElement == '[' && ch == ']')) {
                pop(stack);
            } else {
                freeStack(stack);
                return false;
            }
        }
    }
    bool result = isEmpty(stack);
    freeStack(stack);
    return result;
}

优化方案2:使用有限状态机

有限状态机适用于某些特定的字符串匹配场景,但对于括号匹配,效果不如栈明显。不过,以下是使用有限状态机的简单版本:

#include <stdbool.h>

#define MAX_STATES 128

typedef struct {
    int transitions[MAX_STATES][256];
    int currentState;
} StateMachine;

// 初始化状态机
void initStateMachine(StateMachine* sm) {
    sm->currentState = 0;
    for (int i = 0; i < MAX_STATES; i++) {
        for (int j = 0; j < 256; j++) {
            sm->transitions[i][j] = -1;
        }
    }
}

// 设定状态转换
void setTransition(StateMachine* sm, int fromState, char input, int toState) {
    sm->transitions[fromState][(unsigned char)input] = toState;
}

// 处理输入字符
void processInput(StateMachine* sm, char input) {
    int nextState = sm->transitions[sm->currentState][(unsigned char)input];
    if (nextState != -1) {
        sm->currentState = nextState;
    }
}

// 检查括号是否有效
bool isValid(char* s) {
    StateMachine sm;
    initStateMachine(&sm);

    // 定义状态转换
    setTransition(&sm, 0, '(', 1);
    setTransition(&sm, 1, ')', 0);
    setTransition(&sm, 0, '{', 2);
    setTransition(&sm, 2, '}', 0);
    setTransition(&sm, 0, '[', 3);
    setTransition(&sm, 3, ']', 0);

    for (int i = 0; s[i] != '\0'; i++) {
        processInput(&sm, s[i]);
    }

    return sm.currentState == 0;
}

优化方案3:简化逻辑(不使用动态内存分配)

如果预先知道字符串长度不会过长,可以使用固定大小的数组来避免动态内存分配。

#include <stdbool.h>

#define MAX_LENGTH 10000

bool isValid(char* s) {
    char stack[MAX_LENGTH];
    int top = 0;

    for (int i = 0; s[i] != '\0'; i++) {
        char ch = s[i];
        if (ch == '(' || ch == '{' || ch == '[') {
            stack[top++] = ch;
        } else {
            if (top == 0) {
                return false;
            }
            char topElement = stack[top - 1];
            if ((topElement == '(' && ch == ')') ||
                (topElement == '{' && ch == '}') ||
                (topElement == '[' && ch == ']')) {
                top--;
            } else {
                return false;
            }
        }
    }
    return top == 0;
}

总结

上述优化方法中,第一种使用动态数组的方法是最为灵活和常用的,因为它可以根据需要动态调整栈的大小,避免不必要的内存浪费。第三种方法则适用于已知长度限制的场景,可以有效避免动态内存分配的开销。有限状态机的方法在括号匹配问题上不常用,但在某些特定模式匹配问题上可能会有用。


哲学和编程思想

这些优化方法应用了多种哲学和编程思想,具体如下:

优化方案1:使用动态数组

1. 动态内存管理
  • 哲学思想:最小化资源使用和避免浪费。
  • 编程思想:动态内存分配允许程序根据实际需要调整资源使用,避免过度分配。
2. 抽象
  • 哲学思想:抽象化有助于将复杂问题简化。
  • 编程思想:通过创建 Stack 结构体和相关操作函数,使栈的操作更加模块化、易于理解和维护。
3. 扩展性
  • 哲学思想:系统应该能够适应变化和增长。
  • 编程思想:动态调整栈的大小,使程序能够处理不同规模的数据,增强了程序的灵活性和适应性。
4. 资源管理
  • 哲学思想:有效管理和合理利用资源。
  • 编程思想:在使用动态内存时,确保在不再需要时释放内存,防止内存泄漏。

优化方案2:使用有限状态机

1. 状态化思维
  • 哲学思想:将复杂的过程分解为简单的状态和转移。
  • 编程思想:有限状态机通过明确定义状态和状态之间的转换,简化了复杂系统的设计和实现。
2. 确定性
  • 哲学思想:系统的每个输入都有确定的行为。
  • 编程思想:有限状态机确保每个输入在当前状态下的行为是确定的,有助于提高程序的可靠性和可预测性。
3. 简化逻辑
  • 哲学思想:简化逻辑以提高效率和减少错误。
  • 编程思想:通过有限状态机的方式处理输入,避免了复杂的条件判断和嵌套逻辑,使代码更加清晰和可维护。

优化方案3:简化逻辑(不使用动态内存分配)

1. 预分配
  • 哲学思想:在有限资源环境中优化资源的使用。
  • 编程思想:预先分配固定大小的数组,避免了动态内存分配的开销和复杂性,提高了程序的效率。
2. 空间时间权衡
  • 哲学思想:在时间效率和空间效率之间找到平衡。
  • 编程思想:使用固定大小的数组在空间上可能会浪费一些内存,但可以换取更高的时间效率,适用于已知数据规模的场景。
3. 极简主义
  • 哲学思想:以最简单和直接的方式解决问题。
  • 编程思想:通过使用固定大小数组和简单的条件判断,消除了不必要的复杂性,使代码更加简洁和高效。

通用编程思想

  1. 封装
    • 将数据和操作封装在一起,隐藏实现细节,提高代码的模块化和可维护性。例如,Stack 结构体和相关操作函数实现了栈的封装。
  2. 单一职责原则
    • 每个函数或模块只负责一项功能,提高代码的可读性和可维护性。例如,push、pop、isEmpty 等函数各自负责栈的不同操作。
  3. 责任分离
    • 将不同的功能和职责分离,避免模块之间的耦合。例如,状态机将状态转换逻辑和输入处理逻辑分离,提高了代码的灵活性。
  4. 高内聚,低耦合
    • 模块内部的各个部分紧密相关,而不同模块之间尽量松散耦合。例如,栈的操作函数和主逻辑分开实现,提高了代码的可维护性和复用性。
  5. 资源管理
  • 合理管理资源的分配和释放,避免资源泄漏和浪费。例如,动态数组在使用完后需要释放内存。

通过这些哲学和编程思想,可以更好地理解和优化代码,提高其性能、可维护性和扩展性。


举一反三

理解和应用编程哲学和思想可以大大提升你的编程技巧,以下是一些具体的技巧和建议,帮助你在编程时举一反三:

1. 动态内存管理

技巧:

  • 避免硬编码的大小:尽量不要使用硬编码的大小限制,除非你非常确定数据不会超过这个限制。
  • 动态扩展:学习如何使用动态内存分配(如 malloc、realloc 和 free),以便根据需要动态扩展数据结构的大小。
  • 内存管理工具:熟悉和使用内存调试工具(如 Valgrind)来检测内存泄漏和越界访问。

举一反三:

  • 在处理不确定大小的数据(如文件、网络数据流)时,考虑使用动态数组或链表等数据结构。
  • 当需要高效、快速地处理大量数据时,考虑使用哈希表、红黑树等动态数据结构。

2. 抽象与封装

技巧:

  • 模块化设计:将复杂的功能拆分为多个小模块,每个模块只负责一个功能。
  • 接口与实现分离:通过定义接口(如函数原型、类接口)来隐藏实现细节,减少模块之间的依赖。
  • 使用标准库:尽量使用标准库提供的数据结构和算法,避免重复造轮子。

举一反三:

  • 在设计大型系统时,考虑使用设计模式(如工厂模式、单例模式)来提高代码的可维护性和可扩展性。
  • 在团队开发中,制定和遵循接口规范,确保各模块之间的接口一致。

3. 状态化思维与有限状态机

技巧:

  • 状态图:在处理复杂流程时,先画出状态图,明确各个状态和状态之间的转换。
  • 状态机实现:使用二维数组或哈希表来实现状态机的状态转移,确保状态机的实现简洁明了。
  • 测试状态机:通过单元测试,确保状态机在各种输入下都能正确运行。

举一反三:

  • 在处理交互式应用(如游戏、用户界面)时,使用状态机来管理应用的各个状态。
  • 在处理协议解析(如网络协议、文件格式)时,使用状态机来确保解析过程的正确性和可靠性。

4. 预分配与空间时间权衡

技巧:

  • 预估数据规模:在启动时预估数据规模,并进行合理的资源分配,避免频繁的动态内存分配。
  • 缓冲区:在处理流数据时,使用缓冲区来提高数据处理效率。
  • 空间换时间:在对时间效率要求较高的场景中,可以适当增加内存使用,以换取更快的执行速度。

举一反三:

  • 在处理大量数据时,考虑使用批处理和缓冲区技术来减少处理开销。
  • 在实现缓存机制时,考虑使用合适的缓存策略(如 LRU 缓存)来提高系统性能。

5. 高内聚低耦合

技巧:

  • 单一职责原则:确保每个模块或函数只有一个明确的责任,提高代码的可读性和可维护性。
  • 接口隔离:设计良好的接口,确保模块之间的依赖最小化。
  • 依赖注入:通过依赖注入减少模块之间的耦合,提高模块的可测试性和灵活性。

举一反三:

  • 在设计类和模块时,考虑使用面向对象的设计原则(如 SOLID 原则)来提高代码质量。
  • 在大型项目中,使用依赖注入框架(如 Spring)来管理依赖关系,提高代码的灵活性和可维护性。

6. 资源管理

技巧:

  • 使用 RAII(资源获取即初始化):在 C++ 等语言中,通过构造函数获取资源,析构函数释放资源,确保资源的正确释放。
  • 引用计数:使用引用计数技术来管理共享资源,避免资源泄漏和双重释放。
  • 自动垃圾回收:在支持垃圾回收的语言中,尽量使用语言提供的自动垃圾回收机制,减少手动管理内存的复杂性。

举一反三:

  • 在处理文件、网络连接等资源时,确保在使用完后正确关闭和释放资源。
  • 在多线程环境中,使用智能指针或其他线程安全的资源管理技术,确保资源的正确释放和管理。

总结

通过理解这些编程思想和哲学,并在实际编程中应用这些技巧,你可以更高效地解决问题,提高代码的质量和可维护性。同时,保持学习和实践,不断积累经验,能够在各种编程场景中举一反三,灵活应用这些思想和技巧。

  • 34
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

用哲学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值