【leetcode&C语言】457. Circular Array Loop

目录

 

问题描述

举例说明

注意

后续

问题补充

实现

时间复杂度O(n²),空间复杂度S(n)

 

快慢指针(看这里)


 


 

问题描述

You are given a circular array nums of positive and negative integers. If a number k at an index is positive, then move forward k steps. Conversely, if it's negative (-k), move backward k steps. Since the array is circular, you may assume that the last element's next element is the first element, and the first element's previous element is the last element.

Determine if there is a loop (or a cycle) in nums. A cycle must start and end at the same index and the cycle's length > 1. Furthermore, movements in a cycle must all follow a single direction. In other words, a cycle must not consist of both forward and backward movements.

给你循环数组nums,其中有正数和负数。如果在一个索引上有一个正数k,从该索引移动k个元素。相反地,如果是负数(-k),向后移动k个元素。因为数组是循环的,你可以假设最后一个元素的下一个元素是第一个元素,第一个元素的前一个元素是最后一个元素。

在nums中证实是否存在循环。一个循环必须在同一索引开始和结束,并且循环长度大于1。另外,一个循环的移动必须朝一个方向。换句话说,一个循环必不能都包括向前和向后移动。

举例说明

Example 1:

Input: [2,-1,1,2,2]
Output: true
Explanation: There is a cycle, from index 0 -> 2 -> 3 -> 0. The cycle's length is 3.

Example 2:

Input: [-1,2]
Output: false
Explanation: The movement from index 1 -> 1 -> 1 ... is not a cycle, because the cycle's length is 1. By definition the cycle's length must be greater than 1.

Example 3:

Input: [-2,1,-1,-2,-2]
Output: false
Explanation: The movement from index 1 -> 2 -> 1 -> ... is not a cycle, because movement from index 1 -> 2 is a forward movement, but movement from index 2 -> 1 is a backward movement. All movements in a cycle must follow a single direction.

例子1

输入:[2,-1,1,2,2]

输出:true

说明:有一个周期,索引顺序是0 -> 2 -> 3 -> 0。周期长度是3。

例子2

输入:[-1,2]

输出: false

说明:索引顺序为1 -> 1 -> 1 ...的移动不是一个周期,因为周期长度是1。根据定义,周期长度必须大于1。

例子3

输入:[-2,1,-1,-2,-2]

输出:false

说明:索引顺序为1 -> 2 -> 1 -> ... 的移动不是一个周期,因为索引1->2的移动是向前移动,但是索引2->1的移动是向后移动。循环的移动方向是单一的。

注意

  1. -1000 ≤ nums[i] ≤ 1000
  2. nums[i] ≠ 0
  3. 1 ≤ nums.length ≤ 5000

后续

Could you solve it in O(n) time complexity and O(1) extra space complexity?

你能在时间复杂度O(n),空间复杂度O(1)的条件下解决该问题么?(小声)不能,下次优化解决方法

问题补充

       把上面的文字敲完等于重新再一次审题。结合我走过的弯路,提一下作者没有着重提到的内容。

       千万不要以为循环都是从第一个元素开始!!!可以从第二个,第三个……第n个开始,只要有一个循环就可以返回true。比如数组[3,1,2],如果你只考虑从第一个元素开始循环的情况,索引顺序是0->0->->0……。根据题干,它不是周期长度大于1,所以返回false。然而答案是true!!!因为如果从第二个和第三个元素开始,索引顺序分别是1->2->1和2->1->2,符合要求,返回true。

     

实现

时间复杂度O(n²),空间复杂度S(n)

     由题干可知,符合循环数组的条件是

  1. 循环的索引路径不能只有一个索引,必须是在一个以上
  2. 至少某次索引目的地需要掉头,这样才能达成循环
  3. 有且仅有一个方向 
  4. 开始和结束是在同一个元素 

      我的思路很浅显,就是遍历每一个元素i,以其作为循环起点,再开一个for循环考察其他方面是否符合。

      首先应该考虑第一个条件,因为其他三个条件需要for循环模拟移动的情况在做出判断,而第一个条件不用。判断索引i的元素nums[i]是否是数组长度numsSize的倍数。如果等于是,那就证明循环路径上只有一个索引(如例子2),continue;如果不不是,那就进入for循环考察以下三个方面。(一开始从这个条件上只想到要判断索引i的目的地是不是i,其实隐藏的条件是nums[i]是否等于数组长度。)

     对于移动到下一个索引的算式,是下一个索引j=(i+nums[i])%numsSize,如果k<0,还需要j=numsSize+((i+nums[i])%numsSize)。因为要判断第二个条件,所以取余之前先判断 (i+nums[i])/ numsSize != 0,如果不等于证明还不会走出数组长度,如果等于则需要取余得到目的地索引(掉头)。for循环之前设置一个布尔变量isCycle为false,当(i+nums[i])/ numsSize != 0时,证明其走完了数组需要掉头,即符合第二个条件,isCycle为true。但是,这个判断只适用于正数。假设数组[-1,-2,-4],i=0,i+nums[i]=-1,k=3-1=2。虽然(i+nums[i])/ numsSize=0,但是它也实现了“掉头”。所以如果(i+nums[i])<0 && |i+nums[i]|<numsSize,isCycle也为true。

     得到了j以后判断nums[j]*nums[i]<0,小于证明其有两个方向,即不符合第三个条件。

     第四个条件很好判断,如果起点索引i 等于索引j ,就说明有可能开始和结束是在同一个元素 ,此时还要将isCycle纳入考虑,即开始和结束是同一个元素,且确实循环了一周(或n周)。

