代码随想录–双指针章节总结
1.LeetCode27 移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
数组的元素是不能删的,只能覆盖。
这个题需要注意的在数组中元素的删除实际上是通过覆盖来完成的
比如[1,2,3,4,5] 现在删除数组中4的元素,那么实际上是将4后面的元素都向前移动一个单位,把4覆盖掉,的到[1,2,3,5,5],返回的数组长度-1
解题思路1(暴力法):使用两个for循环。第一个循环用来循环遍历整个数组,第二个用来移动元素。
public int removeElement(int[] nums, int val) {
if (nums == null || nums.length == 0)
return 0;
int size = nums.length;
for (int i = 0; i < size; i++) {
if (nums[i] != val)
continue;
for (int j = i; j < size - 1; j++) {
nums[j] = nums[j + 1];
}
size--;
i--; // 这个需要特别注意,因为我们覆盖元素,实际上就是将后面的元素都向前移动一位,那么i也要向前移动,否则会漏掉一些元素
}
return size;
}
解题思路2(利用快慢指针):首先需要明确快慢指针代表什么意思:
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组。
- 慢指针:指向更新新数组下标的位置
快指针从头开始遍历数组,只要发现不等于给定val的元素,就将元素赋值为slow指针指向的位置。这样循环结束后,所有的不等于val的元素都保存起来
public int removeElement(int[] nums, int val) {
if (nums == null || nums.length == 0)
return 0;
int fast = 0, slow = 0;
for (fast = 0; fast < nums.length; fast++) {
if (nums[fast] != val) {
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
2.LeetCode344 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
示例 1:
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
解题思路: 直接使用双指针,分别从数组的第一个和最后一个元素进行遍历,然后交换这两个元素即可。
public void reverseString(char[] s) {
int left = 0, right = s.length - 1;
while (left < right) {
char temp = s[left];
s[left++] = s[right];
s[right--] = temp;
}
}
3.剑指offer05 替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
示例 1:
输入:s = "We are happy."
输出:"We%20are%20happy."
解题思路1: 直接使用StringBuilder,遍历数组,只要发现有空格,就向StringBuilder中append%20,否则就把原来字符串中字符append。
public static String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
// 这个地方比较的是字符相等不相等
// if (" ".equals(s.charAt(i)))
if (s.charAt(i) == ' ') {
sb.append("%20");
continue;
}
sb.append(s.charAt(i));
}
return sb.toString();
}
这个地方需要注意这个条件
" ".equals(s.charAt(i))
使用charAt(i)函数返回的是一个char,因此应该用==而不是equal方法
解题思路2: 使用双指针
- 首先遍历整个字符串,统计空格个数
- 然后按照空格个数,将字符串进行扩容
- 然后i,j指针分别指向扩容前数组最后的元素,和扩容后数组的元素
- 然后开始遍历,如果s[i]不是空格,则s[j] = s[i]
- 如果是空格,则
s[i] = '0';s[i - 1] = '2';s[i - 2] = '%';
- 直到i==j停止循环。
因为在Java中字符串是常量,因此这道题无法像C++一样直接操作字符串中的字符,但是这个思路是可以在Java中实现的,但是就是实际上还没有方法一效率高。
public class Main {
public static void main(String[] args) {
String s = "We are happy.";
replaceSpace(s);
}
public static void replaceSpace(String s) {
char[] chars = s.toCharArray();
if (chars.length == 0)
return;
// 遍历数组,查找空格次数
int count = 0;
for (char c : chars) {
if (c == ' ')
count++;
}
char[] expand = expand(chars, chars.length + 2 * count);
// 定义两个指针
int p1 = chars.length - 1, p2 = expand.length - 1;
while (p1 != p2) {
// 判断p1指向的是空格字符
if (expand[p1] != ' ') {
expand[p2--] = expand[p1--];
} else if (expand[p1] == ' ') {
expand[p2] = '0';
expand[p2 - 1] = '2';
expand[p2 - 2] = '%';
p2 -= 3;
p1--;
}
}
for (int i = 0; i < expand.length; i++) {
System.out.print(expand[i]);
}
}
/**
* 数组扩容
*
* @param src 原数组
* @param len 新数组长度
* @return 新数组
*/
private static char[] expand(char[] src, int len) {
char[] expand = new char[len];
for (int i = 0; i < src.length; i++) {
expand[i] = src[i];
}
return expand;
}
}
4.Leetcode151 反转单词
给你一个字符串 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"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。
解题思路1:
- 使用trim函数取出字符串开头和结尾的空格
- 使用split分割单词,形成一个string[], 每一个元素是一个单词
- 然后反转string[]数组即可。
public String reverseWords(String s) {
// 如果一个字符串中有多个连续的空格
// trim()的作用
String[] strings = s.trim().split(" ");
int left = 0, right = strings.length - 1;
// 反转数组
while (left < right) {
String temp = strings[left];
strings[left++] = strings[right];
strings[right--] = temp;
}
// 构建字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < strings.length; i++) {
if ("".equals(strings[i]))
continue;
if (i == strings.length - 1) {
sb.append(strings[i]);
continue;
}
sb.append(strings[i] + " ");
}
return sb.toString();
}
这里需要特别注意的是,如果
String str = " hello world ";
,那么使用split(" ")之后的得到的数组是:所以需要在添加到StringBuilder中忽略掉这些空字符串。
第二个需要注意的是,如果想去除掉字符串开头和结尾的空格,可以使用str.trim()函数
解题思路2: 将字符串先整体反转一次,然后在每一个单词内部反转一次,就可以得到最终的结果。这个地方的难点是因为给的字符串可能在开头和结尾都包含若干个空格,以及每一个单词之间还有可能存在多个空格,所以需要删除掉字符串中额外的空格。
public String reverseWords(String s) {
// 判断字符串是否为空
if (s == null || s.length() == 0) return null;
// 去除掉字符串中额外的空格
char[] chars = s.toCharArray();
chars = removeExtraSpaces(chars).toCharArray();
// 整体反转字符串
reverse(chars, 0, chars.length - 1);
// 每一个单词反转一次
int start = 0; // 记录每一个单词开始的下标
for (int i = 0; i <= chars.length; i++) {
if (i == chars.length || chars[i] == ' ') {
// 反转单词
reverse(chars, start, i - 1);
start = i + 1;
}
}
return new StringBuilder().append(chars).toString();
}
// 字符串反转
private void reverse(char[] chars, int start, int end) {
while (start < end) {
char temp = chars[start];
chars[start++] = chars[end];
chars[end--] = temp;
}
}
// 删除所有额外的空格
private String removeExtraSpaces(char[] chars) {
// 定义快慢指针
int slow = 0;
// 开始遍历
for (int fast = 0; fast < chars.length; fast++) {
// 只要fast遍历的不是空格
if (chars[fast] != ' ') {
// 每一个单词前面添加空格,首单词不添加空格
if (slow != 0) chars[slow++] = ' ';
// 复制整个单词
while (fast < chars.length && chars[fast] != ' ') {
chars[slow++] = chars[fast++];
}
}
}
return new StringBuilder().append(chars, 0, slow).toString();
}
for (int i = 0; i < chars.length; i++) { if (chars[i] == ' ') { // 第一个单词遍历完毕 reverse(chars, rec, i - 1); rec = i + 1; } }
需要注意的是,在进行单词内部反转的时候,我们在条件上不能写上面的这种。判断只要有空格,那么就反转之间的单词。如果这样写,就会发现最后一个单词实际上没有反转。
原因是最后一个单词后面没有空格,但是也要反转。下面的这种写法才对。当执行到最后一个元素的时候,也要反转。
为什么是
i<=chars.length
,是因为要反转的区间是[rec, i - 1] 所以i必须执行到=chars.lengthfor (int i = 0; i <= chars.length; i++) { if (i == chars.length || chars[i] == ' ') { // 第一个单词遍历完毕 reverse(chars, rec, i - 1); rec = i + 1; } }
这里面还有一个很坑的细节问题
if (i == chars.length || chars[i] == ' ') { // 反转单词 reverse(chars, start, i - 1); start = i + 1; }
i == chars.length || chars[i] == ' '
这个条件一定不能写成chars[i] == ' ' || i == chars.length
因为假设数组下标区间是[0,3] 长度为4,我们本意是条件是长度为4或者chars[i] == ' '
都可以但是如果按照
chars[i] == ' ' || i == chars.length
这个条件写的,假设i=4,那么在判断第一个条件的时候,直接数组越界,第二个条件不会判断,算法出错。!!因此必须要反过来写才可以
5. LeetCode206 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
解题思路:使用两个指针来做
反转链表实际上就是将每一个链表的next指针指向前驱结点。因此可以定义两个指针,一个是pre指向当前遍历元素的前驱结点,初始值为null,因为第一个结点反转后,变成最后一个结点,而最后一个结点的next正好是null。cur指向当前正在遍历的结点
在循环中执行下面的操作
- 使用temp保存当前遍历元素的next指针
- 然后将当前元素的next指针赋值为pre
- 然后将per=cur
- 最后cur=cur.next
注意:这里一定要将cur.next先使用temp保存起来,否则因为在第二步cur.next值已经被修改了,再次访问cur=cur.next就不对了。
public ListNode reverseList(ListNode head) {
// 如果链表为空,则返回空
if (head == null) return null;
// 如果链表中只有只有一个元素,则直接返回
if (head.next == null) return head;
ListNode pre = null, cur = head;
ListNode temp;
while (cur != null) {
temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
注意最后的返回值不要写成下面这两种
- 写成head,head指向的是原始链表的头结点1,如果返回head,则返回的实际上是反转后链表的最后一个元素
- 写成cur,while退出后,cur==null,所以会返回null
正确的是应该返回pre
解题思路2: 只要是反转顺序的都可以考虑用栈来做。
- 首先将所有的结点入栈
- 然后创建一个虚拟虚拟头结点,让cur指向虚拟头结点。然后开始循环出栈,每出来一个元素,就把它加入到以虚拟头结点为头结点的链表当中,最后返回即可。
public ListNode reverseList(ListNode head) {
// 如果链表为空,则返回空
if (head == null) return null;
// 如果链表中只有只有一个元素,则直接返回
if (head.next == null) return head;
// 创建栈 每一个结点都入栈
Stack<ListNode> stack = new Stack<>();
ListNode cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
// 创建一个虚拟头结点
ListNode pHead = new ListNode(0);
cur = pHead;
while (!stack.isEmpty()) {
ListNode node = stack.pop();
cur.next = node;
cur = cur.next;
}
// 最后一个元素的next要赋值为空
cur.next = null;
return pHead.next;
}
采用这种方法需要注意一点。就是当整个出栈循环结束以后,cur正好指向原来链表的第一个结点,而此时结点1中的next指向的是结点2,因此最后还需要
cur.next = null
6. LeetCode19 删除链表的倒数第N个结点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
解题思路: 使用快慢指针,快指针先走N步,然后快慢指针同时走,最终当快指针指向链表中最后一个元素时,慢指针正好指向倒数N个结点的前一个结点。然后进行删除即可。
这里需要注意n给的值可能不合法的情况。
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null) return null;
// 定义快慢指针
ListNode pHead = new ListNode(0);
pHead.next = head;
ListNode slow = pHead, fast = pHead;
for (int i = 1; i <= n; i++) {
// n不合法
if (fast == null) return null;
fast = fast.next;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return pHead.next;
}
和这个题类似的,找到链表中倒数第K个元素。
public ListNode FindKthToTail(ListNode pHead, int k) {
// write code here
ListNode fast = pHead;
ListNode slow = pHead;
for (int i = 0; i < k; i++) {
// 需要注意的是 如果链表长度小于k的情况
// 通过下面的if判断判断了链表为空的情况和链表长度小于k的情况
if (fast == null)
return null;
fast = fast.next;
}
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
7. 面试题 02.07. 链表相交
输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)
例如,输入{1,2,3},{4,5},{6,7}时,两个无环的单向链表的结构如下图所示:
可以看到它们的第一个公共结点的结点值为6,所以返回结点值为6的结点。
解题思路:分别建立两个指针,从两个链表开始遍历,如果有公共结点,那么两个指针第一次相遇的时候就是第一个公共结点。
当链表1遍历到头结点后,将链表1的下一个指针指向链表2的头结点。
同理,当链表2遍历到头结点后,将链表1的下一个指针指向链表1的头结点。
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
ListNode cur1 = headA;
ListNode cur2 = headB;
while (cur1 != cur2) {
if (cur1 == null) {
cur1 = headB;
continue;
}
if (cur2 == null) {
cur2 = headA;
continue;
}
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur2;
}
有人可能会有疑问,如果链表中没有公共结点,则while会不会是死循环。实际上不会,因为如果没有公共结点,那么最终cur1 == cur2 == null
解题思路2: 利用双指针。
通过上面的图可以看到,如果我们使用两个指针同时指向两个链表的头结点。在两个链表长度相等的情况下,可以一一遍历。但是现在的情况是两个链表长度不相同,因此可以让长的链表指针先移动几位,和短链表的指针保持到同一个位置,然后两个指针同时遍历链表,并比较指向的元素是不是同一个即可。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0, lenB = 0;
// 求两个链表的长度
while (curA != null) {
lenA++;
curA = curA.next;
}
while (curB != null) {
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
if (lenA > lenB) {
int gap = lenA - lenB;
for (int i = 0; i < gap; i++) {
curA = curA.next;
}
} else {
int gap = lenB - lenA;
for (int i = 0; i < gap; i++) {
curB = curB.next;
}
}
// 目前两个指针指向的位置是一样的 然后开始同时向前移动,然后判断两个指针指向的元素是不是一样
// 如果一样,则返回元素
System.out.print(1);
while (curA != null || curB != null) {
if (curA == curB)
return curA;
curA = curA.next;
curB = curB.next;
}
return null;
}
解题思路3:可以利用set集合,将链表1中所有元素都保存到set中,然后遍历链表2,每遍历一个元素,就去set中看一看是否包含,如果包含则直接返回。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null) return null;
// 创建一个set集合,保存链表A中所有元素
Set<ListNode> set = new HashSet<>();
ListNode cur = headA;
while (cur != null) {
set.add(cur);
cur = cur.next;
}
// 遍历链表B
cur = headB;
while (cur != null) {
if (set.contains(cur))
return cur;
cur = cur.next;
}
return null;
}
8. Leetcode142 环形链表II
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
解题思路1: 这个方法很难想到,可以作为结论记录下来。
- 首先定两个指针,一个快指针,一个满指针,都指向链表的头结点。快指针一次走两个元素,慢指针一次走一个元素。
- 开始遍历,如果存在环,那么快慢指针一定会相遇。如果没有环,那么fast指针会率先指向null
- 两个指针相遇后,再让满指针重新指向链表头结点,然后快慢指针同时一次移动一个单位。
- 最后快慢指针再次相遇的时候,就是环中第一个结点位置。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
// 定义快慢指针
ListNode fast = head, slow = head;
// 注意这里的循环条件 因为要fast.next.next,所以一定要保证fast.next不为空
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow)
break;
}
// 不存在环 这里的循环条件要和上面while相对应
if (fast == null || fast.next == null) return null;
slow = head;
while (slow != fast) {
slow =slow.next;
fast =fast.next;
}
return slow;
}
如果单纯判断链表中是否有环,不需要返回环的入口结点,那么直接判断快慢指针是否会相遇即可。相遇则一定有环,否则就没有环。
解题思路2: 利用set集合。遍历链表,将元素存入set,在存入之间判断当前元素是否已经存在于set中,如果存在则直接返回该元素即可。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
Set<ListNode> set = new HashSet<>();
ListNode cur= head;
while (cur != null) {
if (set.contains(cur))
return cur;
set.add(cur);
cur = cur.next;
}
return null;
}
9.Leetcode15 三数之和
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
解题思路:利用双指针解决
- 判断数组中元素如果小于3,则直接返回
- 对数组进行排序
- 使用i遍历排序后的数组,如果arr[i]>0,则直接返回结果,因为是排好序的数组,所以i后面的肯定都>0
- 对于重复的元素,直接跳过,判断条件是arr[i-1]=arr[i]
- 让left指向i+1,right指向arr.len-1。判断arr[i] + arr[left] + arr[right] == 0
- arr[i] + arr[left] + arr[right] > 0 right–
- arr[i] + arr[left] + arr[right] < 0 left++
- arr[i] + arr[left] + arr[right] = 0 找到了,同时对left和right去重
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
// 如果数组中元素个数小于3,直接返回空
if (nums.length < 3) return list;
// 对数组元素排序
Arrays.sort(nums);
// 遍历数组
for (int i = 0; i < nums.length; i++) {
// 首先判断当前遍历的元素是否大于0,如果大于直接返回
if (nums[i] > 0) return list;
// 去重
if (i > 0 && nums[i - 1] == nums[i]) continue;
// 建立双指针
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0)
right--;
else if (sum < 0)
left++;
else {
// 等于0
list.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重
while (left < right && nums[right - 1] == nums[right]) right--;
while (left < right && nums[left] == nums[left + 1]) left++;
left++;
right--;
}
}
}
return list;
}