双指针在解决数组和链表这一类题目的时候是一种很好用的方法,本文将会结合leetcode上的一些题目来总结下双指针这一解题的方法
有序数组的平方
给定一个按非递减顺序排序的整数数组 A,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
示例 1:
输入:[-4,-1,0,3,10]
输出:[0,1,9,16,100]
示例 2:
输入:[-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
- 1 <= A.length <= 10000
- -10000 <= A[i] <= 10000
- A 已按非递减顺序排序。
题目中提到的A已经是排序完成的数组,但是在平方之后,负数会变成正数,此时的序列可能不是正确排序的序列,对此,我们可以使用双指针,左指针从负数序列往后遍历,右指针从正数最大往前遍历,比较左右指针指针的数的平方值
/**
* 【双指针解法】
* O(n) n:n是数组长度
* @param A
* @return
*/
public int[] sortedSquares4(int[] A) {
int left = 0;
int right = A.length - 1;
int cur = A.length - 1;
int[] b = new int[A.length];
while(left <= right){
if(Math.pow(A[left],2) <= Math.pow(A[right],2)){
b[cur--] = (int)Math.pow(A[right],2);
right--;
}else{
b[cur--] = (int)Math.pow(A[left],2);
left++;
}
}
return b;
}
第二种方法就是将数组每项平方后再排序
/**
* 使用函数式编程一行搞定
o(N*logN)
* @param A
* @return
*/
public int[] sortedSquares3(int[] A) {
return Arrays.stream(A).map(x -> x*x).sorted().toArray();
}
合并问题
1.合并两个有序数组
给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。
说明:
初始化 nums1 和 nums2 的元素数量分别为 m 和 n。
你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例:
输入:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
这个题目和前一道题目其实是很相似的,这道题目涉及多了一个数组的复制,根本思想还是通过双指针去遍历数组,比较指针指向的数组元素的大小,实现一边遍历,一边排序和合并
/**
* 【双指针】
* @param nums1
* @param m
* @param nums2
* @param n
*/
public void merge2(int[] nums1, int m, int[] nums2, int n) {
//m:nums1.length
//n:nums2.length
int i = m - 1;
int j = n - 1;
int p = m+n-1;
while(i >=0 && j >= 0){
nums1[p--] = nums1[i] < nums2[j] ? nums2[j--] : nums1[i--];
}
System.arraycopy(nums2,0,nums1,0,j+1);
}
第二种双指针的做法有些不完美,就是这种做法只适用于数组元素都大于等于0,这种做法的思路就是把num1中数组元素为0的元素替换为nums2中的元素,然后再排序
/**
* 【双指针】
* 思路:把nums1中的0替换掉在排序,并不完美
* @param nums1
* @param m
* @param nums2
* @param n
*/
public void merge(int[] nums1, int m, int[] nums2, int n) {
//m:nums1.length
//n:nums2.length
int j = 0;
int i = 0;
while(j < n){
if(nums1[i] == 0){
nums1[i] = nums2[j];
j++;
i++;
}else{
i++;
}
}
Arrays.sort(nums1);
}
2.求两个数组的交集(1)
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2]
示例 2:
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [9,4]
说明:
- 输出结果中的每个元素一定是唯一的。
- 我们可以不考虑输出结果的顺序。
双指针思路:
- 两数组排序
- i指针指向nums1,j指针指向nums2,k指针指向相同元素
- i ,j指针指向的元素进行比较,找出相同的元素
/**
* 【双指针】
* @param nums1
* @param nums2
* @return
*/
public int[] intersection4(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
int len1 = nums1.length;
int len2 = nums2.length;
//指向nums1
int i = 0;
//指向nums2
int j = 0;
//指向交集
int k = 0;
//存放相同元素
int[] num = new int[len1 > len2 ? len1 : len2];
while(i < len1 && j < len2){
if(nums1[i] < nums2[j]){
i++;
}else if(nums1[i] > nums2[j]){
j++;
}else{
//保证元素只出现一次
if(k == 0 || num[k-1] != nums1[i]){
num[k++] = nums1[i];
}
i++;
}
}
//优化数组长度,避免空间浪费
int[] concon=new int[k];
for(int x=0;x<k;x++){
concon[x]=num[x];
}
return concon;
}
3.求两个数组的交集(2)
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入: nums1 = [1,2,2,1], nums2 = [2,2]
输出: [2,2]
示例 2:
输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出: [4,9]
说明:
- 输出结果中每个元素出现的次数,应与元素在两个数组中出现的次数一致。
- 我们可以不考虑输出结果的顺序。
思路:
- 排序
- 找出相等的数放入集合
public int[] intersect1(int[] nums1, int[] nums2) {
Arrays.sort(nums1);
Arrays.sort(nums2);
List<Integer> list = new ArrayList<>();
for (int i = 0, j = 0; i < nums1.length && j < nums2.length; ) {
if (nums1[i] < nums2[j]) {
i++;
} else if (nums1[i] > nums2[j]) {
j++;
} else {
list.add(nums1[i]);
i++;
j++;
}
}
int[] res = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
res[i] = list.get(i);
}
return res;
}
也可以使用映射的hashmap解决这一问题
思路:
- 先用map将在nums1中各元素出现的次数和值存起来
- 遍历nums2过程中,如果在map中有该key,就添加到list中,将对应的出现次数-1,直到次数 = 0
/**
* 使用hashMap
*
* @param nums1
* @param nums2
* @return
*/
public int[] intersect(int[] nums1, int[] nums2) {
//统计重复次数
int k = 0;
Map<Integer, Integer> map = new HashMap<>();
//将nums数值和出现的次数存入map中
for (int i = 0; i < nums1.length; i++) {
if (map.containsKey(nums1[i])) {
map.put(nums1[i], map.get(nums1[i]) + 1);
} else {
map.put(nums1[i], 1);
}
}
//找出重复数,存到list,并减少相应的重复次数
List<Integer> list = new LinkedList<>();
for (int j = 0; j < nums2.length; j++) {
if (map.containsKey(nums2[j]) && map.get(nums2[j]) > 0) {
list.add(nums2[j]);
map.put(nums2[j], map.get(nums2[j]) - 1);
}
}
//最后,将list中的值放入数组中
int count = list.size();
int[] aux = new int[count];
for (int i = 0; i < count; i++) {
aux[i] = ((LinkedList<Integer>) list).poll();
}
return aux;
}
反转问题
1.反转字符数组
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
示例 1:
输入:["h","e","l","l","o"]
输出:["o","l","l","e","h"]
示例 2:
输入:["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]
使用双指针,前指针和后指针,交换指针指向的字符,直到前后指针相遇
public void reverseString(char[] s) {
int i = 0;
int j = s.length - 1;
while(i < j){
char tmp = s[j];
s[j] = s[i];
s[i] = tmp;
i++;
j--;
}
}
2.验证回文串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例 1:
输入: "A man, a plan, a canal: Panama"
输出: true
示例 2:
输入: "race a car"
输出: false
思路:先将字符串转为统一形式,然后前后指针分别从前往后,从后往前遍历,只要不同,立马false掉
/**
* 验证回文串
* 【双指针】
* @param s
* @return
*/
public boolean isPalindrome(String s) {
//只考虑字母和数字字符,可以忽略字母的大小写。
s = s.toLowerCase().replaceAll("[\\p{Punct}\\p{Space}]+", "").trim();
int i = 0;
int j = s.length() - 1;
while(i < j){
//只要不相符,立马false掉
if(s.charAt(i) != s.charAt(j)){
return false;
}else {
i++;
j--;
}
}
return true;
}
3.回文链表
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
思路:
- 快慢指针找到链表中间节点
- 反转链表后半部分
- 比较反转后的后半部分和前半部分是否一样
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
/**
* 回文链表
* @param head
* @return
*/
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head;
ListNode fast = head;
//快慢指针找到链表中点
while(fast.next != null && fast.next.next != null){
fast = fast.next.next;
slow = slow.next;
}
//反转后半部分
slow = reverse(slow.next);
//比较前后部分
while(slow != null){
if(slow.val != head.val){
return false;
}else{
slow = slow.next;
head = head.next;
}
}
翻转链表
//递归实现
public ListNode reverse(ListNode head){
if (head == null || head.next == null) {
return head;
}
ListNode next = reverse(head.next);
head.next.next = head;
head.next = null;
return next;
}
//非递归实现
public ListNode reverse(ListNode head){
if (head == null || head.next == null) {
return head;
}
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode node = cur.next;
cur.next = pre;
pre = cur;
cur = node;
}
return pre;
}
此外,刚刚提到的快慢指针除了可以找链表中间节点,还可以用来判断链表中是否有环
4.环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
if (fast == slow) {
return true;
}
}
return false;
}