当前位置: 首页 > article >正文

13.面试算法-字符串常见算法题(二)

1. 字符串反转专题

我们知道反转是链表的一个重要考点,反转同样是字符串的重要问题。常见问题也就是在LeetCode中列举的相关题目:

【1】LeetCode344. 反转字符串:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
【2】LeetCode541. K个一组反转:给定一个字符串 s 和一个整数 k ,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。
【3】LeetCode.917. 仅仅反转字母:给定一个字符串 S ,返回 “反转后的” 字符串,其中不是字母的字符都保留在原地,而所有字母的位置发生反转。
【4】LeetCode151. 反转字符串里的单词:给你一个字符串 s ,逐个反转字符串中的所有单词。
【5】LeetCode.557. 反转字符串中的单词 III:给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
这几个题目你是否发现前三道就是要么反转字符,要么反转里面的单词。针对字符的反转又可以变换条件造出多问题。我们就从基本问题出发,各个击破。

1.1 LeetCode344. 反转字符串

题目要求

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

示例1:
输入:s = [“h”,“e”,“l”,“l”,“o”]
输出: [“o”,“l”,“l”,“e”,“h”]
示例2:
输入:s = [“H”,“a”,“n”,“n”,“a”,“h”]
输出: [“h”,“a”,“n”,“n”,“a”,“H”]

这是最基本的反转题,也是最简单的问题,使用双指针方法最直接。具体做法是:

对于长度为 N 的待被反转的字符数组,我们可以观察反转前后下标的变化,假设反转前字符数组为 s[0] s[1] s[2] … s[N - 1],那么反转后字符数组为 s[N - 1] s[N - 2] … s[0]。比较反转前后下标变化很容易得出 s[i] 的字符与 s[N - 1 - i] 的字符发生了交换的规律,因此我们可以得出如下双指针的解法:

  • 将 left 指向字符数组首元素, right 指向字符数组尾元素。
  • 当 left < right:
    • 交换 s[left] 和 s[right];
    • left 指针右移一位,即 left = left + 1;
    • right 指针左移一位,即 right = right - 1 。
  • 当 left >= right ,反转结束,返回字符数组即可。
class Solution {
	public void reverseString(char[] s) {  
		if (s == null || s.length() == 0) {
			return s;
		}
		int n = s.length;
		for (int left = 0, right = n - 1; left < right; ++left, --right) {
			char tmp = s[left];
			s[left] = s[right];
			s[right] = tmp;
		}
	}
}

这里使用for循环貌似条件过于复杂了,我们使用while也可以:

class Solution {
	public void reverseString(char[] s) {
		//采用双指针反转即可,左右指针元素互换,然后慢慢靠近 
		int left=0,right=s.length-1;
		while(left<=right){
			char tmp=s[left];
			s[left]=s[right];
			s[right]=tmp;
			left++;
			right--;
		}
	}
}

1.2 LeetCode541. K个一组反转

这个题,我感觉有点没事找事,先看一下要求:

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。

  • 如果剩余字符少于 k 个,则将剩余字符全部反转。
  • 如果剩余字符小于 2k 但大于或等于 k 个,则反转前k 个字符,其余字符保持原样。

示例1:
输入:s = “abcdefg”, k = 2
输出: “bacdfeg”
示例2:
输入:s = “abcd”, k = 2
输出: “bacd”

我们直接按题意进行模拟就可以:反转每个下标从 2k的倍数开始的,长度为 k的子串。若该子串长度不足 k,则反转整个子串。

class Solution {
	public String reverseStr(String s, int k) { 
		if (s == null || s.length() == 0) {
			return s;
		}
		int n = s.length();
		char[] arr = s.toCharArray();
		for (int i = 0; i < n; i += 2 * k) {
			reverse(arr, i, Math.min(i + k, n) - 1);
		}
		return new String(arr);
	}

