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

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;
        stacks;
        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 sA;
        stack 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) {
        sets;
        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;
    }
};



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