1. 顺序表
说明:由于nums[src]等价于*(nums + src),故以下将将下标计数器简化称作指针
1.1 移除元素
思路:
题目要求的空间复杂度O(1)的言外之意是不另外开辟数组空间,直接在原数组上进行删除元素的操作。一般情况下:如果只删除数组中的其中一个元素,只需将它后面的所有元素向前移动一位,将它覆盖即删除。而这道题有多个要删除的元素,核心思想也是一样的
“将它后面的所有元素向前移动一位”的前提条件是后面所有元素都不等于val(要删除的数)
那么有多个val的情况也是类似地,以val为分界线
如何达到分割数组的效果?
dst和src指针同时出发,当遇到val,dst停下,让src继续走直到不遇到val为止,然后再同时走,直到src(src走的快)遍历完数组。这相当于将等于val的元素跳过,分割数组的目的就是删除等于val的元素。注意:dst和src在同时走时,要将src的值赋给dst
- 循环终止条件:src遍历完数组
- 返回值:dst
int removeElement(int* nums, int numsSize, int val){ int dst = 0; int src = 0; while(src < numsSize) { if(nums[src] == val) { src++; } else { nums[dst] = nums[src]; dst++; src++; } } return dst; }
1.2 删除有序数组中的重复项
思路:
int removeDuplicates(int* nums, int numsSize){ int dst = 0; int src = 0; while(src < numsSize) { if(nums[dst] == nums[src]) { src++; } else { nums[++dst] = nums[src++];//注意这里是将src位置的值赋值给dst的下一个位置 } } return dst + 1; }
找到每个分界点,且将它们组成的区间内的元素删除,思路和上一题类似:dst记录覆盖后的位置,src找下一个分界点。当src找到分界点,将其值赋值给dst的下一个位置(因为dst当前的位置是相对于src的上一个分界点),然后src继续找,直到遍历完数组。
- 循环终止条件:src遍历完数组
- 返回修改后的数组长度:dst+1(dst是下标,从0开始)
1.3 合并两个有序数组
思路:
这是归并排序的核心思想,前提是要比较的两个数组已经排好顺序
- 循环终止条件:至少有一个数组遍历完毕。实际上,这里num1一定会遍历完的,因为num1剩余的空间要存放num2
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){ //都是下标 int end = nums1Size - 1; int end1 = m - 1; int end2 = n - 1; while(end1 >= 0 && end2 >= 0)//把小的放进去 { if(nums1[end1] > nums2[end2]) { nums1[end] = nums1[end1]; end--; end1--; } else { nums1[end] = nums2[end2]; end--; end2--; } } while(end2 >= 0)//num2可能有剩余元素 { nums1[end] = nums2[end2]; end--; end2--; } return nums1; }
1.4 轮转数组
思路:
这种轮转(左旋/右旋)的题,不论是轮转字符串、数组,都可以用这种思想:各部分逆置+整体逆置。
- 重点是用k找到分界线
- 逆置功能写成单独的函数
- 当k大于数组长度,将k模上长度即可
//逆置函数 void reverse(int* nums, int start, int end) { int left = start; int right = end - 1; while(left < right) { int tmp = nums[left]; nums[left] = nums[right]; nums[right] = tmp; left++; right--; } } void rotate(int* nums, int numsSize, int k){ k %= numsSize; if(k < numsSize) { reverse(nums, 0, numsSize - k);//前一部分 reverse(nums, numsSize - k, numsSize);//后一部分 reverse(nums, 0, numsSize);//整体 } }
1.5 数组形式的整数加法
2. 链表
2.1 移除链表元素
思路:
因为单向链表的局限性,只能用三个指针维护要删除节点的前中后三个节点
- 三指针同时迭代的条件:cur->val != val
- 让prev指向next之前,要先用cur得到next。并且要先free掉cur当前的值再更新cur的位置
- 删除的节点若为第一个节点,则需要更新head的地址
struct ListNode* removeElements(struct ListNode* head, int val){ struct ListNode* cur = head; struct ListNode* prev = NULL; while(cur) { if(cur->val == val) { struct ListNode* next = cur->next; //如果要删除的是第一个节点 if(prev == NULL)//cur指向head,prve为空 { free(cur); head = next;//更新第一个节点 cur = head;//迭代cur } else { prev->next = next;//迭代prev free(cur); cur = next;//迭代cur } } else { prev = cur; cur = cur->next;//迭代 } } return head; }
注:其实next指针不用每次都更新它,因为只有在碰到要删除的节点要需要用到它,所以这里将最后一个else语句中的next迭代放到了第二个if语句中
2.2 反转链表
思路:遍历,反向
此思路比较简单,同样是三指针同时迭代,为了可读性,这里将要返回的指针命名为newhead
struct ListNode* reverseList(struct ListNode* head){ struct ListNode* cur = head; struct ListNode* newhead = NULL; while(cur) { struct ListNode* next = cur->next;//保存cur的下一个节点 cur->next = newhead;//cur逆指 newhead = cur//更新 cur = next;//更新 } return newhead; }
2.3 链表的中间结点
思路:
快慢指针是一个经典方法,十分巧妙。接下来会介绍快慢指针的更多用法。
- 奇数个节点循环终止的条件是迭代到最后一个节点,偶数个节点循环终止的条件是迭代到NULL(即最后一个节点的next)。所以循环终止的条件有两个。
struct ListNode* middleNode(struct ListNode* head){ struct ListNode* fast = head; struct ListNode* slow = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; } return slow; }
为什么循环条件是&&
而不是||
?
2.4 链表中倒数第k个结点
思路:
让快指针先走k步,作用是锁定了两指针之间的距离
- 注意判断链表是否为空和k是否大于链表长度
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) { int len = 0; struct ListNode* fast = pListHead; struct ListNode* slow = pListHead; struct ListNode* cur = pListHead; while(cur)//计算链表的长度 { len++; cur = cur->next; } if(len == 0 || k > len) { return NULL; } while(k--)//让fast先走k步 { fast = fast->next; } while(fast)//让快慢指针同时走 { fast = fast->next; slow = slow->next; } return slow; }
2.5 合并两个有序链表
思路:
这是归并排序的核心思想
- 新链表的每次更新,都要更新cur
- 注意链表为空的情况。一共有三种情况:只有list1为空;只有list2为空;全为空。如何处理?
- 若原链表长度不同,最后一定会有剩下的节点,只需将它们链接到新链表的当前位置即可。同样需要判断是哪一个链表还剩。
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ struct ListNode* l1 = list1; struct ListNode* l2 = list2; struct ListNode* listhead = NULL; //判断链表为空 if(l1 == NULL) { return l2; } if(l2 == NULL) { return l1; } //把第一个val小的节点作为listhead struct ListNode* cur = NULL;//迭代指针变量 if(l1->val < l2->val) { listhead = cur = l1; l1 = l1->next; } else { listhead = cur = l2; l2 = l2->next; } while(l1 && l2) { if(l1->val < l2->val) { cur->next = l1; l1 = l1->next; } else { cur->next = l2; l2 = l2->next; } cur = cur->next; } //此时至少有一个走完,将剩下的连接上 if(l1 == NULL) { cur->next = l2; } else { cur->next = l1; } return listhead; }
2.6 链表分割
注:头节点(哨兵位节点)和第一个节点不同,头节点仅存放了第一个节点的地址,不存放数据,第一个节点才是真正有数据的节点。可以理解为头节点就是一个代号,代表着这个链表
思路:
其核心思路为:先分组,再链接。
- 链接时注意不要链接到要free掉的节点
- free掉开辟的内存
ListNode* partition(ListNode* pHead, int x) { struct ListNode* cur = pHead; struct ListNode* greaterHead, *greaterTail, *lesstail, *lesshead; //为两个头节点开辟内存 greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode)); lesstail = lesshead = (struct ListNode*)malloc(sizeof(struct ListNode)); lesstail->next = NULL; greaterTail->next = NULL; //分组 while (cur) { if(cur->val < x) { lesstail->next = cur; lesstail = lesstail->next; } else { greaterTail->next = cur; greaterTail = greaterTail->next; } cur = cur->next;//注意迭代! } lesstail->next = greaterHead->next;//跳过头节点再链接 greaterTail->next = NULL; pHead = lesshead->next;//更新pHead free(lesshead); free(greaterHead); return pHead; }
2.7 链表的回文结构
思路:
为什么会出现最后那个巧合?
因为将后半部分逆置时并没有处理中间节点前面的节点(这里是2),也就是说,倒置完以后,2依然是指向3的。这个巧妙的巧合就免去了当链表节点个数为奇数时,还要增加一系列条件判断,例如节点为个数奇数时,cur2后半部分的最后一个节点不需要比较等(因为对称轴没法跟谁比)。
class PalindromeList { public: bool chkPalindrome(ListNode* A) { ListNode* fast = A; ListNode* slow = A; ListNode* cur1 = A; ListNode* cur2 = NULL; while(fast->next && fast)//找中 { fast = fast->next->next; slow = slow->next; } //此时slow指向中间节点 while(slow)//逆置后一半 { ListNode* next = slow->next; slow->next = cur2; cur2 = slow; slow = next; } //此时cur2是后半部分的第一个节点 while(cur1 && cur2) { if(cur1->val != cur2->val) { return false; } else//迭代 { cur1 = cur1->next; cur2 = cur2->next; } } //当前面 return true; } };
2.8 相交链表
思路:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { struct ListNode * cur1 = headA; struct ListNode * cur2 = headB; struct ListNode * cur11 = headA; struct ListNode * cur22 = headB; if(headA == NULL || headB == NULL) { return NULL; } int len1 = 0, len2 = 0; //计算链表的长度 while(cur1) { len1++; cur1 = cur1->next; } while(cur2) { len2++; cur2 = cur2->next; } int k = abs(len1 - len2);//得出链表长度之差的绝对值 if(len1 > len2) { while(k--) { cur11 = cur11->next; } } else { while(k--) { cur22 = cur22->next; } } //此时两链表处于同一起始点(距离交点的长度) while(cur11 && cur22) { if(cur11 != cur22) { cur11 = cur11->next; cur22 = cur22->next; } else { return cur22; } } return NULL; }
更新日志
4/15/2022 man9o