链表笔试题
链表:
1、注意是否有带头结点。
2、单链表的建立:顺序建表(尾插法)、逆序建表(头插法)。单链表插入、删除操作需要寻找前驱结点。
3、双向链表和单向链表相比,多了一个前驱指针,单向链表在删除结点时候要遍历链表,双向链表在删除不需要遍历。
一、判断两个链表是否相交:(假设两个链表都没有环)
1、判断第一个链表的每个节点是否在第二个链表中。
2、把第二个链表连接到第一个后面,判断得到的链表是否有环,有环则相交。
3、如果两个链表有一个公共结点,那么该公共结点之后的所有结点都是重合的,那么,它们的最后一个结点必然是重合的。先遍历第一个链表,记住最后一个节点,再遍历第二个链表,得到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交。
如何判断一个单链表是有环的? 设置两个指针(fast, slow),初始值都指向头,slow每次前进一步,fast每次前进二步,如果链表存在环,则fast必定先进入环,而slow后进入环,两个指针必定相遇。(当然,fast先行头到尾部为NULL,则为无环链表)程序如下:
bool IsExitsLoop(ListNode *head) { ListNode *slow = head, *fast = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; if(slow == fast) break; } return !(fast == NULL || fast->next == NULL); }
寻找到环的入口点
当fast若与slow相遇时,slow肯定没有走遍历完链表,而fast已经在环内循环了n圈(1<=n)。假设slow走了s步,则fast走了2s步(fast步数还等于s 加上在环上多转的n圈),设环长为r,则:2s = s + nr,s = nr 环入口点与相遇点距离为x,起点到环入口点的距离为a,则:a + x = s,a + x = nr,a = nr - x 从链表头到环入口点等于n循环内环减去环入口点到相遇点距离,现让一指针p1从链表起点处开始遍历,指针p2从相遇点处开始遍历,且p1和p2移动步长均为1。则当p1移动距离a时到达环的入口点。由于p2是从相遇点处开始移动,故p2移动nr步是移回到了相遇点处,再退x步则是到了环的入口点,也即移动了(nr-x)的距离,当p1移动a第一次到达环的入口点时,p2也恰好到达了该入口点。程序描述如下:
ListNode* FindLoopPort(ListNode *head) { ListNode *slow = head, *fast = head; while(fast && fast->next) { slow = slow->next; fast = fast->next->next; if(slow == fast) break; } // 无环,返回NULL if(fast == NULL || fast->next == NULL) return NULL; slow = head; while(slow != fast) { slow = slow->next; fast = fast->next; } return slow; }
扩展1:如果链表可能有环,则如何判断两个链表是否相交
思路:
1.先判断带不带环
2.如果都不带环,就判断尾节点是否相等,相等则相交,否则不相交
3.如果一个带环,一个不带环,则不相交
4.如果都带环,判断一链表上俩指针相遇的那个节点,在不在另一条链表上,如果在,则相交,如果不在,则不相交。
//判断是否有环,返回bool,如果有环,返回环里的节点 //思路:用两个指针,一个指针步长为1,一个指针步长为2,判断链表是否有环 bool isCircle(Node * head, Node *& circleNode, Node *& lastNode) { Node * fast = head->next; Node * slow = head; while(fast != slow && fast && slow) { if(fast->next != NULL) fast = fast->next; if(fast->next == NULL) lastNode = fast; if(slow->next == NULL) lastNode = slow; fast = fast->next; slow = slow->next; } if(fast == slow && fast && slow) { circleNode = fast; return true; } else return false; } //判断带环不带环时链表是否相交 bool detect(Node * head1, Node * head2) { Node * circleNode1; Node * circleNode2; Node * lastNode1; Node * lastNode2; bool isCircle1 = isCircle(head1,circleNode1,lastNode1); bool isCircle2 = isCircle(head2,circleNode2,lastNode2); //一个有环,一个无环 if(isCircle1 != isCircle2) return false; //两个都无环,判断最后一个节点是否相等 else if(!isCircle1 && !isCircle2) { return lastNode1 == lastNode2; } //两个都有环,判断环里的节点是否能到达另一个链表环里的节点 else { Node * p = circleNode1->next; while(p != circleNode1) { if(p == circleNode2) return true; p = p->next; } return false; } return false; }
扩展2:求两个链表相交的第一个节点
思路:如果两个尾结点是一样的,说明它们用重合;否则两个链表没有公共的结点。
在上面的思路中,顺序遍历两个链表到尾结点的时候,我们不能保证在两个链表上同时到达尾结点。这是因为两个链表不一定长度一样。但如果假设一个链表比另一个长L个结点,我们先在长的链表上遍历L个结点,之后再同步遍历,这个时候我们就能保证同时到达最后一个结点了。由于两个链表从第一个公共结点开始到链表的尾结点,这一部分是重合的。因此,它们肯定也是同时到达第一公共结点的。于是在遍历中,第一个相同的结点就是第一个公共的结点。
在这个思路中,我们先要分别遍历两个链表得到它们的长度,并求出两个长度之差。在长的链表上先遍历若干次之后,再同步遍历两个链表,直到找到相同的结点,或者一直到链表结束。此时,如果第一个链表的长度为m,第二个链表的长度为n,该方法的时间复杂度为O(m+n)。
ListNode* FindFirstCommonNode( ListNode *pHead1, ListNode *pHead2) { // 得到两链表的长度 unsigned int nLength1 = ListLength(pHead1); unsigned int nLength2 = ListLength(pHead2); int nLengthDif = nLength1 - nLength2; // 得到长链表和长度之差 ListNode *pListHeadLong = pHead1; ListNode *pListHeadShort = pHead2; if(nLength2 > nLength1) { pListHeadLong = pHead2; pListHeadShort = pHead1; nLengthDif = nLength2 - nLength1; } // 长链表先遍历长度之差 for(int i = 0; i < nLengthDif; ++ i) pListHeadLong = pListHeadLong->m_pNext; // 两个链表同时遍历直到相等 while((pListHeadLong != NULL) && (pListHeadShort != NULL) && (pListHeadLong != pListHeadShort)) { pListHeadLong = pListHeadLong->m_pNext; pListHeadShort = pListHeadShort->m_pNext; } // 得到第一个共同的结点 ListNode *pFisrtCommonNode = NULL; if(pListHeadLong == pListHeadShort) pFisrtCommonNode = pListHeadLong; return pFisrtCommonNode; } //得到链表长度 unsigned int ListLength(ListNode* pHead) { unsigned int nLength = 0; ListNode* pNode = pHead; while(pNode != NULL) { ++ nLength; pNode = pNode->m_pNext; } return nLength; }
二、删除链表中的一个结点
1.给定链表的头指针和一个结点指针,在O(1)时间删除该结点。
分析:之所以需要从头结点开始查找要删除的结点,是因为我们需要得到要删除的结点的前面一个结点。换一种思路,我们可以从给定的结点得到它的下一个结点。这个时候我们实际删除的是它的下一个结点,由于我们已经得到实际删除的结点的前面一个结点,因此完全是可以实现的。当然,在删除之前,我们需要需要把给定的结点的下一个结点的数据拷贝到给定的结点中。此时,时间复杂度为O(1)。
void DeleteNode(ListNode* pListHead, ListNode* pToBeDeleted) { if(!pListHead || !pToBeDeleted) return; // 如果要删除的结点不是最后一个结点 if(pToBeDeleted->m_pNext != NULL) { // 把它的next结点的值拷贝给它 ListNode* pNext = pToBeDeleted->m_pNext; pToBeDeleted->m_nKey = pNext->m_nKey; pToBeDeleted->m_pNext = pNext->m_pNext; // 再删除next结点 delete pNext; pNext = NULL; } // 如果要删除的结点是最后一个结点 else { // 先得到要删除结点的前驱结点 ListNode* pNode = pListHead; while(pNode->m_pNext != pToBeDeleted) { pNode = pNode->m_pNext; } // 再删除要删除的结点 pNode->m_pNext = NULL; delete pToBeDeleted; pToBeDeleted = NULL; } }
值得注意的是,为了让代码看起来简洁一些,上面的代码基于两个假设:(1)给定的结点的确在链表中;(2)给定的要删除的结点不是链表的头结点。不考虑第一个假设对代码的健壮性是有影响的。至于第二个假设,当整个列表只有一个结点时,代码会有问题。但这个假设不算很过分,因为在有些链表的实现中,会创建一个虚拟的链表头,并不是一个实际的链表结点。这样要删除的结点就不可能是链表的头结点了。
PS:只给定单链表中的某个结点p(非空结点),在p前面插入一个结点。办法与上面类似,先分配一个结点q,将q插入在p后,接下来将p中的数据copy入q中,然后再将要插入的数据记录在p中。
三、寻找倒数第k个结点
题目:输入一个单向链表,输出该链表中倒数第k个结点。链表的倒数第0个结点为链表的尾指针。
解法一:假设整个链表有n个结点,那么倒数第k个结点是从头结点开始的第n-k-1个结点(从0开始计数)。 这种思路的时间复杂度是O(n),但需要遍历链表两次。第一次得到链表中结点个数n,第二次得到从头结点开始的第n-k-1个结点即倒数第k个结点。如果链表的结点数不多,这是一种很好的方法。
ListNode* FindKthToTail(ListNode* head, int k) { if(head == NULL) return NULL; ListNode *p = head; unsigned int n = 0; while(p->next != NULL) { p = p->next; n ++; } if(n < k) return NULL; p = head; for(int i = 0; i < n-k; ++i) p = p->next; return p; }
解法二:如果我们在遍历时维持两个指针,第一个指针从链表的头指针开始遍历,在第k-1步之前,第二个指针保持不动;在第k-1步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾结点时,第二个指针(走在后面的)指针正好是倒数第k个结点。 这种思路只需要遍历链表一次。对于很长的链表,只需要把每个结点从硬盘导入到内存一次。因此这一方法的时间效率前面的方法要高。
ListNode *FindMToLastElement(ListNode *head, int m) { ListNode *p1, *p2; p1 = head; for(int i = 0; i < m; ++i) { if(p1->next) p1 = p1->next; else return NULL; } p2 = head; while(p1->next) { p1 = p1->next; p2 = p2->next; } return p2; }
四、从尾到头遍历链表
题目:输入一个链表的头结点,从尾到头反过来输出每个结点的值。
1、反转链表,再从头到尾输出
//单链表就地逆置(带头结点的链表) void ReverseList(ListNode* head) { ListNode* p = head->next; // 原链表 head->next = NULL; // 新表(空表) while(p != NULL) { ListNode* q = p->next; // 保存下个结点q p->next = head->next; // 将后面的结点插到头 head->next = p; p = q; // 移动到下个结点 } } // 反转单链表(不带头结点的链表) 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; pTemp->m_pNext = pReversedHead; // 将当前结点摘下,插入新链表的最前端 pReversedHead = pTemp; pCurrent = pCurrent->m_pNext; // 移动到下个结点 } return pReversedHead; }
2、从头到尾遍历链表,每经过一个结点的时候,把该结点放到一个栈中。当遍历完整个链表后,再从栈顶开始输出结点的值,此时输出的结点的顺序已经反转过来了。该方法需要维护一个额外的栈。
// 从尾到头打印链表,使用栈 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(); } }
3、既然想到了栈来实现这个函数,而递归本质上就是一个栈结构。于是很自然的又想到了用递归来实现。要实现反过来输出链表,我们每访问到一个结点的时候,先递归输出它后面的结点,再输出该结点自身,这样链表的输出结果就反过来了。
void PrintListReversely(ListNode* head) { if(head != NULL) { // Print the next node first if(head->next != NULL) { PrintListReversely(head->next); } // Print this node printf("%d", head->data); } }
五、链表的扁平化
给定一个双向链表。这个双向链表中的每一个元素除固有的后指针(指向后一个元素)和前指针(指向前一个元素)外,还有一个子指针,每个指针可能指向也可能不指向另一个双向链表。而那些子双向链表本身还可能有一个或者多个子双向链表,从而形成一种多层次的数据结构。 对这个链表进行扁平化,使全体结点都出现在一个只有一个层次的双向链表里。已知条件原多层次双向链表的第一层次的头指针(head)和尾指针(tail)。下面是各个结点C语言struct定义:
typedef struct node { struct node * next; struct node * prev; struct node * child; int data; }node;
分析:从第一层的头元素开始沿各节点的next指针进行遍历,每遇到一个字节点,就把子接点追加到第一层的末尾去。继续对第一层进行遍历,但你遇到的将是原属于第二层的节点。不断重复把子链表追加链接到第一层的上述步骤,就会得到一个扁平化的链表。
void FlattenList(node *head, node **tail) { node *cur = head; if(cur == NULL) return; while(cur) { if(cur->child) { Append(tail,cur->child); } cur = cur->next; } } //把子链表挂在链尾,子链表的最后一个元素设为新的tail void Append(node **tail, node *child) { (*tail)->next = child; child->prev = *tail; node * cur = child; while(cur->next) { cur = cur->next; } *tail = cur; }
根据上面的例子,得到扁平化的链表,怎么把它恢复到多层次链表呢?
分析:从链表的头元素开始进行遍历:
当你尚未到达链表尾时
如果当前节点有子节点
把相应的子链表与前一个节点分断开
从子链表的头元素开始进行遍历和分断
前进到下一个节点
void Unflatten(node* start,node **tail) { node *cur; ExploreAndSeparate(start); for(cur=start; cur->next; cur=cur->next) ; *tail = cur; } void ExploreAndSeparate(node* child) { node* cur = child; while(cur) { if(cur->child) { //断开子链表与前结点的连接 cur->child->prev->next = NULL; //子链表的新的开始 cur->child->prev = NULL; ExploreAndSeparate(cur->child); } cur = cur->next; } }
六、复杂链表的复制
题目:有一个复杂链表,其结点除了有一个m_pNext指针指向下一个结点外,还有一个m_pSibling指向链表中的任一结点或者NULL。其结点的C++定义如下:
struct ComplexNode { int m_nValue; ComplexNode* m_pNext; ComplexNode* m_pSibling; };
算法:
第一步:复制原链表中的每个结点。根据原始链表的每个结点N,创建对应的N',把N'链接在N的后面。
第二步:设置复制出来的链表上的结点的m_pSibling的指向。假设原始链表上的N的m_pSibling指向结点S,那么其对应复制出来的N'的m_pSibling指向结点S'。
第三步:把这个长链表拆分成两个:把奇数位置的结点链接起来就是原始链表,把偶数位置的结点链接出来就是复制出来的链表。
// Clone all nodes in a complex linked list with head pHead, // and connect all nodes with m_pNext link void CloneNodes(ComplexNode* pHead) { ComplexNode* pNode = pHead; while(pNode != NULL) { ComplexNode* pCloned = new ComplexNode(); pCloned->m_nValue = pNode->m_nValue; pCloned->m_pNext = pNode->m_pNext; pCloned->m_pSibling = NULL; pNode->m_pNext = pCloned; pNode = pCloned->m_pNext; // move to the next node } } // Connect sibling nodes in a complex link list void ConnectSiblingNodes(ComplexNode* pHead) { ComplexNode* pNode = pHead; while(pNode != NULL) { ComplexNode* pCloned = pNode->m_pNext; if(pNode->m_pSibling != NULL) { // key point: 复制结点的随机指针指向原始结点的随机指针指向结点的下一个结点 pCloned->m_pSibling = pNode->m_pSibling->m_pNext; } pNode = pCloned->m_pNext; } } // Split a complex list into two: // Reconnect nodes to get the original list, and its cloned list ComplexNode* ReconnectNodes(ComplexNode* pHead) { ComplexNode* pNode = pHead; ComplexNode* pClonedHead = NULL; ComplexNode* pClonedNode = NULL; if(pNode != NULL) { pClonedHead = pClonedNode = pNode->m_pNext; pNode->m_pNext = pClonedNode->m_pNext; pNode = pNode->m_pNext; } while(pNode != NULL) { pClonedNode->m_pNext = pNode->m_pNext; pClonedNode = pClonedNode->m_pNext; pNode->m_pNext = pClonedNode->m_pNext; pNode = pNode->m_pNext; } return pClonedHead; } // Clone a complex linked list with head pHead ComplexNode* Clone(ComplexNode* pHead) { CloneNodes(pHead); ConnectSiblingNodes(pHead); return ReconnectNodes(pHead); }
来自:http://zhedahht.blog.163.com/blog/static/254111742010819104710337/
七、对链表进行排序
typedef ListNode * List; //采用插入法将单链表中的元素排序。 void InsertionSort(List & L) { List h = L->next; // 原链表 L->next = NULL; // 新空表 List s = NULL, p = NULL; while(h) { // 从原链表中取下结点s s = h; h = h->next; // 在新表中查找插入位置 p = L; while(p->next && p->next->data <= s->data) p = p->next; // 在p之后插入s s->next = p->next; p->next = s; } } //采用选择法将单链表中的元素排序。 void SelectionSort(List & L) { List p = L; List q = NULL, s = NULL, m = NULL; while(p->next) { // 选择最小(从p->next至表尾) q = p; // 最小元素的前驱q s = p; while(s->next) { if(s->next->data < q->next->data) q = s; s = s->next; } m = q->next; // 找到最小m // 最小元素m插入有序序列末尾(p之后) if(q != p) { q->next = m->next; // 解下最小m m->next = p->next; // 插入p之后 p->next = m; } p = p->next; // L->next至p为有序序列 } }
参考:
《程序员面试攻略》
《剑指offer》