	public void reverse(char[] arr, int left, int right) {
		while (left < right) {
			char temp = arr[left];
			arr[left] = arr[right];
			arr[right] = temp;
			left++;
			right--;
		}
	}
}

1.3 LeetCode.917. 仅仅反转字母

这个题有点难度,我们来看一下:

给定一个字符串 S ,返回 “反转后的” 字符串,其中不是字母的字符都保留在原地,而所有字母的位置发生反转。

示例 1:
输入:s = “ab-cd”
输出:“dc-ba”
示例 2:
输入:s = “a-bC-dEf-ghIj”
输出:“j-Ih-gfE-dCba”
示例 3:
输入:s = “Test1ng-Leet=code-Q!”
输出:“Qedo1ct-eeLg=ntse-T!”

这里第一眼感觉不是特别复杂,同样从两头向中间即可,但问题是"-"不是均匀的有些划分的段长,有的短,这就增加了处理的难度。

方法1:使用栈

将 s 中的所有字母单独存入栈中,所以出栈等价于对字母反序操作。(或者,可以用数组存储字母并反序数组。) 然后,遍历 s 的所有字符,如果是字母我们就选择栈顶元素输出。

class Solution {
	public String reverseOnlyLetters(String S) {
		Stack<Character> letters = new Stack();
		for (char c: S.toCharArray())
			if (Character.isLetter(c))
				letters.push(c);

		StringBuilder ans = new StringBuilder();
		for (char c: S.toCharArray()) {
			if (Character.isLetter(c))
				ans.append(letters.pop());
			else
				ans.append(c);
		}

		return ans.toString();
	}
}

方法2:拓展 双转指针

一个接一个输出 s 的所有字符。当遇到一个字母时,我们希望找到逆序遍历字符串的下一个字母。 所以我们这么做:维护一个指针 j从后往前遍历字符串,当需要字母时就使用它。

class Solution {
	public String reverseOnlyLetters(String S) { 
		if (S == null || S.length() == 0) {
			return S;
		}
		StringBuilder ans = new StringBuilder();
		int j = S.length() - 1;
		for (int i = 0; i < S.length(); ++i) {
			if (Character.isLetter(S.charAt(i))) {
				while (!Character.isLetter(S.charAt(j)))
					j--;
				ans.append(S.charAt(j--));
			} else {
				ans.append(S.charAt(i));
			}
		}
		return ans.toString();
	}
}

1.4 LeetCode151. 反转字符串里的单词

题目要求

给你一个字符串 s ,逐个反转字符串中的所有单词 。
单词是由非空格字符组成的字符串。 s 中使用至少一个空格将字符串中的单词分隔开。
请你返回一个反转 s 中单词顺序并用单个空格相连的字符串。

说明:

  • 输入字符串 s 可以在前面、后面或者单词间包含多余的空格。
  • 反转后单词间应当仅用一个空格分隔。
  • 反转后的字符串中不应包含额外的空格。

示例 1:
输入:s = “the sky is blue”
输出:“blue is sky the”
示例 2:
输入:s = " hello world "
输出:“world hello”
解释:反转后的字符串中不能存在前导空格和尾随空格。
示例 3:
输入:s = “a good example”
输出:“example good a”
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

这个题也经常出现在很多面试题中,我记得曾经见过有个题是这样出的,要你按照同样的方式反转“ I love youzan”。

这个题的关键在于如何处理单词。很多语言提供了相关的特性,因此我们可以首先使用语言的特性来实现:

很多语言对字符串提供了split(拆分),reverse(反转)和 join(连接)等方法,因此我们可以简单的调用内置的API 完成操作:

  • 使用 split 将字符串按空格分割成字符串数组;
  • 使用 reverse 将字符串数组进行反转;
  • 使用 join 方法将字符串数组拼成一个字符串。

如图:
在这里插入图片描述

