【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. 性能优化:选择合适的算法和数据结构

关键要点

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

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

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

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

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

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

相关推荐
孬甭_2 小时前
字符函数及字符串函数
c语言·开发语言
summerkissyou19872 小时前
git-命令大全
git
摇滚侠2 小时前
Java 进阶教程,全面剖析 Java 多线程编程
java·开发语言
KevinCyao2 小时前
php彩信接口代码示例:PHP使用cURL调用彩信网关发送图文消息
android·开发语言·php
金融Tech趋势派2 小时前
Hermes Agent开源45天登顶GitHub,深度解析其记忆机制与部署方案
人工智能·微信·开源·github·企业微信·openclaw·hermes agent
装疯迷窍_A2 小时前
以举证方位线生成工具为例,分享如何在Arcgis中创建Python工具箱(含源码)
开发语言·python·arcgis·变更调查·举证照片
楼田莉子2 小时前
Linux网络:IP协议
linux·服务器·网络·c++·学习·tcp/ip
网域小星球2 小时前
C 语言从 0 入门(二十五)|位运算与位段:底层开发、嵌入式核心
c语言·开发语言