反转链表是一道很经典的题目,曾经在微软面试题中出现过。本篇主要详细讲解三指针法,话不多说我们先来看下这道题目:
给定一个常数 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,即最后不到 K 个元素不反转。
输入格式:
每个输入包含 1 个测试用例。每个测试用例第 1 行给出第 1 个结点的地址、结点总个数正整数 N (≤105)、以及正整数 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
这道题比较麻烦的是它并没有给你真正(存储结构上)的链表, 所以结点的结构要根据它给的数据来下定义:
struct Node {
int data;
string addr;
string next;
Node() { }
Node(int _data, string _addr, string _next) {
data = _data; addr = _addr; next = _next;
}
};
写构造函数是为了方便我之后的结点初始化,如果这里构造函数看不懂也没关系,你也可以不写,就是在后面初始化结点的时候一个一个数据赋值也是一样的。
大体思路
我们先学会反转整个链表,然后反转k个结点其实就是把k个结点看成一个链表,将这些链表进行反转,就是分而治之的思想,再将这些链表链接起来。这是大体思路。
另外有一个棘手的问题是这里的结点“地址”并非真实的地址,因此无法直接引出当前结点的下一个结点,换句话说,结点与结点之间连接的桥梁没有了,我们需要自己给它再连接上,这里我采用的办法是哈希表(unordered_map)。
然后我先拿输入样例来生成结点和构建哈希表:
//这里的代码只是拿输入样例的结点数据来测试和教学,并非真正的AC代码
//后面的同理
int main()
{
int data;
string addr, next;
Node* node = new Node[6];//用数组来保存所有结点真正的地址
for (int i = 0; i < 6; i++)
{
cin >> addr >> data >> next; //这里输入的是那6组结点的数据,从输入样例复制粘贴
node[i] = Node(data, addr, next); //每输入一组数据就拿来初始化一个结点
}
Node start;
for (int i = 0; i < 6; i++)
if (node[i].addr == "00100") //寻找头结点
start = node[i];
unordered_map<string, int> map; //声明哈希表
for (int i = 0; i < 6; i++)
map[node[i].addr] = i; //将每个结点的“地址”与其在数组中的下标进行关联
Node p = start;
for (int i = 0; i < 6; i++)
{
cout << p.data << " ";
if (p.next == "-1")
continue;
p = node[map[p.next]]; //利用哈希表就可以很方便地找到下一个结点了
}
cout << endl;
return 0;
}
三指针法
然后我们就要开始写一个反转链表的函数了。在此之前我们先理解三指针法。在写这篇文章之前其实我用过三指针法几次但我差点忘了三指针法是怎样的了,我更希望大家能理解为什么一定要三个指针而不是两个,每个指针存在的意义,这样才能对该方法理解得更深刻不容易忘。
首先,我们要对三指针法有个大概的认识,三指针法是用三个指针指向三个原本连续的结点,将中间结点指向前一个结点,然后三指针各向后移一位,重复该过程,直至中间指针指到最后一个结点。如图所示:
相信这张图很直观地描述了三指针法,然后接下来我们来了解几个概念,这些概念会更方便我之后的讲解和你们的理解。
首先是(被动)被链接者,就是被指向的对象。比如上图的第三次操作中,1结点就是被链接者,而P1永远指向被链接者。
其次是(主动)链接者,就是主动指向被链接者的对象,比如上图的第三次操作中的2结点,而P2永远指向链接者。
最后是准链接者,就是下一个要变成链接者的对象,同时这个对象必须是剩余链表的头结点,比如上图的第三次操作中的3结点。
那么循环的过程我们可以归纳为:链接者链接被链接者,当前链接者变成被链接者,当前准链接者变成链接者,当前准链接者更新为它指向的下一个结点。把这个循坏带入上图初始过程看看是否正确。
下面我会详细地讲解为什么需要三个指针而不是两个,每个指针的作用(会有点啰嗦,主要是加深理解,可跳过)
首先,理解了我说的循环过程后你会发现每轮循环中当前被链接者是上一轮循环的链接者,所以当前被链接者的next指针已经被改写指向前一个结点了(比如说第三次操作中的1结点在当前循环中是被链接者,但在上次循环中是链接者且已经完成了链接,这就导致了每次循环过程中的被链接者的next并不指向后一个结点,于是后一个结点的地址丢失。后面括号里的都是拿第三次操作举例),
所以我们要赶在被改写前把那个时候的Next指针记录下来(于是需要P2指向2结点)以做当前循环的链接者,P1P2解决了链接者与被链接者的地址问题。
但还有一个问题,下次循环中我的链接者是谁呢?被链接者肯定是要更新为链接者的,所以这里我们要引入一个准链接者来被更新为新的链接者,也就是当前链接者(还没链接时)指向的下一个结点(即3结点),可是循环过程中我们要链接(P2指向P1)的呀,必然会造成下一个结点的丢失,所以我们要赶在链接前记录这个结点,于是有了P3.
下面给出反转链表的函数
Node* reverselist(Node* arr,Node* start,unordered_map<string,int> &map) {
//arr是Node数组,start是头结点地址
//这里p1是pre,p2是cur,p3是suc
Node* pre = new Node(-1,"-1","NULL"), * cur = start, * suc = &arr[map[start->next]];
while (cur->next != "-1") { //之所以在这里停下是因为map如果引出-1会让arr出错
cur->next = pre->addr; //进行链接
pre = cur; //被链接者更新为当前链接者
cur = suc; //链接者更新为准链接者
suc = &arr[map[suc->next]]; //引入下一个准链接者
}
cur->next = pre->addr;
return cur; //返回当前头结点
}
然后我们在main函数里再添加几句来检测是否完成反转:
Node* root = reverselist(node, &start, map);
for (int i = 0; i < 6; i++)
{
cout << root->data << " ";
if (root->next == "-1")
continue;
root = &node[map[root->next]];
}
结果证明是OK的:
分而治之
很不幸,刚实现完反转链表,我们又要对它进行改造。为了实现分而治之,我的想法是将其改造成递归。大致思路是这样的:函数多传进来一个参数k,从头结点开始循环k次找到第k个结点,如果这个过程中没遇到结点next为-1的情况就反转当前k个结点,否则再判断当前结点个数是否满足k来判断是否要反转结点。然后递归返回的头结点“地址”我们要拿上一个递归实例反转完后的尾结点来指向它。
//改造后的函数,此为AC代码的函数
Node* reverseK(Node* arr,Node* start,unordered_map<string,int> &map,int k) {
Node* pre = new Node(-1,"-1","Null"), * cur = start, * suc = &arr[map[start->next]];
Node* p = cur;
for (int i = 0; i < k; i++){
if (p->next == "-1") //到达递归基
{
if (i == k - 1) { //题目说最后不到K个的元素不反转
for (int j = 0; j < i; j++) //如果==K 反转链表
{
cur->next = pre->addr;
if (cur->next == "-1")
arr[map[cur->addr]].next = "-1";//同下面的坑
pre = cur;
cur = suc;
if (suc->next == "-1")
break;
suc = &arr[map[suc->next]];
}
cur->next = pre->addr;
}
return cur; //返回头结点
}
p = &arr[map[p->next]];
}
for (int i = 0; i < k-1;i++) { //反转当前从start开始的k个结点
cur->next = pre->addr;
pre = cur;
cur = suc;
suc = &arr[map[suc->next]];
}
cur->next = pre->addr;
int i = map[start->addr]; //这里很坑,本来我想用start->next来直接接收的
arr[i].next = reverseK(arr, suc, map, k)->addr;//但不知为啥跳出函数就出意外了,只能用这种奇淫技巧
return cur;
}
然后与main搭配使用效果更棒哦
int main()
{
//各种变量声明
int data,n,k;
string startaddr,addr, next;
cin >> startaddr >> n >> k;
//初始化结点数组
Node* node = new Node[n];
for (int i = 0; i < n; i++)
{
cin >> addr >> data >> next;
node[i] = Node(data, addr, next);
}
Node start;
for (int i = 0; i < n; i++)
if (node[i].addr == startaddr) //寻找头结点
start = node[i];
unordered_map<string, int> map;
for (int i = 0; i < n; i++)
map[node[i].addr] = i; //建立“地址”与下标的关联
Node *p = &start;
if (k == 1) {
for (int i = 0; i < n; i++)
{
cout << p->addr << " " << p->data << " " << p->next << endl;
if (p->next == "-1") break;
*p = node[map[p->next]];
}
return 0;
}
p = reverseK(node, &start, map,k);
for (int i = 0; i < n; i++)
{
if (i == n - 1) {
cout << p->addr << " " << p->data << " " << p->next;
break;
}
cout << p->addr << " " << p->data << " "<<p->next << endl;
if (p->next == "-1")
break;
*p = node[map[p->next]];
}
return 0;
}