3,165
社区成员




迭代
1→2→∅
,我们想要把它改成 ∅←1←2
。具体思路:
cur
)的 next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点,这里小编用prev
。在更改引用之前,还需要存储后一个节点,,这里小编用next
。最后返回新的头引用,不难想到就是迭代到最后的prev
指针。代码实现:
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr) {
struct ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
O(n)
,其中 n 是链表的长度。需要遍历链表一次。O(1)
。快慢指针
slow
与 fast
一起遍历链表。slow
一次走一步,fast
一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。代码:
struct ListNode* middleNode(struct ListNode* head) {
struct ListNode* slow = head, * fast = head;
while(fast){
if(fast->next == NULL){
break;
}
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
注意:
O(n)
,其中 n 是链表的长度。需要遍历链表一次(快慢指针遍历次数之和)。O(1)
。先后指针
双指针,一个指针p2
先走k步,然后两个(p1,p2
)一起走。又题目说k是有效的,所以就没有判断先走的指针p2
是否越界
代码实现:
int kthToLast(struct ListNode* head, int k) {
struct ListNode* slow = head, * fast = head;
while(k--){
fast = fast->next;
}
while(fast){
fast = fast->next;
slow = slow->next;
}
return slow->val;
}
时间复杂度: O(n)
空间复杂度: O(1)
双指针 + 哨兵位
dummy
,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 cur
指针,我们需要做的是调整它的next
指针。然后,我们重复以下过程,直到 list1
或者 list2
指向了 NULL
:如果 list1
当前节点的值小于等于 list2
,我们就把 list1
当前的节点接在 cur
节点的后面同时将 list1
指针往后移一位。否则,我们对 list2
做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 cur
向后移一位。list1
和 list2
至多有一个是非空的。由于输入的两个链表都是有序
的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。代码实现:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
struct ListNode dummy = {};
struct ListNode* cur = &dummy; //建立哨兵位
while(list1 && list2){
if(list1->val > list2->val){
cur->next = list2;
list2 = list2->next;
}
else{
cur->next = list1;
list1 = list1->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2; //链接剩余部分
return dummy.next;
}
复杂度分析
时间复杂度:O(n+m)
,其中 n 和 m 分别为两个链表的长度。
空间复杂度:O(1)
。
双指针 + 双哨兵位
dummy1
和 dummy2
即可,dummy
链表按顺序存储所有小于x
的节点,dummy2
链表按顺序存储所有大于等于x
的节点。遍历完原链表后,我们只要将 dummy1
链表尾节点指向 dummy2
链表的头节点即能完成对链表的分割。代码实现:
struct ListNode* partition(struct ListNode* head, int x) {
struct ListNode dummy1 = {}, dummy2 = {};
struct ListNode* cur1 = &dummy1, * cur2 = &dummy2, * fail = head;
while(fail){
//把大于等于x的节点放在一个链表1中
if(fail->val >= x){
cur1 = cur1->next = fail;
}
//把小于x的节点放在另一个链表2中
else{
cur2 = cur2->next = fail;
}
fail = fail->next;
}
//合并链表
cur2->next = dummy1.next;
cur1->next = NULL;
return dummy2.next;
}
时间复杂度: O(n)
。
空间复杂度: O(1)
。
我们可以将链表的后半部分反转(修改链表结构),然后将前半部分和后半部分进行比较。比较完成后我们应该将链表恢复原样。
整个流程分为4个步骤:
代码实现:
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* prev = NULL;
struct ListNode* curr = head;
while (curr != NULL) {
struct ListNode* nextTemp = curr->next;
curr->next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
struct ListNode* endOfFirstHalf(struct ListNode* head) {
struct ListNode* fast = head;
struct ListNode* slow = head;
while (fast->next != NULL && fast->next->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
bool isPalindrome(struct ListNode* head) {
if (head == NULL) {
return true;
}
// 找到前半部分链表的尾节点并反转后半部分链表
struct ListNode* firstHalfEnd = endOfFirstHalf(head);
struct ListNode* secondHalfStart = reverseList(firstHalfEnd->next);
// 判断是否回文
struct ListNode* p1 = head;
struct ListNode* p2 = secondHalfStart;
bool result = true;
while (result && p2 != NULL) {
if (p1->val != p2->val) {
result = false;
}
p1 = p1->next;
p2 = p2->next;
}
// 还原链表并返回结果
firstHalfEnd->next = reverseList(secondHalfStart);
return result;
}
时间复杂度:O(n)
。
空间复杂度:O(1)
。我们只会修改原本链表中节点的指向,而在堆栈上的堆栈帧不超过 O(1)。
简单思路:如果相交,则必有交点及之后两链表长度一样,各自加上交点之前不同或相同的链表长度,就是各自的链表总长度。所以我们用两个指针分别指向长链表和短链表,先让指向长链表的指针走差距步,再同时走就可以走到相交点(如果存在)。
代码实现:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
//先得到两条链表的长度
int lA = 0, lB = 0;
struct ListNode* failA = headA, * failB = headB;
while(failA){
failA = failA->next;
++lA;
}
while(failB){
failB = failB->next;
++lB;
}
if(failB != failA) return NULL;
int gap = lA > lB ? lA-lB : lB-lA;
//一个小小的技巧找到长的链表并走差距步
struct ListNode* longlist = headA, * shortlist = headB;
if(lA < lB){
longlist = headB;
shortlist = headA;
}
while(gap--){
longlist = longlist->next;
}
//依次比较
while(longlist && shortlist){
if(longlist == shortlist){ //该处不可用值相等作为相等条件
return longlist;
}
longlist = longlist->next;
shortlist = shortlist->next;
}
return NULL;
}
巧妙思路:只有当链表 headA
和headB
都不为空时,两个链表才可能相交。因此首先判断链表headA
和 headB
是否为空,如果其中至少有一个链表为空,则两个链表一定不相交,返回 NULL
。 当链表headA
和 headB
都不为空时,创建两个指针p1
和 p2
,初始时分别指向两个链表的头节点 headA
和 headB
,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:
p1
和 p2
。p1
不为空,则将指针 p1
移到下一个节点;如果指针 p2
不为空,则将指针 p2
移到下一个节点。p1
为空,则将指针 p1
移到链表 headB
的头节点;如果指针 p2
为空,则将指针 p2
移到链表 headA
的头节点。p1
和 p2
指向同一个节点(NULL
也包含在内)时,返回它们指向的节点(NULL
)。证明方法:
情况一:两个链表相交
链表headA
和 headB
的长度分别是m
和 n
。假设链表 headA
的不相交部分有 a
个节点,链表headB
的不相交部分有 b
个节点,两个链表相交的部分有c
个节点,则有 a+c=m
,b+c=n
。 如果 a=b
,则两个指针会同时到达两个链表相交的节点,此时返回相交的节点; 如果 a!=b
,则指针p1
会遍历完链表 headA
,指针 p2
会遍历完链表 headB
,两个指针不会同时到达链表的尾节点,然后指针 p1
移到链表headB
的头节点,指针 p2
移到链表 headA
的头节点,然后两个指针继续移动,在指针 p1
移动了 a+c+b
次、指针 p2
移动了 b+c+a
次之后,两个指针会同时到达两个链表相交的节点,该节点也是两个指针第一次同时指向的节点,此时返回相交的节点。
情况二:两个链表不相交
链表 headA
和 headB
的长度分别是 m
和 n
。考虑当 m=n
和m!=n
时,两个指针分别会如何移动: 如果 m=n
,则两个指针会同时到达两个链表的尾节点,然后同时变成空值 NULL
,此时返回 NULL
; 如果 m!=n
,则由于两个链表没有公共节点,两个指针也不会同时到达两个链表的尾节点,因此两个指针都会遍历完两个链表,在指针 p1
移动了 m+n
次、指针p2
移动了 n+m
次之后,两个指针会同时变成空值 NULL
,此时返回NULL
。
代码实现:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if (headA == NULL || headB == NULL) {
return NULL;
}
struct ListNode *p1 = headA, *p2 = headB;
while (p1 != p2) {
p1 = p1 == NULL ? headB : p1->next;
p2 = p2 == NULL ? headA : p2->next;
}
return p1;
}
说明:此处小编不完全采用大O的渐进表示法,而用相对精准的表示法,以此看出解法二的优势之处。并且以不相交为例。
2m + 2n
;空间复杂度:O(1)
;2m + 2n
或2m(也是2n)
;空间复杂度:O(1)
;实际上,小编认为两种解法都各有优势和劣势:
代码实现:
struct Node* copyRandomList(struct Node* head) {
if (head == NULL)
return NULL;
//
struct Node* cur = head;
while (cur) {
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
struct Node* next = cur->next;
cur->next = copy;
cur = copy->next = next;
copy->random = NULL;
}
//yyds
cur = head;
while (cur) {
if (cur->random != NULL)
cur->next->random = cur->random->next;
cur = cur->next->next;
}
//
cur = head;
struct Node dummy = {};
struct Node* phead = &dummy;
while(cur){
struct Node* copy = cur->next;
struct Node* next = copy->next;
phead = phead->next = copy;
cur = cur->next = next;
}
return dummy.next;
}
时间复杂度:O(n)
。
空间复杂度:O(1)
。注意返回值不计入空间复杂度。
快慢指针
但要注意:最好使得快指针fast
一次走两步,慢指针slow
一次走一步使得两指针的速度之差为1
。这样就不可能会出现有环不相交的问题。
代码实现:
bool hasCycle(struct ListNode *head) {
//快慢指针看是否会相遇,但最好快指针一次走两步,慢指针一次走一步,就不会出现有环却不相遇的情况
struct ListNode* fast = head, * slow = head;
if(head == NULL) return false;
while(fast && fast->next){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
return true;
}
}
return false;
}
O(N)
,其中 N 是链表中的节点数。O(1)
。我们只使用了两个指针的额外空间。思路一:先找到相交点再把相交的点next指针指向空,这样就转变成了头指针head
和·相遇点的next
指针找交点问题。
代码实现:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
if (headA == NULL || headB == NULL) {
return NULL;
}
struct ListNode *p1 = headA, *p2 = headB;
while (p1 != p2) {
p1 = p1 == NULL ? headB : p1->next;
p2 = p2 == NULL ? headA : p2->next;
}
return p1;
}
struct ListNode *detectCycle(struct ListNode *head) {
if (head == NULL) return NULL;
struct ListNode* fast = head, * slow = head;
while (fast && fast->next) {
//先走,防止因为头指针相等
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
struct ListNode* ptr = fast->next;
fast->next = NULL;
return getIntersectionNode(ptr, head);
}
}
return NULL;
}
思路二:借助定理——两个快慢指针他们相交于环内一位置,而一指针从该位置开始走,同时另一从链表的头结点开始走,它们最终会第一次相交于环开始的那个节点。(怎么得到这个定理的一定要掌握,因为HR问到会问这个)
定理推导:
fast
与 slow
。它们起始都位于链表的头部。随后,slow
指针每次向后移动一个位置,而 fast
指针向后移动两个位置。如果链表中存在环,则 fast
指针最终将再次与 slow
指针在环中相遇。a
。slow
指针进入环后,又走了 b
的距离与 fast
相遇。此时,fast
指针已经走完了环的 n
圈,因此它走过的总距离为 a + n(b + c) + b = a + (n + 1)b + nc
。 2
倍。因此,我们有a + (n + 1)b + nc = 2(a + b)⟹a = c + (n−1)(b + c)
a = c + (n−1)(b + c)
的等量关系,我们会发现:从相遇点到入环点的距离加上 n−1 圈的环长,恰好等于从链表头部到入环点的距离。slow
与fast
相遇时,我们再额外使用一个指针 ptr
。起始,它指向链表头部;随后,它和 slow
每次向后移动一个位置。最终,它们会在入环点相遇。代码实现:
struct ListNode *detectCycle(struct ListNode *head) {
//这题要做出就一定要推出一个定理:两个快慢指针他们相交于环内一位置,而一指针从该位置开始走,同时另一从链表的头结点开始走,它们最终会第一次相交于环开始的那个节点。(怎么得到这个定理的一定要掌握,因为HR问到会问这个)
struct ListNode* fast = head, * slow = head;
if (head == NULL) return NULL;
while (fast && fast->next) {
//先走,防止因为头指针相等
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
struct ListNode* ptr = head;
while(ptr != slow){
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return NULL;
}
文章来源: https://blog.csdn.net/z15879084549/article/details/145353685
版权声明: 本文为博主原创文章,遵循CC 4.0 BY-SA 知识共享协议,转载请附上原文出处链接和本声明。