【C++&string】寻找字符串中第一个唯一字符:两种经典解法详解

引言

在字符串处理问题中,寻找第一个不重复的字符是一个经典面试题。这类问题不仅考察对字符串处理的基本功,还考察对数据结构和算法的灵活应用。本文将详细分析两种不同的解法,并探讨它们的优缺点。

目录

引言

问题描述

[解法一:两次遍历 + 固定数组(哈希表)](#解法一:两次遍历 + 固定数组(哈希表))

算法分析

核心思路

时间复杂度

空间复杂度

适用条件

解法二:使用哈希表(更通用)

算法对比

算法优化与变种

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

[2. 同时记录索引和频率](#2. 同时记录索引和频率)

处理扩展字符集

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

[2. 大小写不敏感版本](#2. 大小写不敏感版本)

实际应用场景

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

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

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

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

测试用例设计

性能优化技巧

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

[2. 使用队列优化](#2. 使用队列优化)

常见错误与注意事项

[1. 字符集假设错误](#1. 字符集假设错误)

[2. 索引越界](#2. 索引越界)

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

扩展问题

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

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

总结


问题描述

给定一个字符串 s,找到字符串中第一个不重复的字符,并返回它的索引。如果不存在这样的字符,则返回 -1。

示例 1:

text

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

示例 2:

text

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

示例 3:

text

复制代码
输入:s = "aabb"
输出:-1
解释:没有不重复的字符

解法一:两次遍历 + 固定数组(哈希表)

cpp

复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        // 统计每个字符出现的次数
        int count[26] = {0};  // 假设字符串只包含小写字母
        
        // 第一次遍历:统计频率
        for(auto ch : s) {
            count[ch - 'a']++;  // 将字符映射到0-25的索引
        }
        
        // 第二次遍历:找到第一个频率为1的字符
        for(int i = 0; i < s.size(); i++) {
            if(count[s[i] - 'a'] == 1) {
                return i;  // 返回索引
            }
        }
        
        return -1;  // 没有找到唯一字符
    }
};

算法分析

核心思路
  1. 频率统计:使用大小为26的数组统计每个小写字母出现的次数

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

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

时间复杂度
  • 第一次遍历:O(n),统计字符频率

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

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

空间复杂度
  • O(1),使用固定大小的数组(26个元素)
适用条件
  • 字符串只包含小写英文字母

  • 字符集有限且已知

解法二:使用哈希表(更通用)

cpp

复制代码
class Solution {
public:
    int firstUniqChar(string s) {
        // 使用哈希表统计频率
        unordered_map<char, int> frequency;
        
        // 统计每个字符出现的次数
        for(char ch : s) {
            frequency[ch]++;
        }
        
        // 查找第一个唯一字符
        for(int i = 0; i < s.size(); i++) {
            if(frequency[s[i]] == 1) {
                return i;
            }
        }
        
        return -1;
    }
};

算法对比

特性 固定数组法 哈希表法
字符集 仅限小写字母 任意字符
空间复杂度 O(1) O(k),k为字符集大小
平均查找时间 O(1) O(1)
内存使用 固定26个int 动态分配
适用场景 已知有限字符集 任意字符集

算法优化与变种

1. 使用位运算优化空间

如果只需要判断字符是否重复,可以使用位运算:

cpp

复制代码
int firstUniqChar(string s) {
    int seen = 0;  // 记录已出现的字符
    int repeated = 0;  // 记录重复的字符
    
    for(char ch : s) {
        int mask = 1 << (ch - 'a');
        if(seen & mask) {
            repeated |= mask;  // 标记为重复
        } else {
            seen |= mask;  // 标记为已出现
        }
    }
    
    for(int i = 0; i < s.size(); i++) {
        int mask = 1 << (s[i] - 'a');
        if(!(repeated & mask)) {
            return i;
        }
    }
    
    return -1;
}

2. 同时记录索引和频率

cpp

复制代码
int firstUniqChar(string s) {
    // 使用vector存储字符的最后出现位置和频率
    vector<pair<int, int>> freq(26, {-1, 0});
    
    for(int i = 0; i < s.size(); i++) {
        int idx = s[i] - 'a';
        if(freq[idx].first == -1) {
            freq[idx].first = i;  // 记录第一次出现的位置
        }
        freq[idx].second++;  // 增加频率
    }
    
    int result = INT_MAX;
    for(auto& p : freq) {
        if(p.second == 1) {
            result = min(result, p.first);
        }
    }
    
    return result == INT_MAX ? -1 : result;
}

处理扩展字符集

1. Unicode字符支持

cpp

复制代码
#include <unordered_map>
#include <string>

int firstUniqChar(string s) {
    // 使用unordered_map支持任意Unicode字符
    unordered_map<char32_t, int> frequency;
    
    // 统计频率
    for(int i = 0; i < s.size(); ) {
        // 处理UTF-8编码的字符
        char32_t codePoint = getNextCodePoint(s, i);
        frequency[codePoint]++;
    }
    
    // 查找第一个唯一字符
    for(int i = 0; i < s.size(); ) {
        char32_t codePoint = getNextCodePoint(s, i);
        if(frequency[codePoint] == 1) {
            return i;
        }
    }
    
    return -1;
}

2. 大小写不敏感版本

cpp

复制代码
int firstUniqCharCaseInsensitive(string s) {
    int count[26] = {0};
    
    // 统计频率(忽略大小写)
    for(char ch : s) {
        if(ch >= 'A' && ch <= 'Z') {
            count[ch - 'A']++;
        } else if(ch >= 'a' && ch <= 'z') {
            count[ch - 'a']++;
        }
    }
    
    // 查找第一个唯一字符
    for(int i = 0; i < s.size(); i++) {
        int idx;
        if(s[i] >= 'A' && s[i] <= 'Z') {
            idx = s[i] - 'A';
        } else if(s[i] >= 'a' && s[i] <= 'z') {
            idx = s[i] - 'a';
        } else {
            continue;  // 非字母字符
        }
        
        if(count[idx] == 1) {
            return i;
        }
    }
    
    return -1;
}

实际应用场景

1. 文本编辑器

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

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

2. 数据清洗

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

  • 检测重复的条目

3. 密码学

  • 分析密码中的字符分布

  • 检测弱密码模式

4. 自然语言处理

  • 分析文本中的特殊字符

  • 识别语言中的独特字母

测试用例设计

全面的测试应该包括:

cpp

复制代码
void testFirstUniqChar() {
    Solution solution;
    
    // 基本测试
    assert(solution.firstUniqChar("leetcode") == 0);
    assert(solution.firstUniqChar("loveleetcode") == 2);
    assert(solution.firstUniqChar("aabb") == -1);
    
    // 边界测试
    assert(solution.firstUniqChar("") == -1);
    assert(solution.firstUniqChar("a") == 0);
    assert(solution.firstUniqChar("aa") == -1);
    
    // 特殊字符测试
    assert(solution.firstUniqChar("aabcc") == 2);
    assert(solution.firstUniqChar("abcabc") == -1);
    assert(solution.firstUniqChar("z") == 0);
    
    // 混合测试
    assert(solution.firstUniqChar("dddccdbba") == 8);
    assert(solution.firstUniqChar("itwqbtcdprfsuprkrjkausiterybzncbmdvkgljxuekizvaivszowqtmrttiihervpncztuoljftlxybpgwnjb") == 61);
}

性能优化技巧

1. 提前终止

如果可以在统计频率时就确定结果,可以提前终止:

cpp

复制代码
int firstUniqChar(string s) {
    int count[26] = {0};
    vector<int> firstIndex(26, -1);
    
    for(int i = 0; i < s.size(); i++) {
        int idx = s[i] - 'a';
        count[idx]++;
        if(firstIndex[idx] == -1) {
            firstIndex[idx] = i;
        }
    }
    
    int result = INT_MAX;
    for(int i = 0; i < 26; i++) {
        if(count[i] == 1) {
            result = min(result, firstIndex[i]);
        }
    }
    
    return result == INT_MAX ? -1 : result;
}

2. 使用队列优化

对于流式数据,可以使用队列:

cpp

复制代码
class StreamChecker {
private:
    queue<char> charQueue;
    int count[26] = {0};
    
public:
    // 添加字符到流中
    void addChar(char ch) {
        int idx = ch - 'a';
        count[idx]++;
        charQueue.push(ch);
        
        // 移除队首重复字符
        while(!charQueue.empty() && count[charQueue.front() - 'a'] > 1) {
            charQueue.pop();
        }
    }
    
    // 获取当前第一个唯一字符
    char getFirstUnique() {
        return charQueue.empty() ? '#' : charQueue.front();
    }
};

常见错误与注意事项

1. 字符集假设错误

cpp

复制代码
// 错误:假设输入只有小写字母
int firstUniqChar(string s) {
    int count[26] = {0};
    for(char ch : s) {
        count[ch - 'a']++;  // 如果ch是大写字母或其他字符,这里会出错
    }
    // ...
}

2. 索引越界

cpp

复制代码
// 错误:没有检查字符范围
int idx = s[i] - 'a';  // 如果s[i]不是小写字母,idx可能是负数或很大
count[idx]++;  // 可能访问非法内存

3. 忘记处理空字符串

cpp

复制代码
// 错误:没有处理空字符串
int firstUniqChar(string s) {
    // 如果s为空,s.size()-1会是很大的正数(无符号整数下溢)
    for(int i = 0; i < s.size(); i++) {
        // ...
    }
}

扩展问题

1. 找到第二个唯一字符

cpp

复制代码
int secondUniqChar(string s) {
    int count[26] = {0};
    int first = -1, second = -1;
    
    // 统计频率
    for(char ch : s) {
        count[ch - 'a']++;
    }
    
    // 找到第一个和第二个唯一字符
    for(int i = 0; i < s.size(); i++) {
        if(count[s[i] - 'a'] == 1) {
            if(first == -1) {
                first = i;
            } else if(second == -1) {
                second = i;
                return second;
            }
        }
    }
    
    return -1;
}

2. 找到第一个重复字符

cpp

复制代码
int firstRepeatingChar(string s) {
    unordered_set<char> seen;
    
    for(int i = 0; i < s.size(); i++) {
        if(seen.count(s[i])) {
            return i;
        }
        seen.insert(s[i]);
    }
    
    return -1;
}

总结

寻找字符串中第一个唯一字符的问题看似简单,但蕴含了许多重要的编程概念:

  1. 数据结构选择:根据字符集大小选择数组或哈希表

  2. 时间空间权衡:不同的解法在时间和空间复杂度上的取舍

  3. 边界条件处理:空字符串、单个字符、全重复字符等情况

  4. 编码细节:字符编码、索引计算、越界检查等

关键要点

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

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

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

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

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

相关推荐
FluxMelodySun2 小时前
机器学习(二十九) 稀疏表示与字典学习(LASSO算法、KSVD算法、奇异值分解)
人工智能·算法·机器学习
木下~learning2 小时前
零基础Git入门:Linux+Gitee实战指南
linux·git·gitee·github·虚拟机·版本控制·ubunt
LG.YDX2 小时前
笔试训练48天:跳台阶
数据结构·算法
汀、人工智能2 小时前
[特殊字符] 第42课:对称二叉树
数据结构·算法·数据库架构·图论·bfs·对称二叉树
zh_xuan2 小时前
修改远程仓库名以及和本地工程同步
git
jwn9992 小时前
Laravel11.x新特性全解析
android·开发语言·php·laravel
feifeigo1232 小时前
航天器交会的分布式模型预测控制(DMPC)MATLAB实现
开发语言·分布式·matlab
于先生吖2 小时前
支持二开与商用,JAVA 漫剧付费观看系统完整源码
java·开发语言
环黄金线HHJX.2 小时前
【从0到1】
开发语言·人工智能·算法·交互