随笔-80  评论-24  文章-0  trackbacks-0
面试经典问题:
“逆置一个单链表”
链表结构为:
 
1 typedef struct node
2 {
3       int data;
4       struct node *next;
5 } NODE;

由于题目限制比较少,所以就需要仔细考虑,链表是否有环?如果链表有小环那么链表将不可逆置,因为环的入口节点有两个前驱节点。所有首先应该判断链表是否有小环,如果没有小环才能对链表进行逆置。而如何判断链表是否有环?一种方法是用一个指针遍历链表,每遍历一个节点便对节点做标记,如果节点走到NULL,则说明没有环,如果遇到了已经做过标记的节点,则说明有环,且这个做标记的节点就是入口。但是如果不允许对节点做标记该怎么办呢?另外一种方法是对每个节点的地址做hash,每次访问一个节点的时候都查看hash表,如果该节点的地址不在hash表中,则说明该节点未被访问,直到遇到NULL或者遇到某个节点在hash表中有记录,则该节点则是环的入口。该算法同样有很大的弊病,需要大量内存,因为节点的地址是32位的。下面提供第三种方法:采用两个指针(假设为p1,p2),初始情况下都指向链表头,之后p2每次走2步,p1每次走1步,当p1和p2相交的时候则说明有环,否则当p2走到NULL的时候则说明无环,证明也比较简单。那么如何找到环的入口点呢?这里提供一种方法:当确定有环后,p2在相交处,p1重新指向链表开头,然后每次两个指针都各走一步,这样两个指针再次相遇的时候便是环的入口处。证明如下:
假设从链表头到环的入口长度为l,环的周长为r,p1和p2相遇的点距离环入口为x
则我们有以下结论:
1、第一次相交的时候p1还没有走完整条链表,而p2则在链表的环内转了n圈
2、第一次相交的时候p2走的距离是p1的两倍,因为p2每次走两步而p1每次走一步
这样我们就得到如下公式:
2(x + l) = l + x + nr
则可以得到l = nr - x
这样我们将p1重新放到链表头开始走,那么当p1节点走了l距离的时候,可以知道p2也走了l距离,而l = nr - x,所以p2走了nr - x,而p2从相交点开始走的,该点距离环入口处为x,这样可以知道当p2走了nr - x步后正好就到了环的入口,而p1此时也正在环的入口,可以知道p1和p2正好相遇,证毕。

下面一段代码是用来判断链表是否有环的,包括大环和小环:

 1 node *list_isloop(node *head) {
 2     if (head == NULL) return head;
 3     node *p1 = head, *p2 = head;
 4     do {
 5         p2 = p2->next;
 6         if (p2 == NULL || p2 == head) return p2;
 7         p2 = p2->next;
 8         if (p2 == NULL || p2 == head) return p2;
 9         p1 = p1->next;
10     } while (p1 != p2);
11     p1 = head;
12     while (p1 != p2) { p1 = p1->next; p2 = p2->next; }
13     return p2;
14 }

其实对于单链表的操作,绝大部分都需要先判断链表是否为空,然后判断链表是否呦环,有大环和有小环在有写问题的处理方式未必一样,比如对于链表逆置,链表为空、或者链表有大环,链表都可以正常被逆置,而如果是有小环的链表,则链表不可被逆置。
下面看链表逆置的代码,其中调用了上面判断链表是否有环的函数:

 1 node *list_reverse(node *head) {
 2     if (head == NULL || head->next == NULL) return head;
 3     node *entry = list_isloop(head);
 4     if (entry != NULL && entry != head) return NULL;
 5     node *p1 = head, *p2 = p1->next, *p3 = p2->next;
 6     p1->next = NULL;
 7     while (p3 && p3 != head) {
 8         p2->next = p1; 
 9         p1 = p2;
10         p2 = p3;
11         p3 = p3->next;
12     }
13     p2->next = p1;
14     return p2;
15 }

下面一段代码是用来对单链表测长的,也许要对单链表判断是否有环:

 1 int list_len(node *head) {
 2     int len = 0;
 3     node *p = head, *entry = NULL;
 4     if (head == NULL) return 0;
 5     entry = list_isloop(head);
 6     while (p != entry) { ++len; p = p->next; }
 7     if (entry != NULL)
 8         do { ++len; p = p->next; } while (p != entry);
 9     return len;
10 }

而下面一段代码是用来判断两个单链表是否相交的,该问题有点复杂:
首先,如果其中任意一条链表为空,则不可能相交
其次,如果两个链表都没有环,则判断链表的尾元素是否相同
再次,如果两个链表一个呦环,一个无环,则不可能相交
最后,如果两个链表均有环,则从其中一个入口处开始走一圈,如果中间和另一环的入口相遇则说明两链表相交

 1 int list_intersecting(node *head1, node *head2) {
 2     if (head1 == NULL || head2 == NULL) return 0;
 3     node *entry1 = list_isloop(head1);
 4     node *entry2 = list_isloop(head2);
 5     //两个链表都没有环
 6     if (entry1 == NULL && entry2 == NULL) {
 7         while (head1->next != NULL) head1 = head1->next;
 8         while (head2->next != NULL) head2 = head2->next;
 9         if (head1 == head2) return 1;
10         return 0;
11     }
12     //两个链表都有环
13     else if (entry1 != NULL && entry2 != NULL) {
14         node *tmp = entry1;
15         while (tmp->next != entry1) {
16             if (tmp == entry2) return 1;
17             tmp = tmp->next;
18         }
19         if (tmp == entry2) return 1;
20     }
21     //一个有环一个无环
22     return 0;
23 }

下面是合并两个有序链表,该问题也需要考虑全面,既然有序,则不需要考虑是否有环,但是两个链表是否都是递增或者递减则需要考虑,我们在此不判断两个链表一个一个递增一个递减的情况。下面代码第三个参数代表链表单调性,flag = 0代表递增,flag != 0代表递减:

 1 node *list_merge(node *head1, node *head2, int flag) {
 2     if (head1 == NULL) return head2;
 3     if (head2 == NULL) return head1;
 4     flag = !!flag;
 5     node *head = NULL;
 6     if (flag == 0) {
 7         if (head1->data < head2->data) {
 8             head = head1;
 9             head->next = list_merge(head1->next, head2, flag);
10         }
11         else {
12             head = head2;
13             head->next = list_merge(head1, head2->next, flag);
14         }
15     }
16     else {
17         if (head1->data > head2->data) {
18             head = head1;
19             head->next = list_merge(head1->next, head2, flag);
20         }
21         else {
22             head = head2;
23             head->next = list_merge(head1, head2->next, flag);
24         }
25         return head;
26     }
27 }

对于将链表排序也有一个非常巧妙的地方,我们知道普通数组排序一般采用快排、堆排或者归并排序,而在链表上却不那么容易,因为链表不能直接用索引来寻址,而是采用指针地址索引,所以快排、堆排都失去了优势,但是归并排序却如鱼得水,而且在数组归并排序当中被诟病的空间复杂度O(n)在链表排序当中也降为O(1)。这里面还用到了一个小技巧,就是如何找到链表的中间元素,这里采用两个指针一快一慢的方法:

 1 node *list_sort(node *head, int flag) {
 2     if (head == NULL || head->next == NULL) return head;
 3     flag = !!flag;
 4     node *p1 = head, *p2 = head;
 5     while (p2 != NULL) {
 6         if ((p2 = p2->next) == NULL) break;
 7         if ((p2 = p2->next) == NULL) break;
 8         p1 = p1->next;
 9     }
10     p2 = p1->next;
11     p1->next = NULL;
12     p1 = list_sort(head, flag);
13     p2 = list_sort(p2, flag);
14     return list_merge(p1, p2, flag);
15 }

找到无环链表倒数第k个节点,该问题比较简单,只需要先设一个指针p2预先走k步,然后再让p2每次走一步,同时在链表开头也有一指针p1每次走一步,这样,当p2走到尾部的时候p1则恰好是倒数第k个节点:

 1 node *list_ktailed(node *head, int k) {
 2     if (k < 0) return NULL;
 3     int i = 0;
 4     node *p1 = head, *p2 = head;
 5     for (; i < k; ++i) {
 6         if (p2 == NULL) return NULL;
 7         p2 = p2->next;
 8     }
 9     while (p2 != NULL) { p2 = p2->next; p1 = p1->next; }
10     return p1;
11 }

关于链表问题暂时写到此。
posted on 2012-02-10 15:43 myjfm 阅读(403) 评论(0)  编辑 收藏 引用 所属分类: 笔试+面试总结

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理