对于无环单链表,计算其结点个数是相当简单的。C代码如下:
int get_length(list_t *head) { int len = 0; for (list_t *p = head; p != NULL; p = p->next) len++; return len; }
那么对于有环的单链表,统计其总的结点个数,就变得复杂一些。
- 首先,统计在环上的总的结点个数,不妨记为N1;
- 其次,找到环的入口结点,不妨记为Joint;
- 然后,从单链表的头结点开始到Joint结束,统计这一线性链表上的总的结点个数(不包括Joint),不妨记为N2;
- 最后,N1+N2就是有环单链表的总的结点个数。
对应的C代码如下:
1 int get_total_length(list_t *head) 2 { 3 int len = get_loop_length(head); 4 5 list_t *joint = (len != 0) ? get_loop_joint(head) : NULL; 6 7 for (list_t *p = head; p != joint; p = p->next) 8 len++; 9 10 return len; 11 }
其中,
- L3的get_loop_length()是获取环上的总的结点个数。
- L5的get_loop_joint()是获取环的入口结点。
在给出get_loop_length()和get_loop_joint()的实现之前,我们不妨先看看如何判断一个单链表有环。
1. 判断一个单链表有环
通常的做法就是使用快慢两个指针,把探索单链表是否有环的过程演绎成一个追击问题。快指针和慢指针都从链表头开始移动,慢指针每次移动一步,而快指针每次移动两步,如果快指针能够追上慢指针,则说明有环。C代码如下:
1 /** 2 * Detect a singly linked list has a loop 3 */ 4 bool is_loop(list_t *head) 5 { 6 list_t *fast = head; 7 list_t *slow = head; 8 9 /* 10 * If loop does not exist, fast should firstly reach the end 11 * a) if the length of list is even, fast will be NULL 12 * b) if the length of list is odd, fast->next will be NULL 13 */ 14 while (fast != NULL && fast->next != NULL) { 15 fast = fast->next->next; 16 slow = slow->next; 17 18 /* 19 * Well, loop is found as the fast catches up with the slow 20 */ 21 if (fast == slow) 22 return true; 23 } 24 25 return false; 26 }
2. 统计有环单链表的环上结点个数
方法还是使用快慢指针。首先使用快慢指针定位一个环上的结点,然后从此结点的下一个结点开始,单步移动指针完成统计。C代码如下:
1 /** 2 * Get the length of the loop if a singly linked list has a loop 3 */ 4 int get_loop_length(list_t *head) 5 { 6 list_t *fast = head; 7 list_t *slow = head; 8 list_t *node = NULL; 9 10 /* get a node in the loop */ 11 while (fast != NULL && fast->next != NULL) { 12 fast = fast->next->next; 13 slow = slow->next; 14 15 if (fast == slow) { 16 node = slow; 17 break; 18 } 19 } 20 21 /* no loop found hence the length should be zero */ 22 if (node == NULL) 23 return 0; 24 25 /* now walk again to get the length of the loop */ 26 int len = 1; 27 for (list_t *p = node->next; p != node; p = p->next) 28 len++; 29 return len; 30 }
3. 获取有环单链表的入口结点
这个算法稍微复杂一点,当然还是使用快慢指针。首先使用快慢指针定位环上的一个结点,不妨记为Node; 然后让快指针从链表头Head开始单步移动,同时慢指针从Node开始单步移动。一旦快慢指针相遇,我们就找到了入口结点Joint。为帮助理解,首先假设:
- Head到Joint的长度记为X
- Joint到Node的长度记为Y1
- Node到Joint的长度记为Y2
然后用一个图给出X=Y2的证明。(图是本人用LibreOffice画的,转载请注明出处)
C代码如下: (注释中也给出了严格的数学证明)
1 /** 2 * Get the joint if a singly linked list has a loop 3 */ 4 list_t * 5 get_loop_joint(list_t *head) 6 { 7 list_t *fast = head; 8 list_t *slow = head; 9 list_t *node = NULL; 10 11 /* get a node in the loop */ 12 while (fast != NULL && fast->next != NULL) { 13 fast = fast->next->next; 14 slow = slow->next; 15 16 if (fast == slow) { 17 node = slow; 18 break; 19 } 20 } 21 22 /* no loop found hence the joint should be NULL */ 23 if (node == NULL) 24 return NULL; 25 26 /* 27 * The slow walks the loop from the node, and let the fast walk the 28 * list from its head. They should meet at the joint. 29 * 30 * Head Joint 31 * | | 32 * O-->O-->O-->O<--O<--O 33 * | ^ 34 * V | 35 * O-->O-->O 36 * 37 * Node 38 * 39 * x : The length from Head to Joint 40 * y1: The length from Joint to Node 41 * y2: The length from Node to Joint 42 * 43 * Total steps of the slow walked: x + y1 44 * Total steps of the fast walked: x + y1 + y2 + y1 45 * Note the fast and the slow have the same times to move, 46 * So x + y1 == (x + y1 + y2 + y1) / 2 47 * ==> 2x + 2y1 == x + 2y1 + y2 48 * ==> x == y2 49 */ 50 fast = head; 51 slow = node; 52 while (fast != slow) { 53 fast = fast->next; 54 slow = slow->next; 55 } 56 57 return slow; 58 }
到此为止,我们就用Top-down的方法把有环单链表的结点个数的统计方法解释清楚了,完整代码请参见这里。也许你会好奇为什么我要研究诸如此类的单链表问题,答案之一就是那些装13的公司(比如BAT)特别喜欢考算法,吼吼:-)