28.找出字符串中第一个匹配项的下标
- 标签:字符串、KMP
- 难度:7.0
关注后续更新。
459.重复的子字符串
- 标签:字符串、KMP
- 难度:7.0
关注后续更新。
字符串总结
-
字符串和字符数组的灵活选择
这取决于语言的特性。比如Python的String类就很好用,API众多;相比而言Java就连取字符都要用charAt()函数,不能直接通过下标取某一字符。Java的数组操作也更方便,例如增强for循环,可以快速遍历数组元素,所以在Java里用字符数组的频率就比Python高。
除此之外,Java还有StringBuilder和StringBuffer这两大可变长字符串类型,需要在字符串处理的题目中灵活运用,区别是一个线程不安全,一个线程安全,单线程的时候选用StringBuilder就行。
-
要不要使用库函数
基础题不用库函数。
如果库函数仅仅是解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。
-
字符串中的双指针
字符串本质是字符数组,或者说可以转化成字符数组(s.toCharArray()),也可以转回来(new String(char[])),所以字符串的双指针本质就是数组的双指针。
在反转字符串中,我们从前后分别遍历left、right指针,借助临时变量或用位运算完成交换操作。
在替换空格中,我们在给数组预先扩容后,用快慢两个指针一个在前面遍历,一个在后面填坑,类似删除元素那道题。
-
反转字符串
是字符串最经典的一类题,基本靠的是手动模拟,结合双指针等算法思想,考察代码能力。
例题与讲解:541.反转字符串II、151.翻转字符串里的单词
双指针回顾
-
移除元素
快慢指针,一个在前面遍历,确定是不是要移除的元素,如果不是,慢指针在后面填坑。
public int removeElement(int[] nums, int val) {
// 快慢指针
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
-
反转字符串
从前后分别遍历left、right指针,借助临时变量或用位运算完成交换操作。
public void reverseString(char[] s) {
int l = 0;
int r = s.length - 1;
while(l < r){
char temp = s[l];
s[l] = s[r];
s[r] = temp;
l++;
r--;
}
}
-
替换空格
先扩容,用快慢两个指针一个在前面遍历,一个在后面填坑。
public String replaceSpace(String s) {
if(s == null || s.length() == 0){
return s;
}
//扩充空间,空格数量2倍
StringBuilder str = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ' '){
str.append(" ");
}
}
//若是没有空格直接返回
if(str.length() == 0){
return s;
}
//有空格情况 定义两个指针
int left = s.length() - 1;//左指针:指向原始字符串最后一个位置
s += str.toString();
int right = s.length()-1;//右指针:指向扩展字符串的最后一个位置
char[] chars = s.toCharArray();
while(left>=0){
if(chars[left] == ' '){
chars[right--] = '0';
chars[right--] = '2';
chars[right] = '%';
}else{
chars[right] = chars[left];
}
left--;
right--;
}
return new String(chars);
}
-
翻转字符串里的单词
-
反转链表
两个指针用于遍历,还有一个临时指针防断链。
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;
while (cur != null) {
temp = cur.next;// 保存下一个节点
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}
-
删除链表的倒数第N个结点
先让快指针走N个,再一起遍历到终点。
注意循环的边界条件,最好画个图防止写错。
适时引入虚拟头结点。
public ListNode removeNthFromEnd(ListNode head, int n){
ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode fastIndex = dummyNode;
ListNode slowIndex = dummyNode;
//只要快慢指针相差 n 个结点即可
for (int i = 0; i < n ; i++){
fastIndex = fastIndex.next;
}
while (fastIndex.next != null){
fastIndex = fastIndex.next;
slowIndex = slowIndex.next;
}
//此时 slowIndex 的位置就是待删除元素的前一个位置。
//具体情况可自己画一个链表长度为 3 的图来模拟代码来理解
slowIndex.next = slowIndex.next.next;
return dummyNode.next;
}
-
链表相交
先用两个指针遍历,记录下两条链表的长度。让长的链表先走一点,让他们回到同一起跑线,在一起往后遍历,直到找到相同结点。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0, lenB = 0;
while (curA != null) { // 求链表A的长度
lenA++;
curA = curA.next;
}
while (curB != null) { // 求链表B的长度
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
//1. swap (lenA, lenB);
int tmpLen = lenA;
lenA = lenB;
lenB = tmpLen;
//2. swap (curA, curB);
ListNode tmpNode = curA;
curA = curB;
curB = tmpNode;
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap-- > 0) {
curA = curA.next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != null) {
if (curA == curB) {
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
-
三数之和
双指针在这里是为了降低时间复杂度,从O(n3)降到O(n2)。
先排序,在定下一个元素之后,另外两个指针分别从前后开始遍历,就不用开两个循环了。
注意去重的实现。
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 去重a
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
-
四数之和
在三数之和外面再套一层循环,也就是用两个循环定下两个元素,再用双指针。
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
// nums[i] > target 直接返回, 剪枝操作
if (nums[i] > 0 && nums[i] > target) {
return result;
}
if (i > 0 && nums[i - 1] == nums[i]) { // 对nums[i]去重
continue;
}
for (int j = i + 1; j < nums.length; j++) {
if (j > i + 1 && nums[j - 1] == nums[j]) { // 对nums[j]去重
continue;
}
int left = j + 1;
int right = nums.length - 1;
while (right > left) {
// nums[k] + nums[i] + nums[left] + nums[right] > target int会溢出
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
left++;
right--;
}
}
}
}
return result;
}
总结
除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n2)的时间复杂度降为O(n)。