双指针算法

162 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

双指针算法

引言: 双指针算法是一种基本算法,我们有必要对其进行掌握。双指针算法,我主要对其进行分为两种:快慢指针左右指针快慢指针:主要解决的是链表的问题。左右指针: 解决的是数组相关的问题。

那么接下来我就分别从这两个方面展开叙述。

快慢指针

算法思想:

其算法思想就和它这个名字的表面意思相同。它者分别含有两个指针,一个是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;
    }

这里双指针算法就差不多介绍完毕了,当然需要大量的习题进行练习,这是我基本算了二三十道双指针算法后进行的总结,希望能对你有所帮助。