14. 最长公共前缀


14. 最长公共前缀
本题都用c++实现
代码均来自leetcode题解,这里只作为个人学习记录,多输入几遍代码,孰能生巧

第一次运用到的函数substr

函数原型:
basic_string substr(size_type _Off = 0,size_type _Count = npos) const;
参数
_Off:所需的子字符串的起始位置。字符串中第一个字符的索引为 0,默认值为0。
_Count:复制的字符数目
返回值:一个子字符串,从其指定的位置开始

方法一:横向扫描

在这里插入图片描述
基于该结论,可以得到一种查找字符串数组中的最长公共前缀的简单方法。依次遍历字符串数组中的每个字符串,对于每个遍历到的字符串,更新最长公共前缀,当遍历完所有的字符串以后,即可得到字符串数组中的最长公共前缀。
在这里插入图片描述
如果在尚未遍历完所有的字符串时,最长公共前缀已经是空串,则最长公共前缀一定是空串,因此不需要继续遍历剩下的字符串,直接返回空串即可。

class Solution
{
public:
	string longestCommonPrefix(vector<string>& strs)
	{
		if (!strs.size())
		{
			return "";
		}
		string prefix = strs[0];
		int count = strs.size();
		for (int i = 0; i < count; i++)
		{
			prefix = longestCommonPrefix(prefix, strs[i]);
			if (!prefix.size())
			{
				break;
			}
		}
		return prefix;
	}
	string longestCommonPrefix(const string &str1, const string& str2)
	{
		int length = min(str1.size(), str2.size());
		int index = 0;
		while (index < length&&str1[index] == str2[index])
		{
			++index;
		}
		return str1.substr(0, index);
	}
};

复杂度分析

  • 时间复杂度:O(mn),其中 mm 是字符串数组中的字符串的平均长度,nn
    是字符串的数量。最坏情况下,字符串数组中的每个字符串的每个字符都会被比较一次。
  • 空间复杂度:O(1)。使用的额外空间复杂度为常数。

方法二:纵向扫描

方法一是横向扫描,依次遍历每个字符串,更新最长公共前缀。另一种方法是纵向扫描。纵向扫描时,从前往后遍历所有字符串的每一列,比较相同列上的字符是否相同,如果相同则继续对下一列进行比较,如果不相同则当前列不再属于公共前缀,当前列之前的部分为最长公共前缀。
。

class Solution
{
public:
	string longestCommonPrefix(vector<string>& strs)
	{
		if (!strs.size())
		{
			return "";
		}
		int length = strs[0].size();
		int count = strs.size();
		for (int i = 0; i < length; i++)
		{
			char c = strs[0][i];
			for (int j = 0; j < count; j++)
			{
				if (i == strs[j].size() || strs[j][i] != c)
				{
					return strs[0].substr(0,i);
				}
			}
		}
		return strs[0];
	}
};

复杂度分析

  • 时间复杂度:O(mn)O(mn),其中 mm 是字符串数组中的字符串的平均长度,nn
    是字符串的数量。最坏情况下,字符串数组中的每个字符串的每个字符都会被比较一次。
  • 空间复杂度:O(1)O(1)。使用的额外空间复杂度为常数。

方法三:分治法

分治法简介:字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。

n=2时,只要作一次比较即可排好序。
n=3时只要作3次比较即可,…。
而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。

分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。

如果原问题可分割成k个子问题,1<k≤n ,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
分治法所能解决的问题一般具有以下几个特征:

  1. 该问题的规模缩小到一定的程度就可以容易地解决
  2. 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
  3. 利用该问题分解出的子问题的解可以合并为该问题的解;
  4. 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

上述的第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。

回到题目
在这里插入图片描述
代码
说实话递归一直没整明白,这里依旧是还没懂!

class Solution
{
public:
	string longestCommonPrefix(vector<string>& strs)
	{
		if (!strs.size())
		{
			return "";
		}
		else
		{
			return longestCommonPrefix(strs, 0, strs.size() - 1);
		}
	}
	string longestCommonPrefix(const vector<string>& strs, int start, int end)
	{
		if (start == end)
		{
			return strs[start];
		}
		else
		{
			int mid = (start + end) / 2;
			string lcpLeft = longestCommonPrefix(strs, start, mid);
			string lcpRight = longestCommonPrefix(strs, mid + 1, end);
			return commonPrefix(lcpLeft, lcpRight);
		}
	}
	string commonPrefix(const string& lcpLeft, const string& lcpRight)
	{
		int minLength = min(lcpLeft.length(), lcpRight.length());
		for (int i = 0; i < minLength; i++)
		{
			if (lcpLeft[i] != lcpRight[i])
			{
				return lcpLeft.substr(0, i);
			}
		}
		return lcpLeft.substr(0, minLength);
	}
};

复杂度分析
在这里插入图片描述

方法四:二分查找

二分法简介:
数学上的定义:对于区间[a,b]上连续不断且f(a) · f(b)<0的函数y=f(x),通过不断地把函数 f(X) 的零点所在的区间一分为二,使区间的两个端点逐步逼近零点,进而得到零点近似值的方法叫二分法。
使用场景:当数据量很大适宜采用该方法。采用二分法查找时,数据需是排好序的。
基本思想:假设数据是按升序排序的,对于给定值x,从序列的中间位置开始比较,如果当前位置值等于x,则查找成功;若x小于当前位置值,则在数列的前半段中查找;若x大于当前位置值则在数列的后半段中继续查找,直到找到为止。
例子:假如有一组数为3,12,24,36,55,68,75,88要查给定的值24.可设三个变量front,mid,end分别指向数据的上界,中间和下界,mid=(front+end)/2.

  • 1.开始令front=0(指向3),end=7(指向88),则mid=3(指向36)。因为mid>x,故应在前半段中查找。
  • 2.令新的end=mid-1=2,而front=0不变,则新的mid=1。此时x>mid,故确定应在后半段中查找。
  • 3.令新的front=mid+1=2,而end=2不变,则新的mid=2,此时a[mid]=x,查找成功。 如果要查找的数不是数列中的数,例如x=25,当第三次判断时,x>a[mid],按以上规律,令front=mid+1,即front=3,出现front>end的情况,表示查找不成功。

复杂度分析:
时间复杂度最小为O(1),第一个就找到要找的数;
时间复杂度最大为O(log2n),最后一次找到要找的数;
空间复杂度两种情况下均为O(1);

回到题目:
显然,最长公共前缀的长度不会超过字符串数组中的最短字符串的长度。用 minLength 表示字符串数组中的最短字符串的长度,则可以在 [0,minLength] 的范围内通过二分查找得到最长公共前缀的长度。每次取查找范围的中间值 mid,判断每个字符串的长度为mid 的前缀是否相同,如果相同则最长公共前缀的长度一定大于或等于 mid,如果不相同则最长公共前缀的长度一定小于 mid,通过上述方式将查找范围缩小一半,直到得到最长公共前缀的长度。
在这里插入图片描述

class Solution
{
public:
	string longestCommonPrefix(vector<string>& strs)
	{
		if (!strs.size())
		{
			return "";
		}
		int minLength = min_element(strs.begin(), strs.end(), [](const string& s, const string& t) {return s.size() < t.size(); })->size();
		int low = 0, high = minLength;
		while (low < high)
		{
			int mid = (high - low + 1) / 2 + low;
			if (isCommonPrefix(strs, mid))
			{
				low = mid;
			}
			else
			{
				high = mid - 1;
			}
		}
		return strs[0].substr(0, low);
	}
	bool isCommonPrefix(const vector<string>& strs, int length)
	{
		string str0 = strs[0].substr(0, length);
		int count = strs.size();
		for (int i = 1; i < count; i++)
		{
			string str = strs[i];
			for (int j = 0; j < length; j++)
			{
				if (str0[j]!=str[j])
				{
					return false;
				}
			}
		}
        return true;
	}
};

复杂度分析

  • 时间复杂度O(mnlogm),其中 mm 是字符串数组中的字符串的最小长度,nn 是字符串的数量。二分查找的迭代执行次数是O(logm),每次迭代最多需要比较 mnmn 个字符,因此总时间复杂度是O(mnlogm)。
  • 空间复杂度:O(1)。使用的额外空间复杂度为常数。

min_element()函数简介

上述例子用到了min_element(),第一次见这个函数,详见参考文档
algorithm 头文件中定义了 3 个可以运用到序列的算法:

  • min_element():会返回一个指向输入序列的最小元素的迭代器;
  • max_element():会返回指向最大元素的迭代器;
  • minmax_element():会以 pair 对象的形式返回这两个迭代器。

序列必须由正向迭代器指定,仅仅有输入迭代器是不够的。对于这 3 个算法,除了序列的开始和结束迭代器,可以选择提供第三个参数来定义比较函数。

algorithm 头文件中也定义了 min()、max()、minmax() 的函数模板,它们分别返回最小值、最大值或者两个对象或一个对象的初始化列表的最小值和最大值。

上面例子用到了lamda表达式,这是我第一次见,参考文档

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙叙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值