前言:
在我们写代码的过程中,经常会碰到需要我们转置链表的情况,这种情况实际上是一个经典题型,也是我们校招面试的高频考题,这篇文章就来带大家全面梳理一下这类题目,以此来加深大家对于链表的理解,也希望大家以后在碰到转置链表这种情况能够从容应对:
创建链表:
在进行链表的转置之前,我们需要先得到一个链表,这里头插和尾插的方式都可以构建链表,为了应对我们转置链表的特殊情况,这里我们构建三条链表,1.空链表 2.只有一个结点的链表 3.正常长度的链表,相信我们对于链表的创建已经非常熟悉了,所以这里直接给出代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
//定义单链表的一个结点
typedef struct SListNode
{
SLDataType val;
struct SListNode* next;
}SLNode;
//创建一个节点
SLNode* BuySListNode(SLDataType x);
//单链表尾插数据
void SListPushBack(SLNode** pphead, SLDataType x);
//打印链表
void SListPrint(SLNode* phead);
SLNode* BuySListNode(SLDataType x)
{
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
if (newnode == NULL)
{
perror("malloc");
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SLNode** pphead, SLDataType x)
{
SLNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
SLNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
void SListPrint(SLNode* phead)
{
SLNode* cur = phead;
while (cur)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
int main()
{
SLNode* plist1 = NULL;
SLNode* plist2 = NULL;
SLNode* plist3 = NULL;
SListPushBack(&plist2, 0);
SListPushBack(&plist3, 1);
SListPushBack(&plist3, 2);
SListPushBack(&plist3, 3);
SListPushBack(&plist3, 4);
SListPushBack(&plist3, 5);
SListPushBack(&plist3, 6);
SListPushBack(&plist3, 7);
SListPushBack(&plist3, 8);
printf("list1:\n");
SListPrint(plist1);
printf("list2:\n");
SListPrint(plist2);
printf("list3:\n");
SListPrint(plist3);
return 0;
}
接下来进入正题
翻转链表:
所谓反转链表,就是使得原来的链表原来各个结点的指向完全翻转,转置过来,例如,
一个为1->2->3->4->5->NULL的链表
翻转后的结果即为:5->4->3->2->1->NULL;
转置链表的方法和思维其实很简单,这里向大家介绍两种方法来实现
1.翻转指针实现反转链表:
//翻转指针法翻转链表
SLNode* SListReverse1(SLNode* phead);
该函数要求翻转链表后返回反转后链表的头结点;
为了实现这个函数,我们需要将第一个结点的next指向NULL,第二个结点的next指向第一个结点,第三个结点的next指向第二个结点…………以此类推;
这样看来我们就需要一个循环,这里特别要注意的是,我们至少需要三个SLNode* 类型的指针变量,那为什么不是两个呢,这里我们来反证一下,假设我们创建了两个指向链表结点的指针变量n1、n2,n1先指向空,n2指向第一个结点,那这个时候当我们让n2->next=n1之后,我们再想进行下一步,就是要使n1指向第一个结点,然后使n2指向第二个结点,再继续让n2->next=n1,前者好办,直接让n1等于n2,那怎么才能让n2指向第二个结点呢?让n2=n2->next? 这是行不通的,因为n2->next已经指向n1,所以我们无法找到第二个结点;
因此,我们还需要第三个SLNode* 类型的指针变量n3,n3的作用即为确定n2的下一个结点;
------------------算法步骤----------------------
1.如果链表中本身无结点或者只有一个结点,转置并不会影响链表的结构,因此,直接返回链表的头结点;
2.当链表中有1个以上的结点,创建三个SLNode* 类型的指针变量n1,n2,n3,使得n1为空,n2指向第一个结点,n2指向第二个结点;
3.创建一个while循环,先让n2->next=n1,再让n1=n2,n2=n3,n3=n3->next,达到后移的效果移到最后我们会发现,当n2为空时,n1恰好走到了链表的最后一个结点,因此n2是否为空就是该循环的结束标志,这里特别要注意,当n3已经走到NULL时,n3就不用继续操作了,否则会造成对空指针的解引用;
4.返回n1结点
来看图示:
再看代码实现:
SLNode* SListReverse1(SLNode* phead)
{
if (phead == NULL || phead->next == NULL)
return phead;//当链表中没有结点或者只有一个结点,无需进行任何操作,直接返回
SLNode* n1 = NULL;
SLNode* n2 = phead;
SLNode* n3 = phead->next;
while (n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if (n3)
n3 = n3->next;
}
return n1;
}
结果展示:
接下来来向大家介绍第二种方法
2.头插法翻转链表:
//头插法翻转链表
SLNode* SListReverse2(SLNode* phead);
这个方法的实现比较直接,是将原链表的结点从头到尾一个一个取出,此时此刻被取出的结点即为头结点,需要将其链接到已取出结点的最前方,最后返回这个新的头结点:
来看图示:
-------------------算法步骤----------------------
1.如果链表中本身无结点或者只有一个结点,转置并不会影响链表的结构,因此,直接返回链表的头结点;
2.当链表中有1个以上的结点,创建三个SLNode* 类型的指针变量cur——用于此时指向的链表结点,next——用于确定cur的下一个结点,newhead——新链表的头结点,先让cur为头结点phead,next为phead->next,newnode置为空
3.创建一个while循环,cur即为我们要取出的结点,让这个被取出的结点的next指向newnode,此时cur即为newnode,然后使得cur=next,实现遍历,当走到最后我们会发现,cur和next会指向NULL,当cur指向空的时候,这就是整个循环结束的标志,特别要注意的是,只有当next不为空时才能对next进行操作,否则会造成对空指针的解引用;
4.返回新的头结点newhead;
来看代码实现:
SLNode* SListReverse2(SLNode* phead)
{
if (phead == NULL || phead->next == NULL)
return phead;
SLNode* cur = phead;
SLNode* next = cur->next;
SLNode* newhead = NULL;
while (cur)
{
cur->next = newhead;
newhead = cur;
cur = next;
if (next)
next = next->next;
}
return newhead;
}
结果展示:
链表内指定区间翻转:
//链表内指定区间翻转
SLNode* SListReverseBetween(SLNode* phead, int m, int n);
描述:
将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n),空间复杂度 O(1)。
例如:
给出的链表为 1→2→3→4→5→NULL 1→2→3→4→5→NULL, m=2,n=4,
返回 1→4→3→2→5→NULL.
这题的难度就开始上升了,不过它的大致思维和算法步骤也是大差不差,相信在我们理解了前面的翻转链表之后,这题我们也有能力将其理解通透;
大致思路:
为了做到链表指定区间的翻转,我们需要找到整个区间的前一个结点(这里用prev表示)和第一个结点(q表示),q需要保存起来,用来链接翻转指定区间后的链表,cur为一个会移动的节点,它需要从翻转区间的第一个结点走到整个区间后的第一个结点,在移动的过程中我们要使cur->next指向prev->next,因为prev的next最后需要指向翻转区间的最后一个结点,因此它的next需要跟随着cur向后走而不断改变,最后cur需要刚好指向整个区间后的第一个结点,prev就指向了翻转区间的最后一个结点;
图示:
我们直接来进行代码实现:
SLNode* SListReverseBetween(SLNode* phead, int m, int n)
{
if (phead == NULL || phead->next == NULL)
return phead;
//先创建一个哨兵位结点,方便定位
SLNode* Head = (SLNode*)malloc(sizeof(SLNode));
if (Head == NULL)
{
perror("malloc");
exit(-1);
}
Head->next = phead;
SLNode* prev = Head;
int i = 0;
for (i = 0; i < m - 1; i++)
{
//用于找到翻转区间的前一个结点
prev = prev->next;
}
//翻转区间的第一个结点需要保存下来
SLNode* p = prev->next;
SLNode* cur = prev->next;
SLNode* q = cur->next;//q为了辅佐cur向前走
for (i = 0; i < n - m + 1; i++)
{
cur->next = prev->next;
prev->next = cur;
cur = q;
if(q)
q = q->next;
}
p->next = cur;
return Head->next;
}
注意这里使用到了哨兵位结点,它可以更方便我们定位到prev和cur,哨兵位的使用也值得大家参考;
以上即为这篇文章的全部内容,希望对大家会有所帮助,如果我们追求严谨,我们还可以设计一个销毁链表的函数,防止造成内存泄漏,这个函数的实现也非常简单,这里就不再演示了,接下来将整个程序实现的代码全部展示给大家,大家可以以此为参考,多写代码才能熟悉代码:
程序代码:
头文件部分:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
//定义单链表的一个结点
typedef struct SListNode
{
SLDataType val;
struct SListNode* next;
}SLNode;
//创建一个节点
SLNode* BuySListNode(SLDataType x);
//单链表尾插数据
void SListPushBack(SLNode** pphead, SLDataType x);
//打印链表
void SListPrint(SLNode* phead);
//翻转指针法翻转链表
SLNode* SListReverse1(SLNode* phead);
//头插法翻转链表
SLNode* SListReverse2(SLNode* phead);
//链表内指定区间翻转
SLNode* SListReverseBetween(SLNode* phead, int m, int n);
功能实现部分:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
SLNode* BuySListNode(SLDataType x)
{
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
if (newnode == NULL)
{
perror("malloc");
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SLNode** pphead, SLDataType x)
{
SLNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
return;
}
SLNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
void SListPrint(SLNode* phead)
{
SLNode* cur = phead;
while (cur)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
SLNode* SListReverse1(SLNode* phead)
{
if (phead == NULL || phead->next == NULL)
return phead;//当链表中没有结点或者只有一个结点,无需进行任何操作,直接返回
SLNode* n1 = NULL;
SLNode* n2 = phead;
SLNode* n3 = phead->next;
while (n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if (n3)
n3 = n3->next;
}
return n1;
}
SLNode* SListReverse2(SLNode* phead)
{
if (phead == NULL || phead->next == NULL)
return phead;
SLNode* cur = phead;
SLNode* next = cur->next;
SLNode* newhead = NULL;
while (cur)
{
cur->next = newhead;
newhead = cur;
cur = next;
if (next)
next = next->next;
}
return newhead;
}
SLNode* SListReverseBetween(SLNode* phead, int m, int n)
{
if (phead == NULL || phead->next == NULL)
return phead;
//先创建一个哨兵位结点,方便定位
SLNode* Head = (SLNode*)malloc(sizeof(SLNode));
if (Head == NULL)
{
perror("malloc");
exit(-1);
}
Head->next = phead;
SLNode* prev = Head;
int i = 0;
for (i = 0; i < m - 1; i++)
{
//用于找到翻转区间的前一个结点
prev = prev->next;
}
//翻转区间的第一个结点需要保存下来
SLNode* p = prev->next;
SLNode* cur = prev->next;
SLNode* q = cur->next;//q为了辅佐cur向前走
for (i = 0; i < n - m + 1; i++)
{
cur->next = prev->next;
prev->next = cur;
cur = q;
if (q)
q = q->next;
}
p->next = cur;
return Head->next;
}
测试部分:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
int main()
{
SLNode* plist1 = NULL;
SLNode* plist2 = NULL;
SLNode* plist3 = NULL;
SListPushBack(&plist2, 0);
SListPushBack(&plist3, 1);
SListPushBack(&plist3, 2);
SListPushBack(&plist3, 3);
SListPushBack(&plist3, 4);
SListPushBack(&plist3, 5);
SListPushBack(&plist3, 6);
SListPushBack(&plist3, 7);
SListPushBack(&plist3, 8);
/*printf("list1:\n");
SListPrint(plist1);
printf("list2:\n");
SListPrint(plist2);
printf("list3:\n");
SListPrint(plist3);*/
/*plist1 = SListReverse1(plist1);
SListPrint(plist1);
plist2 = SListReverse1(plist2);
SListPrint(plist2);*/
plist3 = SListReverse1(plist3);
SListPrint(plist3);
//plist1 = SListReverse2(plist1);
//SListPrint(plist1);
//plist2 = SListReverse2(plist2);
//SListPrint(plist2);
plist3 = SListReverse2(plist3);
SListPrint(plist3);
SListReverseBetween(plist3, 2, 5);
SListPrint(plist3);
return 0;
}