class Solution {
	public String reverseWords(String s) {  
		if (s == null || s.length() == 0) {
			return s; 
		}
		// 除去开头和末尾的空白字符 
		s = s.trim();
		// 正则匹配连续的空白字符作为分隔符分割
		List<String> wordList = Arrays.asList(s.split("\\s+"));
		Collections.reverse(wordList);
		return String.join(" ", wordList);
	}
}

如果我们要自行编写实现函数,对于字符串不可变的语言,例如java中的String,首先得把字符串转化成其他可变的数据结构,同时还需要在转化的过程中去除空格。

第二种解法

对于字符串可变的语言,就不需要再额外开辟空间了,直接在字符串上原地实现。在这种情况下,反转字符和去除空格可以一起完成。
在这里插入图片描述
实现方法:

class Solution {
	public String reverseWords(String s) { 
		if (s == null || s.length() == 0) {
			return s;
		}
		
		StringBuilder sb = trimSpaces(s); 
		// 反转字符串
		reverse(sb, 0, sb.length() - 1);
		// 反转每个单词
		reverseEachWord(sb);  
		return sb.toString();
	}

	public StringBuilder trimSpaces(String s) { 
		int left = 0, right = s.length() - 1;
		// 去掉字符串开头的空白字符
		while (left <= right && s.charAt(left) == ' ') {
			++left;
		}

		// 去掉字符串末尾的空白字符
		while (left <= right && s.charAt(right) == ' ') {
			--right;
		}

		// 将字符串间多余的空白字符去除
		StringBuilder sb = new StringBuilder();
		while (left <= right) {
			char c = s.charAt(left);

			if (c != ' ') {
				sb.append(c);
			} else if (sb.charAt(sb.length() - 1) != ' ') {
				sb.append(c);
			}
			++left;
		}
		return sb;
	}

	public void reverse(StringBuilder sb, int left, int right) {
		while (left < right) {
			char tmp = sb.charAt(left);
			sb.setCharAt(left++, sb.charAt(right));
			sb.setCharAt(right--, tmp);
		}
	}

	public void reverseEachWord(StringBuilder sb) {
		int n = sb.length();
		int start = 0, end = 0;

		while (start < n) {
			// 循环至单词的末尾
			while (end < n && sb.charAt(end) != ' ') {
				++end;
			}
			// 反转单词
			reverse(sb, start, end - 1); 
			// 更新start,去找下一个单词
			start = end + 1;
			++end;
		}
	}
}

另外本题还可以使用双端队列来解决。由于双端队列支持从队列头部插入的方法,因此我们可以沿着字符串一个一个单词处理,然后将单词压入队列的头部,再将队列转成字符串即可。
在这里插入图片描述

class Solution {
	public String reverseWords(String s) {
		int left = 0, right = s.length() - 1; 
		// 去掉字符串开头的空白字符
		while (left <= right && s.charAt(left) == ' ') {
			++left;
		}

		// 去掉字符串末尾的空白字符
		while (left <= right && s.charAt(right) == ' ') {
			--right;
		}

		Deque<String> d = new ArrayDeque<String>();
		StringBuilder word = new StringBuilder();

		while (left <= right) {
			char c = s.charAt(left);
			if ((word.length() != 0) && (c == ' ')) { 
				// 将单词  push 到队列的头部
				d.offerFirst(word.toString());
				word.setLength(0);
			} else if (c != ' ') {
				word.append(c);
			}
			++left;
		}
		d.offerFirst(word.toString());

		return String.join(" ", d);
	}
}

1.5 LeetCode557. 反转字符串中的单词 III

给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

示例 1:
输入:s = “Let’s take LeetCode contest”
输出:“s’teL ekat edoCteeL tsetnoc”
示例 2:
输入: s = “Mr Ding”
输出:“rM gniD”

提示

  • 在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。

上一个题是将单词本身不变,单词之间的关系反转。而本题就是单词反转,单词之间的关系不变。

分析

