跟着Carl学算法--链表

移除链表元素

力扣链接:题目链接

题目:给你一个链表和一个val,删除所有节点值等于val的节点,返回链表

思路:很简单很基础的链表题,但还是踩了不少坑,记录一下

每次判断的都是当前节点的下一个是否符合要求,而不是当前节点,如果不符合直接跳过,符合则指针前移,将此节点加入到最终的符合要求的链表中。

注意这是个带头节点的链表,头节点需要另外处理

还要注意链表是否为空的情况,因为空节点没有下一个,调用p->next 判断下一个节点是否为空时会直接报错

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if(head==NULL)
        return head;
        ListNode* p = head;
        while (p->next != NULL) {
            if (p->next->val == val)
                p->next = p->next->next;
            else
                p=p->next;
        }
        return head->val == val ? head->next : head;
    }
};

设计链表

力扣链接:题目链接

题目:设计链表,实现链表的头部尾部插入,查询删除等

思路:

​ 本来想用基础的单链表就可以,但想到每次在尾部插入需要遍历一次,有些费时间,就想用循环链表,标记头指针和尾指针,再想到尾指针且循环,那不就尾指针向前移动一位不就是头指针了吗,最后就设计就成了单循环尾指针链表。

没想到更复杂了,首先尾指针需要指向尾元素,从尾部插入自然不用说,每次都会更新,但从头部插入就很容易被忽略,因为每次从头部插入不牵扯到尾指针的修改,但这是在尾指针已经指向尾元素的基础上的,因此如果是第一次头部插入就需要手动指定此元素为尾指针的指向

其次如果删除的元素是尾部元素也需要特殊考虑,因为很少用尾指针,这一步被忽略了,检查了好久。最后还不如直接写呢(单链表耗时9ms,这个耗时12ms)

class MyLinkedList {
private:
    int size = 0;
    ListNode* tail = NULL;

public:
    MyLinkedList() {
        ListNode* headNode = new ListNode();
        tail = headNode;
        tail->val = -1;            // 初始化尾指针
        headNode->next = headNode; // 指向自己,形成循环
    }

    
    int get(int index) {
        int count = index + 1; // 索引从0开始
        if (size < count) {
            return -1;
        }
        ListNode* p = tail->next;
        while (count--) {
            p = p->next;
        }
        return p->val;
    }
     void addAtHead(int val) {
        ListNode* Node = new ListNode();
        Node->val = val;
        Node->next = tail->next->next; // 指向头指针的下一位
        tail->next->next = Node;
        if (tail->val == -1)
            tail = Node;
        size++;
    }
    void addAtTail(int val) {
        ListNode* Node = new ListNode();
        Node->val = val;
        Node->next = tail->next; // 指向头指针
        tail->next = Node;
        tail = Node;
        size++;
    }
 	void deleteAtIndex(int index) {
        if (index < 0 || index >= size)
            return;
        ListNode* p = tail->next;
        while (index--) {
            p = p->next;
        }
        if (p->next == tail)
            tail = p;
        p->next = p->next->next;
        size--;
    }
    void addAtIndex(int index, int val) {
        if (index < 0 || size < index) {
            return;
        }
        if (size == index) {
            addAtTail(val);
            return;
        }
		ListNode* Node = new ListNode();
        Node->val = val;
        ListNode* p = tail->next;
        while (index--) {
            p = p->next;
        }
        Node->next = p->next;
        p->next = Node;
        size++;
    }
};

反转链表

力扣链接:题目链接

题目:给你一个链表,翻转一下,返回头节点

思路:普通点的解决方法就是遍历一遍,数组保存,反向遍历数组新建链表

双指针
current记录当前节点,pre记录当前节点的前一个,用于给当前节点改变赋值改变方向。当然,在当前节点的next指向前一个之前,必须有一个临时指针保存current的后一个(即temp),改变方向后,这三个指针整体前移即可。

双指针写法:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        //初始化
        ListNode* pre=NULL;
        ListNode* current=head;
        ListNode* temp;
        while(current!=NULL){
            //改变方向
            temp=current->next;
            current->next=pre;
            //向前移动
            pre=current;
            current=temp;
        }
        return pre;
    }
};

递归写法(参照双指针写法):

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        return
            // 初始化
            reverse(NULL, head);
    }
    ListNode * reverse(ListNode* pre, ListNode* current) {
        if (current == NULL)
            return pre;
        // 改变方向
        ListNode* temp = current->next;
        current->next = pre;
        // 前移(即重新给指针赋值),前移后直接进入下一次循环(递归)
        return reverse(current, temp);
    }
};

除非题目要求,不要写递归,递归涉及到栈的进出很耗时,(双指针解法0ms100%,递归解法4ms62%)

两两交换链表中的节点

力扣链接:题目链接

题目:给你一个链表,每两个节点,进行交换。比如1-2-3-4.改为2-1-4-3

思路:和上一题反转链表差不多,相当于每两个节点反转一次链表,只是再多加了一个反转后的子链表与之后合适节点连接的操作。

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        if (head == NULL || head->next == NULL) {
            return head;
        }
      // 初始化
        ListNode* pre = head;
        ListNode* current = head->next;
        head=current;
        ListNode* temp;
        while (1) {
            // 交换(改变方向)
            temp = current->next;
            current->next = pre;
            //反转两个节点后,在pre与下一个合适节点连接前,须先检查
            if (temp == NULL||temp->next==NULL){
                 pre->next = temp;
                  return head;
            }
            pre->next = temp->next;
            // 前移
            current = temp->next;
            pre = temp;
        }
        return head;
    }
};

跟着Carl学算法--链表_第1张图片

因为每次pre链接后面结点时,正常情况(即剩下的链表元素个数为偶数)是直接连接到temp的下一位,但是如果剩下的元素不足(只剩一个或者0个)时,就需要另作处理。
因此需要先判断temp的情况,如果temp为空(剩余需反转的元素为0)或者temp的next为空(剩余需反转的元素为1),pre就可以直接指向temp,并返回列表结束循环。

反思:为什么循环条件在循环体内,而不能提取到循环条件中?

要实现此任务需要进行的循环中一共有三个步骤,交换,链接,前移。
其中交换的执行条件是当前需操作的两个结点是否为空,而链接和前移则是接下来操作两个结点的情况。
因此按照我的方式将这三个步骤组合,再将条件整合一下,就是需要在执行链接前进行if判断下两个节点情况(前移以后接下来的两个结点就变成了当前两个结点)
但是仔细想想其实只要调换顺序就可以实现先判断,再执行三个步骤了。即链接,前移,交换这样就可以把条件提到前边,最后合并到while判断条件中,当然为了能第一次循环能先执行链接操作就需要先创建一个虚拟节点了。

删除链表的倒数第N个节点

力扣链接:题目链接

题目:删除链表倒数第N的节点,并返回头节点

注意事项:

  1. 创建虚拟头结点,统一头节点与其他结点操作,不需要分情况讨论(容易漏)

  2. 注意最后返回的是虚拟头结点的下一位,而不是头节点(头节点可能删除)

遍历两次:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *first=head;
        int size=0;
        while(first!=NULL){
            size++;
            first=first->next;
        }
        if(size==1){
            return NULL;
        }

        ListNode *dummyNode=new ListNode();
        dummyNode->next=head;
        ListNode *sec=dummyNode;
        int count=size-n;
        while(count--!=0){
            sec=sec->next;
        }
        sec->next=sec->next->next;
        return dummyNode->next;
    }
};

双指针:指定一个快指针用于遍历链表,指定一个慢指针在快指针出发n步后开始出发,这样当快指针指向倒数第一个结点时,慢指针的下一个刚好是倒数第n个结点。

跟着Carl学算法--链表_第2张图片

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyNode = new ListNode();
        dummyNode->next = head;
        ListNode* fast = dummyNode;
        ListNode* slow = dummyNode;
        while (n--) {
            fast = fast->next;
        }
        while (fast->next != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next -> next;
        return dummyNode->next;
    }
};

两种方法时空复杂度基本都相同

第一种方法是“间接法”,将倒数转化为正数,因此还需要一个size存储长度,用来辅助转化

第二种方法是“直接法”,算倒数,给快指针和慢指针一个n的距离,一起前进,这样当快指针到最后一个结点时,慢指针的下一个刚好是倒数第n个结点。

链表相交

力扣链接:题目链接

题目:给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

思路:注意是两链表相交的结点,不是值相等的结点,因此比较时判断条件是两个结点相等,而不是节点值相等,因此还只能直接边遍历边比较,不能使用数组存储后比较。

公共最长子尾序列肯定是最短链表的尾部的子序列,因此可以将两个链表的尾部置于同一位置,跳过长链表前端过长的部分,从与短链表长度相同的部分开始向后比较,碰到第一个相同的结点就返回。
跟着Carl学算法--链表_第3张图片

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {

        ListNode* curA=headA, *curB=headB;
        int lenA=0,lenB=0;
        while(curA!=NULL){
            lenA++;
            curA=curA->next;
        }
         while(curB!=NULL){
            lenB++;
            curB=curB->next;
        }
        curA=headA;
        curB=headB;
		if(lenB>lenA){
            swap(lenA,lenB);
            swap(curA,curB);
        }
        int gap=lenA-lenB;
        while(gap--){
            curA=curA->next;
        }
        while(curA!=NULL){
            if(curA==curB){//结点相等而不是值
                return curA;
            }
            curA=curA->next;
            curB=curB->next;
        }
        return NULL;   
    }
};

环形链表II

力扣链接:题目链接

题目:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

思路:首先因为判断条件是同一结点而不是值相等的结点,使用数组存储后相等条件不好写,暴力方法不能使用。

确定解决方案是双指针解决,慢指针要想单次遍历一步到位也完全不可能,因为不知道快指针遍历到何时时慢指针需要前移,前移会不会错过入口结点也不得而知。

因此重新拆分问题

  1. 判断是否有环
  2. 查找环入口

基于此需要两次遍历

  1. 这次让慢指针和快指针一样“以身入局”,演变成追及问题,速度定为1和2(当然,实际上只要一快一慢就可以追上),如果是环那一定能相遇。
  2. 毫无疑问,相遇点是终点,但是同时也是中点,那么从头结点出发到中点等于从中点出发到终点:x + y = n圈 +y,(体现出慢指针是快指针速度一半的优点了,可以直接得出等式,不需要按比例转化几分之几啥的),根据此等式得出:从头结点和相遇结点以同样的速度出发,最后就会在环形入口节点相遇(因为最后一节路段y是相同的),

跟着Carl学算法--链表_第4张图片
图片出自代码随想录

class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while (fast != NULL && fast->next != NULL) {
            fast = fast->next->next;
            slow = slow->next;
            if (fast == slow) {
                fast = head;
                 while (fast != slow) {
                    fast = fast->next;
                    slow = slow->next;
                }
                return fast;
            }
        }
        return NULL;
    }
};

你可能感兴趣的:(算法,链表,数据结构,c++,leetcode)