01 判断两个字符串是否同源异构

题目描述

给定一个长度为m的字符串aim,以及一个长度为n的字符串str,问能否在str中找到一个长度为m的连续子串,使得这个子串刚好由aim的m个字符组成?其中字符的顺序可以任意排列,若找到满足该条件的子串,返回该子串的起始位置,未找到则返回-1

题目分析:

  1. 要想在str中找到一个长度为m的子串,首先要满足n>=m,否则str的长度还没有aim长,肯定不能满足条件。同时,m、n都必须是大于0的值,否则两个空字符串去比较是否同源异构就没有意义了。由此,界定出边界条件为:n>0, m>0, n>=m。
  2. str中的目标子串首先要连续,再者要与aim的长度相等、字符个数相同,但字符顺序可以任意排列,也就是两者为同源异构的关系,这种情况比单纯去str中找到一个与aim一模一样的子串难度要大,后者只需要在str中截取一个长度为m的子串,然后将这个子串与aim进行比较即可,可以采用KMP模式匹配算法来处理。而本题则不能够直接比较求解,应该回到同源异构字符串的特点——“长度相等、字符个数相同”这一点上寻找突破口。
    由于两者字符个数相同,我们可以想到,如果先将aim内部的所有字符按照ASCII值进行升/降序排序的操作,再取str中与aim相同长度的子串,也进行相应的排序操作,再将两者进行比较,便可以得到结果。但是这种排序后比较的方法有些简单粗暴,时间复杂度很高,因此不予采用。但可以沿着这种思路往下探究,比如能否借助一个统计数组,来记住长度为m的str子串中各个字符的个数,并与aim中的各字符个数进行比较?这种思想下的解法便会比第一种排序比较的暴力解法要方便得多,我们来看下面的具体解题步骤。

编程语言: C++

解法1

解题思路: 定义一个布尔类型的函数,在这个函数中,记录 aimstr 目标子串中内部字符的个数,如果两者相同返回 true,如果不同返回 false。定义一个 int 类型的函数,调用该布尔类型的函数,如果为真,返回此时 str 目标子串的起始位置,如果为假返回-1

代码如下图所示:

bool IsCountEqual(string s, int L, string a)
{
    int count[256] = {};
    for (int i = 0; i < a.length(); i++)
    {
        count[a[i]]++;
    }
    for (int i = 0; i < a.length(); i++)
    {
        if(count[s[L + i]]-- == 0)
        {
            return false;
        }
    }
    return true;
}

int solution1(string str, string aim)
{
    if (str.length() == 0 || aim.length() == 0 || str.length < aim.length())
    {
        return -1;
    }
    for (int L = 0; L <= str.length() - aim.length(); L++)
    {
        if (IsCountEqual(str, L, aim))
        {
            return L;
        }
    }
    return -1;
}

解题步骤:

  1. 定义一个返回值类型为 int 的函数 solution1,形参列表为两个字符串 straim
int solution1(string str, string aim)
  1. 在函数 solution1 中,首先判断边界条件,如果超出边界,直接返回-1
 if (str.length() == 0 || aim.length() == 0 || str.length < aim.length())
    {
        return -1;
    }
  1. 如果没有超出界限,则去遍历字符串 str。由于只需要截取长度与 aim 相同的 str 子串,在遍历过程中只要遍历出所有长度可以达到 aim.length() 的子串开头即可,因此遍历范围为从 i = 0 到 i = s.length() - a.length(),可借助下图进行理解:
for (int L = 0; L <= str.length() - aim.length(); L++)

在这里插入图片描述

  1. for 循环体内部,调用一个返回值为布尔类型的函数 IsCountEqual ,如果为真,则返回此时的子串开头 L,如果为假,则遍历过程继续,L 向后移动一位。如果整个循环结束,IsCountEqual 的值都为假,那么程序最终跳出 for 循环,返回-1
    for (int L = 0; L <= str.length() - aim.length(); L++)
    {
        if (IsCountEqual(str, L, aim))
        {
            return L;
        }
    }
    return -1;
  1. 函数 IsCountEqual 的返回值类型为布尔类型,向形参列表中传入 solution1 函数中的两个字符串和遍历过程中 str 的目标子串开头 L 。在函数的内部,定义出一个大小为 256 的 int 类型统计数组 count [ ],用于统计字符出现的个数:
int count[256] = {};
  1. 遍历 aim 中的所有字符,并统计所有字符的个数。其中,a [ i ] 表示 aim 中第 i 个位置的字符,count [ a [ i ] ] 表示第 i 个位置上的字符的当前个数,count [ a [ i ] ] ++ 表示第 i 个字符遍历一次之后其当前数量就加 1 ,当不同位置上有相同的字符时,该字符的个数便会累计叠加,最终遍历过程结束后,count [ a [ i ] ] 这个数组就记录着 aim 中不同字符的个数:
    for (int i = 0; i < a.length(); i++)
    {
        count[a[i]]++;
    }
  1. 遍历 str 中长度为 aim.length() 的所有字符串(也即前文所述目标子串),并对遍历后的 s[ L + i ] 位置上的字符个数做减操作。其中,L + i 表示在在遍历 str 的过程中要同时兼顾内外两个循环才能移动遍历每一个长度为 aim.length() 的 str 目标子串;s[ L + i ] 位置上的字符个数为 count [ s[ L + i ] ] ,做减操作是因为,如果目标子串里的每一个字符都与 aim 中的相同,那么相应的减操作便可以使得刚刚统计过的所有字符的个数减为 0,减操作的实现代码为 count [ s[ L + i ] ] - -。当某一位置上的字符遍历后,其数量会被减 1,当下一次遍历到其他位置上的这个字符时,首先要判断它上一次的减操作执行结束后其数量是否降为 0,如果已经为 0了,这次遍历过后还要再对它进行减操作,那减完它的数量便成为 -1 了,这说明,在 str 当前的目标子串中,该字符的个数是比 aim 中多的,因此本次遍历便以失败告终,因为 str 当前的目标子串与 aim 一定是不相同的,所以返回 false。如果整个循环结束,都没有返回 false,说明 str 目标子串中的所有字符数量都是大于等于 0 的,又由于 str 目标子串的长度与 aim 相等,因此,可以断定此时的字符数量都是等于 0 的,否则将不能满足长度相等的条件,因此返回 true
    for (int i = 0; i < a.length(); i++)
    {
        if(count[s[L + i]]-- == 0)
        {
            return false;
        }
    }
    return true;
  1. 该条件判断结束,再返回 solution1 函数中,也就是步骤 4,便能判定两串是否为同源异构。

解法2

解题思路: 建立一个负债表(统计数组),先记录 aim 中所有字符出现的次数,然后用一个长度为 aim.length() 的移动表去遍历 str,如果 str 中的某一个字符和 aim 中的相同,则对负债表中的次数做减 1 操作,负债表出现负值则记录为无效值,遍历过程中统计无效值的次数,当无效值的次数为0时,表示此时移动表中的 str 目标子串与 aim 字符串同源异构。

代码如下图所示:

int solution2(string str, string aim)
{
	if (str.length() == 0 || aim.length() == 0 || str.length() < aim.length())
	{
		return -1;
	}
	
	int count[256] = {};
	for (int i = 0; i < aim.length(); i++)
	{
		//对aim数组中所有元素的个数做统计
		count[aim[i]]++;
	}

	//
	int M = aim.length();
	int inValidTimes = 0;//记录无效值的次数
	int R = 0;
	for (; R < M; R++)
	{
		if (count[str[R]]-- <= 0)
		{
			inValidTimes++;
		}
	}
	//R从第一个循环体中出来之后 其值变为M 然后进入下一个循环体
	for (; R < str.length(); R++)
	{
		if (inValidTimes == 0)//先判断上一个移动表的负债情况 即无效值次数是否为0
		{
			return R - M;
		}
		if (count[str[R]]-- <= 0)//再判断新移动表中新字符的次数情况
		{
			inValidTimes++;
		}
		if (count[str[R - M]]++ < 0)//判断新移动表中抛弃字符(即上一个移动表中的第一个字符)的次数情况
		{
			inValidTimes--;
		}
	}
	return inValidTimes == 0 ? R - M : -1;//判断移动表最后一次所在位置的无效值情况并返回
}

解题步骤:

  1. 定义一个返回值类型为 int 的函数 solution2,形参列表为两个字符串 straim
