本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下:
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-应用实例—>多项式加法运算
数据结构基础:P2.5-应用实例—>多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
数据结构基础:P4.2-树(二)—>二叉平衡树
数据结构基础:P4.3-树(二)—>小白专场:是否同一棵二叉搜索树-C实现
一、题目描述
题目:给定一个常数K和一个单链表L,把L上所有K个元素的链接都颠倒过来。例如,给定L为1→2→3→4→5→6,如果K=3,那么输出3→2→1→6→5→4;如果K=4,输出4→3→2→1→5→6。
输入格式: 每个输入文件包含一个测试用例。对于每种情况,第一行包含第一个结点的地址,结点的总数为
N
N
N(
N
⩽
1
0
5
N\leqslant10^{5}
N⩽105),要反转的子列表的长度为
K
K
K(
K
⩽
N
K\leqslant N
K⩽N)。结点的地址为5位非负整数,NULL用-1表示。接着是N行,每一行描述一个结点的格式为:Address Data Next,其中Address是结点的位置,Data是一个整数,Next是下一个节点的位置。
输出格式: 对于每种情况,输出得到的有序链表。每个结点占用一行,并用与输入相同的格式打印。
示例输入:
00100 6 4
00000 4 99999
00100 1 12309
68237 6 -1
33218 3 00000
99999 5 68237
12309 2 33218
示例输出:
00000 4 33218
33218 3 12309
12309 2 00100
00100 1 99999
99999 5 68237
68237 6 -1
1.1 什么是抽象的链表
比较有意思的一个问题是:有同学在论坛上面发言时候说,链表这个东西是不是只对C和C++语言的程序才有意义,我用的是JAVA, JAVA里头没有指针,没有指针哪来的链表呢?所以在这里我们很有必要重申一下,到底什么是链表:
我们说链表是一个抽象的数据结构,链表中间的结点需要两个域来存,一块是有地方来存它的基础数据,另外一个很重要的特征是需要有一块地方来存指针,这个指针指向下一个结点。C和C++只是提供了一种特殊的机制可以实现这个指针的功能,一个抽象的指针指的就是地址。任何一种形式去存了下一个结点的位置,这个东西就叫做指针。
我们来看一下我们这道题目的样例输入。第一行是给了这个单链表头结点的位置、链表结点数量、需要反转的结点数量。接着下面几行分别给出了每个结点的位置、在链表中的位次、每个结点下一个结点的位置。那我们在编程实现的时候到底怎么来存这样一个链表呢?
一个非常直截了当的方法就是整个模拟一个真实的链表在内存里面存在的状态。也就是说,我开一个充分大的结构数组来代表内存空间,这种情况下,什么是一个结点所处的位置?结点的位置就是它在这个数组里面的下标,而元素的下标是可以用一个整数来表示的。在这里我们说到一个指针的时候,实际上我们说的是一个整数,这个整数记录的是下一个结点在这个数组里面的下标而已。因此,链表对应的结构如下:
1.2 链表逆转算法
接下来我们来看怎么样实现单链表的逆转。在这个单链表的前面我个人习惯加一个头结点,虽然说在一定程度上是浪费了空间,但是如果这个头结点的空间对你不是那么紧要的话,那么加一个头结点会使后面的很多操作变得比较简单。这是我们原始的链表的形态。
我们希望完成了逆序以后,整个链表应该是长得这个样子的:前面四个结点的方向是从4指到3指到2指到1的,然后这个头结点要指在4这个位置,而1后面接的是剩余部分的这个链表。
那么怎么去实现它呢?
①我们需要两个指针,第一个指针我把它命名叫做
new
,指的是新的那个已经逆转好的链表它的头结点的位置。它的初始的位置显然可以指向第一个结点,我们认为第一个结点是已经逆转好了的。然后另外一个指针叫做old
,它指向的是当前还没有完成逆转的那个老链表的头结点,我们让其初始位置指向new
的后面结点。
②我们来看看反转链表的具体操作:
那我们要做第一件事情就是要把这个2
的指针逆过来指向1
,但是我在做这个操作之前我得先把3
的这个位置给记住。所以我需要另外一个tmp
指针去指向3
,把这个位置记住。否则的话,我把2
的这个指针一转向之后,后面这段链表就被丢掉了。
记住了3
的位置之后我就可以把2
的指针反向, 让它指向1
。
完成了这一步以后我们就可以把这三个指针向后位移。
然后把old
的这个指针转过去指向这个新的头结点
然后继续向后移动,继续执行前面两步的操作。
这样一直重复,直到达到K
个结点,我们这里K
是4
。
停下来以后我们发现:1
的next
指针现在还指向2
,这是不对的,1
应该指向5
。我怎么知道5是什么呢,5就是当前还没有逆转的这个老的链表的头结点。然后我这个空的头结点它应该指向当前这个已经逆转好的链表的头结点,也就是指向这个new
所指的位置。
对应伪代码如下:
Ptr Reverse( Ptr head, int K )
{
cnt = 1;
new = head->next;
old = new->next;
while ( cnt < K ) {
tmp = old->next;
old->next = new;
new = old; old = tmp;
cnt++;
}
head->next->next = old;
return new;
}
1.3 测试数据
我们在设计一道题目的测试数据的时候,至少有两个方面是一定要考虑到的。
最一般的情形
①也就是像我们的样例,给了你一个很平常的链表,然后有一部分是需要反转的,尾巴是不需要反转的。
边界测试
②在题目里面给定的是地址是一个5位的整数,那么我的地址就一定要取到上下界,就是5个0和5个9的情况。
③元素的个数正好是 k 的整数倍,所以你整个的链表每一段都正好需要全反转的。
④K 就等于 N,整个的大链表做一个完整的反转
⑤最小的 k 就是1,就意味着哪个结点都不需要反转。
⑥最大(最后剩K-1不反转)、最小N
⑦有多余结点。加了一些多余的结点让不合适的算法挂掉。
二、代码实现
整体代码如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100001
//节点:包含数据、下一个节点位置
struct node {
int data;
int next;
};
//反转K个节点函数(我这里没有像陈越老师一样新建个空的头节点)
int Reverse(struct node *list, int head, int K)
{
int cnt = 1;
int new = head;
int old = list[new].next;
while (cnt < K) {
int tmp = list[old].next;
list[old].next = new;
new = old; old = tmp;
cnt++;
}
list[head].next = old;
return new;
}
//打印链表
void PrintList(struct node* list, int head)
{
while (list[head].next != -1) {
printf("%05d %d %05d\n", head, list[head].data, list[head].next);
head = list[head].next;
}
printf("%05d %d -1", head, list[head].data); //尾节点
}
int main()
{
//存放各个节点的一个大数组
struct node list[MAXSIZE];
//第一行输入:链表的头节点位置、节点数量、反转节点数量
int HeadPosition;
int N;
int K;
scanf("%d %d %d", &HeadPosition, &N, &K);
//数据读入:读取每个节点
int address, data, next;
for (int i = 0; i < N; i++) {
scanf("%d %d %d", &address, &data, &next);
list[address].data = data;
list[address].next = next;
}
//进行反转操作
int res = Reverse(list, HeadPosition, K);
//打印反转后的链表
PrintList(list, res);
return 0;
}
运行,输入测试案例,结果正确。