[零基础算法入门①] 双指针法

🎉前言

        学习算法肯定不是一条坦途,正如人生的奋斗之路也是异常曲折的,一个多月的摸爬滚打,也终于让我跨入了算法的大门,希望我这一个多月的一点经验积累能对初入算法之门的同学们,起到一点帮助。然而我的个人总结难免有些偏颇且局限,希望读者不吝赐教。

        如果觉得一个人难以坚持,不妨加入万人千题打卡社区。百人打卡,你怎么会甘心懈怠呢? 万人千题打卡社区icon-default.png?t=LA92https://bbs.csdn.net/forums/hero?category=0

🐱‍💻作者概况:  就读南京邮电大学努力学习的大一小伙

🐱‍🐉联系方式:2879377052(QQ小号)

📚资源分享:《算法竞赛入门经典》 (30天有效)     

🐱‍👤LeetCode主页:Leetcode       


目录

一、案例分析

二、算法详解

三、算法模板

四、实战演练

五、课后练习


一、案例分析

【案例】调整数组使奇数全部都位于偶数前面

 【分析】这个问题当然也有很多种解法,但能否只遍历一次数组就满足题目要求呢?我们可以试试双指针——一种简单却实用的方法。

二、算法详解

>: 什么是双指针法?

双指针法:一般是指的在遍历对象的过程中,不是使用单个指针进行访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应的目的。

一般双指针法有两种表现方式:

  1. 同向移动:在同向移动时,指针移动有快慢之分。
  2. 相向移动:在相向移动中,双指针一个指针在开头,另外一个指针在结尾,向中间逼近。

回到本题,我们可以采用相向移动的双指针,不妨分别定义为 left 和 right。我们通过双指针实现下面的功能:left指针左边的都是奇数,right指针右边的都是偶数。来看看下面的代码体悟一下吧,读代码的能力也是很重要的。这里就呈现核心代码。

void move(int* arr , int n)
{
	int* left = arr;
	int* right = arr + n - 1;
	while (left < right)//(1)
	{
		while (left < right && *left % 2 == 1)//(2)
			left++;
		while (left < right && *right % 2 == 0)//(3)
			right--;
		if (left < right)//(4)
		{
			int tmp = *left;
			*left = *right;
			*right = tmp;
		}
		left++;
		right--;
	}
}

  • (1)左右指针相遇之前说明之间还有数没有被查找过,所以循环继续
  • (2)左指针所指向的数为奇数时,则不需要交换继续查找。注意条件left<right,否则可能越界
  • (3)右指针同理,只是找到奇数时才停止查找
  • (4)当左右指针还没相遇时才需要交换

 三、算法模板

void move(int* arr , int n)
{
	int* left = arr;
	int* right = arr + n - 1;
	while (left < right)
	{
		while (left < right && XXXX)
			left++;
		while (left < right && XXXX)
			right--;
		if (left < right)
		{
			int tmp = *left;
			*left = *right;
			*right = tmp;
		}
		left++;
		right--;
	}
}

四、实战演练

温馨提示:Leetcode是接口型的OJ平台,只要封装一个函数实现对应功能就可以了,不需要主函数。可以尝试先点击蓝色链接写题目,写不出来再看分析。


 [实战题①283. 移动零       难度值:★☆☆☆☆]             

 【初步分析】因为要保持相对顺序不变,所以我们使用同向快慢指针,实现慢指针内的都是非0元素。

