本文已参与「新人创作礼」活动,一起开启掘金创作之路。
双指针算法
引言: 双指针算法是一种
基本算法,我们有必要对其进行掌握。双指针算法,我主要对其进行分为两种:快慢指针,左右指针。快慢指针:主要解决的是链表的问题。左右指针: 解决的是数组相关的问题。
那么接下来我就分别从这两个方面展开叙述。
快慢指针
算法思想:
其算法思想就和它这个名字的表面意思相同。它者分别含有两个指针,一个是
slow指针,一个是fast指针,当然一般情况下,fast指针的前进速度是比slow指针快上一倍。这个相对速度是根据自己所设定的。所以不用太计较。因为链表只能向一个方向移动,所以寻找链表中点我们可以使用快慢指针。因为链表我们无法通过下标访问相关元素。
相关套路代码:
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
//if(fast == slow) return true;
}
快指针每次向前移动两步,慢指针每次值向前移动一步。
链表的相关中点的寻找:
如果链表的结点个数为
偶数个,那么最后slow指针指向的就hi是链表中间靠右的那个结点
如果链表的结点为
奇数个,那么slow指针指向的就是链表的中点。
相关leetcode题目
相关思路: 主要是对
是否含有环的判断,这是典型的快慢指针的应用。如果设置相关的快慢指针的话。如果没有含有环的情况下,程序会自动结束;如果含有环的情况下,会陷入死循环(也就是快指针会在不久的将来赶超慢指针一圈)
废话少说上代码:
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow) return true;
}
return false;
}
}
左右指针
算法思想:
见名知义,也就是设置两个指针,一个是
left指针(指向的是数组的下标为0的元素),一个是right指针(指向的是数组的最后一个元素)。从而根据题目中所要维护的东西进行移动。虽然是双重循环,但还是O(n)算法。
相关套路代码:
int j = nums.length - 1;
for(int i = 0, i < nums.length - 1; i ++){
while(j > i && check(i,j))j --;
if(i == j) break;
}
int j = 0;
for(int i = nums.length - 1; i >= 0; i --){
while(j < i && check(i,j)) j ++;
if(i == j) break;
}
代码解析:
虽然是双重循环,但是他这个算法时间复杂度是O(n)的
原因: 外面的那
一重循环维护的是i这个变量,而内层的while循环维护的是j这个变量。同时if(i == j) break,他们三者同时维护的这个是算法时间复杂度是O(n)。
相关的leetcode题目
思路解析: 先对所给字符串进行
头部去空格,然后使用双指针算法第二个套路代码进行搜索。同时,我们可以在每一次进入循环的时候进行对i的记录。
相关AC代码:
class Solution {
public String reverseWords(String s) {
// 典型的双指针
// 维护某一段区间的关系
StringBuffer sb = new StringBuffer();
int l = 0;
int len = s.length();
while(l < len && s.charAt(l) == ' '){
l ++;
}
for(int i = len - 1; i >= l; i --){
int j = i; // 很有必要优先记录
while(i >= l && s.charAt(i) != ' '){
i --;
}
if(i != j){
sb.append(s.substring(i + 1,j + 1));
if(i > l){
sb.append(" ");
}
}
}
return sb.toString();
/**
s = s.trim();
List<String> list = Arrays.asList(s.split("\\s+"));
Collections.reverse(list);
return String.join(" ",list);
*/
}
}
leetcode题目
思路: 最
简单且暴力的做法就是三重循环。优化做法就是使用双指针算法优化后面的双重循环,最后的算法时间复杂度是O(n2)。本来是三重循环,然而可以通过外层循环确定一个数,然后通过三者的关系确定另外两者的关系。那么另外两者的之和就是定值,那么就是两数之和的问题,可以使用双指针优化变成一重循环,前提是数组排序之后。
AC代码:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int n = nums.length;
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
if(n < 3) return list;
// 不重复去重
/**
1. 下一层循环的起点是上一层循环起点的加一 这个指的是三元组之间的放置顺序的关系的去重
2. if(first > 0 && nums[first] == nums[first - 1]){ 这个是同一层不能放置相同元素的去重
continue; // 这个剪枝和回溯时的一模一样
}
*/
// 本来是三重循环,然而可以通过外层循环确定一个数,然后通过三者的关系确定另外两者的关系
// 那么另外两者的之和就是定值,那么就是两数之和的问题,可以使用双指针优化变成一重循环,前提是数组排序之后
for(int first = 0; first < n; first ++){
if(first > 0 && nums[first] == nums[first - 1]){
continue; // 这个剪枝和回溯时的一模一样
}
int third = n - 1; // 该条件下,保证是O(n);
int res = -nums[first];
// 因为需要对第二个数如果和上一次的相同的进行去重所以不使用while循环
for(int second = first + 1; second < n; second ++){
if(second > first + 1 && nums[second] == nums[second - 1]) continue; // 不重复去重
while(third > second && nums[second] + nums[third] > res){
third --;
}
//前面一层 while循环结束的两个条件的分别判断
if(third == second) break; // 该条件下,保证是O(n);
if(nums[second] + nums[third] == res){
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(nums[first]);
list1.add(nums[second]);
list1.add(nums[third]);
list.add(list1);
}
}
}
return list;
}
}
相关的例题还有:
链表相关题目:
思路: 找到链表的
中点,然后进行链表反转,注意:要将反转之前的那个结点的next指针设为null,然后再进行合并。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public void reorderList(ListNode head) {
if(head == null) return;
ListNode fast = head,slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
ListNode newNode = reverseList(slow.next);
// 因为前面的一半链表的结尾指向的还是这个反转后的链表的结尾,所以形成了环
slow.next = null;
ListNode l1 = head, l2 = newNode;
// 合并链表模板,记住这是如果链表元素个数为奇数个的话,就是反转后的链表头为中间的那一个的后一个,
// 如果是偶数个的话,就是就是中间的那一个的靠右的第二个的为表头
while(l1 != null && l2 != null){
ListNode l1_tmp = l1.next;
ListNode l2_tmp = l2.next;
l1.next = l2;
l1 = l1_tmp;
l2.next = l1;
l2 = l2_tmp;
}
}
public ListNode reverseList(ListNode node){
// 反转链表的模板
// pre cur after
ListNode pre = null;
ListNode cur = node;
while(cur != null){
ListNode after = cur.next;
cur.next = pre;
pre = cur;
cur = after;
}
return pre;
}
}
这里需要补充一下:
链表反转的模板代码
public ListNode reverseList(ListNode node){
// 反转链表的模板
// pre cur after
ListNode pre = null;
ListNode cur = node;
while(cur != null){
ListNode after = cur.next;
cur.next = pre;
pre = cur;
cur = after;
}
return pre;
}
这里双指针算法就差不多介绍完毕了,当然需要大量的习题进行练习,这是我基本算了二三十道双指针算法后进行的总结,希望能对你有所帮助。