力扣刷题 | 复习篇2:链表专题

203 移除链表元素

难度:简单

在创建虚拟头结点dummyhead和用于遍历的指针cur时,我都搞不清楚要用ListNode还是ListNode*,查资料得知:使用new时, C++ 会为新创建的对象在程序的内存堆(heap)上分配足够的空间,然后返回这块内存的地址,这样你就可以通过这个地址来访问这个新对象。也就是说

ListNode* dummyhead = new ListNode(0);

这句话的意思是为新创建的ListNode类型的对象分配一块内存并返回这块内存的地址,然后把这个地址赋给新创建的指针dummyhead,所以这里用的应该是ListNode*也就是一个指向ListNode类型的指针,因为new返回的就是指针类型,所以创建的对象也应该是指针类型。

tmp、cur也是同理。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* cur = dummyhead; //表示定义了一个指向ListNode类型的指
                                //针,可以储存ListNode类型对象的内存地址
        while(cur->next != NULL) {
            if(cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp; //释放删除节点的内存
            }
            else {
                cur = cur->next;
            }
        }
        head = dummyhead->next;
        delete dummyhead;
        return head;
    }
};

707 设计链表

难度:中等

要点:

  • 初始化cur时是指向dummyhead还是dummyhead->next
  • 添加节点时两个指针先对哪一边进行赋值才不会出错
  • 搞不清楚while里面的条件时可以取特殊情况n=0即n为头结点的情况进行判断
  • 链表长度变化时要记得加上size++/size- -

问题:

跑的时候一直报错,后面改了几个地方之后突然可以了,不知道是改了哪里有效果的,改动主要在

  • 把一些NULL改成了nullptr
  • 删除了一些多余的代码,比如不需要引入cur的时候又引入了
  • 其他我也不太记得了
    下次再重新做的时候看看能不能写对吧。。。

代码:

class MyLinkedList {
public:
    struct LinkedNode {
        int val;
        LinkedNode* next;
        LinkedNode(int val): val(val), next(nullptr) {}
    };

    LinkedNode* _dummyhead;
    int _size;

    MyLinkedList() { //初始化链表
        _dummyhead = new LinkedNode(0);
        _size = 0;
    }
    
    int get(int index) { //获取下标为index的节点的值
        LinkedNode* cur = _dummyhead->next;
        if(index < 0 || index > (_size - 1) ) {
            return -1;
        }
        else {
            while(index) {
                cur = cur->next;
                index--;
            }
            return cur->val;
        }
    }

    void addAtHead(int val) {
        LinkedNode* NewNode = new LinkedNode(val);
        NewNode->next = _dummyhead->next;
        _dummyhead->next = NewNode;
        _size++;
    }
    
    void addAtTail(int val) {
        LinkedNode* cur = _dummyhead;
        LinkedNode* NewNode = new LinkedNode(val);
        while(cur->next != nullptr) {
            cur = cur->next;
        }
        cur->next = NewNode;
        _size++;
    }
    
    void addAtIndex(int index, int val) {
        if(index > _size) return;
        if(index < 0) index = 0;
        LinkedNode* NewNode = new LinkedNode(val);
        LinkedNode* cur = _dummyhead;

        while(index) {
            cur = cur->next;
            index--;
        }
        NewNode->next = cur->next;
        cur->next = NewNode;
        _size++;
    }
    
    void deleteAtIndex(int index) {
        LinkedNode* cur = _dummyhead;
        if(index < 0 || index > _size - 1) {
            return;
        }
        while(index) {
            cur = cur->next;
            index--;
        }
        LinkedNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        tmp = nullptr;
        _size--;
    }

};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList* obj = new MyLinkedList();
 * int param_1 = obj->get(index);
 * obj->addAtHead(val);
 * obj->addAtTail(val);
 * obj->addAtIndex(index,val);
 * obj->deleteAtIndex(index);
 */

206 翻转链表

难度:简单

思路:

首先是易于理解的双指针法,可以先手绘一下草图搞清楚pre和cur指针赋值、翻转、遍历的过程,想清楚初始化条件和终止条件再动手。代码按照初始化,引入temp指针,翻转指针,pre、cur赋值,往后遍历的顺序来写即可。

