Leetcode101 双指针

96 阅读4分钟

双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。也可以延伸到多个数组的多个指针。 若两个指针指向同一数组,遍历方向相同且不会相交,则也称为滑动窗口(两个指针包围的区域即为当前的窗口),经常用于区间搜索。 若两个指针指向同一数组,但是遍历方向相反,则可以用来进行搜索,待搜索的数组往往是排好序的。

(1)167. Two Sum II - Input array is sorted (Easy)

题目描述

在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。

本人思路

这个题目由于是做过的,所以很容易想到了双指针。从左边和右边分别遍历,如果小了就左移,大了就右移。

代码展示

vector<int> twoSum(vector<int>& numbers, int target) {
    int l = 0, r = numbers.size() - 1, sum;
    while (l < r) {
        sum = numbers[l] + numbers[r];  
        if (sum == target) {
            break;
        }  
        if (sum < target) {
            ++l;
        } else {
            --r;
        }
    }
    return vector<int>{l + 1, r + 1};
}

(2)88. Merge Sorted Array (Easy)

题目描述

给定两个有序数组,把两个数组合并为一个。

本人思路

第一反应是想到了算法中学的合并排序,但是想了一会没想起来当时的排序细节了。思考了一下能想到的就是一步步移,但是效率太低了,如何利用双指针来解决这个问题呢?

题解思路

因为这两个数组已经排好序,我们可以把两个指针分别放在两个数组的末尾,即 nums1 的m − 1 位和 nums2 的 n − 1 位。每次将较大的那个数字复制到 nums1 的后边,然后向前移动一位。因为我们也要定位 nums1 的末尾,所以我们还需要第三个指针,以便复制。在以下的代码里,我们直接利用 mn 当作两个数组的指针,再额外创立一个 pos 指针,起始位置为 m +n−1。每次向前移动 mn 的时候,也要向前移动 pos。这里需要注意,如果 nums1的数字已经复制完,不要忘记把 nums2 的数字继续复制;如果 nums2 的数字已经复制完,剩余nums1的数字不需要改变,因为它们已经被排好序。

代码展示

void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
    int pos = m+ n- 1;   
		m--;
		n--;
    while (m >= 0 && n >= 0) {
        nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];
    }
    while (n >= 0) {
        nums1[pos--] = nums2[n--];
    }
}

(3)142. Linked List Cycle II (Medium)

题目描述

给定一个链表,如果有环路,找出环路的开始点。

本人思路

完全没什么思路,没遇到过这种类型的题目,思考了一会直接看题解了。

题解

对于链表找环路的问题,有一个通用的解法——快慢指针(Floyd 判圈法) 。给定两个指针,分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast可以走到尽头,那么说明没有环路;如果 fast 可以无限走下去,那么说明一定有环路,且一定存在一个时刻 slow 和 fast 相遇。

当 slow 和 fast 第一次相遇时,我们将 fast 重新移动到链表开头,并让 slow 和 fast 每次都前进一步。当 slow 和 fast 第二次相遇时,相遇的节点即为环路的开始点。

代码展示

ListNode* detectCycle(ListNode* head) {
    ListNode* slow = head;
    ListNode* fast = head;
    // 判断是否存在环路
    do {
        if (!fast || !fast->next) {
            return nullptr;
        }
        fast = fast->next->next;
        slow = slow->next;
    } while (fast != slow);
    // 如果存在,查找环路节点
    fast = head;
    while (fast != slow) {
        slow = slow->next;
        fast = fast->next;
    }
    return fast;
}

(4)76. Minimum Window Substring (Hard)

题目描述

给定两个字符串 ST,求 S 中包含 T 所有字符的最短连续子字符串的长度,同时要求时间

复杂度不得超过 O(n)。

本人思路

这题可以很容易的想到滑动窗口,但还有一个潜在的难点是如何去“计数”,记录存在哪些字符以及相应的数目,可以用哈希表或者数组存储都行,这里采用的是数组。以及需要注意一下边界的处理,我在移动左边界的时候写错了左边界移动的时机导致出现了段错误。

代码展示

string minWindow(string S, string T) {
    vector<int> chars(128, 0);
    vector<bool> flag(128, false);
    
    // 先统计T中的字符情况
    for (int i = 0; i < T.size(); ++i) {
        flag[T[i]] = true;
        ++chars[T[i]];
    }
    
    // 移动滑动窗口,不断更改统计数据
    int cnt = 0, l = 0, min_l = 0, min_size = S.size() + 1;
    for (int r = 0; r < S.size(); ++r) {
        if (flag[S[r]]) {
            if (--chars[S[r]] >= 0) {
                ++cnt;
            }        
            // 若目前滑动窗口已包含T中全部字符,
            // 则尝试将l右移,在不影响结果的情况下获得最短子字符串
            while (cnt == T.size()) {
                if (r - l + 1 < min_size) {
                    min_l = l;
                    min_size = r - l + 1;
                }
                if (flag[S[l]] && ++chars[S[l]] > 0) {
                    --cnt;
                }
                ++l;
            }
        }
    } 
    return min_size > S.size() ? "" : S.substr(min_l, min_size);
}

总结

今天的学习内容是双指针。学到的新知识主要有两点:一个是快慢指针的运用,另外一个是滑动窗口。快慢指针可以用于确认链表是否有环、链表的中位数、判断链表长度是否为偶数;而滑动窗口则是一种新的方法,可以用于解决区间问题。