LeetCode之链表总结

时间:2023-03-09 00:01:40
LeetCode之链表总结

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。作为一种常用的数据结构,链表内置在很多高级编程语言里面。既比数组复杂又比树简单,所以链表经常被面试官用来考察面试者的编程基本功。因此,链表是程序员必须熟练掌握的数据结构之一。近日在LeetCode上刷了很多道关于链表的题目,有几道非常经典巧妙,有助于理解链表的核心思想,所以特写此文进行总结。个人能力有限,如有纰漏,欢迎留言!

反转链表

/*
* 题目:反转一个单链表。
*
* 示例 1:
* 输入: 1->2->3->4->5->NULL
* 输出: 5->4->3->2->1->NULL
*
* 进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *reverse = NULL;
ListNode *next;
while (head != NULL) {
next = head->next;
head->next = reverse;
reverse = head;
head = next;
}
return reverse;
}
};

反转链表的主要思路是:以迭代的方法,从头结点head开始,先保存当前节点的下一个节点next,然后将head节点的next指针指向reverse,reverse指向head,再将head指向next节点,next保存下一个节点。。。直至结束,reverse指向的链表就是所求。讲得貌似挺复杂,其实看代码实现就能一目了然。

两两交换链表中的节点

/*
* 题目:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
* 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
*
* 示例:
* 给定 1->2->3->4, 你应该返回 2->1->4->3.
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* swapPairs(ListNode* head) {
shared_ptr<ListNode> ptr = make_shared<ListNode>();
ptr.get()->next = head;
ListNode* list = ptr.get();
while (list->next && list->next->next) {
ListNode* a = list->next;
ListNode* b = list->next->next;
a->next = b->next;
b->next = a;
list->next = b;
list = a;
}
return ptr.get()->next;
}
};

K个一组反转链表

/*
* 题目:给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
* k 是一个正整数,它的值小于或等于链表的长度。
* 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
*
* 示例:
* 给定一个链表: 1->2->3->4->5
* 当 k = 2 时,应当返回: 2->1->4->3->5
* 当 k = 3 时,应当返回: 3->2->1->4->5
*
* 说明:
* 你的算法只能使用常数的额外空间。
* 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
shared_ptr<ListNode> ptr = make_shared<ListNode>();
ptr.get()->next = head;
ListNode* prev = ptr.get();
ListNode* tail = ptr.get();
while () {
int count = k;
while (count-- && tail) {
tail = tail->next;
}
if (!tail) break;
head = prev->next;
while (prev->next != tail) {
ListNode* cur = prev->next;
prev->next = cur->next;
cur->next = tail->next;
tail->next = cur;
}
prev = head;
tail = head;
}
return ptr.get()->next;
}
};

删除当前节点

/*
* 题目:删除链表当前节点。请编写一个函数,使其可以删除某个链表中给定节点,你将只被给定要求被删除的节点。
*
* 示例 1:
* 输入: head = [4,5,1,9], node = 5
* 输出: [4,1,9]
* 解释:给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
// 这题目就是删除当前节点的意思
void deleteNode(ListNode* node) {
if (node->next == NULL) {
node = NULL;
} else {
node->val = node->next->val;
node->next = node->next->next;
} }
};

这道题难点在于,在单向链表中,要求你删除当前节点,只给出当前节点,无法求出前续节点,无法用常规的方法删除链表节点。这里将后继节点的值赋给当前节点,然后删除后继节点,同样达到删除当前节点的效果。这里需要发散思维,灵活运用才能解决问题。

合并链表

/*
* 题目:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
*
* 示例 1:
* 输入: 1->2->4, 1->3->4
* 输出: 1->1->2->3->4->4
*
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
// 迭代方法
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
} // 递归方法
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == NULL) return l2;
if (l2 == NULL) return l1; if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};

合并两个有序链表主要有迭代和递归两种方法。迭代思路:新建一个带头结点的链表,将两链表最小的节点一个个廉洁到新链表上,返回头结点的下一个节点就是所求。递归就更简单些:将两链表最小的节点抽出,用该节点的下一个节点和另一条链表递归,不断缩小范围。

/*
* 题目:合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
*
* 示例 1:
* 输入: [
* 1->4->5,
* 1->3->4,
* 2->6
* ]
* 输出: 1->1->2->3->4->4->5->6
*
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
} // 合并 k 个排序链表,巧用上面两个链表合并的方法
ListNode* mergeKLists(vector<ListNode*>& lists) {
if (lists.empty()) return NULL; int step = lists.size();
while (step != ) {
for (int i = ; i < step/; ++i) {
lists[i] = mergeTwoLists(lists[i], lists[step--i]);
}
step = step - step/;
}
return lists[];
}
};

合并 k 个排序链表。需要调用合并两个链表的方法,这里的实现非常巧妙:首尾链表两两配对,不断两两合并,最后合并成一条链表即是所求。

链表是否有环

/*
* 题目:给定一个链表,判断链表中是否有环。
*
* 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
*
* 示例 1:
* 输入: head = [3,2,0,-4], pos = 1
* 输出: true
* 解释:链表中有一个环,其尾部连接到第二个节点。
*
* 进阶:你能用 O(1)(即,常量)内存解决此问题吗?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return true;
}
return false;
}
};

判断链表是否有环的方法非常巧妙,用快慢指针的方法同时从链表的头结点出发,满指针每次走一步,快指针每次走两步,如果链表有环两者会相交,否则退出循环。

链表环的入口

/*
* 题目:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
*
* 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
*
* 说明:不允许修改给定的链表。
*
* 示例 1:
* 输入: head = [3,2,0,-4], pos = 1
* 输出: tail connects to node index 1
* 解释:链表中有一个环,其尾部连接到第二个节点。
*
* 进阶:你是否可以不用额外空间解决此题?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode *connectNode(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
return slow;
}
return NULL;
}
ListNode *detectCycle(ListNode *head) {
ListNode *node = connectNode(head);
if (node != NULL) {
while () {
if (head == node)
return node;
head = head->next;
node = node->next;
}
}
return NULL;
}
};

求链表环的入口只需要在判断是否有环的基础上再稍加改进就可以求得,当快慢指针相交时,头结点和满指针同时每次走一步,相交处即为环的入口处,这里暂不证明。

链表相交节点

/*
* 题目:编写一个程序,找到两个单链表相交的起始节点。
*
* 示例 1:
* 输入: intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5],
* skipA = 2, skipB = 3
* 输出: Reference of the node with value = 8
* 输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,
* 链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;
* 在 B 中,相交节点前有 3 个节点。
*
* 注意:
* 如果两个链表没有交点,返回 null.
* 在返回结果后,两个链表仍须保持原有的结构。
* 可假定整个链表结构中没有循环。
* 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
//法1:求两链表的长度之差 n,长的链表先走 n 步,然后同时走判断是否有相同点
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lenA = ;
int lenB = ;
ListNode* nodeA = headA;
ListNode* nodeB = headB;
while (nodeA != NULL) {
lenA++;
nodeA = nodeA->next;
}
while (nodeB != NULL) {
lenB++;
nodeB = nodeB->next;
} nodeA = headA;
nodeB = headB;
int n = lenA - lenB;
if (n > ) {
while (n--) {
nodeA = nodeA->next;
}
} else {
while (n++) {
nodeB = nodeB->next;
}
} while (nodeA != NULL && nodeB != NULL) {
if (nodeA == nodeB) {
return nodeA;
}
nodeA = nodeA->next;
nodeB = nodeB->next;
}
return NULL;
} // 法2: 分别从 A 和 B 链表头结点开始走,A 走到 NULL 就接着从 B 头结点走,
// B 走到NULL 就接着从 A 头节点头走,即 A->NULL->B->NULL 和 B->NULL->A->NULL 两个方向
// 此过程节点相等就退出循环,只存在两种情况:1.找到相交节点,2.两方向走到头,均为NULL,
// 是最佳方法
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* nodeA = headA;
ListNode* nodeB = headB;
while (nodeA != nodeB) {
nodeA = nodeA ? nodeA->next : headB;
nodeB = nodeB ? nodeB->next : headA;
// 不能用下面这样:
// nodeA = nodeA->next ? nodeA->next : headB;
// nodeB = nodeB->next ? nodeB->next : headA;
// 上面两个链表会连在一起,貌似更合理,但如果输出的两条链表无交点则不满足,
// 所以都引入一个空节点可避免上述问题,当同时走完两链表时nodeA和nodeB都为NULL,退出循环
}
return nodeA;
} ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
if (headA == NULL || headB == NULL)
return NULL; // 让A首尾相连成环,变为求环入口节点的问题
ListNode* tailA = headA;
while (tailA->next != NULL) {
tailA = tailA->next;
}
tailA->next = headA; ListNode* slow = headB;
ListNode* fast = headB;
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
break;
} if (fast == NULL || fast->next == NULL) {
// 满足此条件即链表无环
tailA->next = NULL; // 恢复A链表的形状
return NULL;
} fast = headB;
while () {
if (slow == fast) {
tailA->next = NULL; // 恢复A链表的形状
return slow;
}
slow = slow->next;
fast = fast->next;
}
}
};

上面这道题在今年换工作的面试过程中又遇到这道题,当时第一时间没有思路,后来是在面试官的再三提示之下才想出了法1的思路,最后理所当然被拒。所以我对这道题印象深刻,这次我总共总结了三种都不错的解法,以后面试再也不担心遇到这道题了,哈哈。具体的思路这里不再注解,看代码和注释就很容易弄懂。

删除链表倒数第N个节点

/*
* 题目:给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
*
* 示例:
* 给定一个链表: 1->2->3->4->5, 和 n = 2.
* 当删除了倒数第二个节点后,链表变为 1->2->3->5.
*
* 说明:给定的 n 保证是有效的。
*
* 进阶:你能尝试使用一趟扫描实现吗?
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head == NULL || n <= ) return head; ListNode* preK = head;
ListNode* list = head;
int k = ;
while (list->next != NULL) {
if (k < n) {
k++;
} else {
preK = preK->next;
}
list = list->next;
}
if (k == n) { //此时prek指向的是要删除的目标节点的前续节点
ListNode* tmp = preK->next;
preK->next = preK->next->next;
delete tmp;
} else if (k == n-) { //当k为n-1,差一步,此时prek肯定指向head,该删除head节点
ListNode* tmp = head;
head = head->next;
delete tmp;
}
return head;
}
};

链表排序

/*
* 题目:在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
*
* 示例 1:
* 输入: 4->2->1->3
* 输出: 1->2->3->4
*
* 示例 2:
* 输入: -1->5->3->4->0
* 输出: -1->0->3->4->5
*
*/ struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
}; class Solution {
public:
// SGI STL方法
template<class T>
void swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
} // 合并链表
ListNode* merge(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
} // 切割头结点,即将头结点与原链表分类并返回
ListNode* spiceHead(ListNode*& head) {
ListNode* pos = head;
head = head->next;
pos->next = NULL;
return pos;
} ListNode* sortList(ListNode* head) {
if (head == NULL || head->next == NULL)
return head; ListNode* carry;
ListNode* counter[];
int refill = ; while (head) {
carry = spiceHead(head);
int i = ;
while (i < refill && counter[i]) {
counter[i] = merge(carry, counter[i]);
carry = NULL;
swap(carry, counter[i++]);
}
swap(carry, counter[i]);
if (i == refill) refill++;
} for (int i = ; i < refill; ++i) {
counter[i] = merge(counter[i], counter[i-]);
} return counter[refill-];
} // 归并的方法
ListNode* sortList(ListNode* head) {
if (head == NULL || head->next == NULL)
return head;
ListNode* slow = head;
ListNode* fast = head;
ListNode* brk = head;
while (fast != NULL && fast->next != NULL) {
fast = fast->next->next;
brk = slow;
slow = slow->next;
}
brk->next = NULL;
head = sortList(head);
slow = sortList(slow);
return merge(head, slow);
} ListNode* merge(ListNode* l1, ListNode* l2) {
shared_ptr<ListNode> ptr = make_shared<ListNode>(); // 用shared_ptr可自动析构头结点
ListNode* list = ptr.get();
while (l1 != NULL && l2 != NULL) {
if (l1->val < l2->val) {
list->next = l1;
l1 = l1->next;
} else {
list->next = l2;
l2 = l2->next;
}
list = list->next;
} if (l1 != NULL) list->next = l1;
if (l2 != NULL) list->next = l2; return ptr.get()->next;
}
};

链表的排序上面有两种方法实现,一种是我最近在研究的 SGI STL的实现方式,另一种是比较常规的归并实现,第一种方式我是将 SGI 的实现原理照搬在这里,具体的分析过程比较复杂,这里不再讲解,有兴趣的小伙伴可自己研究。