链表的概念:
链表是以节点为单位,每个元素都是一个独立对象,在内存的储存空间是非连续的。链表的节点对象具有两个成员变量:【值 val】【后继节点引用 next】
struct ListNode{
int val;//节点值
ListNode *next;//后继节点引用
ListNode(int x) : val(x), next(NULL) {}
}
在了解链表的概念以后,我们用几个简单的题目来熟悉一下链表的操作。
注:本题来自《剑指offer》T6
1.输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例:
输出:head=[1,3,2]
输出:[2,3,1]
思路:看到这个题目的直觉是把链表中数据放到数组中,再将数据倒序输出,但是这样会多定义一个数组储存数据增加空间复杂度,此时想到栈具有先进后出的性质,我们可以运用栈灵活解题。
实现:
class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> result;
stack<int> st;//创建一个栈,用于储存链表的节点数据
ListNode* curr=head;//创建一个指针,初始时指向链表的头节点
while(curr!=NULL)//指针值向的元素非空时
{
st.push(curr->val);//将指针指向的节点数据压入栈内
curr=curr->next;//将指针移到后驱节点
}
while(!st.empty()){//栈非空时
result.push_back(st.top());//将栈顶的元素压入vector内
st.pop();//弹出栈顶元素
}
return result;
}
};
复杂度分析:
时间复杂度O(n):入栈和入栈共用O(n)时间。
空间复杂度O(n):辅助栈stack和数组result共用O(n)的额外空间。
注:本题来自《剑指offer》T18
2.给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点
返回删除后链表的头节点
示例:
输入:head=[4,5,1,9],val=5
输出:[4,1,9]
分析:看到此题自然想到双指针
1.设置两个指针指向头节点prev(带删节点的前驱节点)和curr(当前节点)
2.遍历链表查找节点值为val的节点,找到则删除,否则继续查找
实现:
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val) {
if(head->val==val){//头节点为待删除的节点
return head->next;
}
ListNode* prev=head;//当前节点
ListNode* curr=head->next;//保存待删节点的下一节点
while(curr!=nullptr&&curr->val!=val)
{
prev=curr;
curr=curr->next;
}
if(curr!=nullptr)prev->next=curr->next;
return head;
}
};
此题也可以采用单指针,递归,栈等多种方式完成,这里不再过多讲解,有兴趣的可以自己尝试
复杂度分析:
时间复杂度O(n):只遍历了链表一次
空间复杂度O(1)
注:本题来自《剑指offer》T24
3.定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点
示例:
输入:1->2->3->4->5->NULL
输出:5->4->3->2->1->NULL
此题有两种解题思路
1.迭代法
分析:首先我们创建curr储存后一个节点,然后创建prev储存前一个节点
每一次操作我们只需要将当前节点的next改为指向前一个节点即可,此时存在一个问题:我们将下一个节点指向next后,curr->next->next我们就无法访问到,因此我们还需要一个变量temp来记录curr->next
具体实现:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* curr=head;
ListNode* prve=nullptr;
while(curr)
{
ListNode* temp=curr->next;
curr->next=prve;
prve=curr;
curr=temp;
}
return prve;
}
};
复杂度分析:
时间复杂度:O(n),其中n是链表的长度。需要遍历链表一次
空间复杂度:O(1)。
2.递归法
分析:使用递归法遍历链表,越过尾节点后终止递归,回溯时修改各节点的next引用指向。
具体实现:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==nullptr||head->next==nullptr) return head;//终止条件
ListNode *p=reverseList(head->next);//递归后继节点
head->next->next=head;//修改节点引用指向
head->next=nullptr;
return p;//返回反转链表的头节点
}
};
复杂度分析:
时间复杂度:O(n):需要遍历链表一次
空间复杂度:O(n):遍历链表的递归深度达到n,系统需要在栈区使用O(n)大小额外空间。
有了前面几个题目的铺垫,我们已经初步了解快慢指针,链表反转的知识,接下来我们通过下面这个题目综合运用
注:此题来自《剑指offer》T27
4.给定一个链表的头节点head,请判断是否为回文链表
如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的
示例:
输入:head=[1,2,3,3,2,1]
输出:true
思路:
1.找到前半部分链表的尾节点(采用快慢指针,慢指针走一步,快指针走两步)
2.反转后半部分链表
3.判断是否是回文
4.恢复链表
5.返回结果
实现:
class Solution {
public:
bool isPalindrome(ListNode* head) {
if(head==nullptr){
return true;
}
ListNode* midList=findMid(head);//寻找前半部分链表的尾节点
ListNode* reveresHalfEnd=reverseList(midList->next);//反转后半部分链表
ListNode* p1=head;
ListNode* p2=reveresHalfEnd;
bool result=true;
while(p2!=nullptr&&result)//判断是否是回文
{
if(p1->val!=p2->val){
result=false;
}
p1=p1->next;
p2=p2->next;
}
midList->next=reverseList(reveresHalfEnd);//恢复链表
return result;//返回结果
}
ListNode* findMid(ListNode* head)
{
ListNode* fastptr=head;//快指针
ListNode* slowptr=head;//慢指针
while(fastptr->next!=nullptr&&fastptr->next->next!=nullptr)
{
fastptr=fastptr->next->next;//快指针走两步
slowptr=slowptr->next;//慢指针走一步
}
return slowptr;//返回前半部分链表的尾节点
}
ListNode* reverseList(ListNode* head)//反转链表
{
ListNode* curr=head;
ListNode* prev=nullptr;
while(curr!=nullptr)
{
ListNode* temp=curr->next;
curr->next=prev;
prev=curr;
curr=temp;
}
return prev;
}
};
此题通用可以用递归或栈的方法解决,不再讨论
复杂度分析:
时间复杂度:O(n)
空间复杂度:O(1)
———————————————————————————————————————————
对以上题目或链表有问题可私信作者微信询问
如果你觉得这篇文章不错,留下你宝贵的点赞关注收藏。
你的支持是我更新的巨大动力!