反转链表相关问题

前言:

在我们写代码的过程中,经常会碰到需要我们转置链表的情况,这种情况实际上是一个经典题型,也是我们校招面试的高频考题,这篇文章就来带大家全面梳理一下这类题目,以此来加深大家对于链表的理解,也希望大家以后在碰到转置链表这种情况能够从容应对:

创建链表:

在进行链表的转置之前,我们需要先得到一个链表,这里头插和尾插的方式都可以构建链表,为了应对我们转置链表的特殊情况,这里我们构建三条链表,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;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值