第一天刷题。一个平实的开始,希望能坚持下来,不求波涛汹涌,大浪淘沙,但求静水流深,川流不息。
先学习双指针。题目方向分为两个:链表和数组。
在处理数组和链表相关问题时,双指针技巧是经常用到的,双指针技巧主要分为两类:左右指针和快慢指针。
所谓左右指针,就是两个指针相向而行或者相背而行;而所谓快慢指针,就是两个指针同向而行,一快一慢。
文章目录
- 链表中的双指针
- 第一道:21.合并两个有序链表
- 第二道:分割链表
- 第三道:合并K个升序链表
- 第四道:单链表的倒数第 k 个节点
- 第五道:链表的中间结点
- 第六道:相交链表
- 第七道:判断是否为环形链表
- 第八道:找到环形链表的起点
- 数组中的双指针
- 第一道: [删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/)
- 第二道:移除元素
- 第三道:移动零
- 第四道:最长回文子串
链表中的双指针
第一道:21.合并两个有序链表
思路一:申请第三个链表的空间,拷贝两个链表中较小的一个。
代码v1.0:
class Solution {
public:bool comp(ListNode* l1, ListNode* l2){if(l1->val > l2->val) return true;else return false;}bool Empty_Judge(ListNode* l){if(l->val==0) return true;else return false;}ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {if(Empty_Judge(list1)) return list2;if(Empty_Judge(list2)) return list1;ListNode* list ;ListNode* head = list;while(list1!=nullptr&&list2!=nullptr){list = new ListNode;if(comp(list1,list2)){list->val = list2->val;list = list->next;list2 = list2->next;}else{list->val = list1->val;list = list->next;list1 = list1->next;}}while(list1!=nullptr){list = new ListNode(list1->val);list=list->next;list1=list1->next;}while(list2!=nullptr){list = new ListNode(list2->val);list=list->next;list2=list2->next;}return head;}};
问题:返回的 head 没有任何数值。
解决:
-
链表到下一个结点前需要空间,否则指向NULL就是野指针,也就不可能连上。
代码开始,head 指向了没有初始化的 list ,即指向一个(指向null的)野指针,它自己也变成了野指针。如果初始化工作做好,不解决后续连接过程的“先申请空间,再连接”,就会只有一个数据。
-
条件判断分不清 NULL 、nullptr 及其背后的 true 、false,造成代码冗余。
Empty_judge 和 comp 函数是完完全全的多余代码,因为本可以直接在原代码中判断。
代码V2.0:
class Solution {
public:ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {if(!list1) return list2;if(!list2) return list1;ListNode* list = new ListNode ;ListNode* head = list;while(list1&&list2){if(list1->val > list2->val){list->val = list2->val;list2 = list2->next;}else{list->val = list1->val;list1 = list1->next;}list->next = new ListNode;list = list->next;}while(list1){list ->val = list1 -> val;if(list1->next) list->next = new ListNode;list=list->next;list1=list1->next;}while(list2){list ->val = list2 -> val;if(list2->next) list->next = new ListNode;list=list->next;list2=list2->next;}return head;}};
规避了野指针问题和相对的代码冗余问题。
思路二:在两个指针现有的空间上,只做穿针引线的工作(连接起来)
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {if(!list1) return list2; //可以省略if(!list2) return list1;ListNode* list = new ListNode;ListNode* head = list;while(list1&&list2){ if(list1->val > list2->val){list -> next = list2;list = list2; //这里可以配合下面简化list2 = list2->next;}else{list -> next = list1;list = list1; //简化list1 = list1->next;//list = list->next}}list -> next = list1 ? list1 : list2; return head -> next;}
问题:
- 开始的判断是否为空可以省略。
- while 中出现重复点(只是需要一点点直觉)
解决:
看思路三。
**思路三:**labuladong ,虚拟头结点值得注意。整体与思路二没什么区别。
ListNode mergeTwoLists(ListNode l1, ListNode l2) {// 虚拟头结点ListNode dummy = new ListNode(-1), p = dummy;ListNode p1 = l1, p2 = l2;
while (p1 != null && p2 != null) {// 比较 p1 和 p2 两个指针// 将值较小的的节点接到 p 指针if (p1.val > p2.val) {p.next = p2;p2 = p2.next;} else {p.next = p1;p1 = p1.next;}// p 指针不断前进p = p.next;
}if (p1 != null) {p.next = p1;
}if (p2 != null) {p.next = p2;
}return dummy.next;
}
代码中还用到一个链表的算法题中是很常见的「虚拟头结点」技巧,也就是
dummy
节点。你可以试试,如果不使用dummy
虚拟节点,代码会复杂很多,而有了dummy
节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性。
拓展:
思路四:递归
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2) {if(l1==NULL)return l2;if(l2==NULL)return l1;if(l1->val < l2->val){l1->next = mergeTwoLists(l1->next,l2);return l1;}else{l2->next = mergeTwoLists(l1,l2->next);return l2;}
}
类似汉诺塔的思路。
第二道:分割链表
V1.0
class Solution {
public:ListNode* partition(ListNode* head, int x) {//初始化ListNode* tail = head;while(tail->val < x ) tail=tail->next;ListNode* temp = tail;ListNode* maxdot = tail;//大于值开始的结点while(head->val > x) head = head->next;tail = head; //小于值开始的结点while(temp){//<xif(temp->val < x){ //如果是小于值结点tail ->next = temp;tail = tail->next;}//>xif(temp->val >x &&temp->next->val < x ){tail->next = temp->next;if(temp->next->next) temp->next = temp->next->next;else {tail->next = temp->next;temp->next = nullptr;break;}}temp = temp->next;}tail->next = maxdot;return head;}
};
现在思路也不是很清晰
思路二:双头结点
ListNode partition(ListNode head, int x) {// 存放小于 x 的链表的虚拟头结点ListNode* dummy1 = new ListNode(-1);// 存放大于等于 x 的链表的虚拟头结点ListNode* dummy2 = new ListNode(-1);// p1, p2 指针负责生成结果链表ListNode* p1 = dummy1, p2 = dummy2;// p 负责遍历原链表,类似合并两个有序链表的逻辑// 这里是将一个链表分解成两个链表ListNode* p = head;while (p != null) {if (p.val >= x) {p2.next = p;p2 = p2.next;} else {p1.next = p;p1 = p1.next;}// 断开原链表中的每个节点的 next 指针ListNode temp = p.next;p.next = null;p = temp;}// 连接两个链表p1.next = dummy2.next;return dummy1.next;
}
创建两个头结点,先断开,后连接。
思路三:递归
class Solution {
public:ListNode* small = nullptr;pair<ListNode*, ListNode*> helper(ListNode* head, int x) {if (head == nullptr) return make_pair(nullptr, nullptr);if (head->val < x) small = head;pair<ListNode*, ListNode*> next = helper(head->next, x);if (head->val < x) {head->next = next.first;next.first = head;}else {head->next = next.second;next.second = head;}return next;}ListNode* partition(ListNode* head, int x) {pair<ListNode*, ListNode*> nodes = helper(head, x);if (nodes.first) {small->next = nodes.second;return nodes.first;}else {return nodes.second;}}
};
大多数链表题递归的时候可以无脑写一句,然后只要让思路从后向前进行就可以了。
cppListNode* node = func(head->next); // func为递归函数
这里返回值选择了两个pair,first保存小于x的结点,second保存大于x的结点。
思路四:原地连接
// 1 4 3 2 5 2
struct ListNode* partition(struct ListNode* head, int x){if(!head || !head->next){return head;}struct ListNode *p = head, *q = head, *temp = NULL;if(p->val < x){ while(p->next->val < x){//此循环让 p 到达最远小值 1p = p->next; if(p->next == NULL){return head;}}q = p->next; //让 q 到达最近大值 4}else{p = NULL; }while(q->next){ //q存在下一个结点if(q->next->val < x){ //如果最近大值下一个为小值temp = q->next; //temp为小值了q->next = temp->next;//跳过小值if(p == NULL){ //如果起始值为大值(即p 为空情况)temp->next = head; //头插法:第一个插入的小值连接起始大值head = temp;p = head;}else{//起始值小值(存在了)temp->next = p->next; //头插法:小值后p->next = temp;p = temp;}}else{ //为大值就串下去q = q->next;}}return head;
}
p指针指向小于 x 的值,p == null 就连接第一个大于x的值,p!=null 就按顺序进行尾插法;
(只是 p 的"尾"是 q 的"头")
q指针指向大于 x 的值,初始是第一个大于x的值, 尾插法依次往后放;
temp 服务于p的尾插法,作为中间变量。
(q 尾插循环的前面,是p,q 的初始化工作,分别指定小于x值的尾端 和 大于x值的尾端)
第三道:合并K个升序链表
思路一:遍历合并两个链表到第一个链表中去
class Solution {
public:ListNode* merge(ListNode* list1,ListNode* list2) //这里和合并有序链表的code一模一样{...}ListNode* mergeKLists(vector<ListNode*>& lists) {//遍历合并if(lists.empty()) return nullptr;vector<ListNode*>::iterator it = lists.begin();vector<ListNode*>::iterator next = lists.begin()+1;for(;next!=lists.end();next++){*it = merge(*it,*next);}return *it;}
};
**思路二:**labladong :配合优先队列的二叉堆,获取最小k结点
ListNode mergeKLists(ListNode[] lists) {if (lists.length == 0) return null;// 虚拟头结点ListNode* dummy = new ListNode(-1);ListNode* p = dummy;// 优先级队列,最小堆PriorityQueue<ListNode*> pq = new PriorityQueue<>(lists.length, (a, b)->(a.val - b.val));// 将 k 个链表的头结点加入最小堆for (ListNode head : lists) {if (head != null)pq.add(head);}while (!pq.isEmpty()) {// 获取最小节点,接到结果链表中ListNode node = pq.poll();p.next = node;if (node.next != null) {pq.add(node.next);}// p 指针不断前进p = p.next;}return dummy.next;
}
思路三:分治递归
class Solution {public ListNode mergeKLists(ListNode[] lists){if(lists.length == 0)return null;if(lists.length == 1)return lists[0];if(lists.length == 2){return mergeTwoLists(lists[0],lists[1]);}int mid = lists.length/2;ListNode[] l1 = new ListNode[mid];for(int i = 0; i < mid; i++){l1[i] = lists[i];}ListNode[] l2 = new ListNode[lists.length-mid];for(int i = mid,j=0; i < lists.length; i++,j++){l2[j] = lists[i];}return mergeTwoLists(mergeKLists(l1),mergeKLists(l2));}public ListNode mergeTwoLists(ListNode l1, ListNode l2) {if (l1 == null) return l2;if (l2 == null) return l1;ListNode head = null;if (l1.val <= l2.val){head = l1;head.next = mergeTwoLists(l1.next, l2);} else {head = l2;head.next = mergeTwoLists(l1, l2.next);}return head;}
}
思路四:STL中的优先队列
class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {auto head = ListNode(0);auto comp = [](ListNode* const &a, ListNode* const &b){return a->val > b->val;};priority_queue<ListNode*, vector<ListNode*>, decltype(comp)> q(comp);for (auto &h : lists) if (h != nullptr) q.push(h);auto p = &head;while (!q.empty()) {p->next = q.top();p = p->next;q.pop();if (p->next != nullptr) q.push(p->next);}return head.next;}
};
第四道:单链表的倒数第 k 个节点
ListNode* removeNthFromEnd(ListNode* head, int n) {if(n==0||!head) return head; //0元素 或 不动if(!head->next) return NULL; //1元素 且 动ListNode* fformer = head;ListNode* former = head;ListNode* latter = head;while(--n>0) latter = latter->next;while(latter->next){if(former!=head) fformer = fformer->next;latter = latter -> next;former = former -> next;} //1元素if(former == fformer) head = former->next; //2元素else fformer->next = former->next; //3+元素return head;
第五道:链表的中间结点
ListNode* middleNode(ListNode* head) {ListNode* fast = head;ListNode* slow = head;while(fast->next){fast = fast->next;if(fast->next) fast=fast->next;slow = slow->next;}return slow;}
第六道:相交链表
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {/**定义两个指针, 第一轮让两个到达末尾的节点指向另一个链表的头部, 最后如果相遇则为交点(在第一轮移动中恰好抹除了长度差)两个指针等于移动了相同的距离, 有交点就返回, 无交点就是各走了两条指针的长度**/if(headA == null || headB == null) return null;ListNode pA = headA, pB = headB;// 在这里第一轮体现在pA和pB第一次到达尾部会移向另一链表的表头, 而第二轮体现在如果pA或pB相交就返回交点, 不相交最后就是null==nullwhile(pA != pB) {pA = pA == null ? headB : pA.next;pB = pB == null ? headA : pB.next;}return pA;}
这个赋值和
?
条件判断 的配合非常有趣。简短的 if 语句可用 ?来代替。
思路二:
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {int lenA = 0, lenB = 0;// 计算两条链表的长度for (ListNode p1 = headA; p1 != null; p1 = p1.next) {lenA++;}for (ListNode p2 = headB; p2 != null; p2 = p2.next) {lenB++;}// 让 p1 和 p2 到达尾部的距离相同ListNode p1 = headA, p2 = headB;if (lenA > lenB) {for (int i = 0; i < lenA - lenB; i++) {p1 = p1.next;}} else {for (int i = 0; i < lenB - lenA; i++) {p2 = p2.next;}}// 看两个指针是否会相同,p1 == p2 时有两种情况:// 1、要么是两条链表不相交,他俩同时走到尾部空指针// 2、要么是两条链表相交,他俩走到两条链表的相交点while (p1 != p2) {p1 = p1.next;p2 = p2.next;}return p1;
}
第七道:判断是否为环形链表
boolean hasCycle(ListNode head) {// 快慢指针初始化指向 headListNode slow = head, fast = head;// 快指针走到末尾时停止while (fast != null && fast.next != null) {// 慢指针走一步,快指针走两步slow = slow.next;fast = fast.next.next;// 快慢指针相遇,说明含有环if (slow == fast) {return true;}}// 不包含环return false;
}
不解释
第八道:找到环形链表的起点
流程起始同上,先让快慢指针相遇。
-
设相遇点距离头结点为 k ,即慢指针的路程。快指针速度是慢指针二倍,路程即 k+k=2k
-
设环形起点距离相遇点为 m , 则慢指针距离起点
k-m
。巧合的是,相遇点指针不断向后走,距离起点同样为k-m
。 -
所以让其中任意一个指针回到起点,经过
k-m
位移,另一个指针就会到达环形链表起点。
ListNode detectCycle(ListNode head) {ListNode fast, slow;fast = slow = head;while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;if (fast == slow) break;}// 上面的代码类似 hasCycle 函数if (fast == null || fast.next == null) {// fast 遇到空指针说明没有环return null;}// 重新指向头结点slow = head;// 快慢指针同步前进,相交点就是环起点while (slow != fast) {fast = fast.next;slow = slow.next;}return slow;
}
数组中的双指针
在数组中并没有真正意义上的指针,但我们可以把索引当做数组中的指针,这样也可以在数组中施展双指针技巧。
第一道: 删除有序数组中的重复项
基本的快慢双指针。
int removeDuplicates(int[] nums) {if (nums.length == 0) {return 0;}int slow = 0, fast = 0;while (fast < nums.length) {if (nums[fast] != nums[slow]) {slow++;// 维护 nums[0..slow] 无重复nums[slow] = nums[fast];}fast++;}// 数组长度为索引 + 1return slow + 1;
}
大同小异的一个链表题:
删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/)
struct ListNode* deleteDuplicates(struct ListNode* head){typedef struct ListNode ListNode;if(!head) return head;ListNode* slow = head;ListNode* fast = head;while(fast){if(fast->val!=slow->val){slow=slow->next;slow->val=fast->val;}fast=fast->next;}slow->next = NULL;return head;
第二道:移除元素
左右指针:
class Solution {
public:int removeElement(vector<int>& nums, int val) {int j = nums.size() - 1;for (int i = 0; i <= j; i++) {if (nums[i] == val) {swap(nums[i--], nums[j--]);}}return j + 1;}
};
快慢指针:
while(fast<numsSize){if(nums[fast]!=val){nums[slow] = nums[fast];slow++;}fast++;}return slow;
扩展:保留重复k个元素的基础上删除多余元素
class Solution { public:int work(vector<int>& nums, int k) {int len = 0;for(auto num : nums)if(len < k || nums[len-k] != num)nums[len++] = num;return len;}int removeDuplicates(vector<int>& nums) {return work(nums, 2);} };
第三道:移动零
v1.0
void moveZeroes(vector<int>& nums) {int fast = 0,slow=0;while(fast<nums.size()){if(nums[fast]!=0){nums[slow++] = nums[fast];}fast++;}while(slow<fast){nums[slow++]=0;}}
v2.0
int left = 0;int right = 0;while(right < nums.size()){if(nums[right]){swap(nums[left], nums[right]);left++;}right++;}
遇到非0直接交换就可以
第四道:最长回文子串
string longestPalindrome(string s) {string res = "";for(int i=0;i<s.size();i++){string s1=palindrome(s,i,i);string s2=palindrome(s,i,i+1);res = s1.size()>res.size()?s1:res;res = s2.size()>res.size()?s2:res;}return res;}string palindrome(string s,int l,int r){while(l>=0&&r<s.size()&&s[l]==s[r]){l--;r++;};return s.substr(l+1,r-l-1);}
左右指针,向两侧扩散。
遍历中心点。
中心点分奇数、偶数两种情况,依次比较即可。