双指针法,将一个时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,

七个双指针题目

  1. leetcode027,移除元素(双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法,空间复杂度从O(n)变为O(1))
  2. leetcode015,三数之和(双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法)
  3. leetcode018,四数之和(双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法)
  4. leetcode206,翻转链表
  5. leetcode142,环形链表II
  6. 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