【习题】双指针法(详解)

理论

在进行时间复杂度为O(N2)的暴力求解时,有的题目可以一次做两步的事情,从而简化代码的时间复杂度为O(N)。

这里的双指针只是一种叫法,不一定是指针,也不一定是两个,也可能是数组下标或者其他的变量,对同一组数据的不同位置进行操作的一种方法。

实践

数组的用法

1.移除元素

力扣链接:点击进入
图:

在这里插入图片描述

题目的大概意思:将数组中与所传的值相同的数字移除,同时将修改后数组的长度作为返回值。

1 .一般思路:将数组不是要删的元素传给一个新数组,然后用新数组对原数组进行覆盖。
2 . 双指针法:使用两个下标,一个下标是将数组进行遍历,另一个下标是用来存放不是所删的数据,并且这个下标的意义有两个:第一个是数组的下标,第二个是数组的当前长度。并且每存一次,下标就加一。

代码:

int removeElement(int* nums, int numsSize, int val)
{
    
    
    int len = 0;
    for(int i = 0; i<numsSize ;i++)
    {
    
    
        if(nums[i]!=val)
        {
    
    
            nums[len++]=nums[i];
            //注意这里后置的意思,加之前是下标,加之后是数组当前长度。
        }
    }
    return len;
}

思想:将不删除的数对原数组进行一次覆盖。
图解:
在这里插入图片描述

2. 删除有序数组中的重复项

力扣链接:点击进入
图:
在这里插入图片描述
题目的关键信息:升序数组,空间复杂度为O(1),在原地修改数组。
一般思路:两层for循环进行嵌套使用,时间复杂度为O(N)
双指针法:用两个下标,初始值为0,第一个下标作为对照,第二个下标找跟第一个下标所指向的不同元素,不同时进行赋值操作。
代码:

int removeDuplicates(int* nums, int numsSize)
{
    
    
    int cur = 0;
    int i = 0;
    for(i = 0; i < numsSize;i++)
    {
    
    
        if(nums[i]!=nums[cur])
        {
    
    
            nums[++cur]=nums[i];
        }
    }
    return cur+1;
}

核心思想:找不相等的元素,将不相等的元素赋值给第二个下标的下一个元素。
图解:
在这里插入图片描述

3.合并两个有序数组

力扣链接:点击进入
图:
在这里插入图片描述
关键:有序
一般思路:把数组2的内容接到数组1的后面,然后对数组1使用快排或者冒泡排序。使用冒泡排序的时间复杂度为O(N2)。
双指针思路:将两数组的最后下标记录,进行比较,较大的放在数组1的最后一个位置,放过后的那个位置向前移到,继续比较,知道有一个数组的下标变成-1为止。
代码:


void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
    
    
    int tailone = m-1;//第一个数组的最后位置的下标
    int tailtwo = n-1;//第二和数组的最后位置下标
    int end = m+n-1;//总长度的最后位置的下标
    while(tailone!=-1&&tailtwo!=-1)
    {
    
    
       // 如果第二个数组的下标大于第一个数组的下标,将
       //第二个数组的最后一个位置的下标赋值给最后一个位置的下标
        if(nums1[tailone]<nums2[tailtwo])
        {
    
    
            nums1[end--]=nums2[tailtwo--];//操作完之后要减去1
        }
        else
        {
    
    
            nums1[end--]=nums1[tailone--];//同理
        }
    }
    //这里有一种情况:就是第二个数组的内容没有移完,需要将第二个数组的内容移动完。
    while(tailtwo!=-1)
    {
    
    
        nums1[end--]=nums2[tailtwo--];
    }
}

我这里只举一种将代码走完的情况:
在这里插入图片描述

链表的用法

1.链表中倒数第k个结点

牛客链接:点击进入
图:
在这里插入图片描述
一般思路:

第一步:求出链表长度:len
第二步:让链表走:len-k步

双指针法:

第一步:设置两个指针
第二步:一个指针走k步
第三步:两个指针一块走,指针的速度相同,一个指针走到结束时,另一个指针是倒数第k个节点,因为两个指针在相同的速度走的情况下的距离总是为k。

理论:

在这里插入图片描述
实际 :

