链表操作在面试过程中也是很重要的一部分,因为它和二叉树一样都涉及到大量指针的操作,而且链表本身很灵活,很考查编程功底,所以是很值得考的地方。下面是本文所要用到链表节点的定义:
template <typename Type> struct ListNode{ Type data; ListNode *next; };
链表的创建可以采用下面的代码,采用尾插法进行链表的创建,返回的链表没有头节点:
/** * Create a list, without head node */ template <typename Type> ListNode<Type> *CreatList(Type *data, int len) { if(data == NULL || len <= 0) return NULL; ListNode<Type> *head, *last; head = new ListNode<Type>; last = head; for (int i = 0; i < len; ++i) { last->next = new ListNode<Type>; last->next->data = data[i]; last = last->next; } last->next = NULL; last = head; head = head->next; delete last; return head; }
单链表的逆序打印就是重表尾开始依次往前打印,直到表头截止,所以可以将链表逆置,然后顺序打印,但这是一种劳民伤财的做法,不仅容易出错,而且还破坏了链表的结构。这里可以采用额外的空间,来保存顺序遍历的节点,在遍历完后,就可以将该辅助空间的值逆序输出,下面是采用stack实现的代码:
/** * reversely print the list * method 1: use the stack */ template <typename Type> void ReversePrintList_1(const ListNode<Type> *head) { if(head == NULL) return; stack<Type> nodeStack; while (head) { nodeStack.push(head->data); head = head->next; } while(!nodeStack.empty()) { cout<<nodeStack.top()<<" "; nodeStack.pop(); } cout<<endl; }
我们知道代码中,递归和栈很多时候可以相互转化,而且通过递归实现的代码会更加简洁。下面是通过递归的方式,来实现上面的功能,代码如下:
/** * reversely print the list * method 2: recursively */ template <typename Type> void ReversePrintList_2(const ListNode<Type> *head) { if(head == NULL) return; ReversePrintList_2(head->next); cout<<head->data<<" "; }
在前面单链表的逆序打印中,有一种方法就是把单链表逆置后,再顺序打印。单链表的逆置最高效的方法,就是顺序扫描链表,然后依次逆置,代码如下:
/** * reverse the list * method 1:sequential scanning */ template <typename Type> ListNode<Type> * ReverseList_1(ListNode<Type> *head) { if(head == NULL) return NULL; ListNode<Type> *pre = NULL; while (head) { ListNode<Type> *nextNode= head->next; head->next = pre; pre = head; head = nextNode; } return pre; }
同样可以通过递归的方式来进行逆置,递归的思想就是:将已经逆置的链表的最后一个节点返回,并把当前节点添到该节点的后面,单面如下:
/** * reverse the list * method 2: recursion */ template <typename Type> ListNode<Type> * ReverseList_2(ListNode<Type> *head) { if(head == NULL) return NULL; ListNode<Type> *newHead; SubReverseList_2(head, newHead); return newHead; } template <typename Type> ListNode<Type> * SubReverseList_2(ListNode<Type> *head, ListNode<Type> *&newHead) { if (head->next == NULL) { newHead = head; return head; } ListNode<Type> *post = SubReverseList_2(head->next, newHead); post->next = head; head->next = NULL; return head; }
题目是:在一个单链表中,通过节点的指针,在O(1)时间删除该节点。该题是一个投机取巧的方法,就是将要删除的节点用下一个节点覆盖,然后删除下一个节点就可以了。但如果该节点时尾节点,O(1)的时间是不成立的。代码如下:
/** * delete a node from list */ template <typename Type> ListNode<Type> * DeleteNode(ListNode<Type> *head, ListNode<Type> *node) { if(head == NULL || node == NULL) return head; //only have one node if (node == head && node->next == NULL) { delete head; return NULL; } //node counts > 1, and delete the tail node if (node->next == NULL) { ListNode<Type> *pre = head; while (pre->next != node) pre = pre->next; delete node; pre->next = NULL; return head; } //other node ListNode<Type> *delNode = node->next; node->data = delNode->data; node->next = delNode->next; delete delNode; return head; }
求单链表的倒数第k个节点,其实是一个很简单的问题,最容易想到的是下面三种方法:
但上面的方法都需要扫描链表超过一次或者是需要O(n)的辅助空间,如果要求只能扫描一遍链表,且是辅助空间为O(1),那么怎么解决呢。这里有一个很巧妙的方法:用两个指针p1,p2,初始都指向第一个节点,指针p1首先向后移动k-1个节点,然后两个指针一起向后移动,直到p1指向尾节点,那么p2所指向的就是倒数第k个节点,代码如下:
/** * return the last k node from list, 1 =< k <= list length */ template <typename Type> const ListNode<Type> * LastKNode(const ListNode<Type> *head, int k) { if(head == NULL || k < 1) return NULL; const ListNode<Type> *ahead, *after; after = ahead = head; for (int i = 0; i < k - 1; ++i) { //the list length less than k if(ahead->next == NULL) return NULL; ahead = ahead->next; } while (ahead->next != NULL) { ahead = ahead->next; after = after->next; } return after; }
将两个排序的单链表合并成一个链表,方法很简单,这里首先创建一个头节点,将合并的链表链接在其后面,以简化代码。代码如下:
/** * merge two sorted list */ template <typename Type> ListNode<Type> * MergeTwoSortedList(ListNode<Type> *H1, ListNode<Type> *H2) { if (H1 == NULL) return H2; if (H2 == NULL) return H1; ListNode<Type> *head, *last; head = new ListNode<Type>; last = head; while (H1 != NULL && H2 != NULL) { if (H1->data <= H2->data) { last->next = H1; last = H1; H1 = H1->next; } else { last->next = H2; last = H2; H2 = H2->next; } } if (H1 != NULL) last->next = H1; else if (H2 != NULL) last->next = H2; H1 = head->next; delete head; return H1; }
如果两个单链表有公共结点,那么它们组成的形状一定是“Y”形的。求它们的第一个公共结点,可以有几种解法。
/** * Find the first common node */ template <typename Type> ListNode<Type> * Find1stCommonNode(ListNode<Type> *h1, ListNode<Type> *h2) { if(h1 == NULL || h2 == NULL) return NULL; int len1, len2; len1 = GetListLength(h1); len2 = GetListLength(h2); if (len1 > len2) { for (int i = 0;i < len1 - len2; ++i) h1 = h1->next; } else { for (int i = 0;i < len2 - len1; ++i) h2 = h2->next; } while (h1 && h1 != h2) { h1 = h1->next; h2 = h2->next; } return h1; } template <typename Type> int GetListLength(const ListNode<Type> *head) { int num = 0; while (head) { ++num; head = head->next; } return num; }
由上面6可知,相交的单链表一定是“Y”形的,所以如果相交,那么最后的一个节点一定相同。所以很简单,代码如下:
/** * judge two list crossing or not */ template <typename Type> bool IsCrossing(ListNode<Type> *h1, ListNode<Type> *h2) { if(h1 == NULL || h2 == NULL) return false; while(h1->next != NULL) h1 = h1->next; while(h2->next != NULL) h2 = h2->next; if(h1 == h2) return true; return false; }
判断单链表是否存在环的思想就是判断遍历的结点是否已经遍历过。那么实现上最简单的就是通过辅助空间来保存已经遍历过的结点,在每遍历一个结点时判断该结点是否已经在空间中,如果在就说明有环,否则把该结点写入辅助空间,直到找到环或访问链表结束。可以通过hashmap来保存访问的结点,查找效率是O(1)。但是需要O(n)的辅助空间。面试官想要的方法是:通过两个指针,分别从链表的头结点出发,一个每次向后移动1步,另一个移动两步,两个指针移动速度不一样,如果存在环,那么两个指针一定会在环里相遇。代码如下:
/** * judge the list has circle or not */ template <typename Type> bool HasCircle(ListNode<Type> *head) { if(head == NULL) return false; ListNode<Type> *fast, *slow; fast = slow = head; while (fast && fast->next != NULL) { fast = fast->next->next; slow = slow->next; if(fast == slow) return true; } return false; }
题目:求链表的中间结点,如果链表的长度为偶数,返回中间两个结点的任意一个,若为奇数,则返回中间结点。
此题的解决思路和第4题求链表的倒数第k个结点很相似。可以先求链表的长度,然后计算出中间结点所在链表顺序的位置。但是如果要求只能扫描一遍链表,如何解决呢?最高效额解法和第4题一样,通过两个指针来完成。用两个指针从链表头结点开始,一个指针每次向后移动两个结点,一个每次移动一个结点,直到移动快的那个指针移到到尾结点,那么慢的那个指针即是所求。代码如下:
/** * get the middle node of list */ template <typename Type> const ListNode<Type> * ListMidNode(const ListNode<Type> *head) { if(head == NULL) return NULL; const ListNode<Type> *fast, *slow; fast = slow = head; while(fast && fast->next != NULL) { fast = fast->next->next; slow = slow->next; } return slow; }
如果要求在链表长度为偶数的情况下,返回中间两个结点的第一个,那么代码中的while循环判断条件可以改为如下:
while(fast && fast->next != NULL && fast->next->next != NULL)
由题4,7,8可知道,在链表的问题中,通过两个的指针来提高效率是很值得考虑的一个解决方案,所以一定要记住这种解题思路,秒杀面试官吧。。。
先写这么多,以后慢慢在加吧, 有新的问题大家可以提出来,一起讨论,共同进步...<^_^>。。。
Date: Sept 4rd, 2013 @lab