【C++】寻找字符串中第一个只出现一次的字符

引言

在字符串处理中,寻找第一个不重复的字符是一个常见问题。这类问题在面试、文本处理和数据分析中都有广泛应用。本文将详细分析一个高效的算法,用于找到字符串中第一个只出现一次的字符。

目录

引言

问题描述

算法实现

算法详解

核心思想

算法步骤

关键代码分析

时间复杂度与空间复杂度

时间复杂度:O(n)

空间复杂度:O(1)

算法优化

[1. 使用哈希表支持任意字符](#1. 使用哈希表支持任意字符)

[2. 使用队列优化流式处理](#2. 使用队列优化流式处理)

[3. 使用位运算优化空间](#3. 使用位运算优化空间)

边界条件处理

[1. 空字符串](#1. 空字符串)

[2. 非小写字母字符](#2. 非小写字母字符)

[3. Unicode字符支持](#3. Unicode字符支持)

测试用例

全面测试

实际应用场景

[1. 文本编辑器](#1. 文本编辑器)

[2. 数据清洗](#2. 数据清洗)

[3. 密码学](#3. 密码学)

[4. 自然语言处理](#4. 自然语言处理)

[5. 编译器设计](#5. 编译器设计)

扩展问题

[1. 找到第二个唯一字符](#1. 找到第二个唯一字符)

[2. 找到第一个重复字符](#2. 找到第一个重复字符)

[3. 统计所有唯一字符](#3. 统计所有唯一字符)

性能优化技巧

[1. 提前终止查找](#1. 提前终止查找)

[2. 使用vector代替数组](#2. 使用vector代替数组)

[3. 使用范围for循环](#3. 使用范围for循环)

[4. 使用string_view避免拷贝](#4. 使用string_view避免拷贝)

常见错误与注意事项

[1. 数组越界](#1. 数组越界)

[2. 未初始化变量](#2. 未初始化变量)

[3. 输出格式错误](#3. 输出格式错误)

[4. 忘记处理空字符串](#4. 忘记处理空字符串)

总结


问题描述

给定一个字符串,找到字符串中第一个只出现一次的字符,并返回该字符。如果不存在这样的字符,则返回特定值(通常是-1或空字符)。

示例 1:

text

复制代码
输入:str = "leetcode"
输出:'l'
解释:字符 'l' 是第一个只出现一次的字符

示例 2:

text

复制代码
输入:str = "loveleetcode"
输出:'v'
解释:字符 'v' 是第一个只出现一次的字符

示例 3:

text

复制代码
输入:str = "aabb"
输出:-1 或 '\0'
解释:没有只出现一次的字符

算法实现

cpp

复制代码
#include <iostream>
using namespace std;

int main() {
    string str;
    cin >> str;  // 读取输入字符串
    
    // 创建频率统计数组,假设字符串只包含小写字母
    int freq[26] = {0};
    
    // 第一次遍历:统计每个字符出现的次数
    for(auto ch : str) {
        freq[ch - 'a']++;
    }
    
    // 第二次遍历:按原始顺序查找第一个频率为1的字符
    char result = '\0';
    for(int i = 0; i < str.size(); i++) {
        if(freq[str[i] - 'a'] == 1) {
            result = str[i];
            break;
        }        
    }
    
    // 输出结果
    if(result == '\0') {
        cout << -1;  // 没有找到唯一字符
    } else {
        cout << result;
    }
    
    return 0;
}

算法详解

核心思想

  1. 频率统计:使用固定大小的数组统计每个字符出现的次数

  2. 顺序查找:再次遍历字符串,按顺序检查每个字符的出现次数

  3. 返回结果:找到第一个出现次数为1的字符并返回

算法步骤

text

复制代码
输入:str = "leetcode"

步骤1:统计频率
  l:1, e:3, t:1, c:1, o:1, d:1

步骤2:顺序查找
  索引0: 'l' → 频率=1 → 找到结果,返回'l'
  
输出:'l'

关键代码分析

cpp

复制代码
// 1. 创建频率数组,假设只有小写字母
int freq[26] = {0};

// 2. 字符到索引的映射
freq[ch - 'a']++;  // 'a'→0, 'b'→1, ..., 'z'→25

// 3. 查找第一个唯一字符
if(freq[str[i] - 'a'] == 1) {
    result = str[i];
    break;  // 找到后立即退出,提高效率
}

时间复杂度与空间复杂度

时间复杂度:O(n)

  • 第一次遍历:O(n),统计字符频率

  • 第二次遍历:O(n),查找第一个唯一字符

  • 总时间复杂度:O(2n) = O(n),其中n是字符串长度

空间复杂度:O(1)

  • 使用固定大小的数组(26个元素)

  • 不随输入字符串长度变化

算法优化

1. 使用哈希表支持任意字符

cpp

复制代码
#include <iostream>
#include <unordered_map>
using namespace std;

int main() {
    string str;
    cin >> str;
    
    unordered_map<char, int> freq;
    
    // 统计频率
    for(char ch : str) {
        freq[ch]++;
    }
    
    // 查找第一个唯一字符
    char result = '\0';
    for(char ch : str) {
        if(freq[ch] == 1) {
            result = ch;
            break;
        }
    }
    
    if(result == '\0') {
        cout << -1;
    } else {
        cout << result;
    }
    
    return 0;
}

2. 使用队列优化流式处理

cpp

复制代码
#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;

int main() {
    string str;
    cin >> str;
    
    queue<char> q;
    unordered_map<char, int> freq;
    
    for(char ch : str) {
        freq[ch]++;
        q.push(ch);
        
        // 移除队首重复字符
        while(!q.empty() && freq[q.front()] > 1) {
            q.pop();
        }
    }
    
    if(q.empty()) {
        cout << -1;
    } else {
        cout << q.front();
    }
    
    return 0;
}

3. 使用位运算优化空间

cpp

复制代码
#include <iostream>
using namespace std;

int main() {
    string str;
    cin >> str;
    
    // 使用两个位掩码:一个记录出现过的字符,一个记录重复的字符
    int seen = 0;
    int repeated = 0;
    
    for(char ch : str) {
        int mask = 1 << (ch - 'a');
        if(seen & mask) {
            repeated |= mask;  // 标记为重复
        } else {
            seen |= mask;      // 标记为已出现
        }
    }
    
    // 查找第一个唯一字符
    char result = '\0';
    for(char ch : str) {
        int mask = 1 << (ch - 'a');
        if(!(repeated & mask)) {
            result = ch;
            break;
        }
    }
    
    if(result == '\0') {
        cout << -1;
    } else {
        cout << result;
    }
    
    return 0;
}

边界条件处理

1. 空字符串

cpp

复制代码
if(str.empty()) {
    cout << -1;
    return 0;
}

2. 非小写字母字符

cpp

复制代码
// 扩展数组大小以支持所有ASCII字符
int freq[256] = {0};  // 支持所有ASCII字符

for(char ch : str) {
    if(ch >= 0 && ch < 256) {
        freq[ch]++;  // 直接使用字符作为索引
    }
}

3. Unicode字符支持

cpp

复制代码
#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;

int main() {
    wstring str;
    wcin >> str;  // 宽字符输入
    
    unordered_map<wchar_t, int> freq;
    
    // 统计频率
    for(wchar_t ch : str) {
        freq[ch]++;
    }
    
    // 查找第一个唯一字符
    wchar_t result = L'\0';
    for(wchar_t ch : str) {
        if(freq[ch] == 1) {
            result = ch;
            break;
        }
    }
    
    if(result == L'\0') {
        wcout << -1;
    } else {
        wcout << result;
    }
    
    return 0;
}

测试用例

全面测试

cpp

复制代码
#include <iostream>
#include <cassert>
using namespace std;

char firstUniqueChar(const string& str) {
    int freq[26] = {0};
    
    for(char ch : str) {
        freq[ch - 'a']++;
    }
    
    for(char ch : str) {
        if(freq[ch - 'a'] == 1) {
            return ch;
        }
    }
    
    return '\0';
}

void test() {
    // 基础测试
    assert(firstUniqueChar("leetcode") == 'l');
    assert(firstUniqueChar("loveleetcode") == 'v');
    assert(firstUniqueChar("aabb") == '\0');
    
    // 边界测试
    assert(firstUniqueChar("") == '\0');
    assert(firstUniqueChar("a") == 'a');
    assert(firstUniqueChar("aa") == '\0');
    assert(firstUniqueChar("ab") == 'a');
    
    // 特殊测试
    assert(firstUniqueChar("z") == 'z');
    assert(firstUniqueChar("abcabc") == '\0');
    assert(firstUniqueChar("abca") == 'b');
    assert(firstUniqueChar("abcba") == 'c');
    
    cout << "所有测试通过!" << endl;
}

int main() {
    test();
    return 0;
}

实际应用场景

1. 文本编辑器

  • 查找文档中首次出现的特殊字符

  • 实现"查找下一个唯一标识符"功能

2. 数据清洗

  • 在数据集中查找唯一的标识符

  • 检测重复的条目

3. 密码学

  • 分析密码中的字符分布

  • 检测弱密码模式

4. 自然语言处理

  • 分析文本中的特殊字符

  • 识别语言中的独特字母

5. 编译器设计

  • 查找代码中未重复使用的变量名

  • 识别唯一的标识符

扩展问题

1. 找到第二个唯一字符

cpp

复制代码
char secondUniqueChar(const string& str) {
    int freq[26] = {0};
    
    // 统计频率
    for(char ch : str) {
        freq[ch - 'a']++;
    }
    
    int count = 0;
    // 按顺序查找第二个频率为1的字符
    for(char ch : str) {
        if(freq[ch - 'a'] == 1) {
            count++;
            if(count == 2) {
                return ch;
            }
        }
    }
    
    return '\0';
}

2. 找到第一个重复字符

cpp

复制代码
char firstRepeatingChar(const string& str) {
    bool seen[26] = {false};
    
    for(char ch : str) {
        int idx = ch - 'a';
        if(seen[idx]) {
            return ch;
        }
        seen[idx] = true;
    }
    
    return '\0';
}

3. 统计所有唯一字符

cpp

复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

vector<char> allUniqueChars(const string& str) {
    int freq[26] = {0};
    vector<char> result;
    
    // 统计频率
    for(char ch : str) {
        freq[ch - 'a']++;
    }
    
    // 收集唯一字符
    for(char ch : str) {
        if(freq[ch - 'a'] == 1) {
            result.push_back(ch);
        }
    }
    
    // 去重(可选)
    sort(result.begin(), result.end());
    result.erase(unique(result.begin(), result.end()), result.end());
    
    return result;
}

性能优化技巧

1. 提前终止查找

cpp

复制代码
// 如果可以在统计频率时就确定结果,可以提前终止
// 但需要额外存储字符的第一次出现位置

2. 使用vector代替数组

cpp

复制代码
vector<int> freq(26, 0);  // 动态大小,更安全

3. 使用范围for循环

cpp

复制代码
for(auto ch : str) {  // C++11特性,更简洁
    freq[ch - 'a']++;
}

4. 使用string_view避免拷贝

cpp

复制代码
char firstUniqueChar(string_view str) {  // C++17特性
    int freq[26] = {0};
    
    for(char ch : str) {
        freq[ch - 'a']++;
    }
    
    for(char ch : str) {
        if(freq[ch - 'a'] == 1) {
            return ch;
        }
    }
    
    return '\0';
}

常见错误与注意事项

1. 数组越界

cpp

复制代码
// 错误示例:假设输入只有小写字母
int idx = ch - 'a';  // 如果ch是大写字母或其他字符,idx可能为负数
freq[idx]++;  // 访问非法内存

// 正确做法:检查字符范围
if(ch >= 'a' && ch <= 'z') {
    freq[ch - 'a']++;
}

2. 未初始化变量

cpp

复制代码
// 错误示例
int freq[26];  // 未初始化,内容随机
// 正确做法
int freq[26] = {0};  // 初始化为0

3. 输出格式错误

cpp

复制代码
// 错误示例:直接输出字符值
cout << result;  // 如果result='\0',可能输出空白

// 正确做法:检查特殊值
if(result == '\0') {
    cout << -1;
} else {
    cout << result;
}

4. 忘记处理空字符串

cpp

复制代码
// 错误示例:直接开始统计
// 正确做法:检查空字符串
if(str.empty()) {
    return '\0';
}

总结

寻找字符串中第一个唯一字符的问题展示了几个重要的编程概念:

  1. 频率统计技巧:使用数组或哈希表统计元素出现次数

  2. 两次遍历策略:第一次统计,第二次查找

  3. 边界条件处理:空字符串、无唯一字符等情况

  4. 性能优化:选择合适的算法和数据结构

关键要点

  • 使用频率统计是解决此类问题的核心思路

  • 两次遍历的方法既简单又高效

  • 根据具体需求选择合适的字符表示方法

  • 注意处理各种边界情况和异常输入

掌握这个基本问题不仅能帮助我们在面试中表现出色,还能在实际开发中解决许多类似的问题,如查找第一个重复字符、统计字符频率等。这种"频率统计+顺序查找"的模式是解决许多字符串问题的通用方法。

记住:优秀的算法不仅在于正确性,还在于对边缘情况的全面考虑、代码的可读性和执行效率。通过不断练习和思考这些基础算法,我们可以培养出解决复杂问题的能力。

相关推荐
lbb 小魔仙4 分钟前
Git + Python 项目工作流最佳实践:pre-commit、CI、CHANGELOG 自动化
git·python·ci/cd
geovindu14 分钟前
java: Facade Pattern
java·开发语言·后端·外观模式·结构型模式
listening77715 分钟前
HarmonyOS 6.1 元服务深度优化:从“秒开”到“常驻”的极致体验
java·开发语言·spring
布朗克16818 分钟前
Go 入门到精通-29-测试与基准
开发语言·后端·golang·测试与基准
dong_junshuai1 小时前
每天一个开源项目#46 World Monitor:6.6万星、56层地图的全球情报中枢
github
格林威1 小时前
工业相机Chunk功能全解析:图像嵌入时间戳、编码器元数据(附堡盟C#代码)
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·工业相机
小大宇1 小时前
Python 集成 Conda
开发语言·python·conda
ShineWinsu2 小时前
对于Linux:模版方法类的解析以及socket、TcpSocket的封装
linux·c++·面试·socket·模板方法模式·封装·tcpsocket
还是奇怪2 小时前
Simon Willison 用 DSPy 优化 Datasette Agent 提示词:提示工程正在变成可测试的软件工程
java·开发语言·软件工程
光年像素2 小时前
Windows Win+R 运行框常用快捷命令大全
windows