递归方法中的三个return一开始还是不太明白,后面大概懂了。

  • 首先是reverselist函数中的return。定义了一个新的reverse函数用于递归翻转链表,reverse函数最后会返回翻转之后链表的头结点,所以在主函数reverselist中有return reverse(xx)这句代码。

接下来是递归函数中的两个return

  • 第一个出现的return是递归结束时的条件语句,返回最后翻转之后的头结点。
  • 第二个return是递归的核心,当函数运行到这里时,说明if条件还不满足,则指针需要继续往后遍历,所以这里return reverse(xx)的意思是返回下一次调用reverse函数的结果。而事实上,在遍历结束之前,也就是cur到达NULL之前,reverse函数都是没有结果的,所以就会不断调用自身函数,直到触发if语句,return pre的时候,代码才会从不断重复的递归中结束,得到一个真正的返回值,返回到reverse函数,也就是主函数那里。

双指针法:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp;
        ListNode* cur = head;
        ListNode* pre = NULL;
        while(cur) {
            temp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

递归法:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverse(ListNode* pre, ListNode* cur) { 
    // 专门写个递归函数来反转
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;  //翻转指针
        return reverse(cur, temp);
    }

    ListNode* reverseList(ListNode* head) {
        return reverse(NULL, head);
    }
};

24 两两交换链表中的结点

难度:中等

思路: 本质是通过链表中各个节点指针指向的改变来进行两两的节点交换,可以通过画图想清楚指针指向的变化过程,需不需要引入虚拟头结点?哪些节点需要先赋值给temp?cur要指向谁?cur往后移动多少步?

另外,while中的条件也需要注意,两个条件的先后顺序是有差异的,必须先判断cur->next再判断cur->next->next,否则会出现空指针异常。因为如果cur->next已经为空的时候,却把判断条件cur->next->next写在cur->next前面,则会出现空指针->next的情况,此即为空指针异常。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* cur = dummyhead;

        while(cur->next != NULL && cur->next->next != NULL) {
        //这里是在寻找链表末尾,即判断什么时候结束遍历,当到达最后一个节点或者                                    
        //只剩一个节点时,都不需要再往后遍历了,没得两两交换了
            //注意,上面必须先判断next在判断next->next,否则会出现
            //空指针异常!
            ListNode* temp = cur->next;
            ListNode* temp1 = cur->next->next->next;
            cur->next = cur->next->next;
            cur->next->next = temp;
            cur->next->next->next = temp1;
            cur = cur->next->next;
        }
        ListNode* result = dummyhead->next;
        delete dummyhead;
        return result;
    }
};

19 删除链表的倒数第n个结点

难度:中等

思路: 一开始以为可以直接通过函数获取链表长度,之后利用一个指针从虚拟头结点往后遍历size-n步便可到达删除结点的前一个结点,再执行删除操作。

但是链表不能通过函数直接获取链表长度的,只能通过循环遍历整个链表来求得长度。那就先用循环获取链表长度,再按照之前的思路即可。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* cur = dummyhead;
        int size = 0;
        while(cur->next != NULL) {
            cur = cur->next;
            size++;
        }

        cur = dummyhead;

        for(int i = 0; i < size-n; i++) {
            cur = cur->next;
        }
        cur->next = cur->next->next;
        ListNode* tmp = dummyhead->next;
        delete dummyhead;
        return tmp;
    }
};

也可以用双指针

快慢指针都从虚拟头结点开始往后走,快指针先走n+1步,然后快慢指针同时往前移动直到快指针指向链表末尾的NULL。快指针走n+1步的目的是为了让快指针到达NULL时慢指针指向删除结点的前一位,方便执行删除操作。

题目限定了n的范围不会超过链表长度,所以不考虑越界问题。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode* fast = dummyhead;
        ListNode* slow = dummyhead;

        while(n--) {
            fast = fast->next;
        }
        fast = fast->next; //让fast多走一步,slow最终才能停在
                           //被删除结点的上一个结点
        while(fast != NULL) {
            slow = slow->next;
            fast = fast->next;
        }
        slow->next = slow->next->next;
        ListNode* result = dummyhead->next;
        delete dummyhead;
        return result;
    }
};