用例:
链表:1->2->3->4->5->NULL
倒数第一个节点
问题:
1.倒数第一个节点是NULL还是5?是5,所以快节点的地址是空时,这时可以求出总长度,并且可以得出结束条件。
2.起点是从哪开始?到1的节点已经是第一步了,从走一步的状态开始时是k-1步。
3.k的范围?闭区间:1到5,如果为6则要加上结束条件进行判断。
4.终点到哪?一个指针到NULL时,即为终点。

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) 
{
    
    
	//k的范围是:闭区间1到5
    if(k<=0)
       return NULL;
	//先让快指针走一步,k要减一   
    struct ListNode* fast = pListHead;
     k--;
     //slow在起点
    struct ListNode* slow = NULL;
    //避免链表为空,避免k为0时,k--成负一
    while(fast&&k!=0&&k--)
    {
    
    
        fast=fast->next;
    } 
    //一起走,直到fast走到终点  
    while(fast)
    {
    
    
        fast=fast->next;
        if(slow==NULL)//slow在原点要让slow走一步
            slow=pListHead;
        else
            slow=slow->next;
    }
    if(k==0)//说明k的长度在闭区间1到5
        return slow;
    else//说明k过大(k过小的情况在开始处理过了)
    {
    
    
        return NULL;
    }
}

2.相交链表

力扣链接:点击进入
图:
在这里插入图片描述
一般思路:

第一步:设置两个指针,分别指向两链表的头
第二步:从一条链表的头开始,与另一条链表的所有节点的地址进行比较,如果没有则指向链表的第二个节点的地址,直到这条链表的空节点为止。如果相等则返回相等的那个结点。

双指针思路:

第一步:设置两个指针,分别指向两个链表的头
第二步:求出两条链表的长度
第三步:求出两条链表差的绝对值
第四步:较长的链表走两条链表差的绝对值
第五步:一块走,直到相等为止

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    
    
    int lenA = 0;
    int lenB = 0;
    struct ListNode *cur1 = headA;
    struct ListNode *cur2 = headB;
    //求出第一条链表的长度
    while(cur1)
    {
    
    
        lenA++;
        cur1=cur1->next;
    }
    //求出第二条链表的长度
    while(cur2)
    {
    
    
        lenB++;
        cur2=cur2->next;
    }
    cur1=headA;
    cur2=headB;
    //求出两条链表的差值的绝对值
    int k = lenA>lenB?(lenA-lenB):(lenB-lenA);
    //int k =abs(lenA-lenB);
    //说明:abs是求绝对值的函数
    while(k--)
    {
    
    
    	//通过长度判断哪条链表是较长的链表
        if(lenA>lenB)
        {
    
    
            cur1=cur1->next;
        }
        //这是lenA<lenB的情况,k等于0进不去循环。
        else
        {
    
    
            cur2=cur2->next;
        }
    }
    //这里是判断相等的情况,如果都不相等则最终的结果为空指针。
    //如果一个为空一个不为空,前面的代码会让cur1和cur2都置为空
    //如果两条链表从交点开始则为同一条链表,直接会返回第一个结点。
    while(cur1!=cur2)
    {
    
    
            cur1=cur1->next;
            cur2=cur2->next;
    }
    return cur1;
}

图解:
在这里插入图片描述

3.反转链表

链接:点击进入
图:
在这里插入图片描述
思路1:

第一步:定义三个指针,指针1指向空,指针2指向第一个结点,指针3指向第二个结点的位置
第二步:将指针2的下一个结点的位置改为指针1.
第三步:将指针2的值赋值给指针1,指针3赋值给指针2,指针3的下一个结点的地址赋值给指针3。
代码:

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
    
    

   //有大于等于两个结点,有两节点的前提是有一个结点
   if(head&&head->next)
   {
    
    
       ListNode* new_before = head;
       ListNode* new_after =NULL;
       ListNode* next = new_before->next;
       while(new_before!=NULL)
       {
    
    
           new_before->next=new_after;
           new_after=new_before;
           new_before=next;
           if(next)
            next=next->next;
       }
    return new_after;
   }
   //否则就返回头结点,0或者一个结点
   return head;
}

思路2:

第一步:定义三个指针,指针1指向空,指针2指向链表的第一个结点的位置,指针3指向链表的第二个结点的位置。
第二步:将指针2指向的结点头插在指针1的前面,并将指针1改为指针2的值,指针2改为指针3的值,指针3指向指针3的下一个节点的位置。
第三步:直到指针2为空为止。

typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
    
    
    ListNode* new_head = NULL;
    ListNode* cur = head;
    ListNode*next = NULL;
    if(cur)
        next = head->next;
    while(cur)
    {
    
    
        cur->next=new_head;
        new_head=cur;
        cur = next;
        if(cur)
        {
    
    
            next= cur->next;
        }
    }
    return new_head;
}

思路3(递归):
满足:
1.化繁为简
2.化简的方式基本相同
3.能够转换成最后一个小问题

拆分:
逆转整个链表相当于逆转头结点和剩余节点
逆转剩余节点相当于逆转第二个结点和剩余结点的位置
以此类推:最终会转换成逆转最后一个节点的问题

代码:

