CSDN 教程:C++ 经典字符串与栈算法题逐行详解
本文为你整理并补充了逐行注释的 C++ 代码示例,覆盖常见的字符串、栈和哈希题目。每段代码都配有:
- 算法思路与要点
- 逐行注释(面向初学者,尽量解释每个语句的目的)
- 复杂度分析与注意事项
目录
- 有效括号匹配(Valid Parentheses)
- 字符串相加(大整数相加)
- 二进制加法(字符串)
- 翻转句中单词(C 风格字符串)
- 反转字符串中的元音字母
- 验证几乎回文(最多删除一个字符)
- 按字符频率排序
- 统计句中单词数(两种方法)
- 前 K 个高频单词(桶排序法)
- 检测大写字母使用规则
- 最长公共前缀
有效括号匹配(Valid Parentheses)
思路要点:遇到左括号入栈,遇到右括号时检查栈顶是否匹配;遍历结束栈应为空。
cpp
// 判断括号字符串是否有效
bool isValid(string s) {
stack<char> st; // 使用栈来存放左括号
for (char ch : s) {
if (ch == '(' || ch == '[' || ch == '{')
st.push(ch); // 左括号直接入栈
else if (ch == ')' || ch == ']' || ch == '}') {
// 遇到右括号需要检查栈顶是否存在对应的左括号
if (st.empty())
return false; // 没有可匹配的左括号
char c = st.top(); // 取出栈顶左括号,但暂时不弹出
// 如果类型不匹配(比如 ')' 对应不是 '('),则无效
if ((ch == ')' && c != '(') || (ch == ']' && c != '[') || (ch == '}' && c != '{'))
return false;
else
st.pop(); // 匹配成功,弹出栈顶
} else
return false; // 出现非括号字符,按题意可判定为无效(视题目而定)
}
// 最终栈空表示所有左括号都被匹配
return st.empty();
}
复杂度:时间 O(n),空间 O(n)(最坏情况所有字符都是左括号)。
注意:若题目允许字符串含其他字符,需要在 else 分支做相应处理。
字符串相加(大整数相加)
思路要点:从低位(字符串末尾)逐位相加,处理进位,最后将结果反转。
cpp
string addStrings(string num1, string num2) {
int i = num1.length() - 1, j = num2.length() - 1, add = 0; // i, j 指向两个字符串的末尾
string ans = ""; // 存放临时结果(逆序)
// 当任一字符串未遍历完或还有进位时继续循环
while (i >= 0 || j >= 0 || add != 0) {
int x = i >= 0 ? num1[i] - '0' : 0; // 取当前位数字,边界外视为 0
int y = j >= 0 ? num2[j] - '0' : 0;
int result = x + y + add; // 当前位相加 + 进位
ans.push_back('0' + result % 10); // 将个位加入结果(字符形式)
add = result / 10; // 更新进位(0 或 1 或更大,但这里是十进制)
i--; j--; // 移动到更高一位
}
reverse(ans.begin(), ans.end()); // 结果当前为低到高,反转为正常顺序
return ans; // 返回字符串形式的和
}
复杂度:时间 O(max(len1, len2)),空间 O(max(len1, len2)).
提示:处理大数时不要把字符串转为整数(会溢出),应使用该逐位相加方式。
二进制加法(字符串)
思路要点:与十进制字符串相加类似,但进位规则为除以 2。
cpp
string addBinary(string a, string b) {
string s; // 存放逆序结果
int c = 0; // 进位(0 或 1)
int sizea = a.size(), sizeb = b.size();
// 当任一字符串还有字符(size>0)时继续
while (sizea + sizeb) {
int num1 = 0, num2 = 0;
if (sizea) num1 = a[--sizea] - '0'; // 先自减再取字符
if (sizeb) num2 = b[--sizeb] - '0';
s += char((num1 + num2 + c) % 2 + '0'); // 当前位(二进制)
c = (num1 + num2 + c) / 2; // 更新进位
}
if (c) s += '1'; // 循环结束后若还有进位,加上最高位
reverse(s.begin(), s.end()); // 翻转为正确顺序
return s;
}
复杂度:同上,时间 O(max(len(a), len(b))).
注意:循环条件 while (sizea + sizeb)
利用非零判断,等价于 while(sizea>0 || sizeb>0)
。
翻转句中单词(C 风格字符串)
思路要点:遍历 C 字符串,遇到空格或终止符时把当前单词区间反转。
cpp
// 原地交换字符(用于 C 风格字符串)
void swap(char *str, int start, int end) {
while (start < end) {
char temp = str[start];
str[start] = str[end];
str[end] = temp;
start++; end--;
}
}
// 翻转句子中的每个单词
char* reverseWords(char *s) {
int start = 0, end = 0;
// 用 while 检测 '\0',注意末尾条件处理
while (s[end++] != '\0') {
// 当遇到空格或字符串末尾(s[end]==' ' 或 '\0')时,反转当前单词
if (s[end] == ' ' || s[end] == '\0') {
swap(s, start, end - 1); // 反转单词区间
start = end + 1; // 下个单词的起点
}
}
return s; // 返回原指针(原地修改)
}
复杂度:时间 O(n),空间 O(1)。
注意:此实现对连续多个空格或字符串边界需要谨慎处理,实际使用时建议做更严谨的边界判断。
反转字符串中的元音字母
思路要点:双指针从两端扫描,交换遇到的元音字符,跳过非元音字符。
cpp
// 判断是否是元音(含大小写)
bool isyuanyin(char c) {
return c=='a'||c=='o'||c=='e'||c=='i'||c=='u'||
c=='A'||c=='O'||c=='E'||c=='I'||c=='U';
}
string reverseVowels(string s) {
int left = 0, right = s.size() - 1;
while (left < right) {
// 向右移动 left 直到遇到元音或左右指针相遇
while (left < right && !isyuanyin(s[left])) left++;
// 向左移动 right 直到遇到元音或左右指针相遇
while (left < right && !isyuanyin(s[right])) right--;
// 交换两个元音
swap(s[left++], s[right--]);
}
return s;
}
复杂度:时间 O(n),空间 O(1)。
边界:若字符串无元音,则不会发生交换,直接返回原字符串。
验证几乎回文(最多删除一个字符)
思路要点:双指针判断,遇到不匹配时分别尝试跳过左侧或右侧一个字符再判断剩下子串是否为回文。
cpp
// 辅助函数:判断 s[left..right] 是否是回文
bool isPalindromeRange(const string &s, int left, int right) {
while (left < right) {
if (s[left] != s[right]) return false;
left++; right--;
}
return true;
}
bool validPalindrome(string s) {
int left = 0, right = s.size() - 1;
while (left < right) {
if (s[left] == s[right]) {
left++; right--;
} else {
// 尝试跳过左边或右边的字符
return isPalindromeRange(s, left + 1, right) || isPalindromeRange(s, left, right - 1);
}
}
return true; // 全程未冲突
}
复杂度:时间 O(n),空间 O(1)。
注:避免复杂的计数逻辑,直接用两个简单的子回文检查更清晰、更可靠。
按字符频率排序
思路要点:统计每个字符出现次数,利用"桶"按频率分组,再按频率降序输出字符。
cpp
string frequencySort(string s) {
unordered_map<char, int> mp;
int maxFreq = 0;
for (auto &ch : s) maxFreq = max(maxFreq, ++mp[ch]);
// buckets[i] 存放出现 i 次的字符(注意使用字符串或 vector<char> 均可)
vector<string> buckets(maxFreq + 1);
for (auto m : mp) buckets[m.second].push_back(m.first);
string ret;
// 从高频到低频输出
for (int i = maxFreq; i > 0; i--) {
string &bucket = buckets[i];
for (auto &ch : bucket) {
// 将字符重复 i 次附加到结果
for (int k = 0; k < i; k++) ret.push_back(ch);
}
}
return ret;
}
复杂度:时间 O(n + sigma)(sigma 为字符种类数),空间 O(n).
提示:当字符集有限时(如 ASCII),桶法非常高效。
统计句中单词数(两种方法)
方法一:使用 stringstream(简洁且能自动跳过空白)
cpp
int countSegments(string s) {
stringstream ss(s);
string st;
int n = 0;
while (ss >> st) ++n; // >> 会自动跳过任意空白
return n;
}
方法二:手动扫描(避免额外流对象开销)
cpp
int countSegments(string s) {
s += ' '; // 在末尾添加空格,方便判断最后一个单词
int cnt = 0;
for (int i = 0; i < (int)s.size() - 1; i++) {
if (s[i] != ' ' && s[i + 1] == ' ') cnt++;
}
return cnt;
}
复杂度:均为 O(n)。
注意:手动扫描方法对多种空白字符需要额外判断(制表符、换行等)。
前 K 个高频单词(桶排序法)
思路要点:统计词频(哈希),按频率分桶,桶内按字典序排序,然后从高频桶依次取前 k 个。
cpp
vector<string> topKFrequent(vector<string>& words, int k) {
unordered_map<string, int> mp; // 统计频率
int freq = 0;
for (auto &word : words) freq = max(freq, ++mp[word]);
// buckets[i] 存放出现 i 次的单词
vector<vector<string>> buckets(freq + 1);
for (auto &m : mp) buckets[m.second].push_back(m.first);
// 桶内按字典序升序排序(题目要求频率相同则字典序从小到大)
for (auto &bucket : buckets) sort(bucket.begin(), bucket.end());
vector<string> res;
for (int i = freq; i > 0 && k > 0; i--) {
for (string &word : buckets[i]) {
res.push_back(word);
if (--k == 0) break; // 取够 k 个即停
}
}
return res;
}
复杂度:统计 O(n),桶分配 O(n),桶内排序最坏 O(m log m),总体约 O(n log n)(m 为词种类数)。
小技巧:若词种类很多且 k 很小,可用堆(priority_queue)做更优实现(O(n log k))。
检测大写字母使用规则(Detect Capital Use)
思路要点:统计大写字母数量,然后判断是否属于三种合法情况之一。
cpp
bool isbig(char ch) { return (ch >= 'A' && ch <= 'Z'); }
bool detectCapitalUse(string word) {
int upperCount = 0;
for (char ch : word) if (isbig(ch)) upperCount++;
// 三种合法情况:全大写、全小写、仅首字母大写
return upperCount == (int)word.size() || upperCount == 0 || (upperCount == 1 && isbig(word[0]));
}
复杂度:时间 O(n),空间 O(1)。
最长公共前缀(Longest Common Prefix)
思路要点:以第一个字符串为参考,逐字符比较其余字符串的对应位置,遇到不一致就返回。
cpp
string longestCommonPrefix(vector<string>& strs) {
if (strs.empty()) return "";
for (int i = 0; i < (int)strs[0].size(); ++i) {
char c = strs[0][i];
for (int j = 1; j < (int)strs.size(); ++j) {
if (i == (int)strs[j].size() || strs[j][i] != c) {
// 一旦出现短串或字符不相等,返回 strs[0] 的前 i 个字符
return strs[0].substr(0, i);
}
}
}
return strs[0]; // 第一字符串完全是公共前缀
}
复杂度:时间 O(n * m),其中 n 为字符串数量,m 为最短字符串长度。
结语与建议
- 以上算法均为面试常见题目,掌握后能应对大量字符串与哈希相关问题。
- 编写代码时要注意边界条件(空串、单字符、所有字符相同、包含特殊字符等)。
- 对于需要稳定排序(例如基数排序),务必使用稳定子排序或反向遍历计数排序以保持稳定性。
- 当题目对性能要求较高且 k 小,优先考虑使用堆(priority_queue)优化到 O(n log k)。