数据结构
字符串
1 旋转字符串
给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部,
如把字符串“abcdef”前面的2个字符'a'和'b'移动到字符串的尾部,使得原字符串变成字符串“cdefab”。
请写一个函数完成此功能,要求对长度为n的字符串操作的时间复杂度为 O(n),空间复杂度为 O(1)。
解法1.1 暴力移位法 将字符一个一个的移动到尾部 或头部
// 左边的第一个字符移动到最右边
void LeftShiftOne(char* s, int n)
{
char tep = s[0]; // 保存左边的第一个字符 tep
for (int i = 1; i < n; ++i)
{
s[i - 1] = s[i];// 从前向后 靠右的字符 依次向左移动1位
}
s[n - 1] = tep; // tep 放入最右边的位置
}
// 右边的第一个字符移动到 最左边
void RightShiftOne(char* s, int n)
{
char tep = s[n - 1];// 保存右边的最后一个字符 tep
for(int i = n-1; i > 0; --i)
{
s[i] = s[i - 1]; // 从后向前 靠左的字符 依次向右 移动1位
}
s[0] = tep; // tep 放入最左边的位置
}
因此,若要把字符串开头的m个字符移动到字符串的尾部,则可以如下操作:
void LeftRotateSring(char *s, int n, int m)
{
while(m--)
{
LeftShiftOne(s, n);// 每次移动一个字符 一共 移动 m次
}
}
而,若要把字符串尾部的m个字符移动到字符串的头部,则可以如下操作:
void ReftRotateSring(char *s, int n, int m)
{
while(m--)
{
ReftShiftOne(s, n);// 每次移动一个字符 一共 移动 m次
}
}
针对长度为n的字符串来说,假设需要移动m个字符到字符串的尾部,对每个字符需要进行移动n次,那么总共需要 mn 次操作,同时设立一个变量保存第一个字符,如此,时间复杂度为O(m n),空间复杂度为O(1),空间复杂度符合题目要求,但时间复杂度不符合,所以,我们得需要寻找其他更好的办法来降低时间复杂度。
解法1.2:三步反转法
对于这个问题,换一个角度思考一下。
将一个字符串XY分成X(m个字符)和Y(n-m个字符)两个部分,在每部分字符串上定义反转操作,
如X^T,即把X的所有字符反转(如,X="abc",那么X^T="cba"),那么就得到下面的结论:
(X^TY^T)^T=YX,显然就解决了字符串的反转问题。
例如,字符串 abcdef ,若要让def翻转到abc的前头,只要按照下述3个步骤操作即可:
1.首先将原字符串分为两个部分,即X:abc,Y:def;
2.将X反转,X->X^T,即得:abc->cba;将Y反转,Y->Y^T,即得:def->fed。
3.反转上述步骤得到的结果字符串X^TY^T,即反转字符串cbafed的两部分(cba和fed)给予反转,cbafed得到defabc,形式化表示为(X^TY^T)^T=YX,这就实现了整个反转。
三个步骤中后两个步骤都用到了 反转一个字符串
void ReverseString(char *s, int form, int to)
{
while(from < to)// 从首尾开始交换首位位置的值,直到相遇
{
char tep = s[from];// 保存左边
s[from++] = s[to]; //右边 交换给左边 同时左边向右移动一个位置
s[to--] = tep; // 前右边保存的值 交换给左边 同时 右边向左移动一个位置
}
}
// 前 m 后 放在尾部
// 反转[0..m - 1] 反转[m..n - 1] 反转[0..n - 1]
void LeftRotateString(char* s, int n, int m)
{
m %= n;// 避免左移数量大于n的情况
ReverseString(s, 0, m - 1);// 反转[0..m - 1],套用到上面举的例子中,就是X->X^T,即 abc->cba
ReverseString(s, m, n - 1);// 反转[m..n - 1],例如Y->Y^T,即 def->fed
ReverseString(s, 0, n - 1);// 反转[0..n - 1],即如整个反转,(X^TY^T)^T=YX,即 cbafed->defabc。
}
// 后m个 放在 头部
// 反转[0..n - m - 1] 反转[n - m..n - 1] 反转[0..n - 1]
void LeftRotateString(char* s, int n, int m)
{
m %= n;// 避免左移数量大于n的情况
ReverseString(s, 0, n - m - 1);// 反转[0..m - 1],套用到上面举的例子中,就是X->X^T,即 abc->cba
ReverseString(s, n - m, n - 1);// 反转[n - m..n - 1],例如Y->Y^T,即 def->fed
ReverseString(s, 0, n - 1);// 反转[0..n - 1],即如整个反转,(X^TY^T)^T=YX,即 cbafed->defabc。
}
1.3 序列反转 举一反三
1、链表翻转。给出一个链表和一个数k,比如,链表为1→2→3→4→5→6,
k=2,则翻转后2→1→6→5→4→3,
若k=3,翻转后3→2→1→6→5→4,
若k=4,翻转后4→3→2→1→6→5,用程序实现。
注解:原序列 XY 则反转后的序列为 X^T Y^T
2、单词翻转。输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变,句子中单词以空格符隔开。
为简单起见,标点符号和普通字母一样处理。例如,输入“I am a student.”,则输出“student. a am I”。
注解:先将整个字符串反转,在将其中每个单词反转回来
void Reverse_word(char *str)
{
if(str == NULL)// 空指针 返回
return;
int len = strlen(str);// 字符串长度
// 反转整个字符串
ReverseString(str, 0, len - 1);
int s = 0;//每个单词的 起始 index
int e = 0;// 每个单词的 后 index
for(int i = 0; i < len; i++) // 遍历字符串 查找每一个单词
{
e = i;
if(str[e] == ' ')// 此处 遇到空格
{
ReverseString(str, s, e-1); // 反转这个单词
s = e+1;// 更新 单词的起始指针
}
}
ReverseString(str, s, len - 1); // 反转最后一个单词 因为最后一个单词后面无空格
}
2 字符串包含
给定两个分别由字母组成的字符串A和字符串B,字符串B的长度比字符串A短。请问,如何最快地判断字符串B中所有字母是否都在字符串A里?
为了简单起见,我们规定输入的字符串只包含大写英文字母,请实现函数bool StringContains(string &A, string &B)
比如,如果是下面两个字符串:
String 1:ABCD
String 2:BAD
答案是true,即String2里的字母在String1里也都有,或者说String2是String1的真子集,当 String 2:BAE 时答案为false
解法2.1 暴力匹配法
判断string2中的字符是否在string1中?最直观也是最简单的思路是,
针对string2中每一个字符,逐个与string1中每个字符比较,看它是否在String1中。
代码可如下编写:
bool StringContain(string &a, string &b)
{
for(int i = 0; i < b.size(); ++i)
{
int j;
//b中的每一个字符,从a的开头一直向后找,直到出现相同字符/已经到达末尾
for(j = 0; (j < a.size()) && (a[j] != b[i]); ++j) ;
if(j >= a.size()) return false;//如果到达末尾 b中的这个字符 都还没有 在a中找到一样的字符 ,则返回错误
}
return true;//b中每个字符都在 a中找到了 返回 true
}
假设n是字符串String1的长度,m是字符串String2的长度,那么此算法,需要O(n*m)次操作。显然,时间开销太大,应该找到一种更好的办法。
解法2.2 排序后匹配法 如果允许排序的话,我们可以考虑下排序
比如可先对这两个字符串的字母进行排序,然后再同时对两个字串依次轮询。
两个字串的排序需要(常规情况)O(m log m) + O(n log n)次操作,之后的线性扫描需要O(m+n)次操作。
代码可如下编写:
//注意A B中可能包含重复字符,所以注意A下标不要轻易移动。这种方法改变了字符串。如不想改变请自己复制
bool StringContain(string &a,string &b)
{
sort(a.begin(),a.end());// 采用最常用的快速排序
sort(b.begin(),b.end());
for (int pa = 0, pb = 0; pb < b.length();)// b中的每一个字符
{
while ((pa < a.length()) && (a[pa] < b[pb]))// 在a中找
{
++pa;
}
if ((pa >= a.length()) || (a[pa] > b[pb]))//找到最后 或 a中的字符已经大于b对应的字符了 就已经无相等的了
{
return false;
}
//到这里就是 有相同的字符 a[pa] == b[pb]
++pb;//遍历 b中 下一个字符
}
return true;
}
解法2.3 素数乘积取余法
按照从小到大的顺序,用26个素数分别与字符'A'到'Z'一一对应。
遍历长字符串,求得每个字符对应素数的乘积f。
遍历短字符串,判断乘积f 能否被短字符串中的字符对应的素数整除,如果整除则包含该字符,如果不能整除则这个字符不包含。
输出结果。
代码可如下编写:
//此方法只有理论意义,因为整数乘积很大,有溢出风险
// 此种素数相乘的方法看似完美,但缺点是素数相乘的结果容易导致整数溢出。
// 算法的时间复杂度为O(m+n)的最好的情况为O(n)
//(遍历短的字符串的第一个数,与长字符串素数的乘积相除,即出现余数,便退出,返回false)n为长字串的长度,空间复杂度为O(1)
bool StringContain(string &a,string &b)
{
const int p[26] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,61, 67, 71, 73, 79, 83, 89, 97, 101};
int f = 1;
for (int i = 0; i < a.length(); ++i)
{
int x = p[a[i] - 'A'];// 字符对应的素数
if (f % x)// 对于长字符串中出现相同的字符 可不用再次 相乘
{
f *= x;// a中所有不相同字符 对应的素数 的乘积
}
}
for (int i = 0; i < b.length(); ++i)
{
int x = p[b[i] - 'A'];// 字符对应的素数
if (f % x)// 能够被整除 则说明出现相同的字符
{
return false;// 否者出现 了一个不相同的字符 则退出
}
}
return true;
}
解法2.4 Hash 签名查找
事实上,可以先把长字符串a中的所有字符都放入一个Hashtable里,
然后轮询短字符串b,看短字符串b的每个字符是否都在Hashtable里,
如果都存在,说明长字符串a包含短字符串b,否则,说明不包含。
再进一步,我们可以对字符串A,用位运算(26bit整数表示)计算出一个“签名”,再用B中的字符到A里面进行查找。
代码可如下编写:
// “最好的方法”,时间复杂度O(n + m),空间复杂度O(1)
// 这个方法的实质是用一个整数代替了hashtable,空间复杂度为O(1),时间复杂度还是O(n + m)。
bool StringContain(string &a,string &b)
{
int hash = 0;
for (int i = 0; i < a.length(); ++i)
{
hash |= (1 << (a[i] - 'A'));// (a[i] - 'A')为0~25 右移动一位 位或上签名hash 得到 长串a的最后签名
// 相当于把 a的每一个不同的 字符 放入 每一个而进行位上
}
for (int i = 0; i < b.length(); ++i)
{
if ((hash & (1 << (b[i] - 'A'))) == 0)// 位与 上(1 << (b[i] - 'A')) 的0的话就说明对应的位上(字符)与a中午匹配
{
return false;// 对应位上 a中未出现相应的字符 则查找失败
}
}
return true;
}
2.5 字符串包含 举一反三 变位词
如果两个字符串的字符一样,但是顺序不一样,被认为是兄弟字符串。
比如bad和adb即为兄弟字符串,现提供一个字符串,如何在字典中迅速找到它的兄弟字符串,请描述数据结构和查询过程。
解答: a包含b b也包含a
数据结构为:
设置一个64bit的变量long long hash1,将字符串A中的字符通过移位在hash1中表示;设置另外一个64bit的变量long long hash2, 将字符串B中的字符通过移位在hash2中表示。
查询过程:
比较hash1与hash2的大小,若相等,则为兄弟字符串;若不等,则不为兄弟字符串。
代码可如下编写:
bool anagramJudge(string &a, string &b){
long long hash1=0;
long long hash2=0;
for(int i=0; i<a.length(); i++){
hash1 |= (1<<(a[i] - 'A'));// a字符串的 hash 签名
}
for(int j=0; j<b.length(); j++){
hash2 |= (1<<(b[j] - 'A'));// b字符串的 hash 签名
}
if(hash1 == hash2)// 若相等,则为兄弟字符串
return true;
else
return false;// 若不等,则不为兄弟字符串
}
3字符串转换成整数
输入一个由数字组成的字符串,把它转换成整数并输出。
例如:输入字符串"123",输出整数123。
给定函数原型int StrToInt(const char *str) ,
实现字符串转换成整数的功能,不能使用库函数atoi。
3.1不考虑溢出 正负号
以"123"作为例子:
当我们扫描到字符串的第一个字符'1'时,由于我们知道这是第一位,所以得到数字1。
当扫描到第二个数字'2'时,而之前我们知道前面有一个1,
所以便在后面加上一个数字2,那前面的1相当于10,因此得到数字:1*10+2=12。
继续扫描到字符'3','3'的前面已经有了12,由于前面的12相当于120,
加上后面扫描到的3,最终得到的数是:12*10+3=123。
因此,此题的基本思路便是:
从左至右扫描字符串,把之前得到的数字乘以10,再加上当前字符表示的数字。
代码可如下编写:
int StrToInt(const char *str)
{
if(str == NULL) // 空指针返回
return -1;
int n = 0;
int isNeg = 0; // 负数标志
if(str[0] == '-') isNeg = 1;
while (*str != 0) // 遍历到字符串末尾
{
if(*str > '9' || *str < '0') {continue; ++str;}// 跳过非数字字符 或者 直接 退出 return -1;
int c = *str - '0';
n = n * 10 + c;// 累加和
++str; // 移动指针
}
if(isNeg) return -n;
else return n;
}
3.2 完整的考虑溢出等情况
int StrToInt(const char* str)
{
static const int MAX_INT = (int)((unsigned)~0 >> 1);
static const int MIN_INT = -(int)((unsigned)~0 >> 1) - 1;
unsigned int n = 0;
//判断是否输入为空
if (str == 0)
{
return 0;
}
//处理空格 跳过第一个空格
while (isspace(*str))
++str;
//处理正负
int sign = 1;
if (*str == '+' || *str == '-')
{
if (*str == '-')
sign = -1;
++str;// 如果有正负号 确定标志后 从符号后一位开始处理
}
//确定是数字后才执行循环
while ( *str > '0' && *str < '9' )
{
//处理溢出
int c = *str - '0';// 字符转换成 数字
if (sign > 0 && (n > MAX_INT / 10 || (n == MAX_INT / 10 && c > MAX_INT % 10)))
{
n = MAX_INT;
break;
}
else if (sign < 0 && (n >(unsigned)MIN_INT / 10 || (n == (unsigned)MIN_INT / 10 && c > (unsigned)MIN_INT % 10)))
{
n = MIN_INT;
break;
}
//把之前得到的数字乘以10,再加上当前字符表示的数字。
n = n * 10 + c;
++str;
}
return sign > 0 ? n : -n;
}
3.3 举一反三
实现string到double的转换
代码可如下编写:
double StrToDou(const char *str)
{
if(str == NULL) // 空指针返回
return -1;
double result = 0.0;// 最后的值
double dec = 10.0;// 小数位比值
int isNeg = 0; // 负数标志
int isDec = 0;
if(str[0] == '-') isNeg = 1;
while (*str != 0) // 遍历到字符串末尾
{
if(*str == '.') {isDec = 1; ++str;}//是小数
else if(*str > '9' || *str < '0') {continue; ++str;}// 跳过非数字字符 或者 直接 退出 return -1;
if(!isDec) // 整数部分
result = result * 10 + *str - '0';// 整数部分 累加和
else //识别小数点之后进入 这个分支
{
result = result + (*str - '0')/dec;// 小数部分 算法
dec *= 10;// 小数位比值递增
}
++str; // 移动指针
}
if(isNeg) return -result;
else return result;
}
4 字符串 回文 判断
4.1 首尾并进判断法
同时从字符串头尾开始向中间扫描字串,如果所有相对的字符都一样,那么这个字串就是一个回文。
采用这种方法的话,我们只需要维护头部和尾部两个扫描指针即可。 Palindrome 英 ['pælɪndrəʊm] pai lin drow mu
代码可如下编写:
// 输入字符指针 s 和 字符串大小 num
bool IsPalindrome(const char* s, int num)
{
if(s == NULL || num < 2) return false;//指针为空 / 字符串长度小于2
// 定义并初始化 首尾指针
const char* low, *high;// 首尾指针
low = s;// 首指针
high = s + num - 1;// 尾指针
while(low < high){
if(*low ! *high) return false;// 出现一次相对应位置的字符不相同 就返回错误
++low;
--high;
}
return true; // 到这里的话 就证明 相对应位置上的 字符相同 为 回文
}
这是一个直白且效率不错的实现,时间复杂度:O(n),空间复杂度:O(1)。
上述解法一从两头向中间扫描,那么是否还有其它办法呢?我们可以先从中间开始、然后向两边扩展查看字符是否相等。
判断一条单向链表是不是“回文
对于单链表结构,可以用两个指针从两端或者中间遍历并判断对应字符是否相等。
但这里的关键就是如何朝两个方向遍历。由于单链表是单向的,所以要向两个方向遍历的话,可以采取经典的快慢指针的方法,
即先位到链表的中间位置,再将链表的后半逆置,最后用两个指针同时从链表头部和中间开始同时遍历并比较即可。
判断一个栈是不是“回文”
对于栈的话,
只需要将字符串全部压入栈,然后依次将各字符出栈,
这样得到的就是原字符串的逆置串,
将逆置串和原字符串各个字符比较,就可以判断了。
5 最长回文子串
如果一段字符串是回文,那么以某个字符为中心的前缀和后缀都是相同的,
例如以一段回文串“aba”为例,以b为中心,它的前缀和后缀都是相同的,都是a。
那么,我们是否可以可以枚举中心位置,然后再在该位置上用扩展法,记录并更新得到的最长的回文长度呢?
5.1 遍历子串中心位置,找最大回文子串(分为子串长度为 奇数和偶数的情况)
参考代码如下:
int LongestPalindrom(const char* s, int num){
if(s == NULL || num < 2) return false;//指针为空 / 字符串长度小于2
int i,j,max,cnt;// ij 为循环变量 cnt 记录每一个回文子串的长度
max = 0;//最大字回文字符串长度 初始化为0
// 遍历每一个子串 的 中心点
for(i = 0; i < n; ++i){
// 对于子串长度为奇数的情况
for(j = 0; (i - j >= 0)&&(i + j < n); ++j){// j为以i字符点为中心 向两边扩展的间隔 区别首尾区间 [0, n-1]
if(s[i - j] != s[i + j]) break; // 间隔相同的位置 字符不同 结束回文
cnt = j*2 + 1;// 已此中心点i 的回文子串长度为 间隔j *2 + 1(中间一个字符)
}
if(cnt > max) max = cnt;// 保存最长的 回文子串的 长度
// 对于子串 长度为 偶数的情况
for(j = 0; (i - j >= 0)&&(i + j + 1 < n); ++j){
if(s[i - j] != s[i + j +1]) break; // 间隔相同的位置 字符不同 结束回文
cnt = j*2 + 2;
}
if(cnt > max) max = cnt;// 保存最长的 回文子串的 长度
}
return max;
}
6 字符串的全排列
输入一个字符串,打印出该字符串中字符的所有排列。
例如输入字符串abc,则输出由字符a、b、c 所能排列出来的所有字符串
abc、acb、bac、bca、cab 和 cba。
6.1 递归实现
从集合中依次选出每一个元素,作为排列的第一个元素,然后对剩余的元素进行全排列,
如此递归处理,从而得到所有元素的全排列。 Permutation 英 [pɜːmjʊ'teɪʃ(ə)n] Peer mu tai xing
以对字符串abc进行全排列为例,我们可以这么做:以abc为例
abc a和a交换后 abc 对a后的 bc进行全排列
abc a和b交换后 bac 对b后的 ac进行全排列
abc a和c交换后 cba 对c后的 ba进行全排列
参考代码如下:
void CalcAllPermutation(char* s, int from, int to)
{
if (s == NULL || to < 2) return;
// 打印排列后的字符串
if (from == to)/
{
for (int i = 0; i <= to; i++)
cout << perm[i];
cout << endl;
}
else
{
for (int j = from; j <= to; j++)// 从当前位置from开始 依次向后和j位置交换元素后 对from后的元素进行全排列
{
swap(perm[j], perm[from]);// 从当前位置from开始 依次向后和j位置交换元素
CalcAllPermutation(perm, from + 1, to);// 对from后的元素 [from + 1, to]进行全排列
swap(perm[j], perm[from]);// 再还原字符串
}
}
}
习题集
1 第一个只出现一次的字符
在一个字符串中找到第一个只出现一次的字符。如输入abaccdeff,则输出b。
遍历法
//O(n^2)的时间复杂度
char FirstNotRepeatingChar(char *pS, int num)
{
//如果是空指针,返回\0
if(pS == NULL) return '\0';
int i,j,flag=0;
bool* bap;
bap = (bool*)maoolc(sizeof(bool)*num);
for(i =0; i < n; ++i) *bap[i] = false;
for(i=0; i<num-1; ++i)// 遍历 前置 字符
{
if(*bap[i]) continue;// 当前 字符串 之前出现过 跳过本次循环 有问题 后面再出现相同的呢???
for(j=i+1; j<len; ++j)// 和其后面的字符 依次作比较
{
if(pS[i] == pS[j]) {*bap[j] = true; break;}// i位置上的字符 后面有出现 跳过 这个字符
}
return pS[i];// 打印这个出现一次的字符
}
return '\0';
}
哈希表法 强烈推荐
//O(n)的时间复杂度
char FirstNotRepeatingChar(char *pS)
{
//如果是空指针,返回\0
if(pS == NULL) return '\0';
//字符(char)是一个长度为8bit的数据类型,因此总共最多能够表示256种字符。
//定义hash表长度256,并创建哈希表
const int len=256;
int hashtable[len];
for(int i=0;i<len;i++) hashtable[i]=0;
char *pHashkey=pS;
// 第一遍遍历字符串,求出每个字符出现的次数
// 字符根据其ASCII值作为数组的下标对应数组的一个数字,而数组中存储的是每个字符出现的次数
while((*pHashkey)!='\0') hashtable[*(pHashkey++)]++;
pHashkey=pS;
//第二遍遍历字符串,求出第一个只出现一次的字符,每次都是按照字符串的顺序遍历
while((*pHashkey)!='\0')
{
if(hashtable[*pHashkey]==1)// 记录只出现一次
return *pHashkey;
pHashkey++;
}
return '\0';
}