单链表结点的定义
//单链表结点结构
struct ListNode {
int val;
struct ListNode* next;
ListNode() {}
ListNode(int n, struct ListNode* next)
{
val = n;
this->next = next;
}
};
1.两个链表的第一个的公共子结点(剑指offer52)
leetcode力扣https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/description/nowcoder牛客https://www.nowcoder.com/exam/oj/ta?page=1&tpId=13&type=13
题目:给出两个链表,找到它们的第一个公共结点,如下图
即要找到node(c1)。
1.1使用哈希或集合
将第一个链表的所有结点存入HashMap里,然后一遍遍历第二个链表一边在检查Hash里是否有该结点,若有,则返回该结点。使用集合的思路跟哈希一样。
1.2使用栈
创建两个栈,分别将两个链表全部入栈,接着两个栈开始检测栈顶元素,相等则出栈,循环此操作,最后出栈的那个元素就是两链表的第一个的公共结点。此方法需要两个O(n)的空间。
1.3双指针法
1.3.1拼接两个链表
如下图,node(4)是我们要找的公共结点
将A,B拼接后,我们发现,此时两次拼接后的倒数第二个结点node(4)就是第一个公共结点,此时,我们找到这个node(4)就可以了。
此时最简单的方法就是两个拼接链表分别一起遍历,当两个结点相同时,返回这个结点就行了。
优化方法:
我们可以不建立新的两个拼接链表AB,BA,直接同时在A,B上进行遍历。
当A遍历至最后的NULL时,让其继续在B的头结点进行遍历。(即p1==NULL时就让p1=head2)
当B遍历至最后的NULL时,让其继续在A的头结点进行遍历。(即p2==NULL时就让p2=head2)
这样最后,如果A,B有公共结点,必定会同时遍历到第一个公共结点。
但如果A,B没有公共结点,则到NULL时又会重复遍历下去,死循环,所以在循环内部必须加个判断,只有(p1!=p2)时,才有(p1==NULL时就让p1=head2)和(p2==NULL时就让p2=head2)
所以如果A,B没有公共结点,最后也必定会同时遍历至NULL,所以(p1==p2)就可以作为循环遍历结束的条件。
如下图
//两个链表的第一个公共子结点 (双指针 拼接法)
struct ListNode* getIntersectionNode(struct ListNode* head1, struct ListNode* head2) {
if (head1 == NULL || head2 == NULL)
return NULL;
struct ListNode* p1 = head1;
struct ListNode* p2 = head2;
while (p1!=p2) {
p1 = p1->next;
p2 = p2->next;
//若无交集则会陷入死循环,需判断
if (p1 != p2) {
if (p1 == NULL) {
p1 = head2;
}
if (p2 == NULL) {
p2 = head1;
}
}
}
return p1;
}
1.3.2做差双指针法
还可以先遍历两个单链表,求出它们的长度 l1 和 l2,它们的差为| l1-l2 |,第二次遍历时,让长的链表先走| l1-l2 |,然后在同步一起遍历,若有公共结点,(p1==p2)时就是第一个公共结点了。若没有,(p1==p2)时就是NULL;
2.判断单链表是否为回文序列
leetcode力扣https://leetcode.cn/problems/palindrome-linked-list/description/
例:1->2->3->3->2->1 则return true
方法有很多
1.将链表全部元素入栈,然后一边出栈一边从头遍历单链表一边比较,只要有一个不相等就return false。
2.优化1,先进行一次单链表的遍历,得到总长度,然后将一半的链表入栈,接着一边出栈一边接着遍历一边比较,只要有一个不相等就return false。
3.在求总长度的第一次遍历时,一边遍历一边入栈,接着一边出栈,一边从头遍历一边比较,只比较一半的元素就停止。
4.反转链表法。将原链表的所有元素逆序保存在一个newList中,接着两个链表都从头开始一边遍历一边比较。
5.优化4,先遍历求总长度,然后反转一半的元素至newList,接着从newList的头和原链表的一半开始一边遍历一边比较。
6.双指针法,使用fast和slow指针,fast一次走两步,slow一次走一步。fast走至尾部时,slow在中间,接着就可以从头开始逆序一半的元素。
这里给出最简单的反转链表法
//判断单链表是否为回文序列 反转链表法
bool isPalindrome(ListNode* head) {
ListNode* newList = NULL;
ListNode* temp = head;
//每次将原链表的值复制后插入新链表的头结点
while (temp != NULL) {
ListNode* newNode = new ListNode(temp->val, NULL);
newList=insertNode(newList, newNode, 1);
temp = temp->next;
}
temp = head;
ListNode* temp2 = newList;
//开始同时遍历比较
while (temp != NULL)
{
if (temp->val != temp2->val)
{
return false;
}
temp = temp->next;
temp2 = temp2->next;
}
destroyList(newList);
return true;
}
3.合并有序单链表
leetcode力扣https://leetcode.cn/problems/merge-two-sorted-lists/description/
3.1合并两个有序单链表
其实很简单,就规规矩矩新建一个链表,然后从两个链表遍历比较,每次将小的接上就行了
直接上代码
//合并两个有序单链表
struct ListNode* mergeTwoList(struct ListNode* list1, struct ListNode* list2) {
ListNode* mergedList = new ListNode(-1, NULL);
ListNode* temp = mergedList;
//两个链表都没遍历至NULL
while (list1 != NULL && list2 != NULL) {
if (list1->val <= list2->val) {
temp->next = list1;
list1 = list1->next;
temp = temp->next;
}
else {
temp->next = list2;
list2 = list2->next;
temp = temp->next;
}
}
//有一个链表遍历至NULL时
temp->next = (list1) ? list1 : list2;
temp = mergedList->next;
delete mergedList;
return temp;
}
3.2合并k个有序单链表
最容易想到的就是按顺序来,先将前两个合并,然后依次与后面的合并
struct ListNode* mergeKLists(struct ListNode* [] lists , int size){
struct ListNode* newList=NULL;
//每次将newList和lists[m]合并
for(int m=0;m<size;m++){
newList=mergeTwoLists(newList , lists[index]);
}
return newList;
}
3.3一道好题
leetcode力扣https://leetcode.cn/problems/merge-in-between-linked-lists/description/如下图
示例:
这题的解题关键就在于要找到的 list1node(a-1) 和 list1node(b+1) ,然后将 list1node(a-1)->next = list2node(0) , list2node(m-1)->next=list1node(b+1)。
ListNode* mergeInBetween(ListNode* list1, int a, int b, ListNode* list2) {
ListNode* pre1=list1;
ListNode* after1=list1;
ListNode* cur2=list2;
//找到remove段的前一个结点和remove段的后一个结点
while(pre1!=NULL && after1!=NULL && b!=-1){
if(a!=1){
pre1=pre1->next;
a--;
}
if(b!=-1){
after1=after1->next;
b--;
}
}
//找到list2的最后一个结点
while(cur2->next!=NULL){
cur2=cur2->next;
}
//开始连接
pre1->next=list2;
cur2->next=after1;
return list1;
}
4.双指针专题
4.1寻找中间结点
leetcode力扣https://leetcode.cn/problems/middle-of-the-linked-list/description/
题目: 给你单链表的头结点 head,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
例: 输入:head=[1,2,3,4,5] 输入:head=[1,2,3,4,5,6]
输出:[3,4,5] 输出:[4,5,6]
只有一个中间结点 有两个中间结点,返回第二个
使用双指针法的快慢指针可以轻松解决,创建两个指针fast,slow都指向head,接着fast一次走两步,slow一次走一步,此时循环结束的条件是个问题。需要我们具体分析。
发现
只有一个中间结点时,fast走到最后一个结点时,slow刚刚好在中间结点
有两个中间结点时,fast走到NULL时,slow刚刚好在第二个中间结点
所以将while循环的条件写成 while(fast!=NULL && fast->next!=NULL)
//快慢指针法
ListNode* middleNode(ListNode* head) {
struct ListNode* fast = head;
struct ListNode* slow = head;
while(fast!=NULL && fast->next!=NULL){
fast=fast->next->next;
slow=slow->next;
}
return slow;
}
4.2寻找倒数第k个结点
leetcode力扣https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/description/
题目:输入一个链表,返回该链表的倒数第k个结点,从1开始计数,即尾结点是倒数第一个
例: head=[1,2,3,4,5] , k=2
返回 [4,5]
这题使用快慢指针也能轻松解决,创建两个指针fast,slow都指向head,接着fast先走k步,此时fast指向第k+1个结点,fast与slow之间相差k步,此时两个指针同时向后遍历,当fast==NULL时,slow恰好指向倒数第k个结点,返回slow即可。
但还有一个问题,就是有可能链表的长度小于k,所以需加判断fast!=NULL
struct ListNode* kthToLast(ListNode* head, int k) {
struct ListNode* fast=head;
struct ListNode* slow=head;
//先让fast走k步,注意判断fast!=NULL
while(fast!=NULL && k>0){
fast=fast->next;
k--;
}
while(fast!=NULL){
fast=fast->next;
slow=slow->next;
}
return slow;
}
4.3旋转链表
leetcode力扣https://leetcode.cn/problems/rotate-list/题目: 给你一个单链表和数字k,旋转链表,将每个结点向后移动k个位置
例:
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
我们只需要找到倒数第k+1个结点和最后一个结点,然后将最后一个结点的next=head,保存倒数第k+1个结点的下一个结点,接着倒数第k+1个结点的next=NULL,返回保存的那个结点。
而找到倒数第k+1个结点刚好可以用4.1的方法,还需注意 if(head==NULL || k==0) return head;并且k>length时要k=k%length,所以之前还需遍历一般求length,期间又刚好可以找到尾结点,所以,代码如下
struct ListNode* rotateRight(ListNode* head, int k) {
if(head==NULL || k==0) return head;
int length=1;
struct ListNode* temp = head;
struct ListNode* cur =head;
//获取链表的长度,同时temp指向尾结点
while(temp->next!=NULL){
length++;
temp=temp->next;
}
k=k%length;
if(k==0) return head;
//cur指向倒数第k+1个结点
cur=kthToLast(head,k+1);
//尾结点next=head
temp->next=head;
//temp指向新头结点
temp=cur->next;
//倒数第k+1个结点变成新尾结点
cur->next=NULL;
return temp;
}
5.删除链表元素专题
题目有很多
5.1删除特定结点
leetcode力扣203https://leetcode.cn/problems/remove-linked-list-elements/description/题目: 给你一个单链表和一个整数val,删除单链表上所有值为val的结点,返回新的链表头结点
例: 输入:head=[1,2,6,3,4,5,6] val=6
输出:head=[1,2,3,4,5]
对于删除操作,头结点的删除与后面的结点不同,为了统一操作,可以创建一个虚拟头结点dummyHead,使其的next=head,这样对于头结点的删除也与后面的结点统一了。
因为删除结点要知道删除结点的前驱结点,所有这里要用cur->next->val来判断是否是要删除的结点,并且不要忘了待删除的那个结点要delete掉。
ListNode* removeElements(ListNode* head, int val) {
struct ListNode* dummyNode =new struct ListNode(-1,head);
struct ListNode* cur=dummyNode;
while(cur->next!=NULL){
if(cur->next->val==val){
struct ListNode* temp=cur->next;
cur->next=cur->next->next;
delete temp;
}
else{
cur=cur->next;
}
}
return dummyNode->next;
}
5.2删除倒数第k个结点
leetcode力扣19https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/
题目: 给你一个链表,删除链表的n个结点,并且返回链表的头结点。
例: 输入:head=[1,2,3,4,5] n=2
输出:head=[1,2,3,5]
这其实就是4.2的小小拓展而已
方法1:先遍历一边算出length,要删除的结点其实就是第length-n+1个结点。
方法2:将全部结点入栈,然后出栈,第n个出栈的结点就是要删除的结点,但该方法需要O(n)的空间,所以不推荐
方法3:双指针法,也就是4.2的方法
方法1:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(head==NULL) return head;
struct ListNode* dummyNode =new struct ListNode(-1,head);
struct ListNode* cur=dummyNode;
struct ListNode* temp=head;
int length =0;
//遍历求出length
while(temp!=NULL){
length++;
temp=temp->next;
}
int index=length-n+1;
//遍历找到第length-n+1个结点的前驱结点,也就是length-n个结点,但因此时cur指向dummyNode,所以循环判断条件稍稍变化
while( (index-1) !=0){
cur=cur->next;
index--;
}
temp=cur->next;
cur->next=cur->next->next;
delete temp;
return dummyNode->next;
}
方法3: 因为要找到删除结点的前一个结点,所以此时slow有稍稍变化
ListNode* removeNthFromEnd(ListNode* head, int n) {
if(head==NULL) return head;
struct ListNode* dummyNode =new struct ListNode(-1,head);
struct ListNode* cur=dummyNode;
struct ListNode* temp=head;
struct ListNode* fast=head;
struct ListNode* slow=dummyNode;//因为要找到删除结点的前一个结点,这样可以使slow指向倒数第n+1个结点
//先让fast走n步,注意判断fast!=NULL
while(fast!=NULL && n>0){
fast=fast->next;
n--;
}
while(fast!=NULL){
fast=fast->next;
slow=slow->next;
}
//此时slow为删除结点的前一个结点
cur=slow;
temp=cur->next;
cur->next=cur->next->next;
delete temp;
return dummyNode->next;
}
5.3删除重复元素
分以下两种情况
5.3.1重复元素留一个
leetcode力扣83https://leetcode.cn/problems/remove-duplicates-from-sorted-list/description/
题目: 给定一个已排序的链表的头head, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表
例: 输入: head=[1,1,2,3,3]
输出: head=[1,2,3]
该题的关键是要知道,只有相邻的才有可能值相同,所以要用(cur->val==cur->nexr->val)来判断
struct ListNode* deleteDuplicates(ListNode* head) {
if(head==NULL) return NULL;
struct ListNode* cur=head;
//一直遍历至最后一个元素
while(cur->next!=NULL){
if(cur->val==cur->next->val){
struct ListNode* temp=cur->next;
cur->next=cur->next->next;
delete temp;
}else{
cur=cur->next;
}
}
return head;
}
5.3.2重复元素一个都不留
leetcode力扣82https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/description/这题与上一题的差别就是只要是重复的元素,一个都不留,全部删除。
用(cur->next->val == cur->next->next->val)来判断,只要成立,若还有相等的,一定就挨在它们后面,所有用x记录一下相等的值,加一个while(cur->next!=NULL && cur->next->val ==x)就能将挨在一起相等的删干净
struct ListNode* deleteDuplicates(ListNode* head) {
if(head==NULL) return NULL;
struct ListNode* dummyNode=new struct ListNode(-1,head);
struct ListNode* cur=dummyNode;
while(cur->next!=NULL && cur->next->next!=NULL){
if(cur->next->val == cur->next->next->val){
int x=cur->next->val;
//删除重复的结点
while(cur->next!=NULL && cur->next->val ==x){
struct ListNode* temp= cur->next;
cur->next=cur->next->next;
delete temp;
}
}else{
cur=cur->next;
}
}
temp=dummyNode->next;
delete dummyNode;
return temp;
}