文章目录

  • 基本知识简单总结
    • 模式匹配
    • 最长回文子串
    • 前缀匹配
    • 扩展和补充*
  • C++11常见API
  • References:

字符串也是一个高频考察点。
虽然可以和数组考点合并,但由于该场景许多优化空间大。问题典型:如子序列和子数组问题。
容易和比较重要的算法思想:如单调栈,滑动窗口,动态规划结合。
并且有些题目的编码细节比较多。
经常面试和笔试题都喜欢问及。

这里先总结基础知识(这里主要针对字符串数组查找算法,并给出源代码)和常用API,具体试题留在下周进行总结。
如字符串匹配,字典树,最长回文子串问题

基本知识简单总结

字符串可以看做一个数组,内部存储的元素为char类型。
所以数组的基本操作字符串都能够进行处理。
由于字符串独特的一些场景。所以关于字符串的特殊的优化算法,相关操作有很多。这里主要展开以下几个场景进行介绍:

  1. 模式匹配(KMP算法)
  2. 最长回文子串(Manacher算法)
  3. 前缀匹配(Trie结构/字典树/前缀树)

模式匹配

  1. 场景描述
    求解串A与子串B的单一匹配问题。
    换句话说,如果B串在A串中存在子串,就返回B串在A串出现的第一个位置(不存在返回-1)=>
    leetcode原题:实现Strstr()
    一个直观的解题想法如下(暴力匹配)

    class Solution {public:int strStr(string haystack, string needle) {for (int i = 0; i + needle.size() <= haystack.size(); ++i) { //遍历的起始位置点,注意细节,不要用-;而是+(unsigned int!!)bool flag = true; //是否完美匹配for (int j = 0; j < needle.size(); ++j) { //每个位置点进行遍历,查找是否与needle匹配if (haystack[i + j] != needle[j]) {flag = false;break;}}if (flag) {return i;}}return -1;}
    };
    

    由于每次匹配,失败从头匹配。导致回溯,时间复杂度为O(n*m),空间复杂度为O(1)。

  2. 算法描述
    KMP算法=> 主要利用空间换时间的方法。(考虑到很多博客已经详细讲解这一过程,我们此处只是将该过程关键点简单梳理一次
    1.由于每次回溯匹配是失败的,那么如果我们直接跳转到重新匹配的新位置,我们就能够继续匹配,减少从头跳转的时间开销。
    2.跳转到对应位置,我们仅需要通过模式串自身就能够构造->Next数组。
    3.Next的数组含义是指匹配失败时候,模式串指向j应该跳转到对应模式串哪一个位置。
    4.这个关键点在于找到该位置前面的字符串,后缀和前缀最长匹配长度即可。
    =>所以整体空间复杂度为O(m)。
    时间复杂度考虑到主串的指针没有进行任何回溯,模式串能够在有限次数内完成回溯,所以匹配过程中总的时间复杂度为O(n)。对于整体算法:时间复杂度为O(n+m)

  3. 核心代码示例

    class Solution {public:int strStr(string haystack, string needle) {if (needle.size() == 0) {return 0;}vector<int> next(needle.size());  //首先构建next数组// 表示失败匹配后,模式串指针j指向的位置// 这个指向位置表示当前匹配失败位置前面字符串,它的前缀与后缀最长匹配长度// for (int i = 1, j = 0; i < needle.size(); ++i) {// i表示匹配失败的位置,j表示i位置下前缀与后缀最长匹配长度;// 如果移动到i位置,前缀失效;// 这里有一个小优化:由于之前的最长匹配前缀已经保存在next,可以考虑直接跳转判断while (needle[i] != needle[j] && j > 0) {j = next[j - 1]; //最难理解就是这一句话-> 等价与 j = next[next[i - 1]-1];// 找到 i - 1 前位置最长前缀的前一个位置重新尝试匹配。}// 否则一直匹配计算if (needle[i] == needle[j]) {++j;}next[i] = j;}// 开始匹配for (int i = 0, j = 0; i < haystack.size(); ++i) {while (haystack[i] != needle[j] && j > 0) {j = next[j - 1];}if (haystack[i] == needle[j]) {++j;}if (j == needle.size()) {return i - needle.size() + 1;}}return -1;}
    };
    
  4. 总结和补充
    这个是一个经典的算法(数据结构都会讲解)。
    难度有一定难,代码却十分简单。
    关键为了减少从头回溯,构造next数组。
    构造next数组关键在前缀和后缀最长匹配。
    代码中求解前缀和后缀最长匹配失效,使用一个小的优化策略:不停寻找i-1最长前缀的下标的前一个位置。

最长回文子串

  1. 场景描述
    最长回文子串
    找出一个字符串的其中一个最长回文子串。
    一个简单的想法:枚举所有子串,检查每一个子串是否为字符串的回文子串,然后求解最长回文子串。
    时间复杂度O(n3)O(n^3)O(n3),空间复杂度O(1)。
    下面对其进行优化(主要针对时间复杂度)
  2. 算法描述和代码示例
    1)动态规划(对左右端点进行枚举)

    class Solution {public:string longestPalindrome(string s) {int len = s.size();if (len < 2) {return s;}int max_len = 1, beg = 0;vector<vector<bool>> dp(len, vector<bool>(len, true));for (int sub_len = 2; sub_len <= len; ++sub_len) { // 由于状态转移方程的递推方式。// 必须先枚举子串长度,然后枚举右边端点for (int left = 0; left < len; ++left)  { // 枚举子串长度int right = left + sub_len - 1;if (right >= len) {break; // 停止改点的枚举}if (s[left] != s[right]) { //判断端点dp[left][right] = false;} else {if (right - left < 3) { // 长度为1或者2,直接跳过dp[left][right] = true;} else {dp[left][right] = dp[left + 1][right - 1];}}if (dp[left][right] && sub_len > max_len) {max_len = sub_len;beg = left;}}}return s.substr(beg, max_len);}
    };
    

    2)中心扩展(对回文串中心点进行枚举)

    class Solution {public:string longestPalindrome(string s) {int start = 0, end = 0;for (int left = 0; left < s.size(); ++left) { // 以left进行枚举auto[left1, right1] = palindrome(s, left, left); // 以left为中点扩展auto[left2, right2] = palindrome(s, left, left + 1); // 以left,left + 1为中点扩展if (right1 - left1 > end - start) {start = left1;end = right1;}if (right2 - left2 > end - start) {start = left2;end = right2;} // 判断两个情况哪一种较长}return s.substr(start, end - start + 1);}pair<int, int> palindrome(string s, int left, int right) {while (left >= 0 && right < s.size() && s[left] == s[right]) {--left;++right;}return {left + 1, right - 1};}
    };
    

    3)Manacher算法(内涵详细注释)
    (代码参考该视频:https://www.bilibili.com/video/BV13g41157hK?p=14)

    class Solution {// 预处理步骤,可以不考虑两种回文子串的情况。void predeal(string& s) {string ans = "#";for (char ch : s) {ans += ch;ans += '#';}ans += '#';s = ans;}// 后处理步骤,解码回需要的子串string postdeal(int max_left, int maxlen, string& s) {string ans = "";for (int offset = 0; offset  < maxlen; ++offset) {if (s[max_left + offset] != '#') {ans += s[max_left + offset];} }return ans;}
    public:string longestPalindrome(string s) {predeal(s);// 这里的优化主要利用回文字符串对称性,从左往右枚举中心点遍历时候必然有一些区间字符对应相等。// 保存这些信息,就可以减少查找的过程,直接跳转。// 类似KMP算法匹配字符串,利用匹配失效后。由于失效匹配前缀必然向前有一段可能与模式串由前向后某一段完全匹配。// 保存这些重复段信息,直接查找,减少回溯,提出:最长匹配前缀和最长匹配后缀概念=> Next数组保存当前最长匹配前缀长度信息。// 这些信息全部包含在以下两个概念中:// 1. 每一个字符的回文半径C(需要保存每一个,所以需要创建一个数组)// 2. 从左往右遍历能够抵达的最长回文区域点Rightvector<int> Radius(s.size()); //回文半径数组int Center = -1; // 到达最右右边界的回文串中心int Right = -1; // 回文右边界终止位置  ... Right -1]  Right ...int max_left = 0, maxlen = 1;// 1) pos 在 R 外, 无优化,继续遍历// 2) pos 在 R 内 // 设R回文串中心为C// pos': pos关于C的回文点,其回文到达左边界为left = pos' - Radius[pos'];// a. left 在 C - R 右边(包含在当前最右的回文串内) 该点回文半径为Radius[pos']// b. left 在 C - R 左边(不包含在当前最右回文串内) 该点回文半径为R - pos;// c. left 落在 C - R 上 , 不需要验证的点为 R - pos ,从该点开始继续遍历for (int pos = 0; pos < s.size(); ++pos) {// 上述情况中,至少不用检验的区域Radius[pos] = Right > pos ?  min(Radius[2 * Center - pos], Right - pos) : 1;// 不管哪种情况,都尝试往外扩展试试while (pos + Radius[pos] < s.size() && pos - Radius[pos] > -1) {if (s[pos + Radius[pos]] == s[pos - Radius[pos]]) {++Radius[pos];} else {break;}}// 更新Right 和 Centerif (pos + Radius[pos] > Right) {Right = pos + Radius[pos];Center = pos;}// 记录最长回文数组左端点和长度if (maxlen < Radius[pos] * 2 - 1) {maxlen = Radius[pos] * 2 - 1;max_left = pos - Radius[pos] + 1;}}return postdeal(max_left, maxlen, s);}
    };
    
  3. 总结和补充
    一般字符串的问题可以考虑从以某一个端点(i为尾的子串)的方式进行枚举。

前缀匹配

  1. 场景描述
    实现前缀树
    字典序的第k小数字

  2. 核心代码示例
    实现一个前缀树,本质上是一个简单的n叉树。核心代码如下所示:

    class Trie {map<char, Trie*> children;bool isend;Trie* SearchPrefix(string word) {Trie* node = this;for (char ch : word) {if (node->children.count(ch) == 0) {return nullptr;}node = node->children[ch];}return node;}public:/** Initialize your data structure here. */Trie(): isend(false) {}/** Inserts a word into the trie. */void insert(string word) {Trie* node = this;for (char ch : word) {if (node->children.count(ch) == 0) {node->children[ch] = new Trie();}node = node->children[ch];}node->isend = true;}/** Returns if the word is in the trie. */bool search(string word) {Trie* node = this->SearchPrefix(word);return node != nullptr && node->isend;}/** Returns if there is any word in the trie that starts with the given prefix. */bool startsWith(string prefix) {return this->SearchPrefix(prefix) != nullptr;}};/*** Your Trie object will be instantiated and called as such:* Trie* obj = new Trie();* obj->insert(word);* bool param_2 = obj->search(word);* bool param_3 = obj->startsWith(prefix);*/
    
  3. 总结和补充
    可以考虑完成一下题目:
    a. 添加与搜索单词 - 数据结构设计
    b. 单词搜索 II
    c. 数组中两个数的最大异或值
    d. 与数组中元素的最大异或值

扩展和补充*

这里只补充一个,其他有时间可以看看下面参考link(如: AC自动机, Z数组)

  1. 字符串哈希 + 滚动哈希
    关键把字符串映射为一个值,这个值和字符串一一对应!

    一个简单的想法是将字符串使用M进制表示,对于str=“abcd”,其hash值为:
    hash[str]=((a∗M+b)∗M+c)∗M+dhash[str] = ((a * M + b) * M + c)* M + dhash[str]=((a∗M+b)∗M+c)∗M+d

    为了避免冲突,M需要取很大的质数。而计算机表示数字优先。这里就需要取一个模(大小为P)。
    (思路大致如以上所示,但是具体实践中需要多次尝试,给出一个代码模板:)

    using ULL = unsigned long long;const ULL BASE = 13331; // Base, 可以考虑其他素数:31,
    const ULL MOD = 10e9 + 7; // MOD 也是数组最大长度// 进行字符串编码
    UUL encode(const string& str) {UUL ans = 0;for (auto ch : str) {ans *= BASE;ans += ch;ans %= MOD;// ans = (ans * BASE + ch) % MOD;}return ans;
    }
    

    当然,如果单一hash值感觉还会导致冲突,可以考虑使用两个BASE 和 MOD。最后存储为一个pair<unsigned long long, unsigned long long>即可。
    该方法可以解决一下问题:(此处参考这个link)
    这里只举出一个:KMP算法中模式匹配问题:

    class Solution {using ULL = unsigned long long;const ULL Base = 29;const ULL MOD = 1e9 + 7;ULL encode(const string& str) {ULL ans = 0;for (auto ch : str) {ans *= Base;ans += ch;ans % MOD;}return ans;}
    public:int strStr(string haystack, string needle) {if (haystack.size() == 0) {return 0;}if (haystack.size() < needle.size()) {return -1;}ULL temp_str = encode(needle);vector<ULL> my_str(haystack.size() - needle.size() + 1); for (int i = 0; i + needle.size() <= haystack.size(); ++i) {my_str[i] = encode(haystack.substr(i, needle.size()));}for (int i = 0; i < my_str.size(); ++i) {if (my_str[i] == temp_str) {return i;}}return -1;}
    };
    

    严格优化后如下

    class Solution {using ULL = unsigned long long;const ULL Base = 29;const ULL MOD = 1e9 + 7;public:int strStr(string haystack, string needle) {if (haystack.size() == 0) {return 0;}if (haystack.size() < needle.size()) {return -1;}ULL temp_str = 0; //模式串对应hash值ULL ans_str = 0; // 答案串对应hash值ULL mul = 1; // 滚动哈希最后一位乘数值 , 注意为 needle.size() - 1 的幂次for (int i = 0; i < needle.size(); ++i) {temp_str = (temp_str * Base + needle[i] - 'a') % MOD;ans_str = (ans_str * Base + haystack[i] - 'a') % MOD;if (i + 1 < needle.size()) {mul = (mul * Base) % MOD;}} if (temp_str == ans_str) { // 第一次就找到了return 0; }for (int i = 1; i + needle.size() <= haystack.size(); ++i) {ans_str = (ans_str - mul * (haystack[i - 1]  - 'a') + MOD ) * Base % MOD; //为了防止滚动到之前位置,我们加上一个MODans_str = (ans_str + (haystack[i + needle.size() - 1] - 'a')) % MOD;if (ans_str == temp_str) {return i;}}return -1;}
    };
    
  2. 其他参考link


C++11常见API

这里主要是对string类型的介绍:

  1. 创建,增删改查(基本与vector类似)

    基本参考vector,这里省略。

    string str("acds");
    string str1{"abcd"};
    string str2{"ddef"};
    //比较特殊变量:
    string::npos; //通常表示索引函数find没有查找成功,类型为size_type, 值为-1.// 一些特殊的函数
    str.data() //返回存储数据的指针
    str.c_str() //返回一个C类型指针
    // 注意以上两个指针类型都是 const char *// 备注:+ 同时可以拼接两个字符串,要求第一个一定为String类型,+后面可以为字符串常量。
    // append() 表示在后面追加,一般用法如下
    str.append(5, 'a');
    str1.append(str2);
    str1.append(str2, 1, 3); //从pos = 1位置,添加长度len = 3,不写默认添加到最后
    str.append("cdd", 5); // 从头到 len = min(添加字符串长度, 5)部分加入str尾部// 替换部分
    str.replace(1, 2, "heat"); // 把str 1位置后2个字符换成"heat"str.replace(0, 2, str2, 1, 2); //表示str pos = 0开始后面2个位置用str2 pos = 1后面2个位置替换。
    // 1, 2 不写默认替换为str2全部
    // 不写2,表示从0开始替换1个字符str.replace(str.begin(), str.begin() + 2, 3, 'A'); //把两个迭代器区域(前闭后开),换成3个'A'
    str.replace(str.begin(), str.begin() + 2 , str2); //str2 替换掉部分
    str.replace(str.begin(), str.begin() + 2 , "heat", 2); // str 0 - 1 部分替换为"heat"前面两个字符str.replace(0 , 2, 10 , 'a');// 0 -> 1区域用10个'a'替换
    str.replace(str.begin(), str.begin() + 1, 10 , 'a');// 上述迭代器版本
  2. 获取子串

    str.sub_str(0, 2);// 获取0位置开始,长度为2的子串
    //不写长度为2,默认到最后一个字符
    
  3. 查找子串

    // 前缀和后缀=>返回true/ false;
    str.starts_with(prefix);
    str.ends_with(suffix);// 查找(都只查找第一个)
    str.find(str1, 2);// 从pos = 2时开始找str1,默认pos = 0;
    str.find("ab", 1);// 从pos = 1开始查找“ab”, 默认pos = 0;
    str.find('a', 2);// 从pos = 2时开始找'a',默认pos = 0; // 其余类似:
    // rfind()
    // find_first_of(), find_first_not_of
    // find_last_of()
    // find_last_not_of()
  4. 与其他类型相互转换

    stoi(str, nullptr, 10);
    stol(str, nullptr, 10);
    stoll(str, nullptr, 10);
    // 从第一个位置开始,找到第一个非空格字符,尽可能多的使用字符形成整数,其中base = 10;
    // 在nullptr处传入变量地址可以返回获得数字长度(size_t)
    // 如果第一个非空字符不是合法符号,抛出std::invalid_argument异常。// 类似的有stof,stod,stold
    // stoul, stoull// 其他类型转字符串
    to_string(123);
    // 转为宽字符串
    to_wstring(1);
    

References:

  1. String API部分: https://en.cppreference.com/w/cpp/string/basic_string
  2. Manacher算法代码参考:https://www.bilibili.com/video/BV13g41157hK?p=14
  3. Trie部分参考:https://www.bilibili.com/video/BV1zz4y1S7Je
  4. 其他字符串算法技巧解析参考:https://oi-wiki.org/string/

欢迎评论指正和补充

0x00000005 3.数据结构和算法 基础数据结构 字符串(上)相关推荐

  1. 【python】一道LeetCode搞懂递归算法!#131分割回文串 #以及刷LeetCode的一点点小心得 [数据结构与算法基础]

    题目:给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串.返回 s 所有可能的分割方案. # 示例 输入: "aab" 输出: [["aa",&q ...

  2. python数据结构与算法知识点_数据结构和算法基础知识点(示例代码)

    数据结构和算法基础知识点 链表 1.链表是一种由节点组成的线性数据集合,每个节点通过指针指向下一个节点.它是 一种由节点组成,并能用于表示序列的数据结构. 2.单链表:每个节点仅指向下一个节点,最后一 ...

  3. 数据结构与算法基础(java版)

    目录 数据结构与算法基础(java版) 1.1数据结构概述 1.2算法概述 2.1数组的基本使用 2.2 数组元素的添加 2.3数组元素的删除 2.4面向对象的数组 2.5查找算法之线性查找 2.6查 ...

  4. 数据结构与算法--基础篇

    目录 概念 常见的数据结构 常见的算法 算法复杂度 空间复杂度 时间复杂度 数据结构与算法基础 线性表 数组 链表 栈 队列 散列表 递归 二分查找 概念 常见的数据结构 常见的算法 算法复杂度 空间 ...

  5. 【数据结构与算法基础】哈夫曼树与哈夫曼编码(C++)

    前言 数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷. 也因如此,它作为博主大二上学期最重 ...

  6. 数据结构与算法基础-学习-19-哈夫曼解码

    一.个人理解 哈夫曼树和哈夫曼编码相关概念.代码.实现思路分享,请看之前的文章链接<数据结构与算法基础-学习-17-二叉树之哈夫曼树>.<数据结构与算法基础-学习-18-哈夫曼编码& ...

  7. 【Java面试高频问题】Java数据结构和算法基础知识汇总

    文章目录 Java数据结构和算法基础知识 一.Java数据结构 1. 线性结构:数组.队列.链表和栈 1.1 数组(Array) 1.2 稀疏数组 1.3 队列(Queue) 1.4 链表(Linke ...

  8. java算法概述,Java数据结构与算法基础(一)概述与线性结构

    Java数据结构与算法基础(二)递归算法 Java数据结构与算法基础(一)概述与线性结构 学习目的:为了能更顺畅的读很多底层API代码和拓宽解决问题的思路 一.数据结构概述 1.数据结构是什么?数据与 ...

  9. 数据结构与算法基础-青岛大学-王卓

    数据结构与算法基础(青岛大学-王卓)_哔哩哔哩_bilibili 文章目录: 第一章:数据结构的基本概念 1.逻辑结构的种类 2.存储结构的种类 ​3.抽象数据类型的形式定义 4.Complex抽象书 ...

最新文章

  1. linux kvm百度云,如何在 Ubuntu Linux 上使用 KVM 云镜像
  2. [Oracle]Oracle 各产品的 生命周期
  3. POJ 2151 Check the difficulty of problems (概率dp)
  4. 微信登录电脑,手机接收消息仍有提示音设置方法
  5. A Deep Reinforcement Learning Network for Traffic Light Cycle Control 【论文阅读】
  6. Java04-day04【IDEA(概述、启动配置、项目结构、内容辅助键、快捷键、模块操作)、数组(定义详解、动态初始化、访问)、内存分配、数组内存图】
  7. 综合模拟试题计算机指南,综合全国计算机文管二级模拟试题.doc
  8. linux制作成后台服务,把dotnetcore 控制台app设置成linux后台服务
  9. 原生javascript淡入淡出焦点图 + Jquery实现方法
  10. 洛谷 P3958 奶酪
  11. Atitit 编程语言语言规范总结 目录 1. 语言规范 3 2. Types 3 2.1.1. Primitive types 3 2.1.2. Compound types 4 3. State
  12. JAVA mysql 常用面试题
  13. 关闭Typora拼写检查功能
  14. 解决阿里巴巴JSONObject工具 com.alibaba.fastjson.JSONObject cannot be cast to 的问题
  15. 打字母案例完整版(C#)
  16. PTA 哈夫曼树与哈夫曼编码
  17. JS中Object的方法汇总,包括assign、create、prototype等等
  18. flash与服务端的交互方法
  19. 洛谷P1781宇宙总统
  20. 有一个学霸对象是什么体验?

热门文章

  1. c++中的typeid和typeof
  2. 深入浅出极大似然估计
  3. 【Anaconda安装与使用】
  4. 离散数学-图论-欧拉图、哈密顿图、二部图、平面图(14)
  5. 瞧不起,与 “瞧不起”
  6. 手撕深度学习中的优化器
  7. Java程序中操作Word表格
  8. UNR#2 梦中的题面 HDU6056
  9. ACL2022 事件抽取
  10. 【日拱一卒行而不辍20220926】自制操作系统