双指针算法-算法入门
双指针算法概述
双指针算法应该是一种特殊的遍历算法,它不止是利用单个指针去遍历,而是用双指针,注意这里的指针指的不是int *ptr
之类的指针,双指针算法大致可以分为两类,一类是两个指针相对方向遍历,称为对撞指针,另一类是两个指针相同方向遍历,称为快慢指针,接下来用例题来分别介绍双指针算法的三大细节:①类型判定(双指针起始位置)②指针移动方法③结束条件
对撞指针
反转字符串
题目描述:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]
解决方案:
class Solution {
public:
void reverseString(vector<char>& 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<int>& nums, int start, int end) {
while (start < end) {
swap(nums[start], nums[end]);
++start;
--end;
}
}
void rotate(vector<int>& 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<LinkNode*>
来存整个链表,然后输出中间节点即可,但如果想要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步,也就是此刻位置就是要删除的结点**(因为设置了哑结点,所以慢指针实际在前一个结点**)。
其他双指针
因为双指针用法比较灵活,还有一些双指针类型有比较难判定,以下是一些其他双指针的题目
总结
当我们用单指针遍历,超过了时间或空间复杂度时,我们就可以思考是否可以用双指针算法来优化我们的算法,对撞指针一般来说,起始状态是一首一尾,移动方式是逐步移动,结束条件是start<end,快慢指针一般来说,起始状态是同起点,移动方式(慢指针一般逐步移动,快指针具体情况看它怎么快,是抢跑,还是迈开的步子大),结束条件是快指针到达了终点。
以上题目均来自leetcode网站:https://leetcode-cn.com/