本文已参与「新人创作礼」活动,一起开启掘金创作之路
一、案例分析
【案例】调整数组使奇数全部都位于偶数前面
【分析】这个问题当然也有很多种解法,但能否只遍历一次数组就满足题目要求呢?我们可以试试双指针——一种简单却实用的方法。
二、算法详解
>: 什么是双指针法?
双指针法:一般是指的在遍历对象的过程中,不是使用单个指针进行访问,而是使用两个相同方向或者相反方向的指针进行扫描,从而达到相应的目的。
一般双指针法有两种表现方式:
- 同向移动:在同向移动时,指针移动有快慢之分。
- 相向移动:在相向移动中,双指针一个指针在开头,另外一个指针在结尾,向中间逼近。
回到本题,我们可以采用相向移动的双指针,不妨分别定义为 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;
}
【思路剖析】
在链表中如何实现快慢指针的效果呢?可以有以下两个思路:
- 让快指针每次走多步
- 让快指针先走
在这里我们采用的方法是每次让快指针走两步。我们不妨也来比较一下单指针写法,显然用单指针遍历两次是不可少的。
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) | 19. 删除链表的倒数第 N 个结点 | ★★☆☆☆ | 如何使用双指针 |
\