双指针套路
双指针分为同向遍历和反向遍历。
1. 同向
通用步骤:
int i, j
(初始化,通常令二者为0)while j < arr.length:
如果需要保留arr[j]
,则使用arr[i]
来保存,同时i后移为下次遍历作准备,即arr[i++] = arr[j++];
;
否则跳过,继续遍历。
2. 反向
通用步骤:
int i = 0, j = arr.length - 1;
while i <= j:
具体操作取决于题目要求
至少在该方向上移动一个指针
15. 三数之和
思路
- 对数组进行排序
- 找一个基准 nums[i],使用L和R指针遍历nums[i++1 … numsSize - 1],初始L=i+1, R=numsSize-1
- 当三数之和sum < 0时,L++;sum > 0 时,R–。但是若基准元素已经大于零,则无需遍历
- 当sum == 0时,将答案加入返回数组中
当遇到多个相同的值时有重复答案,这就需要去重:
- 基准元素nums[i] == nums[i - 1]时,结束本次循环
- 当L < R && nums[L] == nums[L + 1] 即左指针的元素等于下一个元素时,L++。右指针同理
/**
* Return an array of arrays of size *returnSize.
* The sizes of the arrays are returned as *returnColumnSizes array.
* Note: Both returned array and *columnSizes array must be malloced, assume caller calls free().
*/
int comp(const void *a, const void *b) { return *(int *)a - *(int *)b; }
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
*returnSize = 0;
if(numsSize < 3){
return NULL;
}
int n = numsSize;
qsort(nums, n, sizeof(int), comp); //快速排序
int **res = malloc(sizeof(int *) * n * n);
*returnColumnSizes = malloc(sizeof(int) * n * n);
for(int i = 0; i < n; i++){
if(nums[i] > 0)
break;
if(i > 0 && nums[i] == nums[i - 1])
continue;
int L = i + 1, R = n - 1;
while(L < R){
int sum = nums[i] + nums[L] + nums[R];
if(sum == 0){
res[*returnSize] = malloc(sizeof(int) * 3);
res[*returnSize][0] = nums[i];
res[*returnSize][1] = nums[L];
res[*returnSize][2] = nums[R];
(*returnColumnSizes)[*returnSize] = 3; // 记录列数
(*returnSize)++;
while(L < R && nums[L] == nums[L + 1]) L++;
while(L < R && nums[R] == nums[R - 1]) R--;
L++;
R--;
} else if(sum < 0) L++;
else if(sum > 0) R--;
}
}
return res;
}
88. 合并两个有序数组
思路
- 利用
length
记录nums1
复制操作后的总长度,i
和j
指针分别遍历两个数组 - 当数组1的元素小于数组2的元素,即
nums1[i] <= nums2[j] && i < length
时,i
指针向后移动,直到找到第一个大于nums2[j]
的元素- 将nums1[i]以后的元素向后移动一个单位,最后将nums2[j]的值赋给nums1[i](类似于插入排序)。
- 然后令
j++
,nums1
的长度length++
- 重复此操作,直到
length == m + n
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int length = m;
int i = 0, j = 0;
while(length <= m + n && j < n) {
while(nums1[i] <= nums2[j] && i < length)
i++; //找到第一个大于nums2[j]的元素
for(int k = length - 1; k >= i; k--)
nums1[k + 1] = nums1[k]; //依次向后移动
nums1[i] = nums2[j]; //复制到nums1
j++;
length++;
}
}
125. 验证回文串
代码实现
/*算法思想:1.设置双指针i,j分别指向字符串头和字符串尾
2.当表头与表尾指针i,j未相遇时,指针i向右遍历,直到遇到第一个字母或数字字符,指针j向左遍历,直至遇到第一个字母或数字字符
3.判断遍历到元素是否相等,若相等则指针继续相向前进
*/
char Tolower (char a) //将字符转为小写,若本身已经是小写或是数字则直接返回
{
if(a >= 'A' && a <= 'Z')
return a+32;
else
return a;
}
bool isNumOrChar (char a) //判断是否为字符或者数字
{
if(a >= '0' && a <= '9')
return true;
else if(a >= 'a' && a <= 'z')
return true;
else if(a >= 'A' && a <= 'Z')
return true;
else
return false;
}
bool isPalindrome(char * s){
int len = strlen(s);
if(len <= 1)
return true;
int i = 0, j = len - 1;
while(i < j)
{
while(i < j && !(isNumOrChar(s[i]))) i++; //指针i向右遍历,直到遇到第一个字母或数字字符
while(i < j && !(isNumOrChar(s[j]))) j--; //指针j向左遍历,直至遇到第一个字母或数字字符
if(i < j && Tolower(s[i]) != Tolower(s[j]))
return false;
i++;
j--;
}
return true;
}
283. 移动零
给定一个数组 nums
,编写一个函数将所有
0
0
0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
1. 必须在原数组上操作,不能拷贝额外的数组。
2. 尽量减少操作次数。
解题思路
方法:双指针
用两个指针i
, j
从头到尾依次遍历:
- 遇到零元素就记录该位置
- 遇到非零元素就进行替换零的操作
当其中一个指针i
进行完一次遍历,此时所有非零元素已经全部移动到相对前面的位置,只需要将剩余位置上的元素置为零即可。
例如:
代码实现
void moveZeroes(int* nums, int numsSize){
int i, j = 0;
for(i = 0; i < numsSize; i++){
if(nums[i] != 0){ //遇到非零元素进行替换零
nums[j++] = nums[i];
}
// else 继续查找非零元素
}
// 将剩余位置上的元素置为零
while(j < numsSize){
nums[j++] = 0;
}
}
345. 反转字符串中的元音字母
bool isVowel(char c){
if(c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u'
|| c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U')
return true;
return false;
}
char * reverseVowels(char * s){
int i = 0, j = strlen(s) - 1;
while(i < j){
if(isVowel(s[i]) && isVowel(s[j])){
char temp = s[i];
s[i] = s[j];
s[j] = temp;
i++;
j--;
} else if(isVowel(s[i]) && !isVowel(s[j])){
j--;
} else if(!isVowel(s[i]) && isVowel(s[j])){
i++;
} else {
i++;
j--;
}
}
return s;
}
26. 删除排序数组中的重复项
思路
数组完成排序后,我们可以放置两个指针 i i i 和 j j j,其中 i i i 是慢指针,而 j j j 是快指针。只要 n u m s [ i ] = n u m s [ j ] nums[i] = nums[j] nums[i]=nums[j],我们就增加 j j j 以跳过重复项。
当我们遇到 n u m s [ j ] ≠ n u m s [ i ] nums[j] \neq nums[i] nums[j]=nums[i] 时,跳过重复项的运行已经结束,因此我们必须把它( n u m s [ j ] nums[j] nums[j])的值复制到 n u m s [ i + 1 ] nums[i+1] nums[i+1]。然后递增 i i i,接着我们将再次重复相同的过程,直到 j j j 到达数组的末尾为止。
int removeDuplicates(int* nums, int numsSize){
if (numsSize == 0) return 0;
int i = 0;
for (int j = 1; j < numsSize; j++) {
if (nums[j] != nums[i]) {
i++;
nums[i] = nums[j];
}
}
return i + 1;
}
19. 删除链表的倒数第N个节点
给定一个链表,删除链表的倒数第 n n n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的 n n n 保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?
思路
快慢指针,快指针先走n步,然后快慢一起走,直到快指针走到最后,要注意的是可能是要删除第一个节点,这个时候可以直接返回head -> next
代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
if(!head || !head -> next) return NULL;
struct ListNode * fast = head, *slow = head;
for(int i = 0; i < n; i++){
fast = fast -> next;
}
if(!fast){
return head -> next;
}
while(fast -> next){
fast = fast -> next;
slow = slow -> next;
}
slow -> next = slow -> next -> next;
return head;
}
面试题22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
思路 + 代码
与上面思路相同,使用快慢指针同步移动
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* getKthFromEnd(struct ListNode* head, int k){
if(!head)
return NULL;
struct ListNode *fast = head, *slow = head;
for(int i = 0; i < k && fast != NULL; i++){
fast = fast->next;
}
while(fast){
fast = fast->next;
slow = slow->next;
}
return slow;
}
面试题06. 从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
思路
- 先得到链表
head
的长度len
--> 用来动态申请数组returnArray
- 遍历链表,把其中的元素赋值给数组
returnArray
- 数组内部进行逆置
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int* reversePrint(struct ListNode* head, int* returnSize){
int len = 0, i=0;
struct ListNode *p = head;
while(p){
len++;
p=p->next;
}
p = head;
int *returnArray = (int *)malloc(len * sizeof(int));
while(p){
returnArray[i++] = p->val;
p = p->next;
}
for(i=0;i<len/2;i++){
int temp = returnArray[i];
returnArray[i] = returnArray[len - i - 1];
returnArray[len - i - 1] = temp;
}
(*returnSize) = len;
return returnArray;
}
面试题 02.08. 环路检测
解题思路
经典的快慢指针问题
- 设置快慢指针
- 找到第一次相遇【快慢指针相遇】
- 再出发慢指针【快指针与从头结点出发的慢指针】
- 相遇即所求
代码实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *slow = head, *fast = head;
// 快慢指针
while (fast != NULL && fast->next != NULL){
slow = slow->next;
fast = fast->next->next;
// 如果相遇了就break
if (slow == fast) break;
}
// fast到了链表尾部,说明链表无环
if (fast == NULL || fast->next == NULL)
return NULL;
/*
//上方if也可以这么写
if(fast != slow) // 这个判逆必须加,因为上面循环跳出也可能是循环体结束了导致的
return NULL;
*/
// 慢指针从头开始, 快慢指针再一次相遇就是在环的起点
slow = head;
while (slow != fast){
slow = slow->next;
fast = fast->next;
}
return fast;
}
977. 有序数组的平方
给定一个按非递减顺序排序的整数数组 A,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
输入:[-4,-1,0,3,10]
输出:[0,1,9,16,100]
解题思路
因为数组 A 已经排好序了, 所以可以说数组中的负数已经按照平方值降序排好了,数组中的非负数已经按照平方值升序排好了。
举一个例子,若给定数组为 [-3, -2, -1, 4, 5, 6],数组中负数部分 [-3, -2, -1] 的平方为 [9, 4, 1],数组中非负部分 [4, 5, 6] 的平方为 [16, 25, 36]。我们的策略就是从前向后遍历数组中的非负数部分,并且反向遍历数组中的负数部分。
算法:
我们可以使用两个指针分别读取数组的非负部分与负数部分 —— 指针 k 反向读取负数部分,指针 j 正向读取非负数部分。
那么,现在我们就在使用两个指针分别读取两个递增的数组了(按元素的平方排序)。接下来,我们可以使用双指针的技巧合并这两个数组。
int* sortedSquares(int* A, int ASize, int* returnSize){
short i, j = 0, k = ASize-1;
int *res = malloc(ASize * sizeof(int));
for(i = 0; i < ASize; i++){
A[i] = A[i] * A[i];
}
while(i--){
if(A[j] > A[k]){
res[i] = A[j++]; //j正向读取非负数
} else {
res[i] = A[k--]; //k反向读取负数
}
}
*returnSize=ASize;
return res;
}
时间复杂度:
O
(
N
)
O(N)
O(N),其中
N
N
N 是数组 A 的长度。
空间复杂度:
O
(
N
)
O(N)
O(N)。
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
思路:从中间开始向两边扩散来判断回文串。考虑奇偶
for 0 <= i < len(s):
找到以 s[i] 为中心的最长回文子串
找到以 s[i] 和 s[i + 1] 为中心的最长回文子串
更新答案
完整代码:
//截取字符串
char *subStr(char *s, int start, int len){
char *dst = malloc(sizeof(char) * (len + 1));
for(int i = 0; i < len; i++){
dst[i] = s[start + i];
}
dst[len] = '\0';
return dst;
}
//寻找最长回文串
char *palindrome(char *s, int l, int r){
//防止越界
while(l >= 0 && r < strlen(s) && s[l] == s[r]){
//向两边展开
l--; r++;
}
// 返回以 s[l] 和 s[r] 为中心的最长回文串
return subStr(s, l + 1, r - l - 1);
}
char * longestPalindrome(char * s){
char *res = "";
for(int i = 0; i < strlen(s); i++){
char *s1 = palindrome(s, i, i); // 以 s[i] 为中心的最长回文子串(奇数)
char *s2 = palindrome(s, i, i + 1); // 以 s[i] 和 s[i + 1] 为中心的最长回文子串(偶数)
res = strlen(res) > strlen(s1) ? res : s1;
res = strlen(res) > strlen(s2) ? res : s2;
}
return res;
}