最近经历了各种面试,各种东北西跑,各种面试准备,以及各种各样不一样的问题和面试官对自己本科大学的打击,,我相信即使是普通大学也会让考官眼前一亮的,,作为一名非211的湘潭大学的学子,我不知道自己以后路在何方,但依然会继续前行,相信自己会做到最好!
刚从武汉面试回来,打算总结一下面试经验,身为一名通信工程专业的与技术行离不开的技术男,肯定离不开各种电路设计,程序设计,总之在面试过程中,考官也会问道关于数据结构这种最常见的问题,话不多说,先就总结一下吧!
链表是最基本的数据结构,链表相关的操作相对而言比较简单,在面试过程时间有限的过程当中也适合考察写代码的能力。链表的操作离不开指针,而指针又很容易导致出错。综合多方面的原因,链表题目在面试中占据着很重要的地位,甚至可以说必不可少,在我看来也是学习指针的最好办法,是理解C语言最好的学习方式。
下面我先来列出自己敲的代码,有关于静态链表的创建,都有自己详细的解释,大家先看看了解一下:
typedef struct _tag_StaticListNode //结点结构体定义 { unsigned int data; // int next; //保存链表里的数据元素 } TStaticListNode; typedef struct _tag_StaticList //静态链表 结构体定义 { int capacity; //静态链表最多容纳的元素 TStaticListNode header; //链表头 TStaticListNode node[]; //柔性数组 } TStaticList; StaticList* StaticList_Create(int capacity) // O(n)-- 1.创建静态链表 { TStaticList* ret = NULL; //定义返回值变量ret int i = 0; if( capacity >= 0 ) //指定的容量大为0,要合法 { ret = (TStaticList*)malloc(sizeof(TStaticList) + sizeof(TStaticListNode) * (capacity + 1)); //malloc动态的申明空间来申明动态链表以及里面的数组, (capacity + 1)因为有一个被设为表头 } if( ret != NULL ) { ret->capacity = capacity;//赋初值 ret->header.data = 0; // ret->header.next = 0; //开始一个元素都没有故为0 for(i=1; i<=capacity; i++) { ret->node[i].next = AVAILABLE;//所有 位置标志为可用 } } return ret; } void StaticList_Destroy(StaticList* list) // O(1) 直接释放free { free(list); } void StaticList_Clear(StaticList* list) // O(n) 清除 { TStaticList* sList = (TStaticList*)list; //强制类型转换 int i = 0; if( sList != NULL ) //判断合法 { sList->header.data = 0; sList->header.next = 0; //先清空 ,因为所有元素的下标都是大于0 的 for(i=1; i<=sList->capacity; i++) { sList->node[i].next = AVAILABLE; //所有的数组都是可以使用的 } } } int StaticList_Length(StaticList* list) // O(1) { TStaticList* sList = (TStaticList*)list; int ret = -1; //表示传进来的数据不合法 if( sList != NULL ) //判断是否合法,再赋值 { ret = sList->header.data; //把得到的值赋给ret } return ret; } int StaticList_Capacity(StaticList* list) // O(1)返回最大容量 { TStaticList* sList = (TStaticList*)list; int ret = -1; if( sList != NULL ) { ret = sList->capacity; } return ret; } int StaticList_Insert(StaticList* list, StaticListNode* node, int pos) // O(n) //进行插入一个元素的操作 { TStaticList* sList = (TStaticList*)list; //强制转换做一个数据封装 int ret = (sList != NULL); //1代表插入成功,0代表插入失败 int current = 0; int index = 0; int i = 0; ret = ret && (sList->header.data + 1 <= sList->capacity);//插入位置是否合法的 ret = ret && (pos >=0) && (node != NULL); //插入位置补位空且大于0 if( ret ) // ret = 1时 { for(i=1; i<=sList->capacity; i++) //开始寻找哪个位置可用 { if( sList->node[i].next == AVAILABLE ) //判断是否可用 { index = i; //记下插入位置 break; } } sList->node[index].data = (unsigned int)node;//新元素放到这个位置来 sList->node[0] = sList->header; //表头 for(i=0; (i<pos) && (sList->node[current].next != 0); i++)//移动pos次,(此位置下标不为0) { current = sList->node[current].next; //移到下一个元素的下标 } sList->node[index].next = sList->node[current].next;//1. 新元素与下一个元素连接 sList->node[current].next = index;//2.新元素与插入位置前一个位置连接 sList->node[0].data++; //元素长度加1了 sList->header = sList->node[0]; //更新表头信息 } return ret; } StaticListNode* StaticList_Get(StaticList* list, int pos) // O(n) { TStaticList* sList = (TStaticList*)list; StaticListNode* ret = NULL; //定义返回值变量 int current = 0; //表头的下标为0 int object = 0; //要获取的元素下标 int i = 0; if( (sList != NULL) && (0 <= pos) && (pos < sList->header.data) ) //sLIST指针不为空 , 获取元素位置pos>0 并且小于它的长度值 { sList->node[0] = sList->header; //第0个元素就是头结点 for(i=0; i<pos; i++) { current = sList->node[current].next; } object = sList->node[current].next;//当前元素的next域即为要获取元素在数组中的下标 ret = (StaticListNode*)(sList->node[object].data);//获取的值 } return ret; } StaticListNode* StaticList_Delete(StaticList* list, int pos) // O(n)删除第pos元素 { TStaticList* sList = (TStaticList*)list; StaticListNode* ret = NULL; int current = 0; int object = 0; int i = 0; if( (sList != NULL) && (0 <= pos) && (pos < sList->header.data) ) { sList->node[0] = sList->header; for(i=0; i<pos; i++) { current = sList->node[current].next; } object = sList->node[current].next; sList->node[current].next = sList->node[object].next; // 与增加一个元素一样 sList->node[0].data--; //元素删除,减1 sList->header = sList->node[0]; //更新表头信息 sList->node[object].next = AVAILABLE; //可用 ret = (StaticListNode*)(sList->node[object].data);//数据返回 } return ret; }
而对于循环链表来说,其实和静态链表是一个道理,只是在基于单向链表的基础上有一些改动,这里我只是把改动的列出来,当然其余的可能还是有一些小的差异,但是这个并不明显,下面是改动的部分代码,笔者也做了相关解释:
int CircleList_Insert(CircleList* list, CircleListNode* node, int pos) // O(n) //循环链表中要是插入第一个元素的话就得另加说明了 { TCircleList* sList = (TCircleList*)list; int ret = (sList != NULL) && (pos >= 0) && (node != NULL); int i = 0; if( ret ) { CircleListNode* current = (CircleListNode*)sList; for(i=0; (i<pos) && (current->next != NULL); i++) { current = current->next; } node->next = current->next; current->next = node; if( sList->length == 0 ) //插入的是第一个元素 { sList->slider = node; node->next = node; //插入元素得指向自己才能构成循环 } sList->length++; } return ret; } CircleListNode* CircleList_Get(CircleList* list, int pos) // O(n)获取元素 { TCircleList* sList = (TCircleList*)list; CircleListNode* ret = NULL; int i = 0; if( (sList != NULL) && (pos >= 0) )//不需要(pos<sList->length)这个约束 { CircleListNode* current = (CircleListNode*)sList; for(i=0; i<pos; i++) { current = current->next; } ret = current->next; } return ret; } CircleListNode* CircleList_Delete(CircleList* list, int pos) // O(n) { TCircleList* sList = (TCircleList*)list; CircleListNode* ret = NULL; int i = 0; if( (sList != NULL) && (pos >= 0) ) { CircleListNode* current = (CircleListNode*)sList; CircleListNode* first = sList->header.next; //first指向链表的 第一个元素 CircleListNode* last = (CircleListNode*)CircleList_Get(sList, sList->length - 1); for(i=0; i<pos; i++) //开始移动 { current = current->next; } ret = current->next; current->next = ret->next; sList->length--; if( first == ret ) //如果删除的是第一个元素 { sList->header.next = ret->next; //将表头指向ret last->next = ret->next; //最后一个指针移动到第二个位置上 } if( sList->slider == ret )//游标 { sList->slider = ret->next; } if( sList->length == 0 ) //链表是否为空 { sList->header.next = NULL; sList->slider = NULL; } } return ret; }
至于双向链表,就复杂一些了,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,这样代码难度就加大了,其实也是一个意思,但是在短短的面试时间中,我觉得还是很少让你写出来的,一般理解了单链表就可以应付了,下面我详细的介绍在面试过程当中会问道的有关链表的知识。
1 求单链表的节点数,注意检查链表是否为空。时间复杂度为O(n)
// 求单链表中结点的个数 unsigned int GetListLength(ListNode * pHead) { if(pHead == NULL) return 0; unsigned int nLength = 0; ListNode * pCurrent = pHead; while(pCurrent != NULL) { nLength++; pCurrent = pCurrent->m_pNext; } return nLength; }
2 将单链表翻转,从头到尾遍历原链表,每遍历一个结点,将其摘下放在新链表的最前端。注意链表为空和只有一个结点的情况。时间复杂度为O(n)
// 反转单链表 ListNode * ReverseList(ListNode * pHead) { // 如果链表为空或只有一个结点,无需反转,直接返回原链表头指针 if(pHead == NULL || pHead->m_pNext == NULL) return pHead; ListNode * pReversedHead = NULL; // 反转后的新链表头指针,初始为NULL ListNode * pCurrent = pHead; while(pCurrent != NULL) { ListNode * pTemp = pCurrent; pCurrent = pCurrent->m_pNext; pTemp->m_pNext = pReversedHead; // 将当前结点摘下,插入新链表的最前端 pReversedHead = pTemp; } return pReversedHead; }
3 查找单链表中的第n个点(n>0),使用两个指针,先让前面的指针走到正向第k个结点,这样前后两个指针的距离差是k-1,之后前后两个指针一起向前走,前面的指针走到最后一个结点时,后面指针所指结点就是倒数第k个结点。
// 查找单链表中倒数第K个结点 ListNode * RGetKthNode(ListNode * pHead, unsigned int k) // 函数名前面的R代表反向 { if(k == 0 || pHead == NULL) // 这里k的计数是从1开始的,若k为0或链表为空返回NULL return NULL; ListNode * pAhead = pHead; ListNode * pBehind = pHead; while(k > 1 && pAhead != NULL) // 前面的指针先走到正向第k个结点 { pAhead = pAhead->m_pNext; k--; } if(k > 1 || pAhead == NULL) // 结点个数小于k,返回NULL return NULL; while(pAhead->m_pNext != NULL) // 前后两个指针一起向前走,直到前面的指针指向最后一个结点 { pBehind = pBehind->m_pNext; pAhead = pAhead->m_pNext; } return pBehind; // 后面的指针所指结点就是倒数第k个结点 }
4 查找单链表中的单节点,设置两个指针,两个指针同时向前走,前面的指针每次走两步,后面的指针每次走一步,前面的指针走到最后一个结点时,后面的指针所指结点就是中间结点,即第(n/2+1)个结点。注意链表为空,链表结点个数为1和2的情况。时间复杂度O(n)
// 获取单链表中间结点,若链表长度为n(n>0),则返回第n/2+1个结点 ListNode * GetMiddleNode(ListNode * pHead) { if(pHead == NULL || pHead->m_pNext == NULL) // 链表为空或只有一个结点,返回头指针 return pHead; ListNode * pAhead = pHead; ListNode * pBehind = pHead; while(pAhead->m_pNext != NULL) // 前面指针每次走两步,直到指向最后一个结点,后面指针每次走一步 { pAhead = pAhead->m_pNext; pBehind = pBehind->m_pNext; if(pAhead->m_pNext != NULL) pAhead = pAhead->m_pNext; } return pBehind; // 后面的指针所指结点即为中间结点 }
5 从尾部到头部打印单链表,就是倒序打印单链表!注意链表为空的情况,下面使用的是栈。时间复杂度为O(n)
// 从尾到头打印链表,使用栈 void RPrintList(ListNode * pHead) { std::stack<ListNode *> s; ListNode * pNode = pHead; while(pNode != NULL) { s.push(pNode); pNode = pNode->m_pNext; } while(!s.empty()) { pNode = s.top(); printf("%d/t", pNode->m_nKey); s.pop(); } }
// 从尾到头打印链表,使用递归 void RPrintList(ListNode * pHead) { if(pHead == NULL) { return; } else { RPrintList(pHead->m_pNext); printf("%d/t", pHead->m_nKey); } }
6 已知两个单链表pHead1 和pHead2 各自有序,把它们合并成一个链表依然有序,需要O(1)的空间。时间复杂度为O(max(len1, len2))
// 合并两个有序链表 ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2) { if(pHead1 == NULL) return pHead2; if(pHead2 == NULL) return pHead1; ListNode * pHeadMerged = NULL; if(pHead1->m_nKey < pHead2->m_nKey) { pHeadMerged = pHead1; pHeadMerged->m_pNext = NULL; pHead1 = pHead1->m_pNext; } else { pHeadMerged = pHead2; pHeadMerged->m_pNext = NULL; pHead2 = pHead2->m_pNext; } ListNode * pTemp = pHeadMerged; while(pHead1 != NULL && pHead2 != NULL) { if(pHead1->m_nKey < pHead2->m_nKey) { pTemp->m_pNext = pHead1; pHead1 = pHead1->m_pNext; pTemp = pTemp->m_pNext; pTemp->m_pNext = NULL; } else { pTemp->m_pNext = pHead2; pHead2 = pHead2->m_pNext; pTemp = pTemp->m_pNext; pTemp->m_pNext = NULL; } } if(pHead1 != NULL) pTemp->m_pNext = pHead1; else if(pHead2 != NULL) pTemp->m_pNext = pHead2; return pHeadMerged; }
7 判断一个单链表是否有环!如果一个链表中有环,也就是说用一个指针去遍历,是永远走不到头的。因此,我们可以用两个指针去遍历,一个指针一次走两步,一个指针一次走一步,如果有环,两个指针肯定会在环中相遇。时间复杂度为O(n)
bool HasCircle(ListNode * pHead) { ListNode * pFast = pHead; // 快指针每次前进两步 ListNode * pSlow = pHead; // 慢指针每次前进一步 while(pFast != NULL && pFast->m_pNext != NULL) { pFast = pFast->m_pNext->m_pNext; pSlow = pSlow->m_pNext; if(pSlow == pFast) // 相遇,存在环 return true; } return false; }
8 判断两个单链表是否相交:如果两个链表相交于某一节点,那么在这个相交节点之后的所有节点都是两个链表所共有的。也就是说,如果两个链表相交,那么最后一个节点肯定是共有的。先遍历第一个链表,记住最后一个节点,然后遍历第二个链表,到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交,否则不相交。时间复杂度为O(len1+len2),因为只需要一个额外指针保存最后一个节点地址,空间复杂度为O(1)
bool IsIntersected(ListNode * pHead1, ListNode * pHead2) { if(pHead1 == NULL || pHead2 == NULL) return false; ListNode * pTail1 = pHead1; while(pTail1->m_pNext != NULL) pTail1 = pTail1->m_pNext; ListNode * pTail2 = pHead2; while(pTail2->m_pNext != NULL) pTail2 = pTail2->m_pNext; return pTail1 == pTail2; }
9 求两个单链表相交的第一个节点:两个链表均从头节点开始,假设len1大于len2,那么将第一个链表先遍历len1-len2个节点,此时两个链表当前节点到第一个相交节点的距离就相等了,然后一起向后遍历,知道两个节点的地址相同。时间复杂度,O(len1+len2)
ListNode* GetFirstCommonNode(ListNode * pHead1, ListNode * pHead2) { if(pHead1 == NULL || pHead2 == NULL) return NULL; int len1 = 1; ListNode * pTail1 = pHead1; while(pTail1->m_pNext != NULL) { pTail1 = pTail1->m_pNext; len1++; } int len2 = 1; ListNode * pTail2 = pHead2; while(pTail2->m_pNext != NULL) { pTail2 = pTail2->m_pNext; len2++; } if(pTail1 != pTail2) // 不相交直接返回NULL return NULL; ListNode * pNode1 = pHead1; ListNode * pNode2 = pHead2; // 先对齐两个链表的当前结点,使之到尾节点的距离相等 if(len1 > len2) { int k = len1 - len2; while(k--) pNode1 = pNode1->m_pNext; } else { int k = len2 - len1; while(k--) pNode2 = pNode2->m_pNext; } while(pNode1 != pNode2) { pNode1 = pNode1->m_pNext; pNode2 = pNode2->m_pNext; } return pNode1; }
10 已知一个单链表中存在环,求进入环中的第一个节点:首先判断是否存在环,若不存在结束。在环中的一个节点处断开(当然函数结束时不能破坏原链表),这样就形成了两个相交的单链表,求进入环中的第一个节点也就转换成了求两个单链表相交的第一个节点。
ListNode* GetFirstNodeInCircle(ListNode * pHead) { if(pHead == NULL || pHead->m_pNext == NULL) return NULL; ListNode * pFast = pHead; ListNode * pSlow = pHead; while(pFast != NULL && pFast->m_pNext != NULL) { pSlow = pSlow->m_pNext; pFast = pFast->m_pNext->m_pNext; if(pSlow == pFast) break; } if(pFast == NULL || pFast->m_pNext == NULL) return NULL; // 将环中的此节点作为假设的尾节点,将它变成两个单链表相交问题 ListNode * pAssumedTail = pSlow; ListNode * pHead1 = pHead; ListNode * pHead2 = pAssumedTail->m_pNext; ListNode * pNode1, * pNode2; int len1 = 1; ListNode * pNode1 = pHead1; while(pNode1 != pAssumedTail) { pNode1 = pNode1->m_pNext; len1++; } int len2 = 1; ListNode * pNode2 = pHead2; while(pNode2 != pAssumedTail) { pNode2 = pNode2->m_pNext; len2++; } pNode1 = pHead1; pNode2 = pHead2; // 先对齐两个链表的当前结点,使之到尾节点的距离相等 if(len1 > len2) { int k = len1 - len2; while(k--) pNode1 = pNode1->m_pNext; } else { int k = len2 - len1; while(k--) pNode2 = pNode2->m_pNext; } while(pNode1 != pNode2) { pNode1 = pNode1->m_pNext; pNode2 = pNode2->m_pNext; } return pNode1; }
11 给出一单链表头指针pHead和一节点指针pToBeDeleted,O(1)时间复杂度删除节点pToBeDeleted
void Delete(ListNode * pHead, ListNode * pToBeDeleted) { if(pToBeDeleted == NULL) return; if(pToBeDeleted->m_pNext != NULL) { pToBeDeleted->m_nKey = pToBeDeleted->m_pNext->m_nKey; // 将下一个节点的数据复制到本节点,然后删除下一个节点 ListNode * temp = pToBeDeleted->m_pNext; pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext; delete temp; } else // 要删除的是最后一个节点 { if(pHead == pToBeDeleted) // 链表中只有一个节点的情况 { pHead = NULL; delete pToBeDeleted; } else { ListNode * pNode = pHead; while(pNode->m_pNext != pToBeDeleted) // 找到倒数第二个节点 pNode = pNode->m_pNext; pNode->m_pNext = NULL; delete pToBeDeleted; } } }