bool circularArrayLoop(int* nums, int numsSize) {
	int i, j = 0, steps;
	bool isCycle;
	for (i = 0; i < numsSize; i++) {
		steps = 0;
		isCycle = false;
		if (nums[i] % numsSize == 0) continue;
		for (j = i + nums[i]; steps <= numsSize; j = j + nums[j], steps++) 
        {
			if (j / numsSize != 0) 
            {
                isCycle = true;
			    j = j % numsSize;
            }
			if (j < 0)
			{
				j = numsSize + j;
				isCycle = true;
			}
			if (nums[j] * nums[i] < 0) break;
			if (j == i && isCycle) return true;
		}
	}
	return false;
}

 

       接上次的思路。其实排除了起点步长k是数组长度numsSize的倍数(即例子2的情况)之后,就不用考虑是否是循环这种情况。这个结论是从我原先代码得出,两个if语句都有isCycle=true,说明可以忽略条件1。我们可以考虑一下循环时所遇到的情况:

  1. 所有经过的元素和起点元素相乘不小于0,即都在同一个方向;所有经过的元素都不是数组长度的倍数
  2. 有元素nums[j]与起点元素相乘小于0×
  3. 有元素是数组长度的倍数×
  4. 第二种和第三种情况并存×

    按照这个思路,得到下面的代码:

bool circularArrayLoop(int* nums, int numsSize) {
	int i, j = 0, steps;
	for (i = 0; i < numsSize; i++) {
		steps = 0;
		if (nums[i] % numsSize == 0) continue;
		for (j = (i + nums[i])%numsSize; steps <= numsSize; j = (j + nums[j]) % numsSize, steps++) {
			if (j < 0)j = numsSize + j;
			if (nums[j] * nums[i] < 0) break;
			if (nums[j] % numsSize == 0) break;
			if (j == i) return true;
		}
	}
	return false;
}

快慢指针(看这里)

     使用快慢指针解决该方法。需要了解原理的朋友请看这里

     这个解决方法是参考大神的代码来的,所以有一些步骤需要理解。

int getNext(int* nums, int i, int numsSize){
    return ((i+nums[i])%numsSize+numsSize)%numsSize;
}

bool circularArrayLoop(int* nums, int numsSize){
    for(int i=0;i<numsSize;i++){
        int slow=i,fast=getNext(nums,slow,numsSize),val=nums[i];
        while(val*nums[fast]>0 && val*nums[getNext(nums,fast,numsSize)]>0){
            if(slow==fast){
                if(nums[slow]%numsSize==0)
                    break;
                return true;
            }
            slow=getNext(nums,slow,numsSize);
            fast=getNext(nums,getNext(nums,fast,numsSize),numsSize);
        }
        slow=i;
        while(val*nums[slow]>0){
            nums[slow] = 0;
            slow=getNext(nums,slow,numsSize);
        }        
    }
    return false;
}

      首先对于快慢指针中,慢指针走一格,快指针走两格,他们总有一个时间点会相遇。这个结论让我不能信服,如果你也是这样,那就一定要看一下这篇博文。后来我还思考为什么一定是快2慢1?可不可以快3慢1?快6慢3?结果都是一样的,一定会相遇,大家不妨找张循环链表的图试一下。

      第一个while循环是用来确定从该起点走是不是一个环。判断条件val*nums[fast]>0 && val*nums[getNext(nums,fast,numsSize)]>0是用来判断后面的元素是否和起点同一个方向。而之所以判断val*nums[getNext(nums,fast,numsSize)]>0是因为fast每次走两次。如果仅判断val*nums[fast]>0,就会越过一个元素。像是[-2,1,-1,-2,-2]这个例子就会返回true。

     进而我就想那为什么不用val*nums[slow]>0作为判断条件?慢指针slow每次走一次,这样每个环上的元素都可以判断到。但是如果fast先遇到了与起点方向相反的数,此时fast和slow相向而行,最后落在同一个位置上,一套if(slow==fast)把你带走。具体例子就是[1,1,1,1,1,1,1,1,1,-5]。

    这个代码的第二个while循环是用来标记无法成环的元素。我思考了半天才相通了一个浅显易懂的事情,如果从起点A走下去不能成环,那么之后从BCD哪个起点走到了A也都是无法成环的,因为每到一个索引只有一条路可以走,所以起点A的元素应该设为0。并且A之后的索引也是无法成环的,所以也应该设为0。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值