算法之数组篇

140 阅读6分钟

🍓数组篇


在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针快慢指针

所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。

对于单链表来说,大部分技巧都属于快慢指针,比如链表环判断,倒数第 K 个链表节点等问题,它们都是通过一个 fast 快指针和一个 slow 慢指针配合完成任务。

在数组中并没有真正意义上的指针,但我们可以把索引当做数组中的指针,这样也可以在数组中施展双指针技巧,本文主要讲数组相关的双指针算法

只要数组有序,就应该想到双指针技巧。

注意原地删除,原地修改依然需要使用快慢指针技巧

🍊题一:26. 删除有序数组中的重复项

image.png

⭐️思路:

注意有个原地修改。

如果不是原地修改的话,我们直接 new 一个 int[] 数组,把去重之后的元素放进这个新数组中,然后返回这个新数组即可。

但是现在题目让你原地删除,不允许 new 新数组,只能在原数组上操作,然后返回一个长度,这样就可以通过返回的长度和原始数组得到我们去重后的元素有哪些了。

🍉让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到一个不重复的元素就赋值给 slow 并让 slow 前进一步。

⭐️代码:

class Solution {
    public int removeDuplicates(int[] nums) {
    int slow=0,fast=0;
    while(fast<nums.length){
        if(nums[slow]!=nums[fast]){
            slow++;
            nums[slow]=nums[fast];
        }
         fast++;
    }
    return slow+1;
    }
}

🍊题二:83. 删除排序链表中的重复元素

image.png

⭐️思路:

和数组去重是一模一样的,唯一的区别是把数组赋值操作变成操作指针

⭐️代码:

/**
 * 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 deleteDuplicates(ListNode head) {
    if(head==null)return null;
    ListNode slow=head,fast=head;
    while(fast!=null){
        if(fast.val!=slow.val){
        slow.next=fast;//指向fast 相当于数组的nums[slow]=fast;
        slow=slow.next;//相当于slow++;
        }
         fast=fast.next;
    }
    slow.next=null;//退出循环 fast为空的时候
    return head;
    }
}

🍊题三:27. 移除元素

image.png

⭐️思路:

题目要求我们把 nums 中所有值为 val 的元素原地删除,依然需要使用快慢指针技巧 注意这里和有序数组去重的解法有一个细节差异,我们这里是先给 nums[slow] 赋值然后再给 slow++,这样可以保证 nums[0..slow-1] 是不包含值为 val 的元素的,最后的结果数组长度就是 slow

⭐️代码:

class Solution {
    public int removeElement(int[] nums, int val) {
    int slow=0,fast=0;
    while(fast<nums.length){
        if(nums[fast]!=val){
            nums[slow]=nums[fast];
            slow++;
        }
        fast++;
    }
    return slow;
    }
}

🍊题四:283. 移动零

⭐️思路:

可以直接复用 27. 移除元素 的解法,先移除所有 0,然后把最后的元素都置为 0,就相当于移动 0 的效果。

⭐️代码:

class Solution {
    public void moveZeroes(int[] nums) {
    int p=removeElement(nums,0);
    for(;p<nums.length;p++){
        nums[p]=0;
    }
}
    int removeElement(int[]nums,int val){
        int fast=0,slow=0;
        while(fast<nums.length){
            if(nums[fast]!=0){
                nums[slow]=nums[fast];
                slow++;
            }
            fast++;
        }
        return slow;
    }
}

🍊题五:167. 两数之和 II - 输入有序数组

image.png

⭐️思路:

只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 就可以调整 sum 的大小

⭐️代码:

class Solution {
    public int[] twoSum(int[] numbers, int target) {
    int left=0,right=numbers.length-1;
    while(left<right){
    int sum=numbers[left]+numbers[right];
    if(sum==target)
    return new int[]{left+1,right+1};
    else if(sum<target)
    left++;
    else if(sum>target)
    right--;
    }
    return new int[]{-1,-1};
    }
}

🍊二分查找

二分查找法是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

  • 左闭右闭[left,right],这里我们先给出二分查找法的第一种写法:
public int binarySearch(int[] nums, int target) {
        // 在区间[left,right]中查找元素,左闭右闭
        int left = 0;
        int right = nums.length - 1;
        // 由于是在区间[left,right]中查找
        // 因此当left=right时,区间内还有一个元素需要查找
        while (left <= right) {
            // 计算中间点
            int mid = left + (right-left)/2;
            // 如果target == nums[mid]则表示已经找到,返回mid
            if (target == nums[mid]) {
                return mid;
            // 如果target < nums[mid],表示目标值可能在左半边
            } else if (target < nums[mid]){
            // 由于是在左闭右闭的区间[left,right]中查找
            // 而target < nums[mid],因此mid不再需要考虑
            // 所以right = mid - 1,即在[left,mid-1]中继续查找
                right = mid - 1;
    
            // 如果target > nums[mid],表示目标值可能在右半边
            } else if (target > nums[mid]){
           // 由于是在左闭右闭的区间[left,right]中查找
           // 而target > nums[mid],因此mid不再需要考虑
           // 所以left = mid + 1,即在[mid+1,right]中继续查找
                left = mid + 1;
            }
        }
        // 未找到返回-1
        return -1;
    }

接着对代码中需要注意的细节进行说明:

一是程序什么时候停止?

在上述代码中的第2行和第3行,分别定义了left=0,right=nums.length-1。这表明我们是在左闭右闭的区间[left, right]中查找目标值。

既然是查找目标值,那么就有找到和找不到的情况。

对于能找到目标值的情况,只需直接将目标值返回即可

对于折半查找但最终没找到的情况,根据题意是返回-1即可。那么,这时程序什么时候停止呢?由于这里定义的是在[left, right]这个左闭右闭的区间内查找,因此当left=right时,区间[left, right]依然包含一个有效的待查找元素。 所以在不存在目标值的情况下,程序终止的条件是left>right。

  • 左闭右开[left,right),在理解了第一种写法后,接着看下第二种写法。第二种写法与第一种写法的一个主要区别是查找区间变为了左闭右开[left, right)。

这一变化使得代码相比第一种写法有两处不同:

一是while循环继续的条件变为了left<right。因为如果left=right,那么区间[left,right)是不存在的,比如区间[2,2)。

while (left < right) { // 查找逻辑
} 二是当target小于nums[mid]时,右侧边界right=mid而不是之前的mid-1。原因还是由于这里定义的查找区间是[left, right),那么当target<nums[mid]时,nums[mid-1]还是需要考察的,所以right=mid。

if (target < nums[mid]){ right = mid; } 具体的代码实现如下,可参考详细注释进行理解。

public int binarySearch(int[] nums, int target) {
        // 在区间[left,right)中查找元素,左闭右开
        int left = 0;
        int right = nums.length;

        // 由于是在区间[left,right)中查找
        // 当left=right时,区间[left,right)不存在
        while (left < right) {
            // 计算中间点
            int mid = left + (right-left)/2;
            // 如果target == nums[mid]则表示已经找到,返回mid
            if (target == nums[mid]) {
                return mid;
                
                // 如果target < nums[mid],表示目标值可能在左半边
            } else if (target < nums[mid]){
                // 由于是在左闭右开的区间[left,right)中查找
                // 而target < nums[mid],因此mid不再需要考虑
                // 但nums[mid-1]还需考察,所以right = mid
                // 即在[left,mid)中继续查找
                right = mid;
    
                // 如果target > nums[mid],表示目标值可能在右半边
            } else if (target > nums[mid]){
                // 由于是在左闭右开的区间[left,right)中查找
                // 而target > nums[mid],因此mid不再需要考虑
                // 所以left = mid + 1,即在[mid+1,right)中继续查找
                left = mid + 1;
            }
        }

🍇704. 二分查找

⭐️思路:

代码中 left + (right - left) / 2 就和 (left + right) / 2 的结果相同,但是有效防止了 left 和 right 太大,直接相加导致溢出的情况。

⭐️代码:

class Solution {
    public int search(int[] nums, int target) {
    int left=0,right=nums.length-1;
    while(left<=right){
        int mid=left+(right-left)/2;
        if(nums[mid]==target)
        return mid;
        else if(nums[mid]<target)//说明在右半边
        left=left+1;
        else 
        right=right-1;
    }
    return -1;
    }
}

🍇33. 搜索旋转排序数组

image.png image.png

class Solution {
    public int search(int[] nums, int target) {
     int left=0,right=nums.length-1;
     while(left<=right){
         int mid=left+(right-left)/2;
         if(nums[mid]==target){
             return mid;
         }
        else if(nums[left]<=nums[mid]){//左半边递增
             if(target<nums[mid]&&target>=nums[left]){
                 right=mid-1;
             }else{
                 left=mid+1;
             }
         }else{//右半边递增
             if(target>nums[mid]&&target<=nums[right]){
                  left=mid+1;
              
             }else{
                 right=mid-1;
             }
         }
     }
     return -1;
    }
}

🍊题七:344. 反转字符串

⭐️思路:

可以用左右指针,一左一右两个指针相向而行

⭐️代码:

class Solution {
    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;
        left++;
        right--;
    }
    }
}

🍊题八:11. 盛最多水的容器

image.png

⭐️思路:

矩形的高度是由 min(height[left], height[right]) 即较低的一边决定的 用 left 和 right 两个指针从两端向中心收缩,一边收缩一边计算 [left, right] 之间的矩形面积,取最大的面积值即是答案

⭐️代码:

class Solution {
    public int maxArea(int[] height) {
    int left=0,right=height.length-1;
    int res=0;
    while(left<right){
        int area=Math.min(height[right],height[left])*(right-left);
        res=Math.max(res,area);
        if(height[left]<height[right]){
            left++;
        }else{
        right--;
        }
    }
    return res;
    }
}

🍊题九:42. 接雨水

image.png

⭐️思路:

双指针

⭐️代码:

class Solution { public int trap(int[] height) { int left=0,right=height.length-1; int leftMax=0,rightMax=0,res=0; while(left<right){ leftMax=Math.max(leftMax,height[left]); rightMax=Math.max(rightMax,height[right]); if(leftMax<rightMax){ res+=leftMax-height[left]; left++; }else{ res+=rightMax-height[right]; right--; } } return res; } }