void swap(int*a ,int*b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

void moveZeroes(int* nums, int numsSize)
{
    int slow = 0;
    int fast = 0;
    while(fast < numsSize)
    {
        if(nums[fast])
        {
            swap(nums + slow, nums + fast);
            slow++;
        }
        fast++;
    }
}

【思路剖析】fast走的速度肯定 >= slow,而他们之间的差值是0造成的。所以只要fast不是0就赋值给slow。相当于靠fast去找到所有非0元素,而slow只是起一个接收并偏移的作用。


[实战题②977. 有序数组的平方   难度值:★★☆☆☆ ]

 【初步分析】我们要利用好原数组非递减排序的特点。既然开平方,那我们要分别关注数组两边的最大值和最小值,用双指针从两边同时切入恰到好处。

int* sortedSquares(int* nums, int numsSize, int* returnSize)
{
    *returnSize = numsSize;
    int* ans = (int*)malloc(sizeof(int) * numsSize);
    int p1 = 0;
    int p2 = numsSize - 1;
    int index = numsSize - 1;
    while(p1 <= p2)
    {
        if(nums[p1] * nums[p1] < nums[p2] * nums[p2])
        {
            ans[index--] = nums[p2] * nums[p2];
            p2--;
        }
        else
        {
            ans[index--] = nums[p1] * nums[p1];
            p1++;
        }
    }
    return ans;
}

【思路剖析】

我们比较容易知道平方后的最大值肯定落在最左边和最右边,因为我们拿左右指针去比较,倒序插入我们动态开辟的数组中去即可。

【初学者的疑惑】

1. 写题目只给我们这样的界面,什么意思?

 【答】函数的参数是Leetcode提供的,也就是说我们要利用其所提供的参数来设计我们的功能。note中告诉我们返回的数组必须动态开辟(后面讲),并且我们不需要写任何库函数,也不用在free。

2. returnSize什么用?

【答】注意到这里是传址调用,也就是在函数中修改会影响主函数中的returnSize,那它到底什么用呢?函数设计而定对不对,编译器就看最后的结果是否和他预设测试用例匹配。你可以想象成后台把你的数据打印出来再比较,那我们的打印循环是不是需要知道数组的长度,不然打印什么时候会停止。

3.为什么需要动态内存分配?

【答】普通的数组长度必须是一个常量,且开辟在栈区上,栈上数据的特点是出函数销毁,也就是说虽然函数返回了一个指针,但指针指向的空间已经被销毁。但是动态内存分配所开辟的空间开辟在堆区上,堆区的空间一直到程序结束才被销毁,所以我们必须动态内存分配

4.怎么动态内存分配?

【答】这里只做简单的解释,详细请看博主之间的学习笔记 :》 6000字总结动态内存分配

我们以代码说所展示的开辟例子来说明: int* ans = (int*)malloc(sizeof(int) * numsSize);

ans是我们创建的int*变量,接收malloc的返回值。malloc的返回值取决于你开辟什么,所以我们看括号内的内容,表示开辟numsSize个int大小的空间,因为空间存放的数据是int类型的,所以返回的指针是int*类型,也就决定我们的指针是int*类型的。malloc前的强制类型转换最好加上,因为malloc默认返回的是void*类型的数据。

具体不太理解没关系,做题的时候只要懂上面说的照做就行了。


 [实战题③344. 反转字符串      难度值:★☆☆☆☆]

 【初步分析】反转字符串中双指针是比较经典的解法。我们采用相向双指针,交换即可。

void reverseString(char* s, int sSize)
{
    int p1 = 0;
    int p2 = sSize - 1;
    while(p1 < p2)
    {
        char tmp = s[p1];
        s[p1] = s[p2];
        s[p2] = tmp;
        p1++;
        p2--;
    }
    return s;
}

[实战题④876. 链表的中间结点   难度值:★☆☆☆☆]

 【初步分析】大家可能都接触过数组,但对链表不是很熟悉,其实很简单没有那么玄乎,链表和数组都是最基础的数据结构。我们创建一个结构体变量,结构体中有两个元素,val表示他的值,next表示下一个结点的地址,通过next我们找到了下一个结点,我们通过下一个结点的next找到下下个结点,以此类推,将所有结点通过指针串联起来。

  //Definition for singly-linked list.
  struct ListNode 
  {
      int val;
      struct ListNode *next;
  };
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while(fast != NULL && fast -> next != NULL)
    {
        fast = fast -> next -> next;
        slow = slow -> next;
    }
    return slow;
}

 【思路剖析】

在链表中如何实现快慢指针的效果呢?可以有以下两个思路:

  1. 让快指针每次走多步
  2. 让快指针先走

在这里我们采用的方法是每次让快指针走两步。我们不妨也来比较一下单指针写法,显然用单指针遍历两次是不可少的。

struct ListNode* middleNode(struct ListNode* head)
{
    int cnt = 0;
    struct ListNode* p = head;
    while(p != NULL)
    {
        p = p -> next;
        cnt++;
    }
    cnt = cnt / 2;
    while(cnt--)
    {
        head = head -> next;
    }
    return head;
}

 五、课后练习

题目序列题目链接题目难度重点思考
(1)

167. 两数之和 II - 输入有序数组

★★☆☆☆使用相向左右指针,如何实现左右指针的推进
(2)

557. 反转字符串中的单词 III

★★☆☆☆如何确定它是一个单词的结束
(3)

876. 链表的中间结点

★★☆☆☆如何使用双指针

题(1)参考题解

题(2)参考题解

题(3)参考题解

  • 48
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罅隙`

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值