本文首发于微信公众号:聊点技术,原文标题《 面试腾讯时遇到了"链表"的原题,so easy!》 找工作的过程中,不论是参加笔试还是面试,我们都会遇到大量和链表有关的题目。我在找实习的时候,经历的第一场面试是腾讯的电话面试,两道编程题目中有一道就是本文中提到的:复杂链表的复制。秋招期间,在经历了多次笔试和面试后,我把曾经遇到过的与链表有关的题目进行了分类总结,总结成此文期待能够对各位朋友有所帮助。
本文总结了在面试中遇到频率最高的与链表有关的题目,并使用了C++进行编码实现。
链表节点定义如下:
struct List{ int val; struct List* next; List(int x) : val(x), next(nullptr){} };非递归算法:利用两个指针,指针cur指向链表尾部,指针tail指向链表头部。每当tail从头循环到尾部cur时,输出表尾的值。让表尾cur指向tail,再次循环。
void PrintTailToHead(List* head){ //非递归O(n^2) List* cur = nullptr; while(cur != head){ List* tail = head; //重新指向头节点 while(tail->next != cur){ //循环移动到尾 tail = tail->next; } cout<< tail->val << endl; cur = tail; //更新尾节点 } } void TailToHead(List* head){ //递归算法 O(n) if(head == nullptr){ return ; } TailToHead(head->next); cout<< head->val << endl; }若要删除的链表节点非尾 若要删除的节点位于链表的尾部,那么它就没有下一个节点——需要从链表头节点开始,顺序遍历得到该节点的前序节点,并完成删除操作 如果链表中只有一个节点,又要删除链表的头节点,那么在删除节点后,需要把链表头节点置空
void DeleteNode(List** head, List* pos){ if(head == nullptr || pos == nullptr){ return; } if(pos->next != nullptr){ /*要删除节点不是尾节点,采用向前替换法*/ List* next = nullptr; next = pos->next; pos->val = next->val; pos->next = next->next; delete next; next = nullptr; } else if (*head == pos){ /* pos->next为空跳过第一个if. 在第二个if内判断 */ delete pos; /* pos->next为空,且pos和*head相同,此时只有一个节点*/ pos = nullptr; *head= nullptr; /* 使用指向指针的指针的原因 */ } else{ /* pos->next为空,且pos!=*head。此时要删除尾节点*/ List* p = *head; while(p->next != pos){ p = p->next; } p->next = nullptr; delete pos; pos = nullptr; } }//采用向前替换法
void ListDelNode(List* pos){ List* cur = nullptr; cur = pos->next; pos->val = cur->val; pos->next = cur->next; free(cur); cur = nullptr; }从头遍历整个链表。如果当前节点的值与下一个节点的值相同,那么他们就是重复的节点,都可以被删除。为了保证被删除之后的链表仍然是相连的,要把当前节点的前一个节点(pPreNode)和后面值比当前节点的值大的节点相连。
void DeleteDuplication(ListNode** pHead){ //头结点也可能被删除,因此采用指向指针的指针 if (pHead == nullptr || *pHead == nullptr) return; ListNode* pPreNode = nullptr; ListNode* pNode = *pHead; while (pNode != nullptr){ ListNode* pNext = pNode->next; bool needDelete = false; if (pNext != nullptr && pNext->val == pNode->val){ //next节点不为空且当前节点和next节点值相等 needDelete = true; //说明他们就是重复节点,都可以被删除 } if (!needDelete){ //不是重复节点。指针后移 pPreNode = pNode; pNode = pNode->next; } else{ //遇到重复节点 int value = pNode->val; //保存值,因后边会删除该节点 ListNode* ToBeDel = pNode; while (ToBeDel != nullptr && ToBeDel->val == value){ //直至遇到值不等节点 pNext = ToBeDel->next; delete ToBeDel; ToBeDel = pNext; } if (pPreNode == nullptr){ //说明是开头几个节点被删除。 *pHead = pNext; } else{ pPreNode->next = pNext; //当前节点的前驱指向后继。 } pNode = pNext; //当前指针后移 } } }快慢指针法:建立两个指针slow和fast,初始时两个指针都指向单链表的头结点。依次向后移动,fast指针的移动速度是slow指针的2倍。当fast指向末尾节点的时候,slow就正好在正中间。
List* FindMidNode(List* head){ //只能遍历一次链表 if (head == nullptr) return head; List* slow = head; List* fast = head; while(fast != nullptr && fast->next != nullptr){ slow = slow->next; fast = fast->next->next; //循环判断条件中fast和fast->next都不为空才可执行. //因此此处不需要判断fast->next是否为空 /*********************************** fast = fast->next; if(fast != nullptr) fast = fast->next; ***********************************/ } return slow; }分析:假设有节点1、2、3、4、5、6。倒数第3个节点是值为4的节点。假设整个链表有n个节点,倒数第k个节点就是从头节点开始的第n-k+1个节点。 定义两个指针:第一个指针从链表的头指针开始遍历向前走k-1步,第二个指针保持不动;从第k步开始,第二个指针也开始从链表的头指针开始遍历;由于两个指针的距离保持在k-1.当第一个指针到达链表的尾节点时,第二个指针正好指向倒数第k个节点。
List* FindTailKthNode_1(List* head, int k){ //要求只能遍历一次链表 if (head == nullptr || k <= 0) return head; List* slow = head; List* fast = head; for (int i = 0; i < k-1; ++i){ //fast指针先走k-1步 if (fast->next == nullptr){ return nullptr; //节点数少于k } else{ fast = fast->next; } } while (fast->next){ //当fast->next为空. slow指向倒数第K个节点 slow = slow->next; fast = fast->next; } return slow; } List* FindTailKthNode(List* head, int k){ if (head == nullptr || k <= 0) return head; List* slow = head; List* fast = head; for (int i = 0; i < k; ++i){ if (fast->next == nullptr){ return nullptr; } else{ fast = fast->next; } } while (fast){ //当fast==nullptr,slow指向倒数第K个节点 slow = slow->next; fast = fast->next; } return slow; }两个链表都不带环—直接进行常规判断—调用EnterNode(两链表相交,求交点函数); 一个带环,一个不带环—不可能相交; 两个均带环:在环外相交,则入口点相同;在环内相交,则从一个链表的入口点环绕一周即可找到另一链表的入口。
bool ListCrossWithCycle(List* head1, List* head2){ if (head1 == nullptr || head2 == nullptr) return false; List* enter1 = ListCrossEnterNode(head1); //返回环的入口点,无环则返回nullptr List* enter2 = ListCrossEnterNode(head2); if (enter1 == nullptr && enter2 == nullptr){ //两链表无环 if (EnterNode(head1, head2)){ return true; } else return false; } if (enter1 == nullptr || enter2 == nullptr) //一个带环,一个不带环——不可能相交 return false; if (enter1 == enter2){ //两个都有环且入口点相同——在环外相交 return true; } List* cur = enter1->next; //两个都带环,但入口点不同——在环内相交 while (cur != enter1){ if (cur == enter2) // 从一个链表的入口点环绕一周即可找到另一链表的入口。 return true; cur = cur->next; } return false; //两个都带环,但不相交 }样例1: 输入格式:a,b,c,d,a 输出格式:true 样例2: 输入格式:a,b,c,d 输出格式:false 这个题目可以自己根据输入创建一个链表,然后再进行判断。但是比较复杂,可以采用下面的思路:针对输入数组的最后一个元素,在前面的数据中查找,如果查找到,说明存在环。
int IsCyc(){ vector<int> data; char temp; while (cin >> temp){ if (temp == ',') continue; data.push_back(temp); } char c = data[data.size()-1]; auto p = find(data.begin(), data.end() - 1, c); if (p != (data.end() - 1)){ cout << "true" << endl; } else cout << "false" << endl; return 0; }给定一个链表,旋转链表,使得每个结点向后移动k个位置,其中k是非负数。 例. 输入:1->2->3->4->5->NULL k=2 返回:4->5->1->2->3->NULL 解析:找到倒数第k个位置结点,将其变为头结点。
ListNode* rotateList(ListNode* head, int k){ int start = 0; ListNode* fast = head; while (start < k && fast->next != nullptr){ fast = fast->next; ++start; } if (fast->next == nullptr || start < k){ /*循环结束后,若start<k表示k比整个链表还要长。旋转后还是单链表*/ return head; /*如果fast->next==nullptr表示n正好等于原链表的长度,此时不需要旋转*/ } ListNode* pre = fast; /*倒数第k+1个结点*/ ListNode* newHead = fast->next; /*倒数第k个结点。旋转后的头结点*/ while (fast->next != nullptr){ /*fast指针走到链尾*/ fast = fast->next; } fast->next = head; /*原链表的最后一个结点指向原来的头结点*/ pre->next = nullptr; /*原链表的倒数第k+1个结点变为尾节点*/ return newHead; }在复杂链表中,每个节点除了有一个next指针指向下一个节点外,还有一个random指针指向链表中的任意节点或nullptr. 复制过程分为三个步骤: 根据原始链表中的每个节点N创建对应的N’. 把N’链接在N的后面。 设置复制出来的节点的random指针。 把长链表拆分成两个链表。
struct ListNode{ int val; struct ListNode* next; struct ListNode* random; ListNode(int x = -1) :val(x), next(nullptr), random(nullptr){} }; void CloneNodes(ListNode* head){ //复杂链表的复制步骤1. //1.根据原始链表中的每个节点N创建对应的N'. 把N'链接在N的后面。 ListNode* cur = head; while (cur != nullptr){ ListNode* pNode = new ListNode(); pNode->val = cur->val; pNode->next = cur->next; pNode->random = nullptr; cur->next = pNode; cur = pNode->next; } } void ConnectRandomPtr(ListNode* head){ //复杂链表的复制步骤2. //2.设置复制出来的节点的random指针。假设原始链表上的N节点的 //random指针指向节点S.那么其对应复制出来的N'是N的next指向的节点, //同样S'也是S的next指向的节点。 ListNode* cur = head; while (cur != nullptr){ ListNode* pNode = cur->next; if (cur->random != nullptr){ pNode->random = cur->random->next; } cur = pNode->next; } } ListNode* ReconnectNode(ListNode* head){ //复杂链表的复制步骤3. //3.把这个长链表拆分成两个链表:把奇数位置的节点用next链接起来 //就是原始链表,把偶数位置的节点用next链接起来就是复制出来的链表。 ListNode* pNode = head; ListNode* pClonedHead = nullptr; ListNode* pClonedNode = nullptr; if (pNode != nullptr){ pClonedHead = pClonedNode = pNode->next; pNode->next = pClonedNode->next; //将偶数位的第一个节点拆分出来 pNode = pNode->next; //奇数位的指针后移 } while (pNode != nullptr){ pClonedNode->next = pNode->next; pClonedNode = pClonedNode->next; pNode->next = pClonedNode->next; pNode = pNode->next; } return pClonedHead; } ListNode* Clone(ListNode* head){ //合并上述三个步骤。 CloneNodes(head); ConnectRandomPtr(head); return ReconnectNode(head); }给定一个链表,每次按照K个为一组进行翻转操作,并返回修改后的链表。如果节点数不是K的倍数,那么剩余的结点就保持原样。 输入格式:[1,2,3,4,5] 2 输出格式:[2,1,4,3,5] 本题是Leetcode第25题,标注级别为Hard。
ListNode* CreateLinkList(vector<int> &vec) { if (vec.empty()) return nullptr; ListNode *head = new ListNode(vec.back()); head->next = nullptr; for (int i = vec.size() - 2; i >= 0; --i) { ListNode *newNode = new ListNode(vec[i]); newNode->next = head; head = newNode; } return head; } void PrintLinkList(ListNode* head) { ListNode* node = head; cout << "["; while (node != nullptr) { std::cout << node->val; if (node->next != nullptr) cout << ","; node = node->next; } cout << "]" << endl; return; } //链表每次按照K个一组进行翻转 ListNode* ReverseKGroup(ListNode* head, int k) { ListNode* headNode = new ListNode(0); ListNode* cur = head; ListNode* p = headNode; //初始时p指向头结点 headNode->next = head; int num = 0; while (cur){ //计数。统计一共多少个节点 ++num; cur = cur->next; } while (num >= k) { //若长度小于分组长度则不旋转 int n = k; cur = p->next; while (--n) { ListNode *tmp = cur->next; cur->next = tmp->next; tmp->next = p->next; p->next = tmp; } p = cur; num -= k; } p = headNode->next; delete headNode; return p; } void Input(vector<int>& vec) { char c; int tmp; string str; getline(cin, str); //一次性按照字符串个数读入一行 istringstream record(str); //将读入的字符串绑定到流istringstream上 while (record >> tmp >> c) { vec.push_back(tmp); } } int ReversrMain() { int k; vector<int> vec; Input(vec); ListNode* head = CreateLinkList(vec); cin >> k; ListNode* newHead = ReverseKGroup(head, k); PrintLinkList(newHead); system("pause"); return 0; }THE END 好了,面试过程中常见的和链表有关的题目在文中基本都列出来了。一起努力,相信大家都能够找到自己满意的工作!欢迎大家关注 “聊点技术”,我会陆续将我的秋招经验进行总结并发布到公众号上。