双指针算法-算法入门

双指针算法-算法入门

双指针算法概述

双指针算法应该是一种特殊的遍历算法,它不止是利用单个指针去遍历,而是用双指针,注意这里的指针指的不是int *ptr之类的指针,双指针算法大致可以分为两类,一类是两个指针相对方向遍历,称为对撞指针,另一类是两个指针相同方向遍历,称为快慢指针,接下来用例题来分别介绍双指针算法的三大细节:①类型判定(双指针起始位置)②指针移动方法③结束条件

对撞指针

反转字符串

题目描述:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

解决方案

class Solution {
public:
    void reverseString(vector& s) {
        int start = 0, end = s.size()-1;
        while (start < end) {
            swap(s[start], s[end]);
            start++;
            end--;
        }
        s.assign(s.begin(), s.end());
    }
};

分析:类型判定:原地修改数组意味着要用遍历的方式去解决问题,明显对撞指针可以很好地解决此类问题,start和end指针,对撞遍历,并交换,指针移动方法就是逐步移动,结束条件是start>end时,数组遍历结束,非常干净利落地解决问题,接下来看一道这道题的进阶版

轮转数组

题目描述:给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]

解决方案

class Solution {
public:
    void reverse(vector& nums, int start, int end) {
        while (start < end) {
            swap(nums[start], nums[end]);
            ++start;
            --end;
        }
    }
    void rotate(vector& nums, int k) {
        int lens = nums.size();
        k %= lens;
        reverse(nums, 0, lens-1);
        reverse(nums, 0, k-1);
        reverse(nums, k, lens-1);
        nums.assign(nums.begin(),nums.end());
    }
};

分析:如果按照朴素的做法,我们或许需要新开一个空间,去存轮转后的位置,但通过观察我们可以发现向右轮转k%len个位置,相当于将[0,lens-1],[0,k-1],[k,lens-1]分别轮转,从而将问题大大简化,并且节省了时空间,干脆利落!所以说双指针还是十分灵活的。

快慢指针

链表的中间结点

**题目描述:**给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.

解决方案:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
            ListNode* slow;
            ListNode* fast;
            slow = fast = head;
            while (fast != NULL && fast->next != NULL) {//fast != NULL必须加上,不然当fast为空时,fast->next处内存外溢,报错。
                slow = slow->next;
                fast = fast->next->next;
            }
            return slow;
    }
};

分析:当然这个题目,如果不考虑空间的问题的话,我们可以创建一个新的数组vector来存整个链表,然后输出中间节点即可,但如果想要o(1)的空间复杂度,我们就可以用到快慢指针算法,慢指针一次走一步,快指针一次走两步,当快指针走到末端的时候,慢指针就在其中点,判断类型是快慢指针,指针移动方法是快指针的步伐是慢指针的两倍,结束条件是快指针的指的下一个节点为空。我们再来看这道题目的一道进阶版

删除链表的倒数第N个结点

题目描述:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

解决方案

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* fast;
        fast = head;
        ListNode* start = new ListNode(0,head);//创建哑节点,便于删除操作
        ListNode* slow = start;
        for (int i = 1; i < n; i++) fast = fast->next;
        while (fast->next) {
            slow = slow->next;
            fast = fast->next;
        }
        ListNode* t = slow->next;
        slow->next = t->next;
        delete t;
        return start->next;//不包括哑节点
    }
};

分析: 很显然,最简单的想法就是先遍历一遍得到链表的长度,然后再遍历一遍找到L-n的位置删除节点,如果被问到只能一次遍历呢?我们就可以用到我们的快慢指针,我们可以让快指针先走n-1(因为n个结点间,间隔n-1步)步,之后快慢指针同时走,当快指针到达末端时,慢指针刚好就比快指针慢n-1步,也就是此刻位置就是要删除的结点**(因为设置了哑结点,所以慢指针实际在前一个结点**)。

其他双指针

因为双指针用法比较灵活,还有一些双指针类型有比较难判定,以下是一些其他双指针的题目

反转字符串中的单词

移动0

总结

当我们用单指针遍历,超过了时间或空间复杂度时,我们就可以思考是否可以用双指针算法来优化我们的算法,对撞指针一般来说,起始状态是一首一尾,移动方式是逐步移动,结束条件是start慢指针一般逐步移动,快指针具体情况看它怎么快,是抢跑,还是迈开的步子大),结束条件是快指针到达了终点。

以上题目均来自leetcode网站:https://leetcode-cn.com/

你可能感兴趣的:(算法,leetcode,数据结构)