banner
NEWS LETTER

链表综合题解析

Scroll down

【题目1】试编写算法将带头结点的单链表就地逆置,所谓“就地”是指辅助空间的复杂度为O(1)

【分析】就地逆置意味着不可以再额外申请空间进行处理,而必须从链表本身做文章。可以将头结点分离出来,然后按照头插法,进行逆置操作;也可以灵活运用多个指针遍历反转next,进行更加符合“就地“原则的逆置操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 法一:头插法
void reverse_1(LinkList L) {
LNode *p, *r;
p = L->next;
L->next = NULL;
while(p) {
r = p->next; // 保存p的下一个结点,方便后续遍历
p->next = L->next;
L->next = p;
p = r; // p后移
}
}

// 法二:遍历,反转next
void reverse_2(LinkList L) {
LNode *r, *p, *t; // r 为当前结点的前一个结点,t为当前结点的后一个结点
p = L->next; // p初始为起始点
t = p->next;
p->next = NULL; // 先处理第一个结点
while (t) {
r = p;
p = t;
t = t->next;
p->next = r;
}
L->next = p; // 将链表指针指向首结点p
}

【题目2】给定两个单链表,试分析找出两个链表的公共结点的思想。

【分析】这一题需要理解“公共结点“的含义,不同于数组存储的顺序表中某两个位置的值相等,链表中的公共元素意味着数据域和指针域都相等,也就意味着指向内存中同一个结点,而这一结点之后的所有结点都是一样的(每个结点有且仅有一个后继)。

明白了这个概念后,我们来思考一下,如果两个链表的长度相同,那么同时向后遍历,每次遍历都比较,如果结点相同则返回。但是如果两个链表长度不同又该如何处理呢?那么就先遍历两个链表,计算两者的长度,长的链表先进行遍历,遍历步数为两者长度之差,这时两者剩余的结点长度相同,同时遍历即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
LNode *findCommonNode(LinkListt A, LinkList B){
// 遍历两个链表,计算长度
int len_a = 0, len_b = 0;
LNode *pa = A;
LNode *pb = B;
while(pa){
pa = pa->next;
len_a++;
}
while(pb){
pb = pb->next;
len_b++;
}
pa = A;
pb = B;
// 长的链表先指针后移
if (len_a > len_b){
int diff = len_a - len_b;
while(diff--) {
pa = pa->next;
}
}
if (len_b > len_a) {
int diff = len_b - len_a;
while(diff--) {
pb = pb->next;
}
}

// 同时开始遍历
while(pa && pb) {
pa = pa->next;
pb = pb->next;
if (pa == pb) {
return pa;
}
}
// 找不到公共结点,返回NULL
return NULL;
}

【题目3】设有一个带头结点的非循环双链表,其每个结点中除有pre、data和next域外,还有一个访问频度域freq,其值均初始化为零。每当在链表中进行一次Locate(L, x)运算时,令值为x的结点中freq域的值增1,并使此链表中的结点保持按访问频度递减的顺序排列,且最近访问的结点排在频度相同的结点之前,以便使频繁访问的结点总是靠近表头。试编写符合上述要求的Locate(L, x)函数,返回找到结点的地址,类型为指针型。

【分析】本题中的Locate函数不同于一般的根据值寻找位置的函数,不仅需要返回元素的位置,而且在这之前需要对链表中结点的freq域作相应的修改,使得频繁访问的结点总是靠近表头,在每次调用函数Locate寻找到结点时,需要先将结点前移,然后返回其地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DNode *Locate(DLinkList &L, ElemType x) {
DNode *p = L->next;
while (p && p->data != x) p = p->next;
if (!p) return NULL;
else {
// 找到结点
// 修改freq
p->freq++;
// 摘下p所指结点
p->pre->next = p->next;
p->next->pre = p->pre;
// q指向p的前一个结点
DNode *q = p->pre;
// 进行前移
while (q != L && q->freq <= p->freq) q = q->pre;
// 在p前面进行插入
q->next->pre = p;
p->pre = q;
p->next = q->next;
q->next = p;
return p;
}
}

【题目4】单链表有环,是指单链表的最后一个结点的指针指向了单链表中的某个结点(通常单链表中的最后一个指针域是空的)。试编写算法判断单链表是否存在环。如果有环,返回环的入口结点。

【分析】此题需要用到一些数学推理,先设置两个快慢指针fast,slow,slow每次走一步,fast每次走两步。假设链表有环,那么当slow走到环的入口时,fast已经走到了环内,之后slow与fast均在环内移动。当fast与slow第一次相遇时,slow尚未走完一整个环的距离,记此时slow在环上走的距离为x,链表起点到环的入口的距离为a,则slow走的距离为a+x,而fast走的距离为2(a+x),根据fast与slow的位置关系,fast走的距离也可以表示为a+x+nr,其中r是环的周长,n是fast走过的圈数。将fast的距离的两种表示方式写成等式,可以得到a = n * r - x,或a = (n - 1) * r + (r - x),也即链表起点到环入口的距离等于若干个环的周长加上周长减slow到入口的距离,此时设置两个指针p1,p2,p1指向链表起点,p2指向slow的位置,同时移动, 当p1到达环入口时,p2走完若干圈并多走了r-x的距离,恰好也处于环的入口,此时即找到环的入口位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LNode *FindLoopStart(LNode *head) {
// 设置快慢指针
LNode *fast = head, *slow = head;
while(fast && fast->next) { // 当fast及fast->next不为空时,移动两个指针
slow = slow->next;
fast = fast->next->next;
if (slow == fast) break; // 相遇时,退出循环
}
if (!fast || !fast->next) return NULL;// 链表没有环
// 设置两个指针,分别指向链表起点和slow位置
LNode *p1 = head, *p2 = slow;
while(p1 != p2) {
p1 = p1->next;
p2 = p2->next;
}
// 两指针相遇时,找到环入口
return p1;

}

