目录
剑指offer 中的链表题目
JZ6 从尾到头打印链表
JZ18 删除链表的结点
JZ24 反转链表
JZ25 合并两个排序的链表
JZ52 两个链表的第一个公共结点
JZ23 链表中环的入口结点
JZ22 链表中倒数第k 个结点
JZ35 复杂链表的复制
JZ76 删除链表中重复的结点
本次给大家带来所有的在剑指Offer 中的题目,请大家在学习之余也复习一下链表的相关知识。
思路:
我们都知道链表无法逆序访问,那肯定无法直接遍历链表得到从尾到头的逆序结果。但是我们都知道递归是到达底层后才会往上回溯,因此我们可以考虑递归遍历链表,因此三段式如下:
终止条件: 递归进入链表尾,即节点为空节点时结束递归。
返回值: 每次返回子问题之后的全部输出。
本级任务: 每级子任务递归地进入下一级,等下一级的子问题输出数组返回时,将自己的节点值添加在数组末尾。
具体做法:
step 1:从表头开始往后递归进入每一个节点。
step 2:遇到尾节点后开始返回,每次返回依次添加一个值进入输出数组。
step 3:直到递归返回表头。
解法1:
void _printListFromTailToHead(ListNode* head, vector& res){ if(head) { _printListFromTailToHead(head->next, res); res.push_back(head->val); } } vector printListFromTailToHead(ListNode* head) { vector res; _printListFromTailToHead(head, res); return res; }
解法2:
我先从前往后迭代,而后反转链表,也可以。
/** * struct ListNode { * int val; * struct ListNode *next; * ListNode(int x) : * val(x), next(NULL) { * } * }; */ class Solution { public: vectorprintListFromTailToHead(ListNode* head) { vector ret; while(head){ ret.push_back(head->val); head = head->next; } int n = ret.size(); int left = 0, right = n - 1; while(left < right){ int tmp = ret[left]; ret[left] = ret[right]; ret[right] = tmp; left++, right--; } return ret; } };
解法3: 使用栈 先正序把val 输入到栈中,然后取栈顶元素放入vector中。
/** * struct ListNode { *int val; *struct ListNode *next; *ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public: /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param head ListNode类 * @param val int整型 * @return ListNode类 */ ListNode* deleteNode(ListNode* head, int val) { if(!head) return nullptr; // write code here ListNode* newHead = new ListNode(-1); newHead->next = head; for(ListNode* node = newHead; node->next ; node = node->next){ if(node->next->val == val) node->next = node->next->next; } return newHead->next; } };
链表的反转是老生常谈的一个问题了,同时也是面试中常考的一道题。最简单的一种方式就是使用栈,因为栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。
/** * struct ListNode { *int val; *struct ListNode *next; *ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public: /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param head ListNode类 * @return ListNode类 */ ListNode* ReverseList(ListNode* head) { // write code here stacks; while(head){ s.push(head); head = head->next; } ListNode* newHead = new ListNode(-1); ListNode* node = newHead; while(!s.empty()){ ListNode* top = s.top(); node->next = top; top->next = nullptr; node = top; s.pop(); } return newHead->next; } };
双指针解法:
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } };*/ class Solution { public: ListNode* ReverseList(ListNode* pHead) { if(!pHead || !pHead->next){ return pHead; } ListNode* curr = pHead; ListNode* prev = nullptr; while(curr){ ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } };
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:升序排列的方法就是比较两个链表的结点中val 的大小,取小的一个尾插到新链表中。注意在这题中可以使用虚拟头结点来简化过程。
我的解法1:
class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode* pcurr1 = list1; ListNode* pcurr2 = list2; ListNode* head = new ListNode; ListNode* newCurr = head; while(pcurr1 && pcurr2){ if(pcurr1->val > pcurr2->val){ newCurr->next = pcurr2; pcurr2 = pcurr2->next; }else{ newCurr->next = pcurr1; pcurr1 = pcurr1->next; } newCurr = newCurr->next; } //来判断到底是谁终止了,直接修改newCurr 到未终止的结点的指向即可。 newCurr->next = (pcurr1 == nullptr) ? pcurr2 : pcurr1; return head->next; } };
我的解法2: 递归如果 list1 或者 list2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 list1 和 list2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { if(!list1) return list2; else if(!list2) return list1; else if(list1->val < list2->val){ list1->next = mergeTwoLists(list1->next, list2); return list1; }else{ list2->next = mergeTwoLists(list1, list2->next); return list2; } } };
思路:我们可以采用简单的双指针法,可以假设A 长度为m, B 长度为n,如果相交,A 和 B相交片段长度为 x, 不相交的片段长度分别为a 和b。
那么如果我们采用双指针法:
当链表 headA 和 headB 都不为空时,创建两个指针 pA和 pB,初始时分别指向两个链表的头节点 headA 和 headB,然后将两个指针依次遍历两个链表的每个节点。具体做法如下:
每步操作需要同时更新指针 pA 和 pB。
如果指针 pA不为空,则将指针 pA 移到下一个节点;如果指针 pB不为空,则将指针 pB 移到下一个节点。
如果指针 pA为空,则将指针 pA 移到链表 headB的头节点;如果指针 pB 为空,则将指针 pB 移到链表 headA 的头节点。
当指针 pA 和 pB 指向同一个节点或者都为空时,返回它们指向的节点或者 null。
那么如果二者相交,则 m = a + x, n = b + x。pA 和 pB 一定在走过 a + b + x 长度时相等。如果二者不相交,则一定不会出现相等(可以分类讨论)。
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } };*/ class Solution { public: ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) { if(!pHead1 || !pHead2) return nullptr; ListNode* pA = pHead1; ListNode* pB = pHead2; while(pA != pB){ if(pA == nullptr) pA = pHead2; else pA = pA->next; if(pB == nullptr) pB = pHead1; else pB = pB->next; } if(pA == nullptr) return nullptr; else return pA; } };
环的题目中,我们知道如何判断是否有环(快慢指针),那么同样的,入口结点也可以用快慢指针解决。只需要:
让快慢指针从链表头出发,快2慢1,如果有环最终必会相遇(这也是判断是否有环的方法)
将快指针回调回链表头,开始快慢都每次各走一步,相遇点即为入口结点。
下面是证明方法:
假设 环如图所示:
fast = a + b + n * (b + c); slow = a + b;
同时,fast = 2 slow; --> a = (n - 1)b + n * c;
所以如果让 fast 置为链表头,一次走一步,走 a 长度的,那么 slow 也会走 a 长度,因为 a = (n - 1)b + n * c,因为slow 原本在 a + b 位置, 所以相加,正好得到: a + a + b = (n - 1)b + n * c + a + b = a + n (b + c); 所以恰好会在链表环入口处相遇。
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } }; */ class Solution { public: ListNode* EntryNodeOfLoop(ListNode* pHead) { ListNode* fast = pHead; ListNode* slow = pHead; while(fast){ slow = slow->next; if(fast->next) fast = fast->next->next; else return nullptr; if(fast == slow){ fast = pHead; while(fast != slow){ fast = fast->next; slow = slow->next; } return fast; } } return nullptr; } };
我们需要找倒数第k 个节点,那么完全也可以利用双指针法去解,让快指针先走 k步,然后快慢同步走,快指针走到链表尾的时候,由于慢指针和快指针相差k 步,所以慢指针指向的就是第k 个结点。
/** * struct ListNode { *int val; *struct ListNode *next; *ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public: /** * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可 * * * @param pHead ListNode类 * @param k int整型 * @return ListNode类 */ ListNode* FindKthToTail(ListNode* pHead, int k) { // write code here if(!pHead || k <= 0) return nullptr; ListNode* pfast = pHead; ListNode* pslow = pHead; for(int i = k; i > 0; --i){ if(pfast) pfast = pfast->next; else if(!pfast && i == 0) return pHead; else return nullptr; } while(pfast){ pfast = pfast->next, pslow = pslow->next; } return pslow; } };
我们首先将该链表中每一个节点拆分为两个相连的节点,例如对于链表 A→B→C,我们可以将其拆分为 A→A′→B→B′→C→C′ 。对于任意一个原节点 S,其拷贝节点 S′ 即为其后继节点。
这样,我们可以直接找到每一个拷贝节点 S′ 的随机指针应当指向的节点,即为其原节点 SSS 的随机指针指向的节点 T 的后继节点 T‘ 。需要注意原节点的随机指针可能为空,我们需要特别判断这种情况。
当我们完成了拷贝节点的随机指针的赋值,我们只需要将这个链表按照原节点与拷贝节点的种类进行拆分即可,只需要遍历一次。同样需要注意最后一个拷贝节点的后继节点为空,我们需要特别判断这种情况。
复杂度分析
时间复杂度:O(n),其中 n 是链表的长度。我们只需要遍历该链表三次。读者们也可以自行尝试在计算拷贝节点的随机指针的同时计算其后继指针,这样只需要遍历两次。 空间复杂度:O(1)。注意返回值不计入空间复杂度。
/* // Definition for a Node. class Node { public: int val; Node* next; Node* random; Node(int _val) { val = _val; next = NULL; random = NULL; } }; */ class Solution { public: Node* copyRandomList(Node* head) { if(head == nullptr){ return nullptr; //如果为空则不讨论 } for(Node* node = head; node != nullptr; node = node->next->next){ Node* newNode = new Node(node->val); nodeNew->next = node->next; node->next = nodeNew; //创建新节点在原节点之后 } for(Node* node = head; node != nullptr; node = node->next->next){ Node* nodeNew = node->next; newNode->random = (node->random != nullptr) ? node->random : nullptr; //修改新链表random的指向 } Node* headNew = head->next; for(Node* node = head; node != nullptr; node = node->next){ Node* nodeNew = node->next; node->next = node->next->next; //修改原链表的next。 nodeNew->next = (nodeNew->next != nullptr) ? nodeNew->next->next : nullptr; //修改新链表的next指向 } return headNew; } };
注意读题,是排序的链表,所以我们可以使用双指针法,从左往右,如果left 所指的值和 right 所指是否相等,如果相等那么right 右移, 看看新的right 是否也符合相等条件,直至不符合条件,让前面的prev (解答中的node)指向新 right 即可,就是因为我们需要修改前面的指向,所以需要node 变量来记录之前的位置。也恰好是因为prev 不方便,所以我们使用哨兵位头节点来简化。如果不等,那么直接node,left,right都向右移动一个。
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } }; */ class Solution { public: ListNode* deleteDuplication(ListNode* pHead) { ListNode* newHead = new ListNode(-1); newHead->next = pHead; for(ListNode* node = newHead; node->next && node->next->next;){ ListNode* left = node->next; ListNode* right = left->next; if(left->val != right->val){ node = node->next, left = left->next, right = right->next; }else{ while(right && left->val == right->val) right = right->next; node->next = right; } } return newHead->next; } };