目录
前言:
移除链表元素
反转链表
链表的中间结点
链表中倒数第k个结点
合并两个有序链表
哨兵位:
分割链表
链表的回文结构
链表相交
总结:
本章将带来几道关于链表的几道目,其实单链表一点都不简单。
移除链表元素
OJ链接
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:输入:head = [], val = 1
输出:[]
示例 3:输入:head = [7,7,7,7], val = 7
输出:[]
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur, *prev, *next;
cur = head;
next = prev = NULL;
while(cur)
{
next = cur->next;
if(cur->val == val)
{
if(cur == head)
{
free(cur);
head = next;
cur = next;
}
else
{
free(cur);
prev->next = next;
cur = next;
}
}
else
{
prev = cur;
cur = cur->next;
}
}
return head;
}
思路:
用cur指针遍历整个链表来找要删除的数据,prev指针指在cur指针的前一个结点,next指针指向cur后面一个结点,这样做的目的是方便链接。
通常的思路是:
找到要删除的结点,将该结点的后一个链接到该结点的前一个,并将该结点释放掉。
分四种情况讨论:
1.要删除的结点在开头
2.要删除的结点在结尾
3.删除的结点连续
4.删除的结点不在开头
经过简单的不难分析发现:
只有当删除的结点在开头的时候才需要特殊考虑,因为最后返回的是头指针head,如果是头删的话,那么单链表的头就该变动,而上述通常情况下是不需要改头的。
通常情况下:(head没改变)
特殊情况:(head改变删头时)
反转链表
OJ链接
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:输入:head = []
输出:[]
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL)
return NULL;
struct ListNode* prev, *cur, *next;
cur = head;
prev = NULL;
next = cur->next;
while(cur)
{
cur->next = prev;
prev = cur;
cur = next;
if(next != NULL)
next = next->next;
}
return prev;
}
思路:
用cur指针遍历整个链表,prev指针指在cur指针的前一个结点,next指针指向cur后面一个结点,这样做的目的是方便链接。
分两种情况讨论:
1.当链表为空链表时:
直接返回NULL即可。
2.当链表为非空链表时:
将cur->next指向prev,将next指向cur,将cur指向next,最后next = next->next,将前一个结点的头给下一个结点的尾,再去找下一个结点的尾。
注意:
当cur找到链表的最后一个结点时,cur不为NULL,但是next已经指向了NULL,如果不加条件判断,那么next->next就会对空指针引用出错。
链表的中间结点
OJ链接
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//快慢指针
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast, *slow;
fast = slow = head;
//循环条件调换位置会崩,从左向右执行代码
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
思路:
本题引入另一种思路:
那就是快慢指针:快指针一次走两步,慢指针一次走一步,当快指针将链表遍历完之后,慢指针刚好指在链表中间结点。
分两种情况:
当结点个数为奇数时:快指针遍历完时,fast指向最后一个结点,慢指针刚好指在中间结点。
当结点个数为偶数时:快指针遍历完时,fast指向NULL,慢指针刚好指在中间第二个结点。
注意:
循环判断条件while(fast && fast->next)这里的fast和fast->next是不可以调换的。当结点个数为偶数时,fast指向空的时候,循环条件中fast->next就是对NULL指针的非法访问,因为&&逻辑运算是从左向右执行。
结点个数为偶数时:
此时fast->next为空NULL,slow正好指向中间结点,循环结束。
结点个数为奇数时:
此时fast为空NULL,slow正好指向中间两个结点的后一个,循环结束。
链表中倒数第k个结点
OJ链接
描述
输入一个链表,输出该链表中倒数第k个结点。
示例1
输入:
1,{1,2,3,4,5}返回值:
{5}
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*
* C语言声明定义全局变量请加上static,防止重复定义
*/
/**
*
* @param pListHead ListNode类
* @param k int整型
* @return ListNode类
*/
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
struct ListNode* fast, *slow;
fast = slow = pListHead;
while(k--)
{
if(fast == NULL)
{
return NULL;
}
fast = fast->next;
}
while(fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
// write code here
}
思路:
这题还是运用到了快慢指针, 让快指针先走k步,然后快慢指针一起走,当快指针指到尾的时候,slow指针刚好指到倒数第k个结点。
合并两个有序链表
OJ链接
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:输入:l1 = [], l2 = []
输出:[]
示例 3:输入:l1 = [], l2 = [0]
输出:[0]
带哨兵位的写法:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
//带哨兵位的写法
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* head, *tail;
head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
head->next = NULL;
while(list1 && list2)
{
if(list1->val < list2->val)
{
tail->next = list1;
tail = tail->next;
list1 = list1->next;
}
else
{
tail->next = list2;
tail = tail->next;
list2 = list2->next;
}
}
if(list1 == NULL)
{
tail->next = list2;
}
else if(list2 == NULL)
{
tail->next = list1;
}
struct ListNode* list = head->next;
free(head);
head = NULL;
return list;
}
不带哨兵位的写法:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
struct ListNode* head, *tail;
head = tail = NULL;
while(list1 && list2)
{
if(list1->val < list2->val)
{
if(head == NULL)
{
head = list1;
tail = list1;
tail->next = head->next;
}
else
{
tail->next = list1;
tail = tail->next;
}
list1 = list1->next;
}
else
{
if(head == NULL)
{
head = list2;
tail = list2;
tail->next = head->next;
}
else
{
tail->next = list2;
tail = tail->next;
}
list2 = list2->next;
}
}
if(list1 == NULL)
{
tail->next = list2;
}
else if(list2 == NULL)
{
tail->next = list1;
}
return head;
}
思路:
这题用到了归并排序的思想 ,分别遍历两个链表,将小的那个结点接起来,因为两个链表都是有序的,剩下的操作就是简单的链接结点。
链表分为有头链表和无头链表。
无头单链表:
这种链表遍历的时候就是从 head 开始遍历。
带头结点单链表:
这种链表遍历的时候就是从 head->next 开始遍历。
红色方框代表一个结点,这个结点就叫哨兵位的头结点,
这个结点不存储有效数据。
哨兵位存在的意义是:
它的存在可以可以方便一些操作
例如本题:
不带哨兵位时:当一开始链接新表的时候,要将头指针head指到指定位置,因为一开始定义头指针head和尾指针tail都是NULL,这时候就要多加几条判断条件。
而带哨兵位时:就可以少去一些判断的条件。
分割链表
OJ链接
描述
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重
新排列后的链表的头指针。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
ListNode* partition(ListNode* pHead, int x)
{
struct ListNode* lessHead, *greaterHead, *lessTail, *greaterTail;
lessHead = lessTail = (struct ListNode*)malloc(sizeof(struct ListNode));
greaterHead = greaterTail = (struct ListNode*)malloc(sizeof(struct ListNode));
lessHead->next = greaterHead->next = NULL;
struct ListNode* cur = pHead;
while(cur)
{
if(cur->val < x)
{
lessTail->next = cur;
lessTail = lessTail->next;
}
else
{
greaterTail->next = cur;
greaterTail = greaterTail->next;
}
cur = cur->next;
}
lessTail->next = greaterHead->next;
greaterTail->next = NULL;
struct ListNode* list = lessHead->next;
free(lessHead);
free(greaterHead);
lessHead = NULL;
greaterHead = NULL;
return list;
// write code here
}
};
思路:
1.带哨兵位的,方便尾插
2.遍历原链表
3.把 < x的插入到一个链表1
4.把 > x的插入到一个链表2
5.再将链表1和链表2链接起来
本题是采用带哨兵位的写法,可以减少很多判断条件。
和归并的思想类似不再赘述。
链表的回文结构
OJ链接
描述
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
测试样例:
1->2->2->1返回:true
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList{
public:
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast, *slow;
slow = fast = head;
while(fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* prev, *cur, *next;
prev = NULL;
cur = head;
next = cur->next;
while(cur)
{
cur->next = prev;
prev = cur;
cur = next;
if(next)
next = next->next;
}
return prev;
}
//链表相交
bool chkPalindrome(ListNode* A)
{
struct ListNode* mid = middleNode(A);
struct ListNode* rHead = reverseList(A);
while(A && rHead)
{
if(A->val == rHead->val)
{
A = A->next;
rHead = rHead->next;
}
else
{
return false;
}
}
return true;
// write code here
}
};
思路:
1.找到中间结点
2.将中间结点之后的结点反转
3.再从头开始的链表和从中间开始的链表比较相同即为回文结构。
前几个函数在之前都已经实现过不再赘述。
链表相交
OJ链接
给你两个单链表的头节点
headA
和headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回null
。示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* tailA = headA, *tailB = headB;
int lenA = 0;
int lenB = 0;
while(tailA)
{
tailA = tailA->next;
lenA++;
}
while(tailB)
{
tailB = tailB->next;
lenB++;
}
if(tailA != tailB)
return NULL;
struct ListNode* longList, *shortList;
longList = headB,
shortList = headA;
if(lenA > lenB)
{
longList = headA;
shortList = headB;
}
int gap = abs(lenA - lenB);
while(gap--)
{
longList = longList->next;
}
while(longList && shortList)
{
if(longList == shortList)
{
return shortList;
}
else
{
shortList = shortList->next;
longList = longList->next;
}
}
return NULL;
}
思路:
1.分别计算出两条链表的长度(包括公共部分的长度)。
2.计算出两条链表的长度差gap
3.定义两个指针分别指向两个链表的头
4.让指向长链表的指针先向后走gap个单位
5.再让两个指针同时向后走,当两个指针相等时,相等的点即为交点
先思考再码字,理清思路很重要,要把控好细节!