赞
踩
分析:双指针技巧,双在哪? 两个链表一条一个指针,从头开始,每次比较大小,然后把小的一个节点赋值到新链表,并且对应的的指针移动。
例题: 合并两个有序链表
代码:
- class Solution {
- public:
- ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
- ListNode* dum = new ListNode(0);
- ListNode* cur = dum;
- while(list1 != nullptr && list2 != nullptr){
- if(list1->val < list2->val){
- cur->next = list1;
- list1 = list1->next;
- }
- else{
- cur->next = list2;
- list2 = list2->next;
- }
- cur = cur->next;
- }
- cur->next = list1 != nullptr ? list1 : list2;
- return dum->next;
- }
- };
总结:
i). 拼接最后的部分不需要用两个while(数组才需要这么做),直接把节点的结构体赋值过去就可以了。
ii). C++中所有NULL都用nullptr代替,为了迎合C++的重载特性。(*NULL 在C++ 里表示空指针,我们调用testWork(NULL),期望是调用的是testWork(int *index), 但实际调用了testWork(int index))
iii). 虚拟头结点技巧: 也就是 dummy
节点. 如果不使用 dummy
虚拟节点,代码会复杂一些,需要额外处理指针cur为空的情况。而有了 dummy
节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性
什么时候需要用虚拟头结点?当你需要创造一条新链表的时候,可以使用虚拟头结点简化边界情况的处理。
分析:只需要创建两条链表即可,一个用来存大于x的,一个用来存小于x的,最后把他们连接起来。
例题: 分隔链表
代码:
- class Solution {
- public:
- ListNode* partition(ListNode* head, int x) {
- ListNode* dum1 = new ListNode(0);
- ListNode* dum2 = new ListNode(0);
- ListNode* cur1 = dum1;
- ListNode* cur2 = dum2;
- while(head != nullptr){
- if(head->val < x){
- cur1->next = head;
- cur1 = cur1->next;
- }
- else{
- cur2->next = head;
- cur2 = cur2->next;
- }
- //断开的前进方式
- ListNode* tmp = head->next;
- head->next = nullptr;
- head = tmp;
- }
- cur1->next = dum2->next;
- return dum1->next;
- }
- };
总结:
i). 断开的前进方式,防止输出链表中出现环,其本质目的是为了每次赋值给cur1和cur2是一个节点而非节点及其后整段链表。
k
个有序链表(STL的运用——priority_queue)分析:这里很像合并两个升序链表,区别在于,两个节点的大小关系一下就可以比较出来,但是K个(并且不知道K具体大小)就不可能一下子判断出来,如果用普通的函数比较,需要传不定长的参数,所以这里需要借助优先级队列来判断。
优先级队列:辅助找出最小值的数据结构,其底层是用最小堆来实现的。
例题: 合并 K 个升序链表
代码:
- class Solution {
- public:
- ListNode* mergeKLists(vector<ListNode*>& lists) {
- ListNode* dummy = new ListNode(0);
- ListNode* cur = dummy;
- auto cmp = [](ListNode* a, ListNode* b){
- return a->val > b->val;
- };
- priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;
- for(auto head : lists){
- if(head) pq.push(head);
- }
- while(!pq.empty()){
- auto node = pq.top();
- pq.pop();
- if(node->next){
- pq.push(node->next);
- }
- cur->next = node;
- cur = cur->next;
- }
- return dummy->next;
- }
- };
总结:
i)auto这种语法糖很好用
ii)自定义的比较函数,注意两个点:第一是a>b表示最小堆(优先级队列pop出来是最小值),第二是匿名函数的写法 auto fun = [接收参数](形参){函数体}
iii)迭代器遍历的时候,记得加上if(head),处理一开始就为空的情况。
k
个节点(时差指针)分析:倒数第k个节点也就是正数第n-k+1个,但是这种题给出来一般是不会给链表长度n的,比较平庸的做法是,先遍历一遍链表得到n,然后找到第n-k+1个元素,这样就需要遍历两边链表,那么有没有遍历一次的方法呢?
关键是在不数链表长度的情况下找到n-k+1这个量,这种情况就需要用到双指针了
第一个指针走k步后,剩下的就是n-k步了,这时候再加一个指针同步进行,就能够找到n-k+1了。
代码:
- class Solution {
- public:
- ListNode* removeNthFromEnd(ListNode* head, int n) {
- ListNode* dummy = new ListNode(0, head);
- ListNode* fast = dummy;
- ListNode* slow = dummy;
- for(int i = 0; i<n; i++){
- fast = fast->next;
- }
- while(fast->next){
- slow = slow->next;
- fast = fast->next;
- }
- slow->next = slow->next->next;
- return dummy->next;
- }
- };
总结:
i) 哨兵节点一定要加上,非常有用。
ii)最后返回用哨兵节点的next返回而不是原生的head,它们有轻微的差别。(因为改变的是dummy的next而不是head,当dummy->next为空时它不指向head)
iii) fast是用来定位slow位置的,最后赋值是没有用的,全靠slow的赋值来删除节点。一开始被例子所迷惑了,认为slow和fast中间的就是需要删除的节点,实际上是样本数量太少导致的巧合。
分析:
平凡的思路:一个指针走到头后,新设立一个指针,然后相向而行
巧妙的思路:两个指针同时走,一个走两步,一个走一步,当fast走到头时,slow就到了重点(细节,比如奇偶问题,在搭建算法总体架构时不用考虑)
例题: 链表的中间结点
代码:
- class Solution {
- public:
- ListNode* middleNode(ListNode* head) {
- ListNode* dummy = new ListNode(0, head);
- ListNode* fast = dummy;
- ListNode* slow = dummy;
- while(fast){
- slow = slow->next;
- fast = fast->next;
- if(fast){
- fast = fast->next;
- }
- }
- return slow;
- }
- };
总结:
i) 边界情况处理用奇偶举例来写出一个通用的代码。
ii)依然使用哨兵节点dummy
分析:
一对快慢指针,步长为1、2,如果有环那么他们会相遇.
问题1:如果不会相遇,什么时候判断结束?答:当快指针走到头的时候。
问题2:如何得到环的起点?答:当相遇后,只要把其中一个指针设置为head,然后同步步长为1,再次相遇时,就得到了环起点。
例题: 环形链表 II
代码:
- class Solution {
- public:
- ListNode *detectCycle(ListNode *head) {
- ListNode* fast = head;
- ListNode* slow = head;
- while(fast){
- slow = slow->next;
- if(fast->next == nullptr){
- return nullptr;
- }
- fast = fast->next->next;
- if(fast == slow){
- slow = head;
- while(slow != fast){
- slow = slow->next;
- fast = fast->next;
- }
- return fast;
- }
- }
- return nullptr;
- }
- };
总结:
i)要学会画图分析,不要空想
分析:
我的思路:(Trivial的思路)先两个一起走,其中一个走到头后,算另一个还差多少,然后这个差算出来就可以让这两个链表同步,然后再从头对齐一起走,如果再到头之前,他们的listnode能一样就说明有相交的。
巧妙的思路:(对偶法)把两个链表拼接在一起,让他们同时结束。如果结束时,两个指针都是null,说明无相交,否则出来的指针就是相交起点。
两个思路的共同点都是让链表同步!
但是第一个思路处理起来会很麻烦,因为不知道谁先结束,所以需要对两个链表进行特殊的处理。
用对偶法可以很简便的化解这种麻烦。
例题: 相交链表
代码:
我的算法:
- class Solution {
- public:
- ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
- ListNode* ans = nullptr;
- ListNode* A = headA;
- ListNode* B = headB;
- while(A != nullptr && B != nullptr){
- A = A->next;
- B = B->next;
- }
- //不知道谁先结束需要做的处理
- ListNode* tmp = A==nullptr?B:A;
- ListNode* tmphead = A==nullptr?headB:headA;
- ListNode* tmphead2 = A==nullptr?headA:headB;
- //
- int cnt = 0;
- while(tmp != nullptr){
- cnt++;
- tmp = tmp->next;
- }
- for(int i = 0; i<cnt; i++){
- tmphead = tmphead->next;
- }
- while(tmphead){
- if(tmphead != tmphead2){
- tmphead = tmphead->next;
- tmphead2 = tmphead2->next;
- }
- else{
- ans = tmphead;
- break;
- }
- }
- return ans;
- }
- };
拼接算法:
- class Solution {
- public:
- ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
- if(headA == nullptr || headB == nullptr){
- return nullptr;
- }
- ListNode* A = headA;
- ListNode* B = headB;
- while(A!=B){
- A = A==nullptr?headB:A->next;
- B = B==nullptr?headA:B->next;
- }
- return A;
- }
- };
总结:
i)ListNode这个数据结构可以直接比较,来判断是不是一个节点,很方便。
例题:旋转链表
代码:
思路1:双指针法
- class Solution {
- public:
- ListNode* rotateRight(ListNode* head, int k) {
- //算长度和取模
- int length = 0;
- ListNode* tmp = head;
- while(tmp){
- length++;
- tmp = tmp->next;
- }
- if(k == 0 || length == 0){
- return head;
- }
- k %= length;
- //正式的业务逻辑
- ListNode* fast = head, *slow =head;
- for(int i = 0; i< k; i++){
- fast = fast->next;
- }
- while(fast->next){
- fast = fast->next;
- slow = slow->next;
- }
- fast->next = head;
- head = slow->next;
- slow->next = nullptr;
- return head;
- }
- };
思路2:闭合为环(对于链表类型的题目可能有意想不到的作用)
- class Solution {
- public:
- ListNode* rotateRight(ListNode* head, int k) {
- int length = 0;
- ListNode* tmp = head, *ret, *last;
- //算长度,需要一个last指针辅助
- while(tmp){
- length++;
- last = tmp;
- tmp = tmp->next;
- }
- //判断边界
- if( k== 0 || length == 0){
- return head;
- }
- //开始成环,找到节点然后断开
- last->next = head;
- k%= length;
- for(int i = 0; i<length - 1- k; i++){
- head = head->next;
- }
- ret = head->next;
- head->next = nullptr;
- return ret;
- }
- };
1. 双指针分为位置和速度不同的指针,其中速度不同的指针为快慢指针,比如:步长的快慢——步差指针,开始时间的快慢——时差指针...
2. STL很好用,需要重点学习。
3. dummy节点很有用,建议每次都加上。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。