数据结构算法刷题--双指针法

1、移除元素

  • 题目:https://leetcode.cn/problems/remove-element/
  • 思路:双指针,快慢双指针:快指针找到要移除的元素时快指针跳过,正常元素由快指针拷贝到慢指针位置
  • 代码实现:
class Solution {
    // 双指针--快慢指针
    public int removeElement(int[] nums, int val) {
        // 慢指针slow用于维护去掉val值后,数组存放的顺序索引遍历
        // 快指针fast用于遍历nums中原来的值,fast遇到val跳过就行,不是val的放到slow的位置
        int slow = 0;
        for(int fast = 0; fast < nums.length; ++fast){
            while(nums[fast] == val){
                // 命中--fast直接跳过读取下一个应该保留的数
                fast++;
                if(fast == nums.length){
                    return slow;
                }
            }
            // System.out.println("slow-->" + slow + ", fast-->" + fast);
            nums[slow++] = nums[fast];
        }

        return slow;
    }
}

2、反转字符串

  • 题目:https://leetcode.cn/problems/reverse-string/
  • 思路:双指针–首尾双指针,交换左右指针的元素;交换的实现既可以通过传统的设置临时变量,对于数值类型的交换还可以通过三次异或运算实现。
  • 代码实现
class Solution {
    // 双指针法--相向双指针
    public void reverseString(char[] s) {
        // 1、初始化双指针
        int slow = 0;
        int fast = s.length - 1;
        while(slow < fast){
            // 2、通过异或进行交换操作
            s[slow] ^= s[fast];
            s[fast] ^= s[slow];
            s[slow] ^= s[fast];
            // 3、指针移动
            ++slow;
            --fast;
        }
    }
}

3、替换空格

  • 题目:https://leetcode.cn/problems/ti-huan-kong-ge-lcof/
  • 思路:双指针–快慢双指针:先统计空格个数,每有一个空格扩充两个字符位置;然后左右指针分别指向原字符串和扩容后的字符串的最后面,从后往前遍历(可以有效避免数组插入元素的移动)。
  • 代码实现:
class Solution {
    // 双指针法,为了数组的移动方便,无比从后往前
    public String replaceSpace(String s) {
        // 1、先统计空格的个数,对每个空格追加两个位置
        StringBuilder sb = new StringBuilder(s);
        for(int i = 0; i < s.length(); ++i){
            if(s.charAt(i) == ' '){
                sb.append("  ");
            }
        }

        // 2、双指针启动
        // 2.1、指针初始化
        int left = s.length() - 1;
        int fast = sb.length() - 1;

        while(left >= 0){
            // 2.2、核心逻辑--遇到空格补一个 "%20";正常字符的话就正常替换过来
            if(s.charAt(left) == ' '){
                sb.setCharAt(fast--, '0');
                sb.setCharAt(fast--, '2');
                sb.setCharAt(fast--, '%');
                // 2.3、指针移动
                --left;
            }else{
                // 不是空格,正常移动
                sb.setCharAt(fast--, s.charAt(left--));
            }
        }

        return new String(sb);

    }
}

4、翻转字符串里的单词

  • 题目:https://leetcode.cn/problems/reverse-words-in-a-string/
  • 思路:先整体反转(相向双指针),每个单词局部反转(双指针)+移位
  • 代码实现:
class Solution {
    // 整体反转 + (每个单词局部反转 + 移位)

    public String reverseWords(String s) {
        // 1.单词整体反转
        char[] ch = s.toCharArray();
        reverseCharArray(ch, 0, s.length() - 1);

        // 2.局部反转-每个单词
        // i控制获得一个单词的起始结束索引
        // j控制读取一个单词的字符但不读取空格
        // k控制将每个单词放过来同时适当添加单词之间的一个空格
        int j = 0, k = 0;
        for(int i = 0; i < ch.length; ++i){
            // 2.1 得到一个单词
            if(ch[i] == ' '){
                // 跳过空格
                continue;
            }
            // 空格后的第一个字符,获得单词的起始索引
            int wordStartIndex = i;
            // 让i走完一个单词
            while(i < ch.length && ch[i] != ' '){
                ++i;
            }
            int wordEndIndex = i - 1;

            // 2.2 局部反转单词,然后用j来读取单词,然后通过k来放置单词
            reverseCharArray(ch, wordStartIndex, wordEndIndex);
            for(j = wordStartIndex; j <= wordEndIndex; ++j){
                ch[k++] = ch[j];
            }

            // 2.3 每个单词结束追加空格,但注意避免越界(原字符串没有要删除的字符的情况)
            if(k < ch.length){
                ch[k++] = ' ';
            }
        }

        // 3. 返回结果,k体现了当前结束位置,最后末尾可能有多出来的空格可能没有
        // 没有的情况很好判断:k在原字符串末尾并且这个字符不为空格
        return new String(ch, 0, (k == s.length()) && (ch[k - 1] != ' ') ? k : k - 1);
    }

