从暴力到最优:三道 C 语言入门题的解法思路

做算法题有个挺有意思的规律:题目越简单,越能看出思路的差距。暴力解法往往很快就能写出来,但想出更优雅的解法,可能需要多琢磨一会儿。下面这三道题都是 LeetCode 上的简单题,我把自己第一次做时的思路和后来学到的更好写法整理了一下。


题一:存在重复元素

题目:给一个整数数组,判断里面有没有重复的数字。

我最先想到的写法

看到"有没有重复",第一反应肯定是拿每个数跟后面的数都比一遍:

c 复制代码
#include <stdbool.h>

bool containsDuplicate(int nums[], int numsSize) {
    for (int i = 0; i < numsSize; i++) {
        for (int j = i + 1; j < numsSize; j++) {
            if (nums[i] == nums[j]) {
                return true;
            }
        }
    }
    return false;
}

这代码写起来很顺,但问题也很明显:两层循环,时间复杂度是 O(n²)。数组稍长一点,LeetCode 就会报超时。

更好的思路:排个序再看

其实有个特别简单的转化:如果数组是有序的,相同的数字一定会挨在一起。那我们先排序,然后只需要扫一遍,看看相邻的两个数相不相等就行了。

为了代码好懂,这里用冒泡排序(纯数组下标操作,不涉及指针):

c 复制代码
#include <stdbool.h>

bool containsDuplicate(int nums[], int numsSize) {
    // 冒泡排序
    for (int i = 0; i < numsSize - 1; i++) {
        for (int j = 0; j < numsSize - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
            }
        }
    }
    
    // 排序后检查相邻元素
    for (int i = 0; i < numsSize - 1; i++) {
        if (nums[i] == nums[i + 1]) {
            return true;
        }
    }
    return false;
}

关键点 :排序把时间复杂度降到了 O(n log n) (虽然教学代码里写的是冒泡,实际工程中换成快速排序就行),后面的扫描是 O(n)。整体比暴力解法快太多了。


题二:罗马数字转整数

题目:给一个罗马数字字符串,转成对应的整数。

我最先想到的写法

一开始我直接把六种减法特例全写成了 if-else,比如看到 I 就判断后面是不是 VX,是的话就减 1,否则加 1:

c 复制代码
int romanToInt(char s[]) {
    int result = 0;
    int len = 0;
    while (s[len] != '\0') len++;
    
    for (int i = 0; i < len; i++) {
        if (s[i] == 'I') {
            if (i + 1 < len && (s[i+1] == 'V' || s[i+1] == 'X')) {
                result -= 1;
            } else {
                result += 1;
            }
        } else if (s[i] == 'X') {
            if (i + 1 < len && (s[i+1] == 'L' || s[i+1] == 'C')) {
                result -= 10;
            } else {
                result += 10;
            }
        }
        // ... 后面还有 C、V、L、D、M 的一大堆判断
        // 代码太长,这里省略了
    }
    return result;
}

这代码能跑,但写起来特别繁琐,而且逻辑分散。万一规则再多几条,代码还会继续膨胀。

更好的思路:其实规律只有一条

仔细观察会发现,罗马数字的减法规则可以归纳成一句话:

如果当前字符的值 < 右边字符的值,当前字符做减法;否则做加法。

比如 IV:I(1) < V(5),所以 I 减;VI:V(5) >= I(1),所以 V 加。

利用这个规律,我们建一个"字符到数值"的映射表,然后从左到右扫一遍就行:

c 复制代码
int romanToInt(char s[]) {
    int map[256] = {0};
    map['I'] = 1;
    map['V'] = 5;
    map['X'] = 10;
    map['L'] = 50;
    map['C'] = 100;
    map['D'] = 500;
    map['M'] = 1000;
    
    int result = 0;
    int len = 0;
    while (s[len] != '\0') len++;
    
    for (int i = 0; i < len; i++) {
        int current = map[(unsigned char)s[i]];
        int next = (i + 1 < len) ? map[(unsigned char)s[i + 1]] : 0;
        
        if (current < next) {
            result -= current;
        } else {
            result += current;
        }
    }
    return result;
}

关键点 :把一堆特例压缩成了一个统一的判断条件,代码简洁了很多。时间复杂度 O(n) ,空间复杂度 O(1),已经是这道题的最优解。


题三:最长公共前缀

题目:给一个字符串数组,找出所有字符串的最长公共前缀。

我最先想到的写法

我当时的做法是:先求第 0 个和第 1 个字符串的公共前缀,再用这个结果和第 2 个字符串求公共前缀,依此类推。

c 复制代码
// 辅助函数:求两个字符串的公共前缀长度
int commonPrefix(char a[], char b[]) {
    int i = 0;
    while (a[i] != '\0' && b[i] != '\0' && a[i] == b[i]) {
        i++;
    }
    return i;
}

char* longestCommonPrefix(char** strs, int strsSize) {
    if (strsSize == 0) return "";
    
    int minLen = commonPrefix(strs[0], strs[1]);
    
    for (int i = 2; i < strsSize; i++) {
        int len = commonPrefix(strs[0], strs[i]);
        if (len < minLen) minLen = len;
    }
    
    strs[0][minLen] = '\0';
    return strs[0];
}

这个思路没问题,但写起来比较繁琐,需要额外的辅助函数,而且每次两两比较时,前面的字符会被反复扫描。

更好的思路:纵向扫描

换个角度看问题:公共前缀其实就是所有字符串在相同位置上的字符都一样。那我们可以一列一列地检查,而不是一个字符串一个字符串地比较。

c 复制代码
char* longestCommonPrefix(char** strs, int strsSize) {
    if (strsSize == 0) return "";
    
    int col = 0;  // 当前检查第几列
    
    while (1) {
        char c = strs[0][col];  // 拿第一个字符串的第 col 个字符当基准
        
        if (c == '\0') break;   // 第一个字符串到头了
        
        int match = 1;
        for (int row = 1; row < strsSize; row++) {
            if (strs[row][col] != c) {
                match = 0;
                break;
            }
        }
        
        if (!match) break;  // 这一列有不匹配的,前缀到此为止
        
        col++;
    }
    
    strs[0][col] = '\0';  // 截断,剩下的就是最长公共前缀
    return strs[0];
}

关键点:把"字符串之间的比较"转化成了"矩阵按列的检查"。一旦发现某一列不匹配,立刻停止,不需要再往后看。代码更紧凑,而且避免了重复比较。

相关推荐
土司大王13 分钟前
LeetCode hot100——随机链表的复制
算法·leetcode·链表
叠层归一研究院14 分钟前
螺旋时空:E框架的整合宇宙论
人工智能·经验分享·算法·机器学习·agi
sunoo-22917 分钟前
【Linux 系统编程】学习第七天:线程属性 | 互斥锁 | 死锁 | 信号量 核心知识点 + 踩坑实战
linux·c语言·笔记·vscode·学习
磁场转动100万匹25 分钟前
线性回归算法实现训练数据集、测试数据集的缺失值填充
算法·机器学习·线性回归
小雪崩28 分钟前
嵌入式学习 day33:线程进阶
linux·c语言·学习
郑州光合科技余经理38 分钟前
本地生活平台搭建:统一订单表与多后台切换怎么拆
java·开发语言·前端·系统架构·uni-app·php·ai编程
天下无敌笨笨熊43 分钟前
C#视频开发心得
开发语言·c#·音视频
乐迪信息1 小时前
防爆AI摄像机实时监测船舶异常姿态
大数据·人工智能·算法·安全·目标跟踪
淼澄研学1 小时前
基于Docker与iptables的AI智能体网络隔离实操指南
开发语言·python