struct ListNode* reverse(struct ListNode* cur, struct ListNode* next)
{
    
    
    struct ListNode* tail = NULL;
    if (next && next->next)
    {
    
    
       tail = reverse(next, next->next);
    }
    if (next&&next->next == NULL)
    {
    
    
        tail = next;
    }
    if(next)
        next->next = cur;
    return tail;
}
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
    
    
    return reverse(NULL,head);
}

4.合并两个有序链表

力扣链接:点击进入
图:
在这里插入图片描述
一般思路:

第一步:将链表的数值全部放在数组中,将两个链表链接起来。
第二步:将数组的数字进行排序。
第三步:将数组的数据对原来的数据进行覆盖。
缺点:具有局限性,如果说链表的节点里面的值不能变,这道题就做不成了。

双指针思路:

第一步:设置两个指针,指向两个链表的头部。
第二步:把较小的值拿出来进行头插。
第三步:把得到的新链表进行反转,反转上一道题有代码。

代码:

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
    
    
     struct ListNode * phead = NULL;
     struct ListNode *cur1  = list1;
     struct ListNode *cur2 = list2;
     //这是两个链表都不为空的情况
     if(cur1&&cur2)
     {
    
    
        struct ListNode *next1 = cur1->next;
        struct ListNode *next2 = cur2->next;
        while(cur1&&cur2)
        {
    
    
			//当cur1指向的值大于cur2指向的值移动较小的cur2
            if(cur1->val>cur2->val)
            {
    
    
                cur2->next=phead;
                phead=cur2;
                cur2=next2;
                //防止出现空指针解引用的地方
                if(cur2)
                    next2=cur2->next;                
            }
            //移动较小的cur1
            else
            {
    
    
                cur1->next=phead;
                phead=cur1;
                cur1=next1;
                if(cur1)
                    next1=cur1->next;
            }
        }
        //当移动完时会出现三种情况
        //1.cur1为空,cur2不为空
        //2.cur2为空,cur1不为空
        //3.cur1和cur2都为空

		//这是第一种情况
        if(cur1==NULL&&cur2!=NULL)
        {
    
    
            while(cur2)
            {
    
    
                cur2->next=phead;
                phead=cur2;
                cur2=next2;
                if(cur2)
                    next2=cur2->next;
            }
        }
        //第二种情况
        else if(cur2==NULL&&cur1!=NULL)
        {
    
    
            while(cur1)
            {
    
    
                cur1->next=phead;
                phead=cur1;
                cur1=next1;
                if(cur1)
                next1=cur1->next;
            }
        }
        //第三种情况不用处理

		//反转链表,这里采取了思路一。
        struct ListNode* new_phead = NULL;
        struct ListNode* cur3=phead;
        struct ListNode* next3=cur3->next;
        while(cur3)
        {
    
    
            cur3->next=new_phead;
            new_phead=cur3;
            cur3=next3;
            if(cur3)
            next3=cur3->next;
        }
        return new_phead;
     }
     //链表有三种情况:
     //1.第一条链表为空,第二条链表不为空
     //2.第二条链表为空,第一条链表不为空
     //3.两条链表都为空。
     else
     {
    
    
         return cur1==NULL? cur2:cur1;
     }

5.环形链表

理论图解:
在这里插入图片描述

1.确认是否是环

链接:点击进入
根据理论:slow一次走一步,fast一次走两步,最后相遇是环,fast遇到空,不是环

bool hasCycle(struct ListNode *head) 
{
    
    
	//空链表比较特殊需要额外判断
    if(head==NULL)
    {
    
    
        return false;
    }
    struct ListNode * fast = head->next;
    struct ListNode * slow = head;
    //1.对初始化的fast判断是否为空
    //2.fast一次走两步,因此我们需要判断fast的下一个位置是否为空
    while(fast&&fast->next&&fast!=slow)
    {
    
    
        fast=fast->next->next;
        slow=slow->next;
    }
    //不是环形链表
    if(fast==NULL||fast->next==NULL)
    {
    
    
        return false;
    }
    else
    {
    
    
        return true;
    }
}

2.确认入环的起始位置

链接:点击进入

使用结论:从相遇点的下一个结点开始,到相等为止。

代码:

struct ListNode *detectCycle(struct ListNode *head) {
    
    
    if(head==NULL)
    {
    
    
        return NULL;
    }
    struct ListNode * fast = head->next;
    struct ListNode * slow = head;
    while(fast&&fast->next&&fast!=slow)
    {
    
    
        fast=fast->next->next;
        slow=slow->next;
    }
    if(fast==NULL||fast->next==NULL)
    {
    
    
        return NULL;
    }
    else
    {
    
    
    	//环形链表,从相遇点的下一个位置开始
        struct ListNode* start = fast->next;
        //相遇时停止
        while(start!=head)
        {
    
    
            start=start->next;
            head=head->next;
        }
        return start;
    }
}

6.复制带随机指针的链表

