Leetcode记录:二分查找和双指针法

二分查找

二分查找的前提为有序数组,同时要关注数组是否存在重复元素。

二分查找的框架为

int binarySearch(int[] nums, int target) {
    int left = 0, right = ...;

    while(...) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。另外,计算 mid 时需要技巧防止溢出。

基本的二分查找

省略号的部分可以分为两种写法:左闭右闭和左闭右开,可以画图来进行分析,此处略过

int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
            int midd = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[mid] > target) {
                right = mid - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[mid] < target) {
                left = mid + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return mid; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }
// Another version:
int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) >> 1);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }

但是如果给定有序数组 nums = [1,2,2,2,3],target = 2,此算法返回的索引是 2,但是如果我想得到 target 的左侧边界,即索引 1,或者我想得到 target 的右侧边界,即索引 3,这样的话此算法是无法处理的。

寻找左右侧边界的二分搜索

int left_bound(vector<int>& nums, int target) {
    if (nums.size() == 0) return -1;
    int left = 0;
    int right = nums.size() -1; // 注意

    while (left <= right) { // 注意
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            right = mid - 1;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1; // 注意
        }
    }
    // target 比所有数都大
    if (left == nums.size()) return -1;
    return nums[left] == target ? left : -1;
}

int right_bound(vector<int>& nums, int target) {
    if (nums.size() == 0) return -1;
    int left = 0, right = nums.size()-1;

    while (left <= right) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            left = mid + 1; // 注意
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid -1;
        }
    }
    if (right == -1) return -1;
    return nums[right] == target ? right : -1; // 注意
}

也可以统一写为一个函数

int binarySearch(vector<int>& nums, int target, bool left_bound){
    int left = 0; int right = nums.size()-1;
    while(left <= right){
        int mid = (left + right) / 2;
        if(nums[mid] > target || (lower && nums[mid] >= target)){
            right = mid - 1;
        }else{
            left = mid + 1;
        }
    }
    return left;
}
vector<int> searchRange(vector<int>& nums, int target) {
    int leftIdx = binarySearch(nums, target, true);
    int rightIdx = binarySearch(nums, target, false) - 1;
    if (leftIdx <= rightIdx && rightIdx < nums.size() &&
        nums[leftIdx] == target && nums[rightIdx] == target){
        return vector<int>{leftIdx, rightIdx};
    } 
    return vector<int>{-1, -1};
}

寻找两个中序数组的中位数

Leetcode 4. 给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 算法的时间复杂度应该为 O(log (m+n)) 。

这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1。 假设两个有序数组分别是 A 和 B。要找到第 k 个元素,我们可以比较 A[k/2−1]B[k/2−1],对于较小值,最多只会有 k−2 个元素比它小,那么它就不能是第 k 小的数了。

int getKthElement(const vector<int> &nums1, const vector<int> &nums2, int k){
    int m = nums1.size(), n = nums2.size();
    int index1 = 0, index2 = 0;

    while(true){
        if(index1 == m){
            return nums2[index2 + k - 1];
        }
        if(index2 == n){
            return nums1[index1 + k - 1];
        }
        if(k == 1)
            return min(nums1[index1], nums2[index2]);
    
        int newIndex1 = min(index1 + k/2 - 1, m-1);
        int newIndex2 = min(index2 + k/2 - 1, n-1);
        int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
        if(pivot1 <= pivot2){
            k-= newIndex1 - index1 + 1;
            index1 = newIndex1 + 1;
        } else{
            k -= newIndex2 - index2 + 1;
            index2 = newIndex2 + 1;
        }
    }
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    int totalLength = nums1.size() + nums2.size();
    if(totalLength % 2 == 1)
        return getKthElement(nums1, nums2, (totalLength+1)/2);
    else
        return (getKthElement(nums1,nums2,totalLength/2) + getKthElement(nums1, nums2, totalLength/2+1)) / 2.0;
}

