代码随想录算法训练营第四天| 24. 两两交换链表中的节点、 19.删除链表的倒数第N个节点 、面试题 02.07. 链表相交、 142.环形链表II

本文详细讲解了四种链表操作:两两交换链表节点、删除链表倒数第N个节点、判断链表相交以及寻找环形链表的入口节点。对于每种操作,文章提供了思路分析、具体操作步骤、可能遇到的问题以及代码优化建议。特别地,相交链表的处理方法包括通过长度差正向同步以及优化后的同步遍历策略。对于环形链表,文章介绍了利用快慢指针寻找环的起始点的方法。
摘要由CSDN通过智能技术生成

24. 两两交换链表中的节点

思路

考虑将所有节点从头两两分组,每个组之间进行节点交换,在遍历每一组过程中过程中需要存储第一个节点用于组间连接操作。两两分组循环可以用一个对2取余的iter来标记。对单个节点的组并不需要操作,所以可以看作正常组的第一阶段。更重要的是交换顺序后组间的连接,此时需要一个存储上一组的尾节点,这里我们用last_iter_tail表示。此外,第一组节点的交换还要考虑头节点的更新。

具体操作:

初始:
1->2->3->4->null
第一组组内交换:
2->1->3->4->null
第二组组内交换:
2->1->3->null
4->3->null
一二组连接:
2->1->4->3->null

问题

组间的交换很容易考虑到,组间的连接很容易出错

class Solution {
public:
    //奇数个节点
    //空链表
    //用一个循环的0,1来对节点进行标记 %2
    //遍历过程中需要对数据进行存储

    ListNode* swapPairs(ListNode* head) {
        int count = 0;
        int iter = 0;
        ListNode* last_iter_tail;
        ListNode* swap;
        ListNode* ptr = head;
        //遍历有效节点
        // 考虑循环的结束
        while (ptr != nullptr){
            //不做操作,只存住循环的前一个node
            //对奇数的组不需要进行交换
            if (iter == 0) {
                swap = ptr;
                iter = (iter + 1) % 2;
                ptr = ptr->next;
                continue;
            }
            //有变化,考虑head转变
            if (iter == 1){
                swap->next = ptr->next;
                ptr->next = swap;
                if (count++ == 0) {
                    head = ptr;
                }
                else {
                    last_iter_tail->next = ptr;
                }
                last_iter_tail = swap;
                ptr = ptr->next->next;
                iter = (iter + 1) % 2;
            }
            
        }
        return head;
    }
};

代码优化

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
    	//虚拟节点,方便将头节点交换的情况整合到一般情况
        ListNode dummy(0, head);
        ListNode* prev = &dummy;
        ListNode* curr = head;
        int count = 0;
        //同时对两个节点进行处理
        while (curr != nullptr && curr->next != nullptr) {
            ListNode* first = curr;
            ListNode* second = curr->next;
            //循环变量更新
            curr = second->next;
            second->next = first;
            first->next = curr;
            //prev指向上一组的尾节点
            prev->next = second;
            prev = first;
            count += 2;
        }
        //只有0~1个节点,不需要处理,直接返回head
        if (count < 2) {
            return head;
        }
        return dummy.next;
    }
};
In this updated implementation, we use a dummy node to simplify the handling of the head of the linked list, use a temporary variable to simplify the swapping of the pairs of nodes, and use a counter variable instead of a flag variable to count the nodes.




 

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

思路

删除倒数第n个节点,我们只需要获取倒数第n-1个节点,
n-1->next = n->next即可完成删除,考虑到这是一个倒序访问,可以考虑用stack存储节点。
特殊情况要考虑倒数n-1为空,即删除的是头节点时,我们需要返回倒数第n节点的后驱节点n->next(当链表只有一个节点,后驱节点为nullptr,也符合)

问题

使用n-1->next时一定要考虑n-1为空时的特殊情况,要建立这种敏感性。
注意这里的相交是指节点的地址相同而不仅仅是节点的数值相同

class Solution {
public:
	//考虑头节点的删除
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* ptr = head;
        stack<ListNode*>s;
        ListNode* temp = nullptr;
        while (ptr != nullptr){
            s.push(ptr);
            ptr = ptr->next;
        }
        for (int i = 0; i < n; ++i){
            if (i == n - 1){
                temp = s.top();
                s.pop();
                if (s.empty()) return temp->next;
                else s.top()->next = temp->next;
            }
            else s.pop();
        }
        return head;
    }
};

面试题 02.07. 链表相交

思路

本质上是相交链表的对齐,让链表A和链表B同步进行遍历,直到出现相同元素(逆向则是出现不同元素)时找到相交点。相交的链表从尾部开始是同步的,所以可以考虑用两个stack来同步循环,此时空间复杂度为O(N)。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        stack<ListNode*> sA;
        stack<ListNode*> sB;
        ListNode* ptr = headA;
        while (ptr != nullptr) {
            sA.push(ptr);
            ptr = ptr->next;
        }
        ptr = headB;
        while (ptr != nullptr) {
            sB.push(ptr);
            ptr = ptr->next;
        }
        ListNode* res = nullptr;
        while (!sA.empty() && !sB.empty()) {
            if (sA.top() == sB.top()){
                res = sA.top();
                sA.pop();
                sB.pop();
                continue;
            }
            else break;
        }
        return res;
    }
};

通过长度差正向同步,将长度长的链表与短的链表进行遍历

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* res = nullptr;
        int lenA = 0;
        int lenB = 0;
        ListNode* ptr = headA;
        while (ptr != nullptr){
            ++lenA;
            ptr = ptr->next;
        }
        ptr = headB;
        while (ptr != nullptr){
            ++lenB;
            ptr = ptr->next;
        }
        int minLen = lenA < lenB ? lenA : lenB;
        int lenGap = lenA < lenB ? lenB - lenA : lenA - lenB;
        ListNode* orig = lenA < lenB ? headA : headB;
        ListNode* sync = lenA < lenB ? headB : headA;
        while (lenGap-- > 0){
            sync = sync->next;
        }
        for (int i = 0; i < minLen; ++ i){
            if (orig == sync) {
                res = orig;
                break;
            }
            orig = orig->next;
            sync = sync->next;
        }
        return res;
    }
};

更优化做法

一个指针走A+B,另一指针走B+A,假设链表A和B相交且L(A) != L(B),那么他们一定会在第二次相交点相遇:
aaaaccnbbbccn
bbbccnaaaaccn
(其中aaaa表示A中不相交的部分,bbb表示B中不相交的部分,cc表示相交的部分,n表示nullptr)
加入不相交,那么此时:
aaaabbn
bbaaaan
直到结束为nullptr时想等。

假如L(A) = L(B),A与B相交,则有:
aaaccn
bbbccn
A与B不相交,则有:
aaan
bbbn
均能在第一轮遍历得出结果

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* ptr1 = headA;
        ListNode* ptr2 = headB;
        while (ptr1 != ptr2) {
            ptr1 = (ptr1 == nullptr) ? headB : ptr1->next;
            ptr2 = (ptr2 == nullptr) ? headA : ptr2->next;
        }
        return ptr1;
    }
};

142. 环形链表 II

Set 查询

思路

记录是否访问过该元素可以维护一个set进行查询,但是空间复杂度为O(N)

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        set<ListNode*>s;
        ListNode* ptr = head;
        while(ptr != nullptr){
            if (s.find(ptr) == s.end()) {
                s.insert(ptr);
            }
            else break;
            ptr = ptr->next;
        }
        return ptr;
    }
};

构造循环法

思路

假如没有循环,那么遍历一定会碰到相同的node值,由于要求空间为O(1), 只能利用当前步和一个指标来判断是否循环。假设循环长度为b,当前步就和走了kb步后的结果比较(缺乏考虑,但是没考虑到绕圈的情况,但是结果不变),从而得出循环周期的倍数kb。然后从头开始遍历,第一个走过nb步能返回的点就是循环起点。

问题

效率比较低下,尝试步数利用了1+2+…+kb, 时间复杂度为O(N^2), 实际利用快慢双指针的话能讲复杂度降为O(N)。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        int counter = 1;
        int i;
        ListNode* ptr = head;
        ListNode* cmp = nullptr;
        while (true) {
            if (ptr == nullptr) return nullptr;
            i = 0;
            cmp = ptr;
            while(i < counter){
                if (cmp == nullptr) return nullptr;
                cmp = cmp->next;
                ++i;
            }
            if (ptr == cmp) break;
            counter++;
            ptr = ptr->next;
        }
        ptr = head;
        while (true){
            i = 0;
            cmp = ptr;
            while (i < counter){
                cmp = cmp->next;
                ++i;
            }
            if (ptr == cmp) break;
            ptr = ptr->next;
        }
        return ptr;   
    }
};

快慢指针优化解法

思路

考虑快指针每次2步,满指针每次走1步,那么当他们相遇时,有:
2f = s + kb, f = 2s, 从而得出 s = kb。假设非循环段长度为a。假如此时有另一个慢指针s2从起点出发,走过a步后,s1走过a+kb步,此时s1与s2相遇,相遇点为循环起点。时间复杂度为O(N)。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        //一旦遇到nullptr,则不存在环
        while (fast != nullptr && fast->next != nullptr){
            fast = fast->next->next;
            slow = slow->next;
            //快慢指针相遇
            if (slow == fast){ 
                ListNode* slow_start = head;
                while (slow != slow_start){
                    slow = slow->next;
                    slow_start = slow_start->next;
                }
                return slow;
            }
        }
        return nullptr;
    }
};



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值