【题目5】设有一个长度为n(n为偶数)的不带头结点的单链表,且结点值都大于0,设计算法求这个单链表的最大孪生和。孪生和的定义为一个结点值与其孪生结点值之和,对于第i个结点(从0开始),其孪生结点为第n-i-1个结点。

【分析】由于这题给的是链表,无法进行随机访问,所以最简单的暴力解法就是对于每个结点,遍历链表寻找其孪生结点进行求和运算,找出最大值,时间复杂度为O(n2)。可以考虑空间换时间的方法,将链表中的元素存储到数组中,然后利用数组的随机访问的特性进行计算最大值,时间复杂度为O(n),空间复杂度为O(n)。本题亦可以将链表的后半部分进行逆置,然后依次遍历求和,时间复杂度为O(n),空间复杂度为O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 逆置链表后半段
int PairSum(LinkList L) {
// 快慢指针,寻找链表的中间结点
LNode *fast = L, *slow = L;
while(fast != NULL && fast->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
// 此时slow指向链表后移半段的第一个结点
// 对链表后半段进行逆置
LNode *p = slow, *s, *temp = p->next;
p -> next = NULL;
while(temp != NULL) {
// 三个指针依次后移
s = p;
p = temp;
temp = temp->next;
// next指针逆置
p->next = s;
}
// 此时p指向最后一个结点
LNode *q = L; // 前半段第一个节点
int maxSum = 0; // 记录最大值
while(p!=NULL) {
int curSum = p->data + q->data;
if (curSum > maxSum) maxSum = curSum;
p = p->next;
q = q->next;
}
return maxSum;
}

// 数组存储实现
int PairSum_2(LinkList L) {
LNode *p = L;
// 计算链表长度,构建数组
int len = 0;
while (p != NULL) {
p = p->next;
len++;
}
// 这里必须使用动态数组实现
int *arr = (int *) malloc(len * sizeof(int));
// 将链表的数据保存到数组中
p = L;
int idx = 0;
while (p != NULL) {
arr[idx++] = p->data;
p = p->next;
}
// 双指针遍历
int i = 0, j = len - 1;
int maxSum = 0;
while (i < j) {
int curSum = arr[i] + arr[j];
if (curSum > maxSum) maxSum = curSum;
i++;
j--;
}
return maxSum;
}

【题目6 2015统考真题】用单链表保存m个整数,结点的结构为{data,link},且|data| <= n(n为正整数)。现要求设计一个时间复杂度尽可能高效的算法,对于链表中data的绝对值相等的结点,仅保留第一次出现的结点而删除其他绝对值相等的结点。例如,若给定的单链表的head如下:

head -> 21 -> -15 -> -15 -> -7 -> 15

则删除结点后的head为

head -> 21 -> -15 -> -7

【分析】题目中要求时间复杂度尽可能高效,暗示了利用空间换时间的思路来解决问题。单链表中的data存在范围的限制,任意|data|均不超过n,所以可以生成一个长度为n+1的数组,记录链表中绝对值已存在的data,遇到绝对值重复的data则进行删除。时间复杂度为O(n),空间复杂度为O(n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
typedef struct LNode {
int data;
struct LNode *link;
} LNode;
void DeleteSameAbs(LNode *head, int n) {
// 声明长度为n+1的数组,分配内存空间,并初始化为0
int *arr = (int *)malloc((n + 1) * sizeof(int));
for (int i = 0; i < n + 1; i++) {
arr[i] = 0;
}
LNode *p = head, *r;
while(p != NULL) {
int val = p->link->data;
if (val < 0) val = -val;
// 未出现过,则标记为1
if (arr[val] == 0) {
arr[val] = 1;
p = p->link;
}
else { // 否则进行删除
r = p->link;
p->link = r->link;
free(r);
}
}
// 对数组空间进行释放
free(arr);
}

【题目7 2019统考真题】设线性表L=(a1,a2,a3,...an-2,an-1,an)采用带头结点的单链表存储,链表中的结点定义如下:

1
2
3
4
typedef struct node {
int data;
struct node *next;
}NODE;

请设计一个空间复杂度为O(1)且时间上尽可能高效的算法,重新排列L中的各结点,得到线性表L'=(a1,an,a2,an-1,a3,an-2,...)

【分析】观察重新排列后的L',可知,算法将L拆分成了两个部分,将后半部分进行了逆置,然后与前半部分交叉合并。故本题分为三个步骤,先找寻到链表的中间结点,然后将链表的后半部分进行逆置操作,最后将逆置的后半部分交替插入前半部分即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void ChangeList(NODE *h) {
NODE *slow = h, *fast = h;
// 找出中间节点
while (fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
}
// 此时slow指向前半段的最后一个结点
// 逆置后半段链表
NODE *r, *s = slow->next, *t = s->next;
slow->next = NULL;// 将前半段尾结点的next置为空
s->next = NULL;
// 逆置操作
while(t != NULL) {
r = s;
s = t;
t = t->next;
s->next = r;
}// s即为逆置后的首节点

// 交叉归并
NODE *p = h->next; // p 指向前半段第一个结点
NODE *q
while (s != NULL) {
q = s->next; //q指向后半段当前结点的下一个结点
s->next = p->next;
p->next = s;
p = s->next; //更新前半段的当前结点位置
s = q; // 更新后半段的当前结点位置
}
}

链表中一些重要的操作,比如逆置,归并两个链表,双指针(定位链表中间结点)需要非常熟悉地掌握,不必拘泥于书上的代码实现,要在真正理解的基础上形成自己的代码书写风格。

其他文章
10 +
Completed Projects
140 +
Honors and Awards