对应题目:

  1. 704.二分查找
  2. 35.搜索插入位置
  3. 34.在排序数组中查找元素的第一个和最后一个位置

双指针法

双指针是指在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针进行遍历,从而达到相应的目的。

最常见的双指针算法有两种:

  1. 在同一个序列中,用两个指针维护两个位置,或两个位置包含的区间;
  2. 在两个序列里边,两个指针指向不同的序列,来维护某种次序。

按照指针的移动方向,双指针分为同向双指针、异向双指针:

  1. 同向双指针,也称快慢指针(两个指针一快一慢);
  2. 异向双指针,分为对撞指针(从两边向中间移动)、背向指针(从中间向两边移动)。

快慢指针

两个指针,初始在同一位置,然后向相同方向移动,一个移动速度快,一个移动速度慢。

适用场景:查找链表中间节点、链表是否包含环、原地修改数组。

示例1

LeetCode 876.【链表的中间结点】

给你单链表的头结点 head ,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。如下图所示,链表有 5 个节点,返回第 3 个节点;链表有 6 个节点,返回第 4 个节点。

ListNode middleNode(ListNode head) {
    ListNode fast = head;
    ListNode slow = head;
    while (fast != null && fast.next != null) {
        fast = fast.next.next;
        slow = slow.next;
    }
    return slow;
}

示例2

LeetCode 26.【删除有序数组中的重复项】 给你一个非严格递增排列的数组 nums ,请你原地删除重复出现的元素,使每个元素只出现一次 ,返回删除后数组的新长度。元素的相对顺序应该保持 一致 。然后返回 nums 中唯一元素的个数。

int removeElement(vector<int>& nums, int val) {
    int slowIndex=0;
    for(int fastIndex =0;fastIndex<nums.size();fastIndex++)
    {
        if(val!=nums[fastIndex])
        {
            nums[slowIndex++]=nums[fastIndex];
        }
    }
    return slowIndex;
}

对撞指针

两个指针,初始一个在左、一个在右,左指针向右移动,右指针向左移动,直到相撞停止。

适用场景:二分查找、反转数组、回文判定。

示例1

LeetCode 977.【有序数组的平方】 给你一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。

由于数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间,此时可以考虑双指针法了

vector<int> sortedSquares(vector<int>& A) {
        int k = A.size() - 1;
        vector<int> result(A.size(), 0);
        for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素
            if (A[i] * A[i] < A[j] * A[j])  {
                result[k--] = A[j] * A[j];
                j--;
            }
            else {
                result[k--] = A[i] * A[i];
                i++;
            }
        }
        return result;
    }

示例2

LeetCode 344.【反转字符串】

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

void reverseString(char[] s) {
    int left = 0;
    int right = s.length - 1;
    while (left < right) {
        char tmp = s[left];
        s[left] = s[right];
        s[right] = tmp;
        left++;
        right--;
    }
}

背向指针

两个指针,初始都在中间,左指针向左移动,右指针向右移动,直至碰到最左或最右边界。

使用场景:查找字符串中的最长回文子串。

示例1

LeetCode 5.最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

在给定字符串中找回文子串的难点在于,回文子串的的长度可能是奇数也可能是偶数,如果回文串的长度为奇数,则它有一个中心字符,例如bacab;如果回文串的长度为偶数,则可以认为它有两个中心字符,例如baab

string expandCenter(string s, int left, int right) {
    int len = s.length();
    // 要时刻注意避免越界访问
    while (left >= 0 && right < len
           && s[left] == s[right]) {
        left--;
        right++;
    }
    return s.substr(left + 1, right - left - 1);
}

string longestPalindrome(string s) {
    int len = s.length();
    string res;
    for (int i = 0; i < len; i++) {
        string sub1 = expandCenter(s, i, i);
        string sub2 = expandCenter(s, i, i + 1);
        res = res.length() >= sub1.length() ? res : sub1;
        res = res.length() >= sub2.length() ? res : sub2;
    }
    return res;
}