链接:点击进入
题图:
在这里插入图片描述
关键:随机指针的拷贝。
思路:

将拷贝的链表与原来的链表产生联系。
第一步:对原来的链表进行尾插。
第二步:找拷贝的链表的随机值与原链表的随机值得关系:
拷贝的结点的随机值=原结点的下一个结点的next(前提是不为空)
第三步:将链表分割成两个链表。
代码1(自己写的):

void PushBack(struct Node* cur)
{
    
    
    struct Node* NewNode = (struct Node*)malloc(sizeof(struct Node));
    struct Node* NewNextN = cur->next;
    cur->next = NewNode;
    NewNode->val = cur->val;
    NewNode->next = NewNextN;
}
struct Node* copyRandomList(struct Node* head)
{
    
    
 	//由于我们的链表为空的话下面的代码无法进行,所以要进行处理一下
    if (head == NULL)
    {
    
    
        return NULL;
    }
    struct Node* prev_next = head->next;
    struct Node* cur = head;
    //将拷贝的结点放在原来节点的后面,也就是尾插
    while (cur)
    {
    
    
        PushBack(cur);
        cur = prev_next;
        if (cur)
            prev_next = cur->next;
    }
    struct Node* copy = head;
    struct Node* copy_next = head->next->next;
    while (copy)
    {
    
    
        //不为空.则拷贝节点的下一个随机结点的地址就等于原结点的随机结点的下一个结点的位置。
        //这里相当于把原链表与拷贝链表建立关系(关键)    	
        if(copy->random!=NULL)
            copy->next->random = copy->random->next;
        //如果为空,则要特殊处理
        else
            copy->next->random = NULL;
        copy = copy_next;
        if (copy)
            copy_next = copy->next->next;

    }
    struct Node* cut1 = head;
    struct Node* cut2 = head->next->next;
    struct Node* phead = head->next;
    //链表不为空至少有2个结点。
    while (cut1)
    {
    
    
        if (cut2 == NULL)
        {
    
    
            cut1->next = NULL;
            cut1 = NULL;
            break;
        }
        cut1->next->next = cut2->next;
        cut1->next = cut2;
        cut1 = cut2;
        if (cut1)
            cut2 = cut2->next->next;
    }
    return phead;
}

代码2(改进):

struct Node* copyRandomList(struct Node* head)
{
    
    
    //在循环的外面,判断是否有结点。
    struct Node* cur=head;
    while(cur)
    {
    
    
        struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
        copy->next=cur->next;
        copy->val=cur->val;
        cur->next=copy;
        cur=copy->next;
    }
    cur = head;
    //cur为真的话就至少有两个结点。
    while (cur)
    {
    
    
        struct Node* copy =cur->next;
        if(cur->random!=NULL)
            copy->random = cur->random->next;
        else
            copy->random = NULL;
        cur=copy->next;
    }
    cur = head;
    struct Node* copy_head = NULL;
    struct Node* copy_tail = NULL;//用于尾插
    while(cur)
    {
    
    
    	//每次进去都会更新copy和next的值
        struct Node* copy = cur->next;
        struct Node* next = copy->next;
		//由于copy_head和opy_tai没有进行赋初值,要赋值
        if(copy_head==NULL)
        {
    
    
            copy_head=head->next;
            copy_tail=head->next;
        }
        else//将拷贝的结点进行尾插
        {
    
    
            copy_tail->next=copy;
            copy_tail=copy;
        }
        //恢复原来的链表
        cur->next = next;
        cur=next;
    }
    return copy_head;
}

7.链表的回文结构

链接:点击进入
题图:
在这里插入图片描述

要求:时间复杂度O(N),额外空间复杂度O(1)
思路:

1.找到中间结点(偶数有两个,按第二个算)。
2.将中间结点的进行反转
3.从中间结点和头结点的位置进行依次比较,如果不同不为回文结构,如果走向了空,则为回文结构。

图解:

在这里插入图片描述

class PalindromeList {
    
    
public:
    bool chkPalindrome(ListNode* A) {
    
    
        // write code here
        ListNode*fast =A;
        ListNode*slow = A;
        while(fast&&fast->next)
        {
    
    
            slow=slow->next;
            fast=fast->next->next;
        }
        //逆转链表,头插
        ListNode*phead = NULL;
        while(slow)
        {
    
    
            ListNode*next = slow->next;
            slow->next=phead;
            phead=slow;
            slow=next;
        }
        //进行比较
        ListNode*cur=A;
        while(phead)
        {
    
    
            if(phead->val!=cur->val)
            {
    
    
                return false;
            }
            cur=cur->next;
            phead=phead->next;
        }
        return true;
    }
};

猜你喜欢

转载自blog.csdn.net/Shun_Hua/article/details/128855856