    // 自定义字符串反转方法——左右双指针
    public void reverseCharArray(char[] ch, int left, int right){
        while(left < right){
            // 反转操作
            ch[left] ^= ch[right];
            ch[right] ^= ch[left];
            ch[left] ^= ch[right];

            // 指针移动
            ++left;
            --right;
        }
    }
}

5、翻转链表

  • 题目:https://leetcode.cn/problems/reverse-linked-list/
  • 思路:三指针–分别记录正序条件下的前一个元素、当前元素、下一个元素;然后通过让当前元素的 next 为前一个元素实现一次翻转;指针移动到下一个元素准备下一次翻转操作–当前元素指针到下一个元素、前一个元素指针到当前元素
  • 代码实现:
/**
 * 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 ListNode reverseList(ListNode head) {
        // 1、指针初始化
        ListNode prev = null;
        ListNode cur = head;

        while(cur != null){
            // 2、核心逻辑--反转:从头开始倒着指
            // 2.1、记录下cur的下一个,免得一会一倒就找不到了
            ListNode temp = cur.next;
            // 2.2、反转,让当前指向前一个
            cur.next = prev;

            // 3、指针移动
            prev = cur;
            cur = temp;
        }
        
        return prev;
    }
}

6、删除链表的倒数第N个节点

  • 题目:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
  • 思路:双指针–快慢指针法,先让快指针走n步,然后快慢指针同步走,这样当快指针走到最后一个元素时,慢指针就走到了要删除的前一个节点。注意:链表中的删除操作要走到要删除节点的前一个,第一个节点也存在被删除的可能–添加虚拟节点。
  • 代码实现:
/**
 * 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 {
    // 双指针,快指针先走n+1步,然后和慢指针一起走,当快指针走到末尾,慢指针就到了要删除节点的前一个
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 头结点可能被删除--设置虚拟头结点
        // 1、指针初始化
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        ListNode slow = dummyHead, fast = dummyHead;
        for(int i = 0; i < n; ++i){
            fast = fast.next;
        }

        // 2、指针移动
        while(fast.next != null){
            fast = fast.next;
            slow = slow.next;
        }

        // 3、删除节点
        slow.next = slow.next.next;

        return dummyHead.next;
    }

}

7、链表相交

  • 题目:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/
  • 思路:双指针–相交点及其以后的节点一定是全部都对齐的,所以可以先让两个链表的尾部对齐–统计两个链表的长度,计算长度差,然后让长的链表指针先从头节点走长度差,这个时候长链表指针和位于短链表头部的指针对齐,逐个向后判断两个节点是否相等就可以了。
  • 代码实现:
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    // 双指针法
    // 先求取两者长度,然后算得长度差,将长的那一个指针走长度差从而对齐两个链表的末端,逐个判断是否相交
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        // 1、计算两个链表的长度
        int lenA = 0, lenB = 0;
        ListNode nodeA = headA, nodeB = headB;
        while(nodeA != null){
            ++lenA;
            nodeA = nodeA.next;
        }
        while(nodeB != null){
            ++lenB;
            nodeB = nodeB.next;
        }

        // 2、整理,将A设为长的链表,方便操作
        if(lenA < lenB){
            // 长度交换
            int lenTemp = lenA;
            lenA = lenB;
            lenB = lenTemp;
            // 链表交换
            nodeA = headA;
            headA = headB;
            headB = nodeA;
        }

        // 3、移动长的指针使二者末尾对齐
        int diff = lenA - lenB;
        nodeA = headA;
        nodeB = headB;
        for(int i = 0; i < diff; ++i){
            nodeA = nodeA.next;
        }

        // 4、逐个判断
        while(nodeA != null){
            if(nodeA == nodeB){
                return nodeA;
            }
            nodeA = nodeA.next;
            nodeB = nodeB.next;
        }

        return null;
        
    }
}

8、环形链表Ⅱ

  • 题目:https://leetcode.cn/problems/linked-list-cycle-ii/
  • 思路:快慢双指针–快指针每次走两步,慢指针每次走一步,如果是环,必定相遇
    • 1、记起点到环入口为x,环入口到相遇点长度为y,相遇点再到入口长度为z;
    • 2、于是有 2(x + y) = x + (y + z) + y
    • 3、得到 x = z
    • 4、重点–所以在相遇后找到入口,只需要再从头开始一个指针,让原来的慢指针和新的指针同步一步一步走到相遇的地方就是入口!
  • 代码实现:
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    // 快慢双指针:快指针每次走两步,慢指针每次走一步,如果是环,必定相遇
    // 1、记起点到环入口为x,环入口到相遇点长度为y,相遇点再到入口长度为z
    // 2、于是有 2(x + y) = x + (y + z) + y
    // 3、得到 x = z
    // 4、所以在相遇后找到入口,只需要再从头开始一个慢指针,让原来慢指针和新的慢指针相遇就是入口!

    public ListNode detectCycle(ListNode head) {
        // 特殊情况判断
        if(head == null){
            return null;
        }

        // 1、快慢指针初始化
        ListNode slow = head;
        ListNode fast = head;

        // 2、移动,直到两者相遇
        while(fast.next != null && fast.next.next != null){
            fast = fast.next.next;
            slow = slow.next;
            // System.out.println("fast-->" + fast + ", slow-->" + slow);
            
            // 相遇
            if(fast == slow){
                // 3、再从头开始一个,和慢指针同步走到相遇--入口
                // 直接使用fast作为新的从头开始的慢指针
                fast = head;
                while(fast != slow){
                    fast = fast.next;
                    slow = slow.next;
                }
                return fast;
            }
        }

        return null;

    }
}

9、三数之和

  • 题目:https://leetcode.cn/problems/3sum/
  • 思路:第一个数通过遍历固定,然后后两个数的遍历通过相向双指针将时间复杂度由O(n^2)压缩到O(n);第一个数注意剪枝、去重,后两个数也注意去重。
  • 代码实现:
class Solution {
    // 双指针:固定第一个数,第二三个数用相向双指针--注意先排序,判断过程中注意去重

    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> ret = new ArrayList<>();

        // 1、排序
        Arrays.sort(nums);

        // 2、遍历固定第一个数
        for(int i = 0; i < nums.length; ++i){
            // 2.1、第一个数剪枝
            if(nums[i] > 0){
                break;
            }
            // 2.2、第一个数去重
            if(i > 0 && nums[i - 1] == nums[i]){
                continue;
            }

            // 3、双指针获得后两个数
            int left = i + 1, right = nums.length - 1, sum = 0;
            while(left < right){
                sum = nums[i] + nums[left] + nums[right];
                if(sum > 0){
                    // 右指针的数大了
                    --right;
                }else if(sum < 0){
                    // 左指针的数小了
                    ++left;
                }else{
                    // 这组结果是对的
                    List<Integer> list = new ArrayList<>();
                    list.add(nums[i]);
                    list.add(nums[left]);
                    list.add(nums[right]);
                    ret.add(list);

                    // 后两个数的去重
                    while(left < right && nums[right - 1] == nums[right]){
                        --right;
                    }
                    while(left < right && nums[left + 1] == nums[left]){
                        ++left;
                    }
                    
                    // 左右指针同时移动
                    ++left;
                    --right;
                }
            }
        }

        return ret;

    }
}

10、四数之和

  • 题目:https://leetcode.cn/problems/4sum/
  • 思路:三数之和进阶,遍历固定第一个数后,再遍历固定第二个数,最后剩下的两个数通过双指针压缩时间复杂度到O(n),前两个数注意剪枝、去重;最后两个数注意去重。
  • 代码实现:
class Solution {
    // 三数之和进阶:固定住前两个数,双指针获得后两个数,使后两个数的时间复杂度为O(n),降一阶

    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> ret = new ArrayList<>();

        // 1、数组排序
        Arrays.sort(nums);

        // 2、遍历固定第一个数
        for(int i = 0; i < nums.length - 3; ++i){
            // System.out.println("-------------");
            // System.out.println("i-->" + i);
            // 2.1 第一个数剪枝,注意target可能是负数
            if(nums[i] > target && target > 0){
                break;
            }
            // 2.2 第一个数去重
            if(i > 0 && nums[i - 1] == nums[i]){
                continue;
            }

            // 3、遍历固定第二个数
            for(int j = i + 1; j < nums.length - 2; ++j){
                // System.out.println("j-->" + j);
                // 3.1、第二个数剪枝
                if(nums[j] > target - nums[i] && nums[i] > 0){
                    break;
                }
                // 3.2、第二个数去重
                if(j > i + 1 && nums[j - 1] == nums[j]){
                    continue;
                }

                // 4、双指针获得后两个数
                int left = j + 1, right = nums.length - 1, sum = 0;
                // System.out.println("left-->" + left);
                // System.out.println("right-->" + right);

                // 5、结果判断
                while(left < right){
                    sum = nums[i] + nums[j] + nums[left] + nums[right];
                    if(sum > target){
                        // 右指针的数大了
                        --right;
                    }else if(sum < target){
                        // 左指针的数小了
                        ++left;
                    }else{
                        // 结果对了
                        List<Integer> list = new ArrayList<>();
                        list.add(nums[i]);
                        list.add(nums[j]);
                        list.add(nums[left]);
                        list.add(nums[right]);
                        ret.add(list);

                        // 后两个数的去重
                        while(left < right && nums[right - 1] == nums[right]){
                            --right;
                        }
                        while(left < right && nums[left + 1] == nums[left]){
                            ++left;
                        }

                        // 指针移动
                        ++left;
                        --right;
                    }
                }
            }

        }
        return ret;
    }
}

11、双指针总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值