int solution2(string str, string aim)
  1. 在函数 solution2 中,首先判断边界条件,如果超出边界,直接返回-1
 if (str.length() == 0 || aim.length() == 0 || str.length < aim.length())
    {
        return -1;
    }
  1. 如果没有超出界限,先创建一个负债表 int count [256] = {} ,容量为256,以确保能够容纳所有字符型元素。再遍历 aim 中的所有字符,并统计所有字符的个数。其中,a [ i ] 表示 aim 中第 i 个位置的字符,count [ a [ i ] ] 表示第 i 个位置上的字符的当前个数,count [ a [ i ] ] ++ 表示第 i 个字符遍历一次之后其当前数量就加 1 ,当不同位置上有相同的字符时,该字符的个数便会累计叠加,最终遍历过程结束后,count [ a [ i ] ] 这个数组就记录着 aim 中不同字符的个数,也就是说,负债表中的数据 = 不同字符的个数,遍历结束后,负债表中的数据都为正数:
	int count[256] = {};
	for (int i = 0; i < aim.length(); i++)
	{
		//对aim数组中所有元素的个数做统计
		count[aim[i]]++;
	}
  1. 定义 M 为字符串 aim 的长度,inValidTimes 为无效值的次数(初始值设定为 0),R 为遍历的索引值(初始值设定为 0)。遍历 str 中长度为 M 的目标子串字符(这里也就是第一个移动表的位置),并判断 str[ R ] 这个字符的负债情况 count[str[ R ]] (即次数)是否小于等于 0,如果在本次遍历时,已经等于 0,说明这次遍历之后再进行减 1 操作的话它的值肯定是小于 0 的,那就可以判定本次遍历中 str 目标子串的 str[ R ] 字符的个数比 aim 中多,于是对无效值 inValidTimes 做加 1 的操作,并对自身负债情况做减 1 操作。这里的无效值便是指当前遍历过程中 str 目标子串同 aim 字符串相比的字符差异情况,如果都相同,那么无效值便一直为 0,一旦出现字符个数不相同,无效值 inValidTimes 便会记录这种状态,数值便会大于 0。需要说明的是,本次只遍历了 str 中的第一个目标子串:
	int M = aim.length();
	int inValidTimes = 0;//记录无效值的次数
	int R = 0;
	for (; R < M; R++)
	{
		if (count[str[R]]-- <= 0)
		{
			inValidTimes++;
		}
	}
  1. R 从上一个循环出来之后其值变为 M,此时上一个目标子串的首字符为 str[ R - M]。进入下一个循环体后,循环条件为从 M 到 str.length(),此时移动表开始向后移动,也就是说后面要进来一个新的字符 str[ R ],同时要去掉上一个目标子串的首字符 str[R - M] 。由于遍历条件为 M 到 str.length(),而目标子串的长度只需要和aim 长度相等即可,因此需要在该循环内部的实现中做相应处理。进入循环体,首先判断上一次循环中的无效值 inValidTimes 个数是否已经为 0,如果已经为 0 了,表明此时没有无效值,也就是说上一个 str 目标子串中的字符与 aim 完全相同,返回上一个目标子串的开头 R - M 即可。如果不为 0,判断移动表中新进来的字符的负债情况,如果已经小于或等于 0 了,就对无效值做加 1 操作,并且自身再做减 1 操作。如果大于 0,判断被去掉的上一个目标子串的首字符负债情况,如果该字符负债情况小于 0,则需要对无效值的次数做减 1 操作,并对自身做加 1 操作,因为在新的移动表中,我们需要把这个字符造成的负债还回去。由于循环内部首先判断的是上一次的无效值情况,因此当循环结束之后,最后一次遍历 str 目标子串后的无效值情况还没有进行判断,需要在循环体外面再做一次单独的判断,此时如果无效值 inValidTimes 的个数为 0,则返回最后一个 str 目标子串的开头 R - M,如果不等于 0,则返回 -1。至此,两串是否同源异构便判断结束。
	//R从第一个循环体中出来之后 其值变为M 然后进入下一个循环体
	for (; R < str.length(); R++)
	{
		if (inValidTimes == 0)//先判断上一个移动表的负债情况 即无效值次数是否为0
		{
			return R - M;
		}
		if (count[str[R]]-- <= 0)//再判断新移动表中新字符的次数情况
		{
			inValidTimes++;
		}
		if (count[str[R - M]]++ < 0)//判断新移动表中抛弃字符(即上一个移动表中的第一个字符)的次数情况
		{
			inValidTimes--;
		}
	}
	return inValidTimes == 0 ? R - M : -1;//判断移动表最后一次所在位置的无效值情况并返回
}

题解源码

.src源码文件已上传至Gitee码云,点击此处获取:https://gitee.com/je-9664/codes-of-public-class/blob/master/src/Code02_ContainAllCharExactly.cpp

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值