常见的双指针技巧分为两类,一类是快慢指针,另一类是左右指针。
所谓快慢指针中的快慢指的是移动的步长,即每次向前移动速度的快慢。例如可以让快指针每次沿链表向前移动2,慢指针每次向前移动1次。一般用来解决链表中的问题。例如判定链表中是否有环,单链表是否为循环链表等等。
而左右指针(对撞指针)呢,用于在已排序数组中找到两个数使其和为特定值,在字符串中判断是否回文,主要用来解决数组、字符串中的问题。例如二分查找等。
快慢指针的常见算法
快慢指针一般都初始化指向链表的头节点head,前进时快指针fast在前,慢指针slow在后。
循环链表
循环链表是一种链式存储结构,它的最后一个结点指向头结点,形成一个环。
循环链表的特点
//循环链表最后一个节点的指针指向第一个节点,形成闭环。 //可以通过任意节点进行遍历,每个节点都有下一个节点的指针。 //在遍历时可以无限循环下去,不会出现尾部节点的空指针错误。 //可以从任意节点开始遍历,选择一个合适的起始节点可提高操作效率。 //不增加额外存储花销
判断单链表是否为循环链表
typedef struct node
{
int data;
struct node *next;
}Node,*pnode;
int is_Clink( pnode p)
{
pnode fastpoint,slowpoint;
fastpoint=p;
slowpoint = p;
while(slowpoint &&fastpoint)
{
slowpoint=slowpoint->next;
fastpoint=fastpoint->next->next;
if (slowpoint==fastpoint||fastpoint->next==slowpoint)
printf("The si Clink\n");
return 0;
}
printf("This is linklist\n");
return 1
}
环形链表
环形链表的概念
在解答这个问题之前,我们先要弄清楚一个概念,什么是环形链表。
环形链表是一种特殊类型的链表数据结构,其最后一个节点的"下一个"指针指向链表中的某个节点,形成一个闭环。换句话说,链表的最后一个节点连接到了链表中的某个中间节点,而不是通常情况下连接到空指针(null)。如图:
bool hasCycle(struct ListNode *head) {
struct ListNode*slow=head,*fast=head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(slow==fast)
{
return true;
}
}
return false;
}
eg:
由于链表最后一个节点的下一个指针没有指向NULL(空指针),而是指向前面的某一个节点,所以我们不能再用 “current->next==NULL” 作为判断条件来遍历链表(会造成死循环),这时候就需要用到我们的快慢指针了。
判断链表是否有环
首先先说一下做这道题的思路,大体上类似于高中学习过的追及相遇问题
1.首先,让快慢指针fast、slow指向链表的头节点head
2.根据快慢指针的定义,快指针每次移动两个节点,慢指针移动一个节点
3.判断fast和slow是否移动到同一节点上,如果移动到同一节点上,就返回true,否则返回false
4.以上步骤会循环进行,直至fast或fast->next指向null(当链表中有环存在时,fast和fast->next永远不会指向null;当一个链表中没有环时,fast一定会移动到链表的结尾;又因为fast一次移动两个节点,所以有两种情况:①fast移动两次后,刚好指向NULL,结束循环;②fast移动一次后就已经指向NULL,此时再进行移动,就会出现对NULL的解引用
)
struct ListNode* fast, *slow;
fast=slow=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
if(slow=fast)
return true;
}
return false;
左右(对撞)指针的常见算法
两个指针,一个位于头,一个位于尾,它们向中间移动(主要应用于有序数组)
举个在力扣中做到的例子,找相应数字求和等于一个特定的值【题目:给定一个有序数组(数组是递增的),如数组arr = {1,4,5,7,9};找两个数之和为12,找到一组即可停止。】
如果用暴力求解法,那么就是for循环的两次遍历,这时的时间复杂度为O(n²)
代码如下
void find(int *a,int n,int temp)
{
for(int i=0;i<n;i++)
{
for(int j=i;j<n;j++)
{
if(a[i]+a[j]==temp)
{
printf("%d+%d=%d\n",a[i],a[j],temp);
}
}
}
}
这时就需要用到对撞指针了,那么显而易见会有三种情况:
(1)a[i]+a[j]<temp,我们则可知这两个指针对应的数组元素的和小于特值,但此时a[j]已经是数组里最大的元素了,所以使i++,对应下个更大的a[i]元素
(2)a[i]+a[j]>temp,同理,a[i]已经是数组里最小的元素了,两数之和还要比temp大,就只能让j--,从右向左寻找依次寻找更小的数,所以j--,对应下个更小的a[j]元素
(3)a[i]+a[j]==temp,满足条件,跳出循环
那么显然一次判断是出不了结果的,所以我们要用到循环,让循环跳出的条件是:左指针下标大于右指针下标,这时整个数组都已经在程序中筛查过一遍了,如果还是没有的话那么就不可能再有满足的元素了。
那么此时的代码就变成了:(这时的时间复杂度是O(N))
void find(int *a,int n,int temp,int i,int j)//i首,j尾
{
while(i<j)
{
if(a[i]+a[j]<temp)
{
i++;
}
else if(a[i]+a[j]>temp)
{
j--;
}
else
{
printf("%d+%d=%d\n",a[i],a[j],temp);
break;
}
}
}
二分查找
对撞指针的思路类似于二分查找
那么什么是二分查找呢,首先我们要知道二分查找的逻辑条件:①用于查找的数据是有序的②查找的数量只能是一个
同时一定要注意查找的区间问题,因为查找的区间是不断迭代的,所以确定查找的范围十分重要,主要就是左右区间的开和闭的问题,开闭不一样,对应的迭代方式也不一样,主要分两种:
一、左闭右闭
[left, right]
二、左闭右开
[left, right)
思路大概就是这样,具体操作呢会在下一篇文章中总结。