一个认为一切根源都是“自己不够强”的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,表示字符串括号匹配
}
}
这段代码实现了检查括号字符串是否有效的功能。下面是专业的点评和复杂度分析:
专业点评
- 代码结构与逻辑
- 代码结构清晰:函数 isValid 逻辑清晰,分为遍历字符串、检查括号类型、维护栈等部分。
- 使用栈数据结构合适:对括号匹配的问题,使用栈来解决是非常合适的,符合括号匹配的后进先出(LIFO)特性。
- 可能存在的改进点
- 内存管理:代码中使用了 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;
}
- 内存管理:
- 动态调整栈大小(可选):
- 如果预期字符串长度会很长,可以考虑动态调整栈大小以避免栈溢出。
总结来看,这段代码在逻辑上是正确的,但在内存管理和栈溢出检查方面还有优化空间。时间复杂度为 O(n),空间复杂度为 O(n),在大多数情况下表现良好。
我要更强
在括号匹配问题上,时间复杂度已经是最优的 O(n),因为每个字符都需要检查一次。空间复杂度的优化可能性有限,因为在最坏情况下,所有字符都是左括号或有嵌套括号时,仍然需要 O(n) 的空间来存储这些括号。但是,我们可以考虑一些具体的优化方法:
- 使用动态数组(矢量):动态调整栈的大小,以节省空间。
- 使用有限状态机:对某些特定情况可以优化,但一般情况下仍然需要 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. 极简主义
- 哲学思想:以最简单和直接的方式解决问题。
- 编程思想:通过使用固定大小数组和简单的条件判断,消除了不必要的复杂性,使代码更加简洁和高效。
通用编程思想
- 封装
- 将数据和操作封装在一起,隐藏实现细节,提高代码的模块化和可维护性。例如,Stack 结构体和相关操作函数实现了栈的封装。
- 单一职责原则
- 每个函数或模块只负责一项功能,提高代码的可读性和可维护性。例如,push、pop、isEmpty 等函数各自负责栈的不同操作。
- 责任分离
- 将不同的功能和职责分离,避免模块之间的耦合。例如,状态机将状态转换逻辑和输入处理逻辑分离,提高了代码的灵活性。
- 高内聚,低耦合
- 模块内部的各个部分紧密相关,而不同模块之间尽量松散耦合。例如,栈的操作函数和主逻辑分开实现,提高了代码的可维护性和复用性。
- 资源管理
- 合理管理资源的分配和释放,避免资源泄漏和浪费。例如,动态数组在使用完后需要释放内存。
通过这些哲学和编程思想,可以更好地理解和优化代码,提高其性能、可维护性和扩展性。
举一反三
理解和应用编程哲学和思想可以大大提升你的编程技巧,以下是一些具体的技巧和建议,帮助你在编程时举一反三:
1. 动态内存管理
技巧:
- 避免硬编码的大小:尽量不要使用硬编码的大小限制,除非你非常确定数据不会超过这个限制。
- 动态扩展:学习如何使用动态内存分配(如 malloc、realloc 和 free),以便根据需要动态扩展数据结构的大小。
- 内存管理工具:熟悉和使用内存调试工具(如 Valgrind)来检测内存泄漏和越界访问。
举一反三:
- 在处理不确定大小的数据(如文件、网络数据流)时,考虑使用动态数组或链表等数据结构。
- 当需要高效、快速地处理大量数据时,考虑使用哈希表、红黑树等动态数据结构。
2. 抽象与封装
技巧:
- 模块化设计:将复杂的功能拆分为多个小模块,每个模块只负责一个功能。
- 接口与实现分离:通过定义接口(如函数原型、类接口)来隐藏实现细节,减少模块之间的依赖。
- 使用标准库:尽量使用标准库提供的数据结构和算法,避免重复造轮子。
举一反三:
- 在设计大型系统时,考虑使用设计模式(如工厂模式、单例模式)来提高代码的可维护性和可扩展性。
- 在团队开发中,制定和遵循接口规范,确保各模块之间的接口一致。
3. 状态化思维与有限状态机
技巧:
- 状态图:在处理复杂流程时,先画出状态图,明确各个状态和状态之间的转换。
- 状态机实现:使用二维数组或哈希表来实现状态机的状态转移,确保状态机的实现简洁明了。
- 测试状态机:通过单元测试,确保状态机在各种输入下都能正确运行。
举一反三:
- 在处理交互式应用(如游戏、用户界面)时,使用状态机来管理应用的各个状态。
- 在处理协议解析(如网络协议、文件格式)时,使用状态机来确保解析过程的正确性和可靠性。
4. 预分配与空间时间权衡
技巧:
- 预估数据规模:在启动时预估数据规模,并进行合理的资源分配,避免频繁的动态内存分配。
- 缓冲区:在处理流数据时,使用缓冲区来提高数据处理效率。
- 空间换时间:在对时间效率要求较高的场景中,可以适当增加内存使用,以换取更快的执行速度。
举一反三:
- 在处理大量数据时,考虑使用批处理和缓冲区技术来减少处理开销。
- 在实现缓存机制时,考虑使用合适的缓存策略(如 LRU 缓存)来提高系统性能。
5. 高内聚低耦合
技巧:
- 单一职责原则:确保每个模块或函数只有一个明确的责任,提高代码的可读性和可维护性。
- 接口隔离:设计良好的接口,确保模块之间的依赖最小化。
- 依赖注入:通过依赖注入减少模块之间的耦合,提高模块的可测试性和灵活性。
举一反三:
- 在设计类和模块时,考虑使用面向对象的设计原则(如 SOLID 原则)来提高代码质量。
- 在大型项目中,使用依赖注入框架(如 Spring)来管理依赖关系,提高代码的灵活性和可维护性。
6. 资源管理
技巧:
- 使用 RAII(资源获取即初始化):在 C++ 等语言中,通过构造函数获取资源,析构函数释放资源,确保资源的正确释放。
- 引用计数:使用引用计数技术来管理共享资源,避免资源泄漏和双重释放。
- 自动垃圾回收:在支持垃圾回收的语言中,尽量使用语言提供的自动垃圾回收机制,减少手动管理内存的复杂性。
举一反三:
- 在处理文件、网络连接等资源时,确保在使用完后正确关闭和释放资源。
- 在多线程环境中,使用智能指针或其他线程安全的资源管理技术,确保资源的正确释放和管理。
总结
通过理解这些编程思想和哲学,并在实际编程中应用这些技巧,你可以更高效地解决问题,提高代码的质量和可维护性。同时,保持学习和实践,不断积累经验,能够在各种编程场景中举一反三,灵活应用这些思想和技巧。