文章目录
双指针法,将一个时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,
七个双指针题目
- leetcode027,移除元素(双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法,空间复杂度从O(n)变为O(1))
- leetcode015,三数之和(双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法)
- leetcode018,四数之和(双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法)
- leetcode206,翻转链表
- leetcode142,环形链表II
- leetcode344,反转字符串
记忆方式:
【数组删除元素】
【两数之和】【三数之和】【四数之和】
【反转字符数组(左右指针),反转链表(头插法+反转指针法)】
【环形链表II】
leetcode027,移除元素,要求空间复杂度为O(1),时间复杂度为O(n),禁止两层循环,只能一层循环,专门为双指针设计的一道题
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
使用一个后返回
空间复杂度要求:不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并「原地」修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
「你不需要考虑数组中超出新长度后面的元素。」
思路
有的同学可能说了,多余的元素,删掉不就得了。
「要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。」
数组的基础知识可以看这里程序员算法面试中,必须掌握的数组理论知识。
暴力解法
这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。
删除过程如下:
很明显暴力解法的时间复杂度是O(n^2),这道题目暴力解法在leetcode上是可以过的。
暴力解法C++代码
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为下表i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
双指针法在数组和链表中还有很多应用,后面还会介绍到。
双指针法C++代码:
// 时间复杂度:O(n)
// 空间复杂度:O(1)
Java版本如下:
class Solution {
public int removeElement(int[] nums, int val) {
int slowIndex=0;
for(int fastIndex=0;fastIndex<nums.length;fastIndex++){
if(val != nums[fastIndex]){
nums[slowIndex]=nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
}
核心思想:slowIndex发挥一个计数器的作用,
遍历数组,每遇到一个数组中元素不为val的,slowIndex加一(slowIndex从0开始),最后
解释一下双指针法,如下:
要求有两个:
第一个要求:删除元素;
第二个要求:删除元素后的数组的长度。
用覆盖法删除元素只需要一个for循环:即遇到一个不为val的元素,后面的覆盖过来,删掉掉这个val,但是,覆盖法仅仅使用一个指针是做不到的
如果仅仅使用一个指针,程序为
for(int i=0;i<nums.length;i++){
if(val != nums[i]){
nums[i]=nums[i+1];
}
}
这样会导致一个问题,数组长度无法达到删除,输入为[3,2,2,3],输出为[3,2,3,3],无法删除元素,
所以使用两个指针,fastIndex顺序遍历数组,slowIndex用来存储元素,但是只有当fastIndex遍历到一个不等于val的元素的时候,slowIndex才会记录这个元素,slowIndex++,这就是双指针法的精髓。
注意,这种双指针法无法删除数组元素,仅仅覆盖元素而已,这是双指针法需要注意的地方。但是,如果只取出[0,slowIndex]长度的元素话,确实是删除元素后的新数组。
leecode015,三数之和
双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。
两数相加用双指针法不好,因为排序让时间复杂度O(n)变成了O(nlgn)
leetcode018,四数之和
双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。
leetcode206,反转链表(三种解法)
问题描述
第206题:反转链表
题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
解法一:新定义一个链表,原链表中元素头插法放入新链表中
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
class Solution {
public ListNode reverseList(ListNode head) {
ListNode first=new ListNode(-1); // 新链表建立一个虚拟启始节点
first.next=null; // 这一句可以有,可以没有,有的话更优美些
ListNode pre = head; // 原链表从实际节点出发
while(pre!=null){ // 只要下面没有pre.next.next,就不会有空指针
ListNode temp = pre.next; // 记录下pre.next,因为下面要重新设置pre.next
pre.next = first.next; // 这是头插法关键的两步骤
first.next = pre;
pre = temp;
}
return first.next; // first本身这个元素不要(就是val为-1的这个)
}
}
但是这样,leetcode也可以通过,但是面试中如果你这样回答,面试官一定会问题,是否还有其他解法。
链表的两条经验
经验1:while(pre!=null){ // 只要下面没有pre.next.next,就不会有空指针
经验2:ListNode temp = pre.next; // 记录下pre.next,因为下面要重新设置pre.next
经验3:头插法最最关键的两行代码:pre->next = first->next; first->next = pre;
最重要的经验:cur本身没有指针,它是掌握着链表中的各个节点的next指针
解法二:双指针法(核心:修改单链表指针指向)
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如下:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur=head;
ListNode pre=null;
while(cur!=null){
ListNode temp = cur.next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur.next = pre; // 这里就是翻转操作,pre里面存放着上一个元素(因为cur曾经指向上一个元素,又pre=cur),cur操作当前所指向的链表元素的next指针变为指向上一个元素,
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
}
解释while(cur!=null)
可以遍历到链表末尾,但是,只要循环体里面没用到cur.next.next,就不会有空指针异常
解释ListNode temp = cur.next; cur = temp;
为什么不直接写成 cur=cur.next,因为中间要重新设置cur.next的值
翻转指针最最关键的两行代码:cur.next = pre; pre=cur;
pre里面存放着上一个元素(因为cur曾经指向上一个元素,又pre=cur),cur操作当前所指向的链表元素的next指针变为指向上一个元素
最重要的经验:cur本身没有指针,它是掌握着链表中的各个节点的next指针
对于代码的解释:
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。此时我们return pre指针就可以了,pre指针就指向了新的头结点。
解法三:递归法
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。
具体可以看代码(已经详细注释),「双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。」
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}
};
leetcode142,环形链表II(附加leetcode141,环形链表)
题意:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
如果 pos 是 -1,则在该链表中没有环。
「说明」:不允许修改给定的链表。
图片
思路
这道题目,不仅考察对链表的操作,而且还需要一些数学运算。
主要考察两知识点:
判断链表是否环
如果有环,如何找到这个环的入口
判断链表是否有环
可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点:「fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。」
那么来看一下,「为什么fast指针和slow指针一定会相遇呢?」
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。
会发现最终都是这种情况, 如下图:
图片
fast和slow各自再走一步, fast和slow就相遇了
这是因为fast是走两步,slow是走一步,「其实相对于slow来说,fast是一个节点一个节点的靠近slow的」,所以fast一定可以和slow重合。
如果有环,如何找到这个环的入口
「此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。」
假设从头结点到环形入口节点 的节点数为x。环形入口节点到 fast指针与slow指针相遇节点 节点数为y。从相遇节点 再到环形入口节点节点数为 z。如图所示:
图片
那么相遇时:slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y ,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
这个公式说明什么呢?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。
当 n为1的时候,公式就化解为 x = z,
这就意味着,「从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点」。
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。
Java代码如下:
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slowIndex=head;
ListNode fastIndex=head; // 两个指针都是从head出发
while(fastIndex!=null && fastIndex.next !=null){ // 下面用到fastIndex.next.next,所以这里要判断fastIndex.next!=null,即如果无环,fastIndex最能到链表的倒数第二个元素,如果有环,fastIndex最能到链表的每一个元素
slowIndex=slowIndex.next;
fastIndex=fastIndex.next.next;
// 这个if条件得到了相遇点
if(fastIndex==slowIndex){ //证明有环,只有在有环的情况下才会return index2;
// 没有环的情况下就是return null; 所以return index;这行代码只会出现在这个if代码块中
ListNode index1=fastIndex;
ListNode index2=head;
while(index1!=index2){
index1=index1.next;
index2=index2.next;
}
// 结束这个循环之后,就是得到了环形入口 index2,也可以说是index1
return index2;
}
}
return null;
}
}
问题:为什么fastIndex一定是走两个节点?
回答:如果fast是一次走三个节点,那么可能会跳过slow,相遇不是成环的充要条件。
while(fastIndex!=null && fastIndex.next !=null){ // 下面用到fastIndex.next.next,所以这里要判断fastIndex.next!=null,即如果无环,fastIndex最能到链表的倒数第二个元素,如果有环,fastIndex最能到链表的每一个元素
双指针在数组和链表中都是可以使用的,在链表中slowIndex=slowIndex.next,相当于数组中的slowIndex++
另外,链表中如果 ListNode temp = slowIndex.next; …
slowIndex=temp; 中间省略的地方一定是修改了slowIndex.next,所以只能暂存起来
if(fastIndex == slowIndex) 这里得到交遇点
while(index1 == index2) 这里得到环形入口
这里return index1;也是可以的
顺便看看leetcode 141,仅仅判断环,代码如下:
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fastIndex=head;
ListNode slowIndex=head;
while(fastIndex!=null && fastIndex.next!=null){
slowIndex=slowIndex.next; // 如果是数组,相当于slowIndex++;
fastIndex=fastIndex.next.next; // 如果是数组,相当于fastIndex=fastIndex+2
if(fastIndex==slowIndex){
return true;
}
}
return false;
}
}
leetcode344,反转字符串
转到 字符串第一篇,反转字符串(左右指针)+反转链表(头插法+反转指针法+递归法)
小结:双指针法
双指针法定义
双指针法(快慢指针法):「通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。」
「双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。」
我们来回顾一下,之前已经讲过有四道题目使用了双指针法。
双指针法作用
第一,双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法,也就是降一个数量级,题目如下:
15.三数之和
18.四数之和
第二,双指针来记录前后指针实现链表反转:
206.反转链表
第三,使用双指针来确定有环:
142题.环形链表II