在之前单链表和双链表两个专题中我们学习了链表相关的概念和性质,同时了解了单链表和双链表各自的特征,那么接下来在本篇中我们就将使用这些链表的知识来解决链表相关的算法题,在本篇中这些算法题能强化我们的算法思想,会对我们之前的编程学习有很大的益处,一加油吧!!!
1.移除链表元素
通过以上的题目描述就可以了解到该算法题要我们实现的是将单链表中节点中为指定数据的都移除
例如以下示例
在以上链表中要将链表中节点值为6的节点移除,要实现移除的操作就要将存储值为6的节点之前的节点的next指针指向该节点的下一个节点,但这种方法在删除过程中还要将指定节点释放,这会稍嫌麻烦。其实还可以直接在创建一个新的链表之后将节点的数据不为6的节点拷贝到新链表当中,通过遍历原来的链表就可以实现新链表中无指定数据6的节点,这就实现了移除链表元素
通过以上示例的分析接下来我们就来试着实现该算法题的代码
在以下代码中使用的是创建一个新链表的思想来实现原链表中元素的移除,创建两个结构体指针变量newhead和newpcur先置为NULL ,在此newhead是用来指向新链表的第一个节点,newpcur是用来让新节点能接入到新链表中而进行新链表内的遍历 。再创建一个结构体指针变量pcur来遍历原链表,当在遍历原链表节点过程中如果节点内存储的值不为val就将该节点指针pcur拷贝newpcur ,在此之后在将newptail指向原来newpcur指向节点内的next指针指向的节点,在以上过程中由于一开始newptail指向NULL,因此要再判断在进行以上操作时newhead是否为NULL,如果是就直接将newhead和newpcur都指向pcur指向的节点也就是将newhead和newpcur都赋值为pcur
在以下代码中当遍历完原链表并将相关的节点都拷贝到新链表内时,就会出现当原链表尾节点为要移除的节点时,原链表中的尾节点回随着前一个结点拷贝到新新链表中 ,就例如以下示例,因此要在遍历完原链表后将指针newpcur的next置为NULL 。但这时还有一个问题就是如果原链表全部结点都为要移除的节点,那么这时newpcur就为NULL,如果再将newpcur->next=NULL就会出现对****空指针的解引用,因此在进行以上这句代码时还要使用if语句来判断指针newpcur是否为NULL,只有当不为NULL才执行该语句
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val)
{
if(head==NULL)//当节点head为NULL时就说明原链表为空链表这时直接返回
{
return head;
}
ListNode* newhead,*newpcur;
newhead=newpcur=NULL;
ListNode* pcur=head;
while(pcur)
{
if(pcur->val!=val)//当节点内的值不为val时
{
if(newhead==NULL)
{
newhead=newpcur=pcur;
}
else
{
newpcur->next=pcur;
newpcur=newpcur->next;
}
}
pcur=pcur->next;
}
if(newhead)
newpcur->next=NULL;
return newhead;
}
2.反转链表
通过以上的题目描述就可以了解该算法题要我们实现的是链表的反转也就是要使改变后链表内的节点顺序和原来倒序一样
例如以下示例
要将以上链表反转你可能想到是先创建一个数组后通过遍历链表将链表节点的值存储到数组当中 ,之后将数组反转后再通过遍历链表将反转后的数组的值依次存储到节点内,这种方法是可行的但这种方法要通过多次遍历链表 ,这就会使得时间复杂度较高,那么有什么更好的算法呢?
在这里我们来学习一种3指针的方法来解决该算法题,例如以下的链表示例
一开始定义3个指针n1,n2,n3,一开始将n1初始化置为NULL,n2指向链表的第一个节点,n3指向n2的下一个节点。之后将n2指向的的节点的next指针指向n1所指向的数据,之后将n1赋值为n2,n2赋值为n3,n3赋值为n3所指向的节点的下一节点的地址 ;一直重复以上的操作直到n1指向原链表最后节点时,这时原链表就变为以下形式,就完成了链表的反转
通过以上示例的分析接下来我们就来试着实现该算法题的代码
当原链表为空时就直接返回NULL
以下使用一个循环来实现链表中每个节点next的改变,在此改变的思想就是用到上面讲解的3指针法
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head)
{
if(head==NULL)
{
return NULL;
}
ListNode* n1,*n2,*n3;
n1=NULL;
n2=head;
n3=n2->next;
while(n2)
{
n2->next=n1;
n1=n2;
n2=n3;
if(n3)//当节点n3为NULL时就不再进行n3=n3->next
{
n3=n3->next;
}
}
return n1; //返回反转后的第一个节点的指针
}
3.合并两个有序链表
通过以上题目的描述就可以了解该算法题要我们实现的是将两个升序的链表合并为一个升序的链表
例如以下示例
要将以上两个链表合并这时你可能会想到遍历其中一个链表将该链表中的节点插入到另一个链表中,但这种方法就需要在插入节点过程中改变多个节点内的next指针这就会比较繁琐。因此我们不会使用该方法,而是选择再创建一个链表来存储合并后的链表 ,在该过程中通过同时遍历两个原链表比较节点内值得大小来确定节点排序
通过以上示例的分析接下来我们就来试着实现该算法题的代码
在以下代码中当指针list1为NULL时就说明list1指向的链表为空,这时就直接返回list2;当指针list2为NULL时就说明list2指向的链表为空,这时就直接返回list1
在创建新链表时,创建一个头节点也就是哨兵位节点,这样就可以不用再单独处理当新链表为空
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1==NULL)
{
return list2;
}
if(list2==NULL)
{
return list1;
}
ListNode* pcur1;
pcur1=list1;
ListNode* pcur2;
pcur2=list2;
ListNode* newhead,*newptail;
newhead=newptail=(ListNode*)malloc(sizeof(ListNode));
while(pcur1&&pcur2)
{
if(pcur1->val<pcur2->val)
{
newptail->next=pcur1;
pcur1=pcur1->next;
}
else
{
newptail->next=pcur2;
pcur2=pcur2->next;
}
newptail=newptail->next;
}
if(pcur1)
{
newptail->next=pcur1;
}
if(pcur2)
{
newptail->next=pcur2;
}
ListNode* p=newhead->next;
free(newhead);
newhead=NULL;
return p;
}
4. 链表的中间结点
通过以上题目的描述就可以了解到该算法题要我们实现的是找出链表的中间节点,并且返回中间节点
例如以下示例
当链表节点个数为奇数和偶数时中间节点有什么区别呢?
来看以下链表,当链表节点个数为偶数时中间节点就为链表节点数除二后再加一得到就是中间节点的序号
那么要使用什么方法才能得到链表的中间节点呢?
在此我们来学习一种快慢指针的方法,先定义两个指针fast和slow,一开始都指向链表的第一个节点,之后让fast指针一次走两步;slow指针一次走一步直到fast或者fast->next为空时,此时slow指向的节点就为中间节点
通过以上示例的分析接下来我们就来试着实现该算法题的代码
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head)
{
if(head==NULL)
{
return head;
}
ListNode* fast,*slow;
fast=slow=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
5.链表分割
面试题 02.04. 分割链表 - 力扣(LeetCode)
通过以上的题目描述就可以了解到该算法题要我们实现的是将链表中比特定值x小的节点都放在比特定值x大的节点之前,在此调整完的链表不一定要为升序
接下来就来分析如何实现链表的分割
例如以下示例
要将以上链表中比3大的节点都排在比3小的节点之前要如何实现呢?
在此我们学习一种大小链表 的算法来解决链表的问题,就比如以上链表先创建两个新链表一开始里面无节点,之后通过遍历链表先将比3小的节点保存在第一个新链表中;将大于等于3的节点保存在第二个节点中。在此将第一个链表称为小链表,第二个链表称为大链表
之后再将小链表的尾节点和大链表的第一个节点连接就得到分割完的链表
分析完解决链表分割的方法后接下来就试着实现链表分割的代码
在实现链表分割的代码中在大小链表的初始化时都让其有哨兵位的节点,这样做的是为了之后在大链表或者小链表插入节点时不用再单独去处理大链表或者小链表为空这种情况
在将原链表内的节点都分到大小链表后,greattail->next=NULL这句代码是将大链表最后一个节点内的next指针置为NULL,这是避免大链表最后一个节点还连着其他节点
注意:在代码的最后要将大小链表的哨兵位节点的内存空间释放,避免形成内存泄漏
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
typedef struct ListNode ListNode;
struct ListNode* partition(struct ListNode* head, int x)
{
ListNode* smallhead,*smalltail;
ListNode* greathead,*greattail;
smallhead=smalltail=(ListNode*)malloc(sizeof(ListNode));
greathead=greattail=(ListNode*)malloc(sizeof(ListNode));
ListNode* pcur=head;
while(pcur)
{
if(pcur->val<x)
{
smalltail->next=pcur;
smalltail=smalltail->next;
pcur=pcur->next;
}
else
{
greattail->next=pcur;
greattail=greattail->next;
pcur=pcur->next;
}
}
greattail->next=NULL;
smalltail->next=greathead->next;//将大小链表连接
ListNode* ret=smallhead->next;
free(smallhead);
free(greathead);
smallhead=greattail=NULL;
return ret;
}
6.链表的回文结构
链表的回文结构_牛客题霸_牛客网 (nowcoder.com)
在通过以上的题目描述可以了解到该算法题要我们实现的是链表回文结构的判断,在此链表为回文结构简单来说就是链表是对称的
在此在实现该算法题的代码之前先要来分析使用说明算法来解决
首先来看以下的回文结构链表示例
在以上的链表当中我们可以看出当一个链表为回文结构时,链表相对应的节点内的值是相同的,了解了这个特性后你可能就会有一个想法就是可以先通过遍历得到链表的节点数,之后根据得到的节点数来创建一个元素个数和节点数相同的数组,之后再遍历一次链表将链表内的值一一都保存到数组当中,在此之后定义两个变量一个为数组首元素的下标;另一个为数组最后一个元素的下标,之后让这两个下标一个从左到右;一个从右向左遍历数组,在此比较相对应下标元素的值是否相同,如果当两个变量都相同时都为出现不相同的情况就说明原链表为回文结构
以上的这种算法思想的确是可以来解决该算法题的,不过使用这种方法时会多次的遍历原链表这就会使得算法的效率不高因此在此我们不使用这种算法
那么如果不用以上这种算法还有什么更好的解法呢?
对于是否是回文结构的链表如果我们能先找到原链表的中间节点后再将中间节点之后的链表反转,之后两个指针一个指向链表第一个节点,另一个指向链表尾节点,再从两段来遍历链表比较相对应节点内的值不就可以判断原链表是否是回文结构了吗?
例如以上示例按照这种方法过程图如下所示:
对回文结构的链表判断进行分析后接下来就来实现该算法题吧
以下在实现链表找中间节点的函数就用到之前链表的中间结点算法题的解决方法,在实现将链表中间之后的链表反转的函数就用到之前反转链表算法题的解决方法
cpp
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
#include <valarray>
ListNode*SLTFindMid(ListNode* head)
{
ListNode* slow,*fast;
slow=fast=head;
while(fast &&fast->next)
{
slow=slow->next;
fast=fast->next;
}
return slow;
}
ListNode* SLTReverse(ListNode* head)
{
ListNode*n1,*n2,*n3;
n1=NULL,n2=head,n3=n2->next;
while(n2)
{
n2->next=n1;
n1=n2;
n2=n3;
if(n3)
{
n3=n3->next;
}
}
return n1;
}
class PalindromeList {
public:
bool chkPalindrome(ListNode* A)
{
//找出链表的中间节点
ListNode*mid=SLTFindMid(A);
//反转中间节点之后的链表
ListNode*right=SLTReverse(mid);
//判断前后两段链表是否相同
ListNode* left=A;
while(right)
{
if(left->val!=right->val)
{
return false;
}
left=left->next;
right=right->next;
}
return true;
}
};
注:在以上的代码语言环境选择的是C++,这时因为该算法题牛客没有提供C语言的代码环境,但C++其实是兼容C语言的,所以我们可以在此使用C语言来实现代码,在此ListNode其实在C++使用struct就成为一个类,所以不用使用typedef也之后可以直接使用LIstNode。如果你想了解更多原因可以观看C++篇章的内容
以上就是链表算法题(上)的全部内容了,在链表算法题(下)中我们将继续来解决链表的算法题,未完待续......