面试题 02.07 链表相交

难度:简单

这题需要注意链表相交是什么意思?不是数值相等,而是指针相等!

思路: 首先求出两个链表的长度,然后将链表从末尾对齐,将两个链表的指针都指向他们重合处的第一个节点,从这里依次往后遍历,不比较数值,只对比指针是否相等。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        int lenA = 0, lenB = 0;
        ListNode* curA = headA;
        ListNode* curB = headB;
        //求链表长度
        while(curA != NULL) {
            curA = curA->next;
            lenA++;
        }
        while(curB != NULL) {
            curB = curB->next;
            lenB++;
        }

        curA = headA;
        curB = headB;

        if(lenA > lenB) { // 如果链表A比B长
            for(int i = 0; i < lenA - lenB; i++) {
                curA = curA->next;
            }
        }
        else {
                for(int i = 0; i < lenB - lenA; i++) {
                curB = curB->next;
            }
        }
        
        while(curA != NULL) {
            if(curA == curB) return curA;
            curA = curA->next;
            curB = curB->next;
        }
        return NULL;
    }
};

上面是我自己写的,思路比较好理解。

不同长度链表的处理方法也可以使用以下写法,可能会更简洁一些:

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* curA = headA;
        ListNode* curB = headB;
        int lenA = 0, lenB = 0;
        while (curA != NULL) { // 求链表A的长度
            lenA++;
            curA = curA->next;
        }
        while (curB != NULL) { // 求链表B的长度
            lenB++;
            curB = curB->next;
        }
        curA = headA;
        curB = headB;
        // 让curA为最长链表的头,lenA为其长度
        if (lenB > lenA) {
            swap (lenA, lenB);
            swap (curA, curB);
        }
        // 求长度差
        int gap = lenA - lenB;
        // 让curA和curB在同一起点上(末尾位置对齐)
        while (gap--) {
            curA = curA->next;
        }
        // 遍历curA 和 curB,遇到相同则直接返回
        while (curA != NULL) {
            if (curA == curB) {
                return curA;
            }
            curA = curA->next;
            curB = curB->next;
        }
        return NULL;
    }
};

142 环形链表II

难度:中等

思路: 一开始没想清楚第一个while里面的条件是什么,其实这里的条件针对的是链表中不存在环的情况,这里有两个需要注意的点:

一是快指针走在前面,所以只需要判断快指针是否为空即可

二是快指针一次走两步,为了避免出现空指针异常,即出现让空指针指向next的情况,需要判断fast->next也不为空才可以

当满足了上述条件之后,说明链表中存在环,则进入下一步,当两指针相遇时需要做什么操作。这时候就需要引入index1和index2,引入这两个指针是为了求解开始入环的第一个节点。当这两个指针相遇时即在环的入口处,即为所求节点。

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while(fast!= NULL && fast->next != NULL) {
            // 这里是考虑链表是否有环,没有的时候则会存在NULL
            slow = slow->next;
            fast = fast->next->next;
            if(fast == slow) {
                ListNode* index1 = head;
                ListNode* index2 = slow;

                while(index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index1;
            }
        }
        return NULL;
    }
};

链表专题小结

203 移除链表元素: 虚拟头结点,cur指向目标节点的前一位,判断cur->next->val是否为目标值val,是的话执行删除操作,直接从前一个节点(即当前cur所指的节点)指向后一节点,再把当前节点删除即可。

707 设计链表: 涵盖链表增删改查的大综合题目,多多练习,即可熟练掌握链表基本操作!

206 翻转链表: 双指针法,需想清楚翻转过程的指针变化。

24 两两交换链表中的结点: 同上,需想清楚指针变化过程。

19 删除链表的倒数第n个结点: 可用普通方法也可用双指针。

面试题 02.07 链表相交: 搞清楚相交的定义,通过对齐末尾来判断。

142 环形链表II: 首先判断是否有环存在,其次寻找环的入口节点。

小结: 链表章节主要需要掌握虚拟头结点和双指针法,其他特殊题目可能涉及数学公式推导或其他方法,只能具体情况具体分析。需要搞清楚cur指针的指向,是dummyhead还是head,链表长度变化时size也要随之变化,删除节点时要释放内存,等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值