我们可以使用额外的空间来执行,开辟一个新字符串。然后从头到尾遍历原字符串,直到找到空格为止,此时找到了一个单词,并能得到单词的起止位置。随后,根据单词的起止位置,可以将该单词逆序放到新字符串当中。如此循环多次,直到遍历完原字符串,就能得到反转后的结果。

class Solution {
	public String reverseWords(String s) { 
		if (s == null || s.length() == 0) {
			return s;
		}
		StringBuffer ret = new StringBuffer();
		int length = s.length();
		int i = 0;
		while (i < length) {
			int start = i;
			while (i < length && s.charAt(i) != ' ') {
				i++;
			}
			for (int p = start; p < i; p++) {
				ret.append(s.charAt(start + i - 1 - p));
			}
			while (i < length && s.charAt(i) == ' ') {
				i++;
				ret.append(' ');
			}
		}
		return ret.toString();
	}
}

此题也可以直接在原字符串上进行操作,避免额外的空间开销。当找到一个单词的时候,我们交换字符串第一个字符与倒数第一个字符,随后交换第二个字符与倒数第二个字符……如此反复,就可以在原空间上反转单词。

需要注意的是,原地解法在某些语言(比如 Java,JavaScript)中不适用,因为在这些语言中 String 类型是一个不可变的类型,需要先转换。在写转换的时候有一个更大的问题经常会被忽略,下面这段代码是有问题的,执行之后会发现无法完成反转,你能找到问题在哪里吗?

class Solution {
	public String reverseWordsError(String s) {
		int length = s.length();
		char[] charArray = s.toCharArray();
		int i = 0;
		while (i < length) {
			int start = i;
			while (i < length && charArray[i] != ' ') {
				i++;
			}

			int left = start, right = i - 1;
			while (left < right) {
				swap(charArray[left], charArray[right]);
				left++;
				right--;
			}
			while (i < length && charArray[i] == ' ') {
				i++;
			}
		}
		return String.valueOf(charArray);
	}

	public void swap(char a ,char b){
		char c=a;
		a=b;
		b=c;
	}
}

这里的问题在于swap方法只是在局部反转了a和b,而并没有调整数组charArray中的元素,那该怎么写呢?这个留给读者好好思考。


http://www.kler.cn/news/314318.html

相关文章:

  • 【论文阅读】3D Diffuser Actor: Policy Diffusion with 3D Scene Representations
  • 人工智能与机器学习原理精解【25】
  • 【电路笔记】-运算放大器积分器
  • 数模方法论-整数规划
  • Python类及元类的创建流程
  • C#进阶-基于雪花算法的订单号设计与实现
  • [Python数据可视化] Plotly:交互式数据可视化的强大工具
  • 15.9 grafana-deployment-yaml讲解
  • 掌上高考爬虫逆向分析
  • [Python数据可视化]探讨数据可视化的实际应用:三个案例分析
  • lvs-nat模式实验详解
  • 【全网最全】2024年华为杯研赛A题成品论文获取入口(后续会更新)
  • 面试时被问的问题
  • 后台数据管理系统 - 项目架构设计-Vue3+axios+Element-plus(0917)
  • 新版ssh客户端无法连接旧版服务器sshd的方法
  • PHP基础语法入门指南
  • CMake中的PUBLIC、PRIVATE 和 INTERFACE用法
  • C++ | Leetcode C++题解之第423题从英文中重建数字
  • 【CPU】CPU的物理核、逻辑核、超线程判断及L1、L2、L3缓存、CacheLine和CPU的TBL说明
  • vue-入门速通
  • C++_数据结构详解
  • MATLAB入门基础篇
  • 一个安卓鸿蒙化工具
  • SpringBoot环境配置(Spring Boot Profile)
  • sql执行流程经典案例分析
  • 从Profinet到Ethernet IP网关技术重塑工业网络,数据传输更流畅
  • Go语言并发编程中的超时与取消机制解析
  • 基于菜鸟教程的flask学习记录 —— Flask视图函数
  • Java.猜数字小游戏
  • Go 语言字典探秘:操作指南与约束解析