和字符串有关的经典OJ题——字符串的逆置和字符串的翻转

news2024/9/21 21:57:05

学习完字符串有关的函数之后,那当然在这个章节有两道经典的子题也要给大家分享一下。

分别是字符串的逆置字符串的翻转

一、字符串的逆置:

1. 问题描述:

问题很容易理解:对于用户任意给定的字符串,就比如说是原来的字符数组是abcdef,逆置之后应该变成fedcba。

2. 解题分析与源代码:

解决这一类问题有两大类思路:一类是通过非递归的方式解决这里的问题,一类是通过递归的方式来解决这里的问题。

这里的递归是指函数的递归(一个函数反复调用自身,可以通过少量的代码来实现重复大量的运算,递归的版本相对来说难理解一些,但是有能力的小伙伴最好把这两种都了解一下。因为在各种计算机相关校招的笔试当中曾出现过让你写递归版本的逆置。

非递归的版本

思路讲解:

非递归的逆置非常简单,用两个指针:一个left,一个right;left指向字符头部第一个有效字符,right指向字符尾部最后一个有效字符。

然后两个交换。之后left++往后走,right--往前走。然后再交换。

什么时候结束呢:只要left >= right就结束(如果字符串的字符个数是偶数,最后left > right。反之是奇数,最后left == right)。

源代码:
#include<stdio.h>
#include<string.h>
char* reverse(char* str)
{
	int left = 0;
	int right = strlen(str) - 1;
	while (left < right)
	{
		char tmp = str[left];
		str[left] = str[right];
		str[right] = tmp;
		left++;
		right--;
	}
	return str;
}

int main()
{
	char str[20] = "abcdef";
	reverse(str);
	printf("%s", str);
	return 0;
}

递归的版本

思路讲解:

如何使用递归来实现我们字符串的逆置呢?对于函数的递归来说,有两个条件是缺一不可的:

  1. 函数递归必须存在限制条件,当满足这个限制条件时,函数递归便不再继续;
  2. 每次递归结束之后,应当比之前更加接近这个限制条件

由此得出用递归解决问题的一般过程就是:第一步写出递归思路,第二步根据递归思路写出递归结束条件,第三步根据递归思路和递归结束条件来写代码。

其中写递归思路的过程,就是思考如何不断将一个问题拆分为多个重复子问题的过程。那对于这一题它的递归思路又应该怎么写呢?我们如果以逆序a b c d e f为例,那递归思路和递归结束条件应该是下面这样的:

源代码:

来到这里,还有两个问题值得大家思考一下:

问题一:以逆置a b c d e f为例,第一步是交换a和f,如何变成转换到逆序b c d e这一步呢?

答:有两种思路。思路一,多传递一个参数,传两个参数过来,一个是待处理字符串的首元素地址str,另一个则是字符串的长度len。每一次递归结束只要让字符串的长度减去2就可以了(头尾)。但是值得注意的是:字符串的长度是size_t类型的,简单来理解就是无符号整数(注意这里是整数不是整型),无符号整数都是大于或者等于0的,如果字符串的初始长度只有1,那1 - 2不是小于0的-1,而是一个非常大的正数,所以这种情况要单独处理!

由此我们可以写出下面这个递归版本的字符串的逆置:

#include<stdio.h>

//逆序版本一:
void reverse(char* str, size_t len)
{
	/*如果字符串的长度是1,就不需要进行处理。*/
	if (len == 1)
	{
		return;
	}

	/*其他情况的处理:*/
	char tmp = *str;
	str[0] = str[len - 1];
	str[len - 1] = tmp;
	//递归结束条件:len - 2 == 0(首尾中间没有元素了)或者len - 2 == 1(首尾中间只有一个元素)结束
	if (len - 2 >= 2)
	{
		reverse(str + 1, len - 2);
	}
}

int main()
{
	char str[20] = "abc";
	reverse(str, strlen(str));
	printf("%s", str);

	return 0;
}

思路二,以逆序字符串a b c d e f为例,我们先创建创建一个临时变量tmp,第一步将a字符放到tmp里面去;第二步将最后一个字符f放到字符a所在的位置;第三步将最后一个字符f的位置用"/0"进行替换。

上面这个过程可以用下面这个图进行形象描述:(图中的数字表示步骤,比如1表示步骤一)

这样,你要逆序下面的b c d e只需要让str++(字符串首元素地址往后走一步)就可以了,后面同理。那什么时候结束呢?和前面一样当首尾元素中间只有一个元素或者没有元素就结束了。这个方法只需要你传一个参数过来就可以了。具体代码如下所示:

#include<stdio.h>

//递归版本二:
void reverse(char* str)
{
    //易错点一:tmp的类型不要写成char*
	char tmp = *str;

	size_t len = strlen(str);
	str[0] = str[len - 1];
	str[len - 1] = '\0';

    //递归结束条件:
    //易错点二:不能用一次自增运算,来替换这里两次的str+1
	if (strlen(str + 1) >= 2)
	{
		reverse(str + 1);
	}

	str[len - 1] = tmp;
}

int main()
{
	char str[20] = "a";
	reverse(str, strlen(str));
	printf("%s", str);

	return 0;
}

思路二相比思路一的话,更加抽象一些,但是对于只有一个元素的情况也能很好地处理啊,而不需要程序员单独进行处理。而且它只需要用户传递一个参数过来就可以了!但是思路一这种传两个参数str,len的方式,能有效处理局部字符串的逆置(任意取中间一小段字符串进行逆置)。但是你用思路二目前是做不到字符串的局部逆置的。

除此之外呢还是有一些比较容易出错的地方需要大家注意:易错点一很好理解,如果你不小心用char*类型的tmp,那后面str[0] = str[len - 1]这一步会导致tmp里面原有的数据被覆盖。

易错点二,大家可以尝试一下在草稿纸上走读代码(用一个例子根据代码一步一步来更新结果),最后你会发现用自增运算替代 str+1 的话,会导致最后一行代码syt[len - 1] = tmp这一步出现意外,走读代码分析问题还是不太复杂的,所以这个问题就交给各位读者朋友了。

二、字符串的旋转:

紧接着下面我们进入今天的第二个话题:字符串的旋转。

1. 问题描述:

问题描述如下图所示:

2. 解题分析与源代码:

首先先带大家明确一个问题:这个字符串的旋转不知道大家发现没有,这是一个循环反复的过程——以旋转ABCDE为例子,第一次旋转得到BCDEA,第二次是CDEAB,第三次是DEABC,第四次是EABCD,第五次旋转又变成了ABCDE。

什么意思呢?不妨假设用户指定的旋转次数是k待旋转的字符串的长度是len,那么有效的旋转次数:k有效 = k % len(%是取余运算)。好这是我们的初步认识啊,知道这一点可以有效减少我们的程序运行起来之后的实际旋转次数。

另外这一题也是属于一题多解啊,这里就给大家介绍三种思路:

解题思路一:

这里以左旋ABCDE,我们先写出第一次左旋之后得到的结果:也就是:BCDEA,我们在这里值得使用观察法:我们发现字符串ABCDE每旋转一次等价于先把第一个字符A以外的其余字符全部前移一位,然后第一个字符A来到字符串最后一个元素的位置上。

这也是旋转的本质,好的上面的话翻译过来可以用下面的代码进行阐释:

//方法一:一步一步旋转,每次旋转一个元素,需要一个额外的变量:
//不足:当你的数组特别大,且旋转次数也很大,作为时间复杂度o(kn)级别的算法就很糟糕
void left_rotate_one(char* num, size_t len, int k)
{
	char tem = 0;
	int i = 0;

	//计算有效旋转次数
	k %= len;

	while (k--)
	{
		tem = *num;
		for (i = 0; i < len - 1; i++)
		{
			*(num + i) = *(num + i + 1);
		}
		*(num + len - 1) = tem;
	}
}

这也就是思路一的源代码程序。

解题思路二:

解题思路一虽然简单,但是这个效率好不好呢,实际上很糟糕。我们不妨来简单分析一下:如果假设字符串的长度是n,旋转次数是k,由前面的代码我们不难看出来:旋转一次的成本有多大呢,答案是数组要在这里挪动覆盖地跑n - 1次才行,那k次旋转,就是k(n - 1)。

如果大家有算法的时间复杂度地概念的话,会知道,这里的n,k是表示问题规模的参数。表达式k(n-1)是可以反映出算法效率的表达式。这样的算法在n规模特别大,那计算机CPU都要冒烟。

好的,那有没有什么办法来优化我们的程序呢,有小伙伴就率先表态说:我发现字符串旋转k次,等价于将原来的字符前(k % len)个字符挪到其他字符后面,这个过程我们可以通过多开辟一个数组空间的方法一步到位

好的据此我们可以写出下面这个代码:

//方法二:开辟额外的空间:
void left_rotate_two(char* num, size_t len, int k)
{
	char tmp[256] = { 0 };

	//计算有效旋转次数:
	k %= len;

	//先用strcpy函数把前k个字符后面的字符拷贝到tmp中:
	strcpy(tmp, num + k);

	//再用strncat函数把前k个字符追加在tmp后面:
	strncat(tmp, num, k);

	//最后把tmp数组的值拷贝给num即可:
	strcpy(num, tmp);
}

上面这个代码普适性不是很好,因为有些时候你会发现待旋转的数组不是字符数组,也有可能是int类型的数组啥的。所以有没有办法使用我们前面学习过的memcpy或memmove函数(这两个函数可以适用于任意类型)来把我们的代码改造成普适性比较好的代码。

显然这是不难的,另外在数组中如果你巧妙地运用取余运算(%),往往有意料之外的结果,这种技巧往往用于数组的整体移动:(某元素数组下标 + 数组移动次数 + (数组长度)) % (数组长度) = 新数组中该元素下标 。(其中数组右移则移动次数是正数,反之是负数)。

其中紫色字体标注出来的数组长度选项是可选项,也就是说一般情况加不加都不影响最终的结果,加上数组长度这个选项主要是为了处理数组左移过程中下标计算可能产生负数的情况,如果当前数组是整体左移,那么不加可能会导致当前结果出现负数,从而发生数组越界访问的结果!

我们以一个例子来帮助大家理解上面这个过程:以左旋字符ABCD为例子,左旋一次变成字符BCDA的这个过程,就相当于将ABCD这个字符串整体左移了1位:

字母A原来的下标是0,根据公式新数组中A的下标 = (0 - 1 + 4) % 4 = 3。

字母B原来的下标是1,根据公式新数组中B的下标 = (1 - 1 + 4) % 4 = 0。

依此类推,可以得到新数组中C,D的下标分别是1,2。

由此可以将上面的代码写成下面这个形式:

//方法二:开辟额外的空间:
void right_rotate_three(int* num, size_t len, int k)
{
	int* newNum = (int*)malloc(len * sizeof(int));
	int i = 0;
	for (i = 0; i < len; i++)
	{
		newNum[(i + len - k) % len] = num[i];
	}
	memmove(num, newNum, len * sizeof(int));
}

 解题思路三:

前面已经给大家介绍了两种进行字符串旋转的方法,思想都比较地直接粗暴,但是接下来要给大家介绍的方法,可能就有点会让大家吃惊了。

它抓住字符串的旋转和字符串的逆序之间微妙的联系,巧妙地将一个复杂的字符串的旋转问题转变成为一个字符串的逆序问题,那究竟是怎么做到的呢?

我们不妨以左旋字符串ABCDEFG为例子,假设我现在希望左旋四次,最终得到的结果显然是EFGABCD —— 诶,它可以看成是原字符串DCBAGFE通过逆序操作得到的。而该字符串可以看成原字符串ABCDEFG先逆序前k (= 4)个字符ABCD,再逆序后三个字符EFG得到的。

由此我们得到:一个字符左旋k次 <=> 先逆序字符串前k % len个字符,再逆序其余字符,最后整体逆序。 

这是左旋,那如果是右旋呢,比如ABCDEFG,右旋四次变成DEFGABC,同样地去逆向分析一下,你很快就会发现那只要将原来公式里面的k % len变成 len - (k % len)即可!

这种解决字符串的旋转的方法由于只需要几乎固定的三步,所以很多地方也把它称之为三步旋转法。由此我们的字符串的旋转的代码也可以这么写(以左旋为例子):

//方法三:使用逆序函数,采用三步旋转法:
void reverse(int* num,size_t len)
{
	int* left = num;
	int* right = num + len - 1;
	while (left<=right)
	{
		int tem = *left;
		*left = *right;
		*right = tem;
		left++;
		right--;
	}
}
void right_rotate_two(int* num, size_t len, int k)
{
	//方法三这里的k %= len非常重要,对于其他方法而言不做只是增加了循环次数,但对于方法三不这样做会可能导致数组的越界访问
	k %= len;

	reverse(num, k);
	reverse(num + k, len - k);
	reverse(num, len);
}

3. 彩蛋题:

最后再留一个彩蛋题吧!也考验一下大家做题的思维是不是开阔。这题目其实也很简单,就是给你两个字符串,判断其中一个字符串是否可以由另一个字符串旋转得到:

诶,这时有小伙伴就说了:我都已经知道怎么去旋转一个字符串了,碰到这种题不就等于是秒杀吗?于是框框一顿操作——对其中一个字符串进行旋转操作,每旋转一次和另一个字符串比较一下看是否相同,如果旋转了len - 1(len是字符串长度),还没有相同的,那就是false,反之就true。

咋一看也行,但是效率太低下了。我们以旋转字符串ABCDE为例子,无论该字符串怎么旋转,所有的结果都包含在了字符串ABCDEABCD这个字符串里面了。

所以这个题也是只需要一步就到位的:只需要将其中一个字符串的原字符串再来一遍接在后面,然后找一找待查找的字符串是不是两倍原字符串的子集即可。

代码如下图所示:

int findRound(const char * src, char * find)
{
	char tmp[256] = { 0 }; //用一个辅助空间将原字符串做成两倍原字符串
	strcpy(tmp, src); //先拷贝一遍
	strcat(tmp, src); //再连接一遍
	return strstr(tmp, find) != NULL; //看看找不找得到
}

哈哈还在啃之前老本的小伙伴,在面对这一道题的时候可能就吃亏了哦😁!

以上就是字符串学习过程中两道必刷的经典OJ题以及相对应的解题方法及思路了,如果觉得有所收获的话,也请点点赞啦,谢谢大家!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2098936.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MFC自定义消息实例

1、新建一个SHOW名称的对话框文件 2、在SHOWDlg.h中添加代码 #define WM_Display (WM_USER100)afx_msg LRESULT OnDisplay(WPARAM wParam, LPARAM lParam);3、在SHOWDlg.cpp中添加代码 BEGIN_MESSAGE_MAP(CSHOWDlg, CDialog)...ON_MESSAGE(WM_Display, OnDisplay) END_MESSA…

全面解读 HTTP 缓存机制:200 内存与硬盘缓存、304 状态码

更多内容&#xff1a;孔乙己大叔 在探讨网页性能优化时&#xff0c;HTTP 缓存机制是不可或缺的一环。它不仅能够减少数据传输量&#xff0c;降低带宽消耗&#xff0c;还能显著提升网页的加载速度和用户体验。本文将深入解析 HTTP 状态码 200 和 304 在缓存机制中的作用&#xf…

不懂就问,净水器到底过滤了什么?

在水质问题日益受到关注的今天&#xff0c;净水器已成为许多家庭的必备品。然而&#xff0c;对于净水器究竟能够过滤掉多少脏东西&#xff0c;很多人可能并不十分清楚。 净水器的核心功能是去除水中的杂质和有害物质&#xff0c;确保我们饮用的水更加安全和健康。这就如同我们…

【kafka】在Linux系统中部署配置Kafka的详细用法教程分享

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

零工市场 Java 版源码开发:开启灵活就业新潮流

现如今&#xff0c;数字化经济发展的越来越迅速&#xff0c;那么灵活就业的方式也就更加受到大众的青睐。其中&#xff0c;零工市场的兴起为求职者和招聘方提供了更加便捷、高效的对接平台。 系统概述 零工市场系统是一个连接求职者和招聘方的在线平台&#xff0c;主要功能包…

Carmeker C接口

本文主要是简单介绍如何将C代码的模型嵌入到carmaker中运行 一.项目创建与例程生成 在创建项目的时候勾选source选项&#xff0c;勾选后才能在项目目录下生成src的文件夹 在src文件夹下&#xff0c;存在大量IPG的例程&#xff0c;这些例程简单介绍了如何使用了src的接口 编译…

递归 与 dfs 综合练习(四)

目录 一、单词搜索 1.题目链接&#xff1a;79. 单词搜索 2.题目描述&#xff1a; 3.解法 &#x1f334;算法思路&#xff1a; &#x1f334;算法代码&#xff1a; 一、黄金矿工 1.题目链接&#xff1a;1219. 黄金矿工 2.题目描述&#xff1a; 3.解法 &#x1f334;算…

《从C/C++到Java入门指南》- 25.final 关键字

final 关键字 final 变量 final变量可以理解为C中的const&#xff0c;变量一经定义无法修改。 public class Main {public static void main(String args[]) {final double PI 3.1415926;System.out.println(PI);// PI 3.14; // 尝试修改会报错} }final 方法 一个定义为f…

私有仓库

创建私有仓库,在企业中分享项⽬ # 创建仓库 # 重启docker服务 # 为要上传的镜像添加标记 1.拉取registry 2.创建挂载⽬录 3.启动容器&#xff0c;映射端⼝&#xff0c;挂载⽬录 4.访问仓库 5.配置pull和push&#xff0c;修改daemon.json 6.修改了配置⽂件&#xff0c;重启docke…

WordPress资源产品展示类主题 官网主题 CeoNova-Pro_v4.4

WordPress资源产品展示类主题 官网主题 CeoNova-Pro_v4.4 开心版 CeoNova-Pro主题是一款轻量级、且简洁大气、产品官网类主题&#xff0c;定位于高端产品官网、同时包含了知识付费、定制服务、问答社区、论坛交流、网址导航、以及付费产品购买下载等全方位覆盖。 CeoNova主题…

C#和数据类型转换

C#是一种强类型语言&#xff0c;这意味着每个变量的类型在编译时都是已知的。数据类型转换是指将一种数据类型的值转换为另一种数据类型的过程。在C#中&#xff0c;数据类型转换分为隐式转换和显式转换。本文将详细探讨这两种转换方式&#xff0c;并提供字符转16进制的解决方案…

除尘雾炮机的寿命一般是多久呢,需要维护吗

除尘雾炮机的寿命一般是5-10年左右&#xff0c;但具体寿命会受到设备质量、使用环境、使用频率及维护保养情况等多种因素的影响。朗观视觉小编接下来带您扒一扒&#xff01; 除尘雾炮机的寿命影响因素 设备质量&#xff1a;高质量的雾炮机采用优质材料和先进工艺制造&#xff…

自动化数据汇总:使用Python从多个数据源汇总数据

目录 引言 一、理论基础 1.1 数据源介绍 1.2 数据处理流程 1.3 常用库介绍 二、实践操作 2.1 数据读取 2.1.1 从CSV文件读取数据 2.1.2 从Excel文件读取数据 2.1.3 从数据库读取数据 2.2 数据处理 2.2.1 数据合并 2.3 数据汇总 2.4 数据存储 2.4.1 存储到CSV文件…

深度解析MFT损坏:原因、恢复策略与预防措施

一、MFT损坏现象揭秘 在Windows操作系统中&#xff0c;主文件表&#xff08;Master File Table&#xff0c;简称MFT&#xff09;是NTFS文件系统的核心组成部分&#xff0c;它记录了文件系统中所有文件的元数据&#xff0c;包括文件名、大小、创建和修改时间、数据位置等关键信…

【Spring】获取cookie,session,header(3)

本系列共涉及4个框架&#xff1a;Sping,SpringBoot,Spring MVC,Mybatis。 博客涉及框架的重要知识点&#xff0c;根据序号学习即可。 目录 本系列共涉及4个框架&#xff1a;Sping,SpringBoot,Spring MVC,Mybatis。 博客涉及框架的重要知识点&#xff0c;根据序号学习即可。…

ARM————体系结构

1、ARM结构框架 RAM&#xff1a;随机存储&#xff0c;存储速度快&#xff0c;掉电数据丢失SRAM 静态存储DRAM 动态存储SDRAM 同步存储DDR 双倍速率同步存储ROM&#xff1a;只读存储&#xff0c;存储速度慢&#xff0c;掉电不丢失数据PROM 可编程存储EPROM 可擦…

eureka一

Eureka 什么是eureka eureka服务调用流程 springcloud技术栈应用 分布式理论 CAP CAP理想运行情况 CAP不理想运行情况 CAP取舍 BASE BASE原理 搭建单机注册中心 服务提供者 服务消费者 集群服务注册中心 eureka功能详解 核心功能演示 Eureka源码解析 lifecycle的start

0基础轻松玩转.NET Web API 8.0【CICD】项目实战

1.背景 最近在学习CI/CD&#xff0c;为了加快熟悉CI/CD&#xff0c;我实操了下基于.Net 8.0的CI/CD项目实战。Ci/CD就是自动化作业&#xff0c;实现项目自动编译、发布、执行等&#xff0c;也有用于拉取推送数据的场景。本文主要介绍了在win 11下搭建CI/CD&#xff0c;实现自动…

【JS】事件捕获和事件冒泡的区别

事件捕获和事件冒泡是指在 DOM 树中处理事件时的两种不同的传播方式。它们之间的主要区别在于事件传播的方向和顺序&#xff1a; 事件捕获&#xff08;Capture&#xff09; 方向&#xff1a; 从最外层的祖先元素向目标元素传播。顺序&#xff1a; 事件首先从最外层的祖先元素…

【C++ 面试 - STL】每日 3 题(四)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/fYaBd &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏&…