暑期算法训练.1

目录

[1.力扣 283.移动零](#1.力扣 283.移动零)

[1.1 双指针法:](#1.1 双指针法:)

[1.2 算法思路:](#1.2 算法思路:)

[1.3 代码演示:](#1.3 代码演示:)

​编辑

[1.4 总结反思:](#1.4 总结反思:)

[2.力扣 1089.复写零](#2.力扣 1089.复写零)

[2.1 题目解析:](#2.1 题目解析:)

[2.2 算法思路:](#2.2 算法思路:)

[2.3 代码演示:](#2.3 代码演示:)

​编辑

[2.4 总结反思:](#2.4 总结反思:)

[3.力扣 141.环形链表I:](#3.力扣 141.环形链表I:)

[3.1 题目解析:](#3.1 题目解析:)

[3.2 快慢指针:](#3.2 快慢指针:)

[3.3 为什么快指针每次走两步,慢指针走⼀步可以相遇,有没有可能遇不上 ?](#3.3 为什么快指针每次走两步,慢指针走⼀步可以相遇,有没有可能遇不上 ?)

[3.4 快指针⼀次走3步,走4步,...n步行吗?](#3.4 快指针⼀次走3步,走4步,...n步行吗?)

[3.3 算法思路:](#3.3 算法思路:)

[3.4 代码演示:](#3.4 代码演示:)

[​编辑 ​编辑](#编辑 编辑)

[3.5 总结反思:](#3.5 总结反思:)

[4.力扣 142.环形链表II:](#4.力扣 142.环形链表II:)

[4.1 题目解析:](#4.1 题目解析:)

[4.2 结论证明:](#4.2 结论证明:)

[4.3 代码演示:](#4.3 代码演示:)

[4.4 总结反思:](#4.4 总结反思:)

[5.力扣 202.快乐数](#5.力扣 202.快乐数)

[5.1 题目解析:](#5.1 题目解析:)

[5.2 算法思路:](#5.2 算法思路:)

[5.3 代码演示:](#5.3 代码演示:)

​编辑

[5.4 总结反思:](#5.4 总结反思:)


这个算法训练博客,我会以题目解析,算法思路(重点),总结反思进行讲解。

1.力扣 283.移动零

咱们先来看一下题目:

题目读起来很简单,理解起来也很简单。那么就是让你"就地"的进行移动(不另外再开空间)。那么 这种情况下,博主第一个想到的方法,就是"双指针法"。首先,你可以用暴力移动交换,但是这样的话,如果超出长度限制,就尴尬了。

1.1 双指针法:

顾名思义,双指针,就是两个指针,但其实,并不真正的是两个指针,而是利用数组的下标进行代替指针,从而解决问题。

那么就以这个题为例子,双指针:

cur:从左到右的扫描数组,遍历数组。

dest:已处理的区间内,非零元素的最后一个位置。

其实这个思想还是挺简单的。只要把握好cur与dest的位置即可。把握住什么时候cur动(++,--等),什么时候dest动(++,--)。

1.2 算法思路:

基本上这道题的算法思路,可以用一张图片来概括了。

1.3 代码演示:

cpp 复制代码
移动零
int main()
{
    vector<int> nums = { 0, 1, 0, 3, 12 };
    int cur = 0;
    int dest = 1;

    while (dest < nums.size())//这个地方不可以写dest<=nums.size();
    {
        if (nums[cur] == 0 && nums[dest] == 0)
        {
            dest++;
        }
        else if (nums[cur] == 0 && nums[dest] != 0)
        {
            swap(nums[cur], nums[dest]);
            cur++;
            dest++;
        }
        else if (nums[cur] != 0)
        {
            cur++;
            dest++;
        }
    }
    for (int i = 0; i < nums.size(); i++)
    {
        cout << nums[i] << endl;
    }
}

1.4 总结反思:

一般来说,可以暴力解决的题很少,基本上都要采取一点技巧,也就是优秀的算法。

2.力扣 1089.复写零

2.1 题目解析:

还是不让你进行另外再开空间,只可以在"本地"进行交换。

其实,这道题,咱们如果说还是用双指针法,从左往右的对它进行复写,不可以!因为,你试一试会发现,这样的话,有些数字会被覆盖。所以本题也很巧妙。

2.2 算法思路:

本题采用了逆向思维的方法,既然从左往右不可以,那咱们从右往左反正可以呀。确实可以

但是呢,这个时候,就涉及到一个问题,就是如何去找到这个最后一个"复写"的数字呢?这个其实也是用到了双指针法,相当于本题是双指针套双指针。

其实呢,这个思路,针对于倒数第二个数字不是0的来说很好用。但是对于倒数第二个数字是0的,就不行了。这种情况就需要特殊处理。

OK,再加上这个特殊处理的条件,本题的解决才算完整。

2.3 代码演示:

cpp 复制代码
//复写零
int main()
{
	vector<int> arr = { 1,0,2,4,0,3 };
    int n = arr.size();
    int cur = 0;
    int dest = -1;
    while (dest < n - 1)
    {
        if (arr[cur] != 0)
        {
            dest++;
            if (dest >= n - 1)
            {
                break;
            }
        }
        else if (arr[cur] == 0)
        {
            dest += 2;
            if (dest >= n - 1)
            {
                break;
            }
        }
        cur++;
    }
    if (dest > n - 1)
    {
        arr[n - 1] = 0;
        dest -= 2;
        cur--;
    }
    while (cur>=0)
    {
        if (arr[cur] == 0)
        {
            arr[dest--] = arr[cur];
            arr[dest--] = arr[cur];
            cur--;
        }
        else if (arr[cur] != 0)
        {
            arr[dest--] = arr[cur--];
        }
    }

2.4 总结反思:

本题不同于其他的题目,本题要采用逆向的思维方法。并且要注意寻找最后一个复写数字的位置。

3.力扣 141.环形链表I:

3.1 题目解析:

其实就是让你来判断一下一个链表中是否有环。这个题其实涉及到了快慢指针。那么咱们就来讲一下快慢指针。

3.2 快慢指针:

就是一个移动的快的"指针",一个移动的慢的"指针",然后就在一个环内,相当于如果说跑的快的那个"指针"追上了跑得慢的那个"指针",就说明其实是存在环的。否则就是不存在环。(它只要存在环,一个速度快,一个速度慢,就一定会相遇)。

3.3 为什么快指针每次走两步,慢指针走⼀步可以相遇,有没有可能遇不上 ?

不可能遇不上,一定可以遇上的。这个其实是可以证明的。

假设fast与slow之间的距离是N,fast每一次走2步,slow每一次走1步。(但其实,fast每一次也就比slow快了1步)。就是他俩之间的距离每一次都缩小了一步。那么N的距离,每一次缩小1,..............................是不是总有缩小到0的时候,那么此时,他们俩不是就相遇了嘛?

因此,在带环链表中慢指针走⼀步,快指针走两步最终⼀定会相遇。

3.4 快指针⼀次走3步,走4步,...n步行吗?

也是可以的,不论fast一次走多少不,他们总可以相遇。证明:

咱们就以fast一次走3步,slow一次走2步来计算。那么其实fast是每次都比slow快了2步。这个2步,如果说N为偶数,那么2步,是不是第一圈就可以减为0,他俩就可以相遇,就不需要再进行套圈了(就是第二圈)。如果N为奇数呢?那么这样的话,每一次减偶数差距,最终会减到-1,-1也就是意味着套圈了(要进行在第二圈进行追逐了)。假设周长为C,那么剩下的fast与slow的距离就是C-1,如果说这个C-1还是奇数,那么就意味着他俩永远也不可能相遇了(因为会无限的套圈,奇数套圈后还是奇数)。如果C-1为偶数,说明,这是他俩相遇的最后一次机会了。

OK,那么咱们接下来证明:

假设: 环的周长为C,头结点到slow结点的长度为L,slow走⼀步,fast走三步,当slow指针入环后, slow和fast指针在环中开始进行追逐,假设此时fast指针已经绕环x周。 在追逐过程中,快慢指针相遇时所走的路径长度:

slow:L

fast:L+C-N+nC

又因为3倍的慢的路程等于一倍的快的路程。

所以2L=C-N+nC。

即2L=(n+1)C-N

对上述公式继续分析:由于偶数乘以任何数都为偶数,因此 ⼀定为偶数,则可推导出可能得情 况:

• 情况1:偶数=偶数-偶数

• 情况2:偶数=奇数-奇数

又因为咱们上面分析的:

又因为N为奇数,所以前面的(n+1)C一定也得是奇数,那么C只能是奇数了。因为C为偶数的话,那一整项就都是偶数了。

所以,一定会相遇。fast走4,5,6.....n步的证明方法是一样的。

所以说,fast与slow是一定会相遇的。这个不需要担心。所以说,咱们通常做题的时候,只需要假设fast比slow快2步即可。因为fast走的步数大,会导致代码繁多。

3.3 算法思路:

那么再回过头来看这个题目,就简单了吧:1.让fast一次走2步,slow一次走一步。

2.若有环,那么他们是一定会相遇的。

3.若没有环,那么fast或者fast的下一个位置就为空了(这个地方只可以写fast,因为fast是快的,只可能是快的先碰到空的位置)。

3.4 代码演示:

cpp 复制代码
//环形链表I
  struct ListNode {
      int val;
      ListNode* next;
      ListNode(int x) : val(x), next(NULL) {}
  };
  class Solution {
  public:
      bool hasCycle(ListNode* head) {
          ListNode* slow = head;
          ListNode* fast = head;
          while (fast && fast->next)//因为fast走的最快,所以只可能是fast先碰null,所以这里只可以写fast
          {
              slow = slow->next;
              fast = fast->next->next;
              if (slow == fast)
              {
                  return true;
              }
          }
          return false;
      }
  };

3.5 总结反思:

使用快慢指针的时候,只需要将fast走两步即可。否则会导致代码冗余。

4.力扣 142.环形链表II:

4.1 题目解析:

这个题目就是比上面那个多了一个返回进入环形链表的初始位置而已。

那么要解决这个题,还需要用到一个知识就是:

快慢指针相遇点到入环位置的距离等于头结点到入环位置的距离。

4.2 结论证明:

说明: H为链表的起始点,E为环入口点,M与判环时候相遇点 设: 环的长度为R,H到E的距离为L,E到M的距离为X,则:M到E的距离为R-X, 在判环时,快慢指针相遇时所走的路径长度:

fast: L+X + nR

slow : L+X

而快指针速度是 满指针的两倍,因此有如下关系是:

2 * (L+X)=L+X+nR

L+X=nR

L=nR-X

L = (n-1)R+(R-X)

(n为1,2,3,4......,n的大小取决于环的大小,环越小n越大)

极端情况下,假设n=1,此时:L=R-X。

所以说,这个结论就得到了证明。

4.3 代码演示:

由于这个题的思想和上面那道题基本一样,就是多了一个结论。所以,咱们直接演示代码:

cpp 复制代码
//环形链表II
    class Solution {
    public:
        ListNode* detectCycle(ListNode* head) {
            ListNode* slow = head;
            ListNode* fast = head;
            while (fast && fast->next)
            {
                slow = slow->next;
                fast = fast->next->next;
                //此时就是找到了相遇点
                if (slow == fast)
                {
                    //由于相遇点离进入环的位置的距离等于起始点距离进入环的位置的距离。所以说,咱们为了找到进入环的位置,只需要相遇点的距离与起始点的距离相同,此时的位置就是进入环的位置。
                    ListNode* pcur = head;
                    while (pcur != slow)//等于就是找到了入环的位置了
                    {
                        pcur = pcur->next;
                        slow = slow->next;
                    }
                    return pcur;
                }
            }
            return NULL;
        }
    };

4.4 总结反思:

这道题目其实就是运用了一个结论,只需要记住这个结论就可以将这道题做出来了。

5.力扣 202.快乐数

5.1 题目解析:

其实这个题你要是可以想到用环(快慢指针)去做,那确实挺简单的。

5.2 算法思路:

题目告诉我们,当我们不断重复 x 操作的时候,计算⼀定会「死循环」,死的方式有两种:

▪ 情况⼀:⼀直在 1 中死循环,即 1 -> 1 -> 1 -> 1......

▪ 情况⼆:在历史的数据中死循环,但始终变不到 1 由于上述两种情况只会出现⼀种,因此,只要我们能确定循环是在「情况⼀」中进行,还是在「情 况⼆」中进行,就能得到结果。

根据上述的题目分析,我们可以知道,当重复执行 x 的时候,数据会陷入到⼀个「循环」之中。 而「快慢指针」有⼀个特性,就是在⼀个圆圈中,快指针总是会追上慢指针的,也就是说他们总会 相遇在⼀个位置上。如果相遇位置的值是 1,那么这个数⼀定是快乐数;如果相遇位置不是 1 的话,那么就不是快乐数。

所以,快慢指针还是fast走两步,slow走一步。当他俩相等的时候,如果slow等于1,那么就返回true。否则,返回false。

5.3 代码演示:

cpp 复制代码
//快乐水
class Solution {
public:
    int bitSum(int n) //返回 n 这个数每⼀位上的平⽅和 
    {
        int sum = 0;
        while (n) {
            int t = n % 10;
            sum += t * t;
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        int slow = n, fast = bitSum(n);
        while (slow != fast) {
            slow = bitSum(slow);
            fast = bitSum(bitSum(fast));
        }
        return slow == 1;
    }

};

5.4 总结反思:

这道题主要是用到了快慢指针,但要是没想到这个方法,那可能不太好做。

本篇完..................