单链表逆序(逆置)
逆序操作是单链表操作中比较复杂的操作
设序列为 a 1 , ⋯ , a n a_{1},\cdots,a_{n} a1,⋯,an,逆序得到 a n , ⋯ , a 1 a_{n},\cdots,a_{1} an,⋯,a1,从结果上看, a i a_{i} ai的后继为 a i − 1 a_{i-1} ai−1( i = n , n − 1 , ⋯ , 2 i=n,n-1,\cdots,2 i=n,n−1,⋯,2)
基本思路遍历原序列时,将 a i + 1 a_{i+1} ai+1的后继设置为 a i a_{i} ai,利用循环推进 i i i重复此流程
为了简单起见,假设链表长度足够长
设
p
p
p是
a
i
a_{i}
ai的指针,则p->next
是
a
i
+
1
a_{i+1}
ai+1的指针,如果执行
a
i
+
1
→
a
i
a_{i+1}\to{a_{i}}
ai+1→ai,则p->next=p
,此时是
a
i
+
1
a_{i+1}
ai+1将继续无法访问(原本靠p->next
访问的
a
i
+
1
a_{i+1}
ai+1不可用了),因此需要借助其他辅助指针;
让第二个指针q=p->next
,表示q
是p
的后继,这样
a
i
+
1
→
a
i
a_{i+1}\to{a_{i}}
ai+1→ai后
a
i
+
1
a_{i+1}
ai+1仍然可以通过q
访问,然而
a
i
+
2
a_{i+2}
ai+2不能访问了,所以还需要第三个赋值指针变量r
;
令第三个指针r=q->next
,表示r
是q
的后继
则当p
是
a
i
a_{i}
ai的指针时,q,r
分别是
a
i
+
1
,
a
i
+
2
a_{i+1},a_{i+2}
ai+1,ai+2;如此
a
i
+
1
→
a
i
a_{i+1}\to{a_{i}}
ai+1→ai也不会造成后续
a
i
+
2
,
⋯
a_{i+2},\cdots
ai+2,⋯结点无法访问
a
i
+
1
→
a
i
a_{i+1}\to{a_{i}}
ai+1→ai完成后,为了继续将
a
i
+
2
→
a
i
+
1
a_{i+2}\to{a_{i+1}}
ai+2→ai+1,需要更新三个指针p,q,r
;
p
的移动顺序:
a
1
,
a
2
,
⋯
a_{1},a_{2},\cdots
a1,a2,⋯
q
的移动顺序
a
2
,
a
3
,
⋯
a_{2},a_{3},\cdots
a2,a3,⋯
r
的移动顺序:
a
3
,
a
4
⋯
a_{3},a_{4}\cdots
a3,a4⋯
看似p,q,r
只是是前驱后继的关系,在逆序过程中,
a
i
,
a
i
+
1
a_{i},a_{i+1}
ai,ai+1不一定总是前驱后继的关系,算法中是要把q
的后继改为p
,创建对应辅助指针时p
是用来指示已经被逆序的部分链表的首元指针,随着逆序工作的推进,p
会更新为q
(和p=p->next
不同,p
的后继结点是朝着
a
1
a_{1}
a1的方向去的),q
会更新为r
//初始化
p=head;
q=p->next;
r=q->next;
p=q;//更新已逆序部分的首指针(作为头插法的着陆点)
q=r;//未逆序部分的首指针
r=r->next;//未逆序部分的第二结点指针
算法代码描述
为了便于算法描述,需要做一些调整和非空检查,代码如下
// 逆序链表函数
void reverseList(Node *head)
{
Node *prev = NULL; // 指示被逆序部分的新序列(链表)的表头(从NULL开始,进行头插法);刚开始时没有元素完成逆序,因此已被逆序的部分为空
Node *current = head->next; // 指示逆序进度(当前要被逆序的结点),跳过头结点(从首元开始)
Node *next = NULL; // 作为current的后继(能够直接初始化为next=current->next),因为首元结点还没判断是否非空;
// prev,next在第一次循环中进行第一次更新值
while (current != NULL)
{
next = current->next; // 保存下一个节点
current->next = prev; // 反转指针(将current的后继指针指向prev,current结点的逆序就完成了);注意第一个被反转的结点将作为新序列的尾结点,其后继为NULL,循环的主要工作是从NULL(尾结点开始)找到前驱,然后修改前驱结点的后继指针;
// 推进进度并维护指针间的逻辑关系
prev = current; // 更新prev指针(pre新序列的表头)
current = next; // 向后移动current指针
// next=next->next;//这个语句需要确保next不是NULL,否则会报错;此外,前面next赋值给了current,next=next->next;等价于next=current->next;然而这需要确保current不是NULL,否则会报错,而对于循环而言,while(current!=NULL)正好能做这个检查,所以这个工作就留给下一次循环处理
}//循环的核心是指针反转current->next=prev和current=next推进逆序进度,其余语句维护指针的语言(逻辑关系)以便进行下一个循环
head->next = prev; // 头结点指向新的头部(原尾节点)
}
把啰嗦的注释清理一部分
// 逆序链表函数
void reverseList(Node *head)
{
Node *prev = NULL;
Node *current = head->next; // 指示逆序进度(当前要被逆序的结点),跳过头结点(从首元开始)
Node *next = NULL;
while (current != NULL)
{
next = current->next; // 保存下一个节点
current->next = prev; // 反转指针
prev = current; // 更新prev指针(pre新序列的表头)
current = next; // 向后移动current指针
}//循环的核心是指针反转current->next=prev和current=next推进逆序进度,其余语句维护指针的语言(逻辑关系)以便进行下一个循环
head->next = prev; // 头结点指向新的头部(原尾节点)
}
可执行代码
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct Node
{
int data;
struct Node *next;
} Node;
// 初始化链表并从数组创建链表
Node *createList(int arr[], int size)
{
// 使用尾插法创建链表,创建链表的时间复杂度为O(n),一个尾指针来辅助(否则时间复杂度会上升值O(n^2))
Node *head = (Node *)malloc(sizeof(Node)); // 创建头结点
head->next = NULL; // 初始化头结点的next指针为空
Node *current = head; // 定义辅助的当前指针(或称为尾指针),初始化为指向头结点
for (int i = 0; i < size; i++)
{
Node *newNode = (Node *)malloc(sizeof(Node)); // 创建新节点
newNode->data = arr[i]; // 设置数据
newNode->next = NULL;
current->next = newNode; // 链接新节点到链表尾部
current = newNode; // 移动当前指针到新节点
}
return head; // 返回带头结点的链表
}
// 逆序链表函数
void reverseList(Node *head)
{
Node *prev = NULL; // 指示被逆序部分的新序列(链表)的表头(从NULL开始);刚开始时没有元素完成逆序,因此已被逆序的部分为空
Node *current = head->next; // 指示逆序进度(当前要被逆序的结点),跳过头结点(从首元开始)
Node *next = NULL; // 作为current的后继(能够直接初始化为next=current->next),因为首元结点还没判断是否非空;
// prev,next在第一次循环中进行第一次更新值
while (current != NULL)
{
next = current->next; // 保存下一个节点
current->next = prev; // 反转指针(将current的后继指针指向prev,current结点的逆序就完成了);注意第一个被反转的结点将作为新序列的尾结点,其后继为NULL,循环的主要工作是从NULL(尾结点开始)找到前驱,然后修改前驱结点的后继指针;
// 推进进度并维护指针间的逻辑关系
prev = current; // 更新prev指针(pre新序列的表头)
current = next; // 向后移动current指针
// next=next->next;//这个语句需要确保next不是NULL,否则会报错;此外,前面next赋值给了current,next=next->next;等价于next=current->next;然而这需要确保current不是NULL,否则会报错,而对于循环而言,while(current!=NULL)正好能做这个检查,所以这个工作就留给下一次循环处理
}//循环的核心是指针反转current->next=prev和current=next推进逆序进度,其余语句维护指针的语言(逻辑关系)以便进行下一个循环
head->next = prev; // 头结点指向新的头部(原尾节点)
}
void reverseList2(Node *head)
{
// 逆序过程中分为两部分,一部分是已经逆序处理的部分,另一部分是待逆序的部分
Node *p = head->next; //
Node *q = p->next;
Node *r = q->next;
while (p->next)
{
/* code */
}
}
// 打印链表函数
void printList(Node *head)
{
Node *current = head->next; // 跳过头结点
while (current != NULL)
{
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
// 释放链表内存
void freeList(Node *head)
{
while (head != NULL)
{
Node *temp = head;
head = head->next;
free(temp); // 释放节点内存
}
}
// 测试函数
int main()
{
int arr[] = {1, 2, 3, 4, 5}; // 初始化数组
int size = sizeof(arr) / sizeof(arr[0]); // 计算数组大小
Node *head = createList(arr, size); // 创建链表
printf("原链表: ");
printList(head);
reverseList(head); // 逆序链表
printf("逆序后的链表: ");
printList(head);
freeList(head); // 释放链表内存
return 0;
}
确定单链表中间结点
确定链表的中间指针,可以使用双指针,让两个指针在同一个循环中前进,快指针每次前进2步,慢指针每次前进1步,直到快指针走到尽头为止
// 寻找单链表的中间节点
NODE *findMiddleNode(NODE *head)
{
NODE *slow = head->next; // 慢指针从头结点后面开始
NODE *fast = head->next; // 快指针从头结点后面开始
while (fast != NULL && fast->next != NULL)//fast是尾结点时退出循环
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
可执行代码
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct Node
{
int data;
struct Node *next;
} Node;
// 寻找单链表的中间节点
Node *findMiddleNode(Node *head)
{
Node *slow = head->next; // 慢指针从头结点后面开始
Node *fast = head->next; // 快指针从头结点后面开始
while (fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
// 初始化链表并从数组创建链表
Node *createList(int arr[], int size)
{
Node *head = (Node *)malloc(sizeof(Node)); // 创建头结点
head->next = NULL; // 初始化头结点的next指针为空
Node *current = head; // 定义辅助的当前指针(或称为尾指针),初始化为指向头结点
for (int i = 0; i < size; i++)
{
Node *newNode = (Node *)malloc(sizeof(Node)); // 创建新节点
newNode->data = arr[i]; // 设置数据
newNode->next = NULL;
current->next = newNode; // 链接新节点到链表尾部
current = newNode; // 移动当前指针到新节点
}
return head; // 返回带头结点的链表
}
int main(int argc, char const *argv[])
{
int arr[] = {1, 2, 3, 4, 5}; // 初始化数组
int size = sizeof(arr) / sizeof(arr[0]); // 计算数组大小
Node *head = createList(arr, size); // 创建链表
Node *middle = findMiddleNode(head);
printf("中间节点的数据: %d\n", middle->data);
freeList(head); // 释放链表内存
return 0;
}
综合应用
先观察 L = ( a 1 , a 2 , a 3 , ⋯ , a n − 2 , a n − 1 , a n ) L=(a_{1},a_{2},a_{3},⋯,a_{n-2},a_{n-1},a_n) L=(a1,a2,a3,⋯,an−2,an−1,an)和 L ′ = ( a 1 , a n , a 2 , a n − 1 , a 3 , a n − 2 , ⋯ ) L'=(a_1,a_n,a_2,a_{n-1},a_3,a_{n-2},\cdots) L′=(a1,an,a2,an−1,a3,an−2,⋯),
可以发现 L ′ L' L′是由 L L L摘取第一个元素,再摘取倒数第一个元素⋯依次合并而成的。
用公式描述,就是将 a n − k + 1 a_{n-k+1} an−k+1插入到 i = 2 k i=2k i=2k的位置( k = 1 , 2 , ⋯ , [ n / 2 ] k=1,2,\cdots,[n/2] k=1,2,⋯,[n/2])
虽然访问 i = 2 k i=2k i=2k的位置相对容易(只需要扫描一遍,可以顺路访问到)
然而逆序访问数据(依次访问 a n , a n − 1 , ⋯ a_{n},a_{n-1},\cdots an,an−1,⋯对于链表来说是麻烦和低效率的,如果将序列 L L L中的后半部分逆序,可使得访问 a n , a n − 1 ⋯ , a [ n 2 ] + 1 a_{n},a_{n-1}\cdots,a_{[\frac{n}{2}]+1} an,an−1⋯,a[2n]+1不需要重复扫描
为了方便链表后半段取元素,需要先将L后半段原地逆置,否则每取最后一个节点都需要遍历一次链表。
- 先找出链表L中的中间节点,为此设置两个指针p和q。指针p每次走一步,指针q每次走两步,当指针q到达链尾时,指针p正好在链表的中间节点;
- 然后将L的后半段节点原地逆置;(方法有多种,主要依赖于多个指针辅助)
- 从单链表前后两段中依次各取一个节点,按要求重排。
重排算法实现
下面的片段提供大致的实现,但是部分细节有漏洞,无法实际运行(可运行代码完整版另见下一节可执行代码)
void change_list(NODE*h){
NODE *p,*q,*r,*s;//定义4个辅助指针;
p=q=h;//将p,q都初始化为h
//q前进到速度是p的两倍
while(q->next!=NULL){ //寻找中间节点
p=p->next;
q=q->next;
//为了避免q->next为NULL,q执行第二步前进需要再判断q->next!=NULL,才能再前进一步
if(q->next!=NULL) q=q->next; //条件允许时q再走一步
}//至此,p->next是后半部序列的首元,而q是尾元
q=p->next; //p所指节点为中间节点,其后继赋值给q为后半段链表的首节点
p->next=NULL;//中间结点的后继置为NULL,即L前半段的尾指针的后继为NULL
while(q!=NULL){ //将链表后半段逆置
r=q->next;//q的后继赋值给辅助指针r
q->next=p;
p=q;
q=r;
}
s=h->next; //s指向前半段的第一个数据节点,即插入点
q=p->next; //指向后半段的第一个数据节点
while(q!=NULL){ //将链表后半段的节点插入到指定位置
r=q->next; //指向后半段的下一个节点
q->next=s->next; //将q所指节点插入到s所指节点之后
s->next=q;
s=q->next; //s指向前半段的下一个插入点
q=r;
}
}
第一步找中间节点的时间复杂度为O(n),第二步逆置的时间复杂度为O(n),第三步合并链表的时间复杂度为O(n),所以该算法的时间复杂度为O(n)。
可执行代码
代码和之前的可执行代码有所重复,为了便于读者验证,放在了同一个片段里;
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct Node
{
int data;
struct Node *next;
} Node;
// 初始化链表并从数组创建链表
Node *createList(int arr[], int size)
{
// 使用尾插法创建链表,创建链表的时间复杂度为O(n),一个尾指针来辅助(否则时间复杂度会上升值O(n^2))
Node *head = (Node *)malloc(sizeof(Node)); // 创建头结点
head->next = NULL; // 初始化头结点的next指针为空
Node *current = head; // 定义辅助的当前指针(或称为尾指针),初始化为指向头结点
for (int i = 0; i < size; i++)
{
Node *newNode = (Node *)malloc(sizeof(Node)); // 创建新节点
newNode->data = arr[i]; // 设置数据
newNode->next = NULL;
current->next = newNode; // 链接新节点到链表尾部
current = newNode; // 移动当前指针到新节点
}
return head; // 返回带头结点的链表
}
// 打印链表函数
void printList(Node *head)
{
Node *current = head->next; // 跳过头结点
while (current != NULL)
{
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
void printList_NoHeader(Node *head)
{
Node *current = head; // 跳过头结点
while (current != NULL)
{
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
// 释放链表内存
void freeList(Node *head)
{
while (head != NULL)
{
Node *temp = head;
head = head->next;
free(temp); // 释放节点内存
}
}
void change_list(Node *h)
{
Node *p, *q, *r, *s; // 定义4个辅助指针;(前2个确定中间结点和尾结点);r辅助逆序后半段链表,s用于后边段链表的遍历指针
p = q = h; // 将p,q都初始化为h
printList(h);
// q前进到速度是p的两倍(p是慢指针,q是快指针)
while (q->next != NULL)
{ // 寻找中间节点(这里p在结点数量为奇数时n/2+1)
p = p->next;
q = q->next;
// print p,q
// printf("p,q: %d,%d\n", p->data, q->data);
// 为了避免q->next为NULL,q执行第二步前进需要再判断q->next!=NULL,才能再前进一步
if (q->next != NULL)
q = q->next; // 条件允许时q再走一步
// printf("p,q: %d,%d\n", p->data, q->data);
// printf("\tq:%d\n", q->data);
} // 至此,p->next是后半部序列的首元,而q是尾元
// printList_NoHeader(p->next);
// printList(h);
q = p->next; // p所指节点为中间节点,其后继赋值给q为后半段链表的首节点
p->next = NULL; // 中间结点的后继置为NULL,即L前半段的尾指针的后继为NULL;此后p作为已逆序的部分链表的首元指针,作为头插法着陆点
while (q != NULL)
{ // 将链表后半段逆置
r = q->next; // q的后继赋值给辅助指针r
q->next = p; // 未逆序的部分链表首元后继改为已逆序部分的首元
// 维护指针间的逻辑关系,以便下一轮操作
p = q; // 迭代已逆序部分的首元指针
q = r; // 迭代未逆序部分的首元指针
} // p此时指向后半段链表的首元(尾元是前半段的尾元(重叠))
printf("reverse result of first half list:");
printList(h);
printf("reverse result of later half list:");
printList_NoHeader(p);
/* 准备重排 */
// 准备两段链表的首元指针
s = h->next; // s指向前半段的第一个数据节点,即插入点
q = p; // 指向后半段的第一个数据节点
while (s->next != NULL) // 第一个链表长度可能短一个元素(总链表长度为偶数的时候);为了防止循环内赋值空指针造成segment fault(运行时难以察觉的错误),这里用s->next!=NULL判断是否进入循环;
{ // 将链表后半段的节点插入到指定位置
r = q->next; // 将q插入到前半段链表前,标记q的后继,即指定指向后半段的下一个节点备用
q->next = s->next; // s后继交接给q:将q所指节点插入到s所指节点之后
s->next = q; // s的新后继为q;
printf("s,q: %d,%d\n", s->data, q->data);
// 更新下一个带插入结点的前驱
s = q->next; // s指向前半段的下一个插入点
q = r; // 更新第二段链表中下一个要插入的节点(第二段链表的首元)
// print s,q
// printf("s,q: %d,%d\n", s->data, q->data);
}
// 退出时第二段链表可能尾结点多出来了, 实际上两段链表尾结点重合(相同),检查第一段尾结点取值和第二段相同位序结点q数据是否相同,如果不同,那么就接上,并且尾结点置空,否则就不用接了(第一段的尾结点作为做终链表的尾结点,其后继为NULL)
if (s->data != q->data)
{
q->next = NULL;
s->next = q;
}
// if()
printf("resort res:\n");
printList(h);
}
int main(int argc, char const *argv[])
{
// int arr[] = {1, 2, 3, 4, 5, 6, 7, 8}; // 初始化数组(偶数个结点)
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; //奇数个结点
int size = sizeof(arr) / sizeof(arr[0]); // 计算数组大小
Node *head = createList(arr, size); // 创建链表
change_list(head);
// printList(head);
freeList(head); // 释放链表内存
return 0;
}