目录
- 移除链表元素
- 反转链表
- 链表的中间节点
- 倒数第k个节点
- 反转链表(初阶)
- 快慢指针法(进阶)
- 合并两个有序链表
- 链表分割
- 链表的回文结构
移除链表元素
思路:由题目可知我们需要在给定的一个链表中移除值为val的节点,这里需要注意的情况就是全是val的链表移除后为空链表和传过来的链表是空链表的情况,如果直接对不是val的节点进行连接,返回头结点会无法处理头结点也是val的情况,这里不妨我们用两个指针开始都设置NULL,用一个指针指向头结点开始判断,必须是从头结点开始遍历不等于val的节点才能连接,不然就不能连接。这时候就处理了两种情况
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {
ListNode* NewHead = NULL; //一开始就为空处理全是相同元素的情况和传过来是空链表的情况
ListNode* NewTail = NULL;
ListNode* pcur = head;
while(pcur)
{
if(pcur->val != val)
{
if(NewHead == NULL)
{
NewHead = NewTail = pcur;
}
else
{
NewTail->next = pcur;
NewTail = NewTail->next;
}
}
pcur = pcur->next;
}
if(NewTail != NULL) //防止堆空链表和移除后是空链表的空指针解引用
{
NewTail->next = NULL;
}
return NewHead;
}
注意最后一个节点连接的时候,万一他的下一个节点还是val值,必须把他的next指针设置NULL;
反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
思路:由题目可知,我们需要把给定的链表所以节点反转,并返回新的头结点。这里要注意链表的最后一个节点是一个NULL,所以我们反转的链表的尾节点也是一个NULL。反转无非就是改变next指针的指向即以下
- 定义三个指针n1,n2,n3.n1指向NULL(新尾节点),n2指向原头结点,n3指向第二个节点
- 把n2头节点的next指针指向n1,然后n1移到n2,n2移到n3,以此类推改变节点的next指针方向
- 直到n2指向NULL,循环结束,返回n1
注意:为什么定义三个指针:定义两个指针不行吗?不行,因为我们要先改变节点next指针的方向,但是如果直接改变了这个节点的next指针我们如何找到下一个节点呢?所以定义三个指针
n1:记录指针指向将要反转的结点反转后要指向的位置。
n2:记录指针指向将要反转的结点。
n3:记录指针指向将要反转的结点的下一个结点。
注意,这时这3个指针统一后移时,n3指针的后移将失败,因为n3后移前指向的是NULL,我们不能执行以下这句代码:
n3 = n3->next;
所以我们后移n3指针前需判断其是否为空。
typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {
if(head == NULL)
{
return NULL;
}
ListNode* n1 = NULL;
ListNode* n2 = head;
ListNode* n3 = head->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
{
n3 = n3->next;
}
}
return n1;
}
链表的中间节点
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。(给定的链表是非空链表)
思路:快慢指针法,定义两个指针,两个都指向头节点,慢指针每次走一步,快指针每次走两步,最终返回的中间节点就是慢指针
注意:当fast指向空或者fast指针的next指针指向的节点为空的时候,就停止遍历返回慢指针
偶数个有两个中间节点(如上面3,4),通常我们取的是第二个中间节点,也是为什么fast快指针也从头结点开始遍历。这样不用单独处理slow指向第一个中间节点的情况
typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {
ListNode* slow = head;
ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
倒数第k个节点
实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
反转链表(初阶)
思路:倒数第二个节点,是不是就是反转链表后的第二个节点?所以我们结合前面的反转链表的三指针法,然后堆反转链表变量k次就可以找的倒数的第k个节点
typedef struct ListNode ListNode;
int kthToLast(struct ListNode* head, int k){
ListNode* n1 = NULL;
ListNode* n2 = head;
ListNode* n3 = n2->next;
while(n2)
{
n2->next = n1;
n1 =n2;
n2 = n3;
if(n3)
{
n3 = n3->next;
}
}
while(--k)
{
n1 = n1->next;
}
return n1->val;
}
快慢指针法(进阶)
如果改一下题目,不能改变原链表的结构并且k不一定有效(有可能大于总结点数)该如何做呢?这样反转链表就不适用了吧。这时候就又可以用我们的快慢指针了
思路:这里的"快慢指针"不是一个指针走得快,一个指针走的慢。这里我们是为了找倒数第k个节点。我们每个链表的最后一个节点都是NULL,那么从倒数第k个节点开始走k步是不是就是null呢?所以我们不妨先让快指针fast走k步,然后慢指针和快指针再同时向后移动,直到fast快指针指向null时slow慢指针就是倒数第k个节点
注意:如果fast在走k步时就提前遇到NULL,那么说明k大于总节点数。这时不存在倒数第k个节点。
合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:创建一个新链表,对两个链表进行比较,那个链表的节点数据小,就插入到新链表中,直到其中两个链表其中一个为空停止(不存在同时为空的情况,数据一致也会有一个数据还没插入)。然后判断两个链表谁还没有为空,将不为空的链表尾插到新链表中。
注意:
- 两个链表为空直接返回NULL
- 其中一个链表为空返回不为空的链表
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
if(list1 == NULL)
{
return list2;
}
if(list2 == NULL)
{
return list1; //这样既可以处理其中一个为空,又可以处理同时为空的情况
}
ListNode* NewHead = NULL, *NewTail = NULL;
ListNode* l1 = list1, *l2 = list2;
while(l1 && l2)
{
if(l1 -> val < l2->val)
{
if(NewHead == NULL)
{
NewHead = NewTail = l1;
}
else
{
NewTail->next = l1;
NewTail = NewTail->next;
}
l1 = l1->next;
}
else
{
if(NewHead == NULL)
{
NewHead = NewTail = l2;
}
else
{
NewTail->next = l2;
NewTail = NewTail->next;
}
l2 = l2->next;
}
}
if(l1)
{
NewTail->next = l1;
}
else
{
NewTail->next = l2;
}
return NewHead;
}
上面的链表结构我们是用一个空链表来存储,会导致插入头结点的时候总是会判断是不是NULL,导致代码冗余,这里我们不再使用空链表,使用带头的链表来存储,这样就直接可以在带头链表的下一个节点的位置插入。(为什么不在这个带头节点直接插入,这是动态开辟的空间,后面需要释放)
typedef struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
if(list1 == NULL)
{
return list2;
}
if(list2 == NULL)
{
return list1; //这样既可以处理其中一个为空,又可以处理同时为空的情况
}
ListNode* NewHead,*NewTail;
NewHead = NewTail =(ListNode*)malloc(sizeof(ListNode));
ListNode* l1 = list1, *l2 = list2;
while(l1 && l2)
{
if(l1 -> val < l2->val)
{
NewTail->next = l1;
NewTail = NewTail->next;
l1 = l1->next;
}
else
{
NewTail->next = l2;
NewTail = NewTail->next;
l2 = l2->next;
}
}
if(l1)
{
NewTail->next = l1;
}
else
{
NewTail->next = l2;
}
ListNode* ret = NewHead->next;
free(NewHead);
NewHead = NULL;
return ret;
}
链表分割
描述
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
思路:由题目可知,我们需要把小于x的节点排在其他指点之前,不能改变原来的数据顺序
不改变数据顺序:
所以这里不是让我们排升序
我们不妨创建两个链表,一个链表存储小于x的数据,一个链表存吃啥大于等于x的数据,然后把两个链表的首尾相连即可
1.把小于x的结点尾插到less链表,把大于x的结点尾插到greater链表
2.将less链表与greater链表链接起来。
注意:
1.链接后的链表的最后一个结点的指针域需要置空,否则可能造成链表成环。
2.返回的头指针应是lessHead->next,而不是lessHead。因为这是头节点
这是带头版本的做法
#include <cstddef>
class Partition {
public:
ListNode* partition(ListNode* pHead, int x) {
// write code here
//创建两个非空链表,小链表(存储小于定值x)和大链表(存储大于等于定值x)
ListNode* LessNewhead,*LessNewtail;
ListNode* GreaterNewhead,*GreaterNewtail;
LessNewhead=LessNewtail=(ListNode*)malloc(sizeof(ListNode));
GreaterNewhead=GreaterNewtail=(ListNode*)malloc(sizeof(ListNode));
if(LessNewhead==NULL && GreaterNewhead==NULL)
{
perror("malloc failed");
exit(1);
}
ListNode* pcur=pHead;
while(pcur)
{
if(pcur->val<x)
{
LessNewtail->next=pcur;
LessNewtail=LessNewtail->next;
}
else
{
GreaterNewtail->next=pcur;
GreaterNewtail=GreaterNewtail->next;
}
pcur=pcur->next;
}
//将小链表的尾连接到大链表的头
LessNewtail->next=GreaterNewhead->next; //这里nGext是因为大链表的头还是指向第一个无效空间
GreaterNewtail->next=NULL; //一定要把大链表的下一个节点设置NULL,因为大链表村上的是大于等于x的节点,若其中一个节点的next指针指向小链表节点,则会死循环形成环,如 5 1 3 6 2,其中小链表:1 2,大链表:5 3 6,因为6的下一个节点是2,下一次循环又会指向小链表的节点2,导致 2 5 3 6,2 5 3 6....死循环
ListNode* ret=LessNewhead->next;
free(LessNewhead);
free(GreaterNewhead);
LessNewhead=NULL;
GreaterNewhead=NULL;
return ret;
}
但是不带头版本又多了一个判断
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
#include <cmath>
#include <functional>
#include <sys/ucontext.h>
class Partition {
public:
ListNode* partition(ListNode* pHead, int x) {
// write code here
ListNode* LessHead = NULL, *LessTail = NULL;
ListNode* GreaterHead = NULL, *GreaterTail = NULL;
ListNode* pcur = pHead;
while (pcur) {
if(pcur->val < x)
{
if(LessHead == NULL)
{
LessHead = LessTail = pcur;
}
else
{
LessTail->next = pcur;
LessTail = LessTail->next;
}
}
else
{
if(GreaterHead == NULL)
{
GreaterHead = GreaterTail =pcur;
}
else
{
GreaterTail->next = pcur;
GreaterTail = GreaterTail->next;
}
}
pcur = pcur->next;
}
if (GreaterTail) {
GreaterTail->next = nullptr;
}
if (LessTail) {
LessTail->next = GreaterHead;
return LessHead;
} else {
return GreaterHead;
}
}
};
为什么要判断连接的链表为空呢,因为题目也没说给的数据全部比x大或者全部比x小呀
例如:4 4 4 4 ,x=3
如果直接对LessTail解引用此时Less这个链表为空,就是对空指针解引用
有的人就要问了,那为啥带头的不判断,带头本身就有一个头结点不存在链表为null的情况,也就不会对null解引用
这里还是推荐带头的写法,会清除对头结点插入判断的代码冗余
链表的回文结构
我们需要找到传入链表的中间结点,并将中间结点及其后面结点进行反转,然后再将原链表的前半部分与反转后的后半部分进行比较,若相同,则该链表是回文结构,否则,不是回文结构。
1.找中间节点并返回
2.对中间节点及以后得节点进行反转
3.比较链表的前半部分与后半部分的结点值,若相同则是回文结构,否则,不是回文结构。
注意:就算传入的链表是结点数为奇数的回文结构,该思路也可以成功判断。
例如,以下链表反转其后半部分后,我们看似链表应该是这样的。
但反转后的链表并不是这样的,而应该是下面这样:
因为我们反转的是中间结点及其后面的结点,并没有对前面的结点进行任何操作,所以结点5所指向的结点应该还是结点7。
于是该链表的比较过程应该是这样的:1等于1,3等于3,5等于5,7等于7,然后RHead指针指向NULL。所以判断该链表是回文结构。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
#include <list>
class PalindromeList {
public:
ListNode* FIndMiddle(ListNode* phead)
{
ListNode* slow = phead;
ListNode* fast = phead;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
ListNode* ReverseList(ListNode* phead)
{
ListNode* n1 = NULL;
ListNode* n2 = phead;
ListNode* n3 = n2->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
{
n3 = n3->next;
}
}
return n1;
}
bool chkPalindrome(ListNode* A) {
// write code here
//找到中间节点
ListNode* mid = FIndMiddle(A);
//将中间节点之后反转
ListNode* right = ReverseList(mid);
ListNode* pcur = A;
while(right)
{
if(right->val != pcur->val)
{
return false;
}
right = right->next;
pcur = pcur->next;
}
return true;
}
};