typedef struct node {
char *data;
struct node *next;
} node_t;
我们约定一个打印链表的函数:
void list_display(node_t *head)
{
for (; head; head = head->next)
printf("%s ", head->data);
printf("\n");
}
下面是几个常见的链表笔面问题:
1.计算链表长度
很简单:(复杂度O(n))
int list_len(node_t *head)
{
int i;
for (i = 0; head; head = head->next, i++);
return i;
}
测试:
int main(int argc, const char *argv[])
{
node_t d = {"d", 0}, c = {"c", &d}, b = {"b", &c}, a = {"a", &b};
printf("%d\n", list_len(&a));//4
return 0;
}
2.反转链表
我们多用几个指针就可以在O(n)完成反转任务:
算法:t遍历链表, q记录t的上一个结点, p是一个临时变量用来缓存t的值
void reverse(node_t *head)
{
node_t *p = 0, *q = 0, *t = 0;
for (t = head; t; p = t, t = t->next, p->next = q, q = p);
}
测试:
node_t d = {"d", 0}, c = {"c", &d}, b = {"b", &c}, a = {"a", &b};
list_display(&a);
reverse(&a);
list_display(&d);
3.查找倒数第k个元素(尾结点记为倒数第0个)
算法:2个指针p, q初始化指向头结点.p先跑到k结点处, 然后q再开始跑, 当p跑到最后跑到尾巴时, q正好到达倒数第k个.复杂度O(n)
node_t *_kth(node_t *head, int k)
{
int i = 0;
node_t *p = head, *q = head;
for (; p && i < k; p = p->next, i++);
if (i < k) return 0;
for (; p->next; p = p->next, q = q->next);
return q;
}
测试:
node_t d = {"d", 0}, c = {"c", &d}, b = {"b", &c}, a = {"a", &b};
printf("_0 :%s _1: %s _2:%s _3:%s\n", _kth(&a, 0)->data, _kth(&a, 1)->data, _kth(&a, 2)->data, _kth(&a, 3)->data);
输出:
_0 :d _1: c _2:b _3:a
4.查找中间结点
找出中间的那个结点
算法:设两个初始化指向头结点的指针p, q.p每次前进两个结点, q每次前进一个结点, 这样当p到达链表尾巴的时候, q到达了中间.复杂度O(n)
node_t *middle(node_t *head)
{
node_t *p, *q;
for (p = q = head; p->next; p = p->next, q = q->next){
p = p->next;
if (!(p->next)) break;
}
return q;
}
测试:
node_t e = {"e", 0}, d = {"d", &e}, c = {"c", &d}, b = {"b", &c}, a = {"a", &b};
printf("%s\n", middle(&a)->data);
5.逆序打印链表
给你链表的头结点, 逆序打印这个链表.使用递归(即让系统使用栈), 时间复杂度O(n)
void r_display(node_t *t)
{
if (t){
r_display(t->next);
printf("%s", t->data);
}
}
6.判断一个链表是否有环
如果一个链表有环, 那么它肯定只有一个环.(一个相交结点)
算法:设两个指针p, q, 初始化指向头.p以步长2的速度向前跑, q的步长是1.这样, 如果链表不存在环, p和q肯定不会相遇.如果存在环, p和q一定会相遇.(就像两个速度不同的汽车在一个环上跑绝对会相遇).复杂度O(n)
int any_ring(node_t *head)
{
node_t *p, *q;
for (p = q = head;p; p = p->next, q = q->next){
p = p->next;
if (!p) break;
if (p == q) return 1; //yes
}
return 0; //fail find
}
测试:
node_t e = {"e", 0}, d = {"d", &e}, c = {"c", &d}, b = {"b", &c}, a = {"a", &b};
e.next = &a;
printf("%d\n", any_ring(&a));
7.找出链表中环的入口结点
先看下面这张图:
我们设链表的无环的部分长度为L1,即有L1个节点,注意,这个L1是包括环的入口节点的。然后让环的长度是L2,这个L2也是包括环的入口节点。这个时候,p1和p2的交点如图所示,交点距离环的入口节点为a(从入口节点沿着行走方向走到交点),即在环的入口节点后面的第a个节点,就是交点,我用红色标记出a。
然后我们来考察一下L1,L2,a,以及n(n是走过的步数,不是走过的节点数,p1一步一个节点,p2一步两个节点)的关系。
忘记说一点了,我们可以明确的是,p1在进入环后,走了不到一圈就在交点处和p2重合,為什麼肯定没有走完一圈?因为p1在进入环的时候,p2和p1之间的距离(沿着行走方向)至多为 L2-1,不可能超过L2-1,因为环的大小也才只有L2 。p2追赶p1,最多只需要走L2-1步,因为每走一步,p1和p2的相对距离减小1,那么p1最多只走了L2-1步,就是最多只经过了L2-1个节点,不可能走完一圈。
现在可以列公式了:
L1+a=n #1 //n是p1走过的节点数
L1+k*L2+a=2*n #2 //2*n这个是p2走过的节点数,其中的k表示p2可能在环里面走了k圈,k>=1
由#2式减去#1式,有:
k*L2 = n #3
同时由#1和#3得到:
L1+a = k*L2 #4
接着由#4就得到了如下式:
L1 = k*L2 - a = (k-1)*L2 + (L-a)
得到这条式子就拨得云开见月明啊有木有,因为(L-a)表示的是交点与环入口的距离(从交点沿着行走方向到环入口),然后(k-1)是>=0的,因为p2在环中至少绕了一圈,这样我们就发现:L1的长度 = 环长度的整数倍 + 交点与环入口的距离
也就是说,p1再走L1步就可以达到环的入口。问题是L1不是已知的,没关系,在表头设置一个p3指针,p3每步前进一个节点。让p1和p3同时走,每次走1步,等p3和p1重合了,就是到了环口的位置了。