算法训练营 Day4-链表 2 | 24.两两交换链表中的节点 | 19.删除链表的倒数第 N 个节点 | 面试题 02.07. 链表相交 | 142.环形链表

60 阅读6分钟

算法训练营 Day4-链表 2 | 24.两两交换链表中的节点 | 19.删除链表的倒数第 N 个节点 | 面试题 02.07. 链表相交 | 142.环形链表 II

查阅文档地址:programmercarl.com/

本期题目地址:

  1. 24.两两交换链表中的节点 - 中等 - 力扣链接
  2. 19.删除链表的倒数第 N 个节点 - 中等 - 力扣链接
  3. 面试题 02.07. 链表相交 - 简单 - 力扣
  4. 142.环形链表 II - 中等 - 力扣

本期题目答案地址:

  1. 链表相交 - 官方答案链接

目录:

  1. 基本概念(做题前要理解的概念)
  2. 我的解法
  3. 疑问点(过程中产生了问题并且查找资料解决)

语言

采用C++,一些分析也是用于 C++,请注意。

基本概念

虚拟头节点(dummy head)是一种非常有用的技巧,它可以帮助我们简化链表操作的逻辑,避免特殊情况的处理。

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

24.两两交换链表中的节点 - 中等 - 力扣链接

我的代码

// 方法一:递归 + 栈(力扣模式)
// 时间复杂度:O(n)
// 空间复杂度:O(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* swapPairs(ListNode* head) {
        if(!head || head->next == nullptr) return head;

        ListNode* two = head->next;
        ListNode* three = swapPairs(two->next);
        two->next = head;
        head->next = three;

        return two;
    }
};
// 方法二:迭代(力扣模式)
// 时间复杂度:O(n)
// 空间复杂度:O(1)
/**
 * 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* dummyNode = new ListNode();
        dummyNode->next = head; // 问题一:别忘记连上原来的链表

        ListNode* temp = dummyNode;
        // 问题二:循环条件很重要,关键点 temp = one(经过移动后)
        while(temp->next != nullptr && temp->next->next != nullptr) {
            ListNode* one = temp->next;
            ListNode* two = temp->next->next;

            temp->next = two;
            one->next = two->next;
            two->next = one;
            temp = one;
        }
        ListNode* res = dummyNode->next;
        delete dummyNode;
        return res;  
    }
};

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

19.删除链表的倒数第 N 个节点 - 中等 - 力扣链接

我的代码

// 进阶:双指针法(虚拟头节点)力扣模式
// 时间复杂度:O(m),这里的 m 是链表的长度。
// 空间复杂度:O(1),只使用了常数级的额外空间。
/**
 * 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) { // 已经确保 n 必然有效
        ListNode* dummyNode = new ListNode(0);
        dummyNode->next = head;

        ListNode* lNode = dummyNode;
        ListNode* rNode = dummyNode ;

        for(int i = 0; rNode->next != nullptr; i ++) {
            rNode = rNode->next;
            if(i >= n) {
                lNode = lNode->next;
            }
        }
        lNode->next = lNode->next->next;

        return dummyNode->next;
    }
};

我的问题

  1. 对于单链表只能从头节点进入遍历,先得到链表的 size;再遍历一遍进行删除。如果对于双链表,就可以从尾结点进行遍历倒着进行删除。
  2. 进阶:你能尝试使用一趟扫描实现吗?!双指针 + 虚拟头节点 !倒数为 num,前后指针一直保持这个距离 l - r + 1 = num,此时 l 维持在要删除节点的前一个节点。

面试题 02.07. 链表相交

面试题 02.07. 链表相交 - 简单 - 力扣 链表相交 - 官方答案链接

我的代码

// 方法一:哈希集合
// 时间复杂度 O(m+n)
// 空间复杂度 O(m)
/**
 * 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) {
        unordered_set<ListNode *> unset;

        ListNode *cur = headA; // 问题一:将其赋值为 nullptr,之后却尝试访问其成员。
        while(cur != nullptr) {
            unset.insert(cur);
            cur = cur->next;
        }

        cur = headB;
        while(cur != nullptr) {
            if(unset.count(cur)) {
                return cur;
            }
            cur = cur->next;
        }
        return nullptr;
    }
};
// 方法二、双指针(链表)
// 时间复杂度 O(m+n)
// 空间复杂度 O(1)
/**
 * 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) {
        ListNode *nodeA = headA;
        ListNode *nodeB = headB;

        if(nodeA == nullptr || nodeB == nullptr) {
            return nullptr;
        }

        while(nodeA != nodeB) {
            nodeA = nodeA == nullptr ? headB : nodeA->next;
            nodeB = nodeB == nullptr ? headA : nodeB->next;
        }
        return nodeA;
    }
};

我的疑惑

  1. val 值相等不一定是要找的目标节点?问题的关键在于明确链表相交的定义以及链表数据结构的特点,基于一个隐含的前提:如果两个链表相交,从相交节点开始,后续节点必然是完全重合的。
  2. 可以要遍历整条,可以使用 cnt 次数标记,出现相同就++,出现不同就清零。(显式检查后续是否都相等,但是因为检查的是数值,可能出现错误)
  3. 还有别的技巧嘛?双指针!看官方给的答案,很详细。
  4. 哈希集合如何解题?unordered_set<ListNode *>visited;保存节点,遇上第一个节点相等的就结束。

142.环形链表 II

142.环形链表 II - 中等 - 力扣链接

我的代码

// 力扣模式
// 哈希表
// 时间复杂度:O(n)
// 空间复杂度:O(n)
/**
 * 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) {
        // 进阶:使用 O(1)空间解决

        unordered_map<ListNode *,int> unmp;
        ListNode* cur = head;
        while(cur != nullptr) {
            if(unmp[cur]) {
                return cur;
            }
            unmp[cur] ++;
            cur = cur->next;
        }
        return nullptr;
    }
};

力扣官方的方法二 - 链接

// 力扣 模式
// 快慢指针法(易错 数学)
// 时间复杂度:O(n)
// 空间复杂度:O(1)
//  slow 和 fast 同时前进,fast 的速度是 slow 的两倍。当 slow 抵达环的入口处时,fast 一定在环上
// 此时因为 fast 比 slow 快 1 个单位的速度,且 y 为整数,所以再经过 y 个单位的时间即可追上 slow。
/**
 * 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, *fast = head;
        while(fast != nullptr) {
            slow = slow->next;
            if(fast->next == nullptr) {
                return nullptr;
            }
            fast = fast->next->next;

            if(fast == slow) {
                ListNode* ptr = head;
                while(ptr != slow) {
                    ptr = ptr->next;
                    slow = slow->next;
                }
                return ptr;
            }
        }
        return nullptr;
    }
};
// acm 模式

//假设输入是链表节点的值,用空格分隔,以 -1 表示链表结束,若链表存在环,则环的入口节点的前一个节点后会紧跟环的入口节点的值。
#include <iostream>
#include <vector>

// 定义链表节点  
struct ListNode {
	int val;
	ListNode * next;
	ListNode(int x):val(x),next(nullptr){} 
};

// 查找链表中环的入口节点  
ListNode *detectCycle(ListNode *head) {
	ListNode *slow = head, *fast = head;
	while(fast != nullptr) {
		slow = slow->next;
		if(fast->next == nullptr) {
			return nullptr;
		}
		fast = fast->next->next;
		
		if(fast == slow) {
			ListNode *ptr = head;
			if(slow != ptr) {
				slow = slow->next;
				ptr = ptr->next;
			}
			return ptr; 
		}
	}
	nullptr;
}

// 根据输入构建链表
ListNode *buildList(const std::vector<int>& values) {
	if(values.empty())return nullptr;
	ListNode *dummy = new ListNode(0);
	ListNode *current = dummy;
	std::vector<ListNode*> nodes;
	for(int val : values) {
		if(val == -1)break;
		ListNode *newNode = new ListNode(val);
		current->next = newNode;
		current = newNode;
		nodes.push_back(newNode); 
		nodes.push_back(newNode);
	}
	// 处理环
	if(nodes.size() > 1) {
		for(size_t i = 0;i < nodes.size() - 1; ++ i) {
			    if(nodes[i+1]->val == nodes[i]->val) {
			    	nodes[i]->next = nodes[i + 1];
			    	break;
				}
		}
		return dummy->next;
	} 
} 
// 主函数 
int main() {
	std::vector<int> values;
	int val;
	while(std::cin >> val) {
		values.push_back(val);
	}// 输入 -1 算是到末尾 
	ListNode *head = buildList(values); // 构建链表后返回头节点 
	ListNode *cycleEntry = detectCycle(head); // 查找链表中的入口节点 
	if(cycleEntry) {
		std::cout << cycleEntry->val << std::endl;
	} else {
		std::cout << "No cycle" << std::endl;
	}
	return 0;
}

总结

这篇文章围绕算法训练营 Day4 的链表相关题目展开,介绍了 4 道力扣题目,包括两两交换链表中的节点、删除链表的倒数第 N 个节点、链表相交、环形链表 II,以 C++ 语言为例给出多种解题代码,涵盖递归、迭代、双指针、哈希表等方法,并分析了各方法的时间和空间复杂度,还提出解题过程中的疑问及思考,如虚拟头节点的运用、不同链表操作技巧、如何优化解法等内容。