【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. 编码细节:字符编码、索引计算、越界检查等

关键要点

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

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

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

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

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

相关推荐
chaofan98016 小时前
GPT-5.5 全压力测试:为什么 API 聚合调度是解决“首字延迟”的技术关键?
开发语言·人工智能·python·gpt·自动化·api
William_wL_16 小时前
【C++】stack和queue的使用和实现(附加deque的简单介绍)
开发语言·c++
山甫aa16 小时前
二叉树遍历----从零开始的数据结构
数据结构·c++·二叉树
xlq2232216 小时前
1.git
git
hhb_61816 小时前
D架构底层调度与性能优化实践指南
开发语言
grant-ADAS16 小时前
Overlay套刻测量
算法
猿长大人16 小时前
算法 | Douglas-Peucker 拯救“腰椎间盘突出的三角形”
算法
忡黑梨16 小时前
eNSP_ACL原理及应用
运维·服务器·网络·tcp/ip·github·负载均衡
秋916 小时前
Java AI编程工具全景解析:功能、收费与工单系统实战指南
java·开发语言·ai编程
会编程的土豆16 小时前
【go】 Go语言中的 defer:从入门到理解底层机制(讲透版)
开发语言·后端·golang