链表
-
链表相关知识
C/C++的定义链表节点方式,如下所示:
struct ListNode { int val;//节点值 ListNode *next;//指向下一个节点地址 ListNode():val(x),next(null){}//有参构造函数 };
如果不定义有参构造函数,默认构造函数无参,在初始化链表时不能给值,需要分两步
ListNode *head=new ListNode();//堆区创建一个链表头节点,new返回的是链表地址,需要用指针去接收 head->val=5;//头节点的值赋值为5
链表与数组性能对比
性能分析
再把链表的特性和数组的特性进行一个对比,如图所示:
-
203移除链表元素
1)题目描述
给你一个链表的头节点
head
和一个整数val
,请你删除链表中所有满足Node.val == val
的节点,并返回 新的头节点 。示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
2)解题思路
链表删除时分两种情况,如果删头节点,直接令head=head->next,然后删除原有头节点。
若为非头节点,需要先设置一个当前节点,当前节点从头开始遍历,依次判断当前节点下一个节点是否为要删除的节点。如果是,那么将下一节点地址暂存,当前节点的next指向下下节点,然后删除暂存的节点。
3)代码实现
class Solution { public: ListNode* removeElements(ListNode* head, int val) { //删除头节点 while(head!=nullptr&&head->val==val){ ListNode *tmp=head;//暂存头节点地址 head=head->next;//头节点指向下一个节点 delete tmp;//删除原始的头节点 } //删除非头节点 //定义一个当前节点 ListNode *cur=head;//当前节点从头节点开始遍历 while (cur!=nullptr&&cur->next != nullptr)//遍历链表,如果指向下一个节点指针地址不为空 { if (cur->next->val == val)//如果下一节点值等于目标值 { ListNode * tmp = cur->next;//暂存的是等会要清空的节点 cur->next = cur->next->next;//更新后的cur-next可能指空,因此while中要判cur-next delete tmp; } else//如果不等,指针移到下一个节点 { cur = cur->next;//更新后的cur可能指空 } } return head; } };
4)总结
链表删除需要注意三点:一,区分头节点和非头节点删除方法不同,二,取链表属性前需要判断指向是否为空,若为空是不能访问值的;三,tmp暂存的是需要删除的节点,并且delete操作也删除的是tmp。
虚拟头节点的方法可以统一头节点和非头节点的代码,但是逻辑太绕,就不研究了,待熟悉链表编程后再更新。
-
707设计链表
1)题目描述
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:
val
和next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性prev
以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。在链表类中实现这些功能:
- get(index):获取链表中第
index
个节点的值。如果索引无效,则返回-1
。 - addAtHead(val):在链表的第一个元素之前添加一个值为
val
的节点。插入后,新节点将成为链表的第一个节点。 - addAtTail(val):将值为
val
的节点追加到链表的最后一个元素。 - addAtIndex(index,val):在链表中的第
index
个节点之前添加值为val
的节点。如果index
等于链表的长度,则该节点将附加到链表的末尾。如果index
大于链表长度,则不会插入节点。如果index
小于0,则在头部插入节点。 - deleteAtIndex(index):如果索引
index
有效,则删除链表中的第index
个节点。
示例:
MyLinkedList linkedList = new MyLinkedList(); linkedList.addAtHead(1); linkedList.addAtTail(3); linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3 linkedList.get(1); //返回2 linkedList.deleteAtIndex(1); //现在链表是1-> 3 linkedList.get(1); //返回3
2)解题思路
草稿纸上画链表结构图,按照操作步骤实现代码。
3)代码实现
class MyLinkedList { public: //创建链表数据结构 struct ListNode { int val; ListNode *next; ListNode(int x):val(x),next(nullptr){} }; //初始化链表,创建虚拟头节点 MyLinkedList() { dummyHead=new ListNode(0); m_size=0; } int get(int index) { if(index>(m_size-1)||index<0) { return -1; } ListNode *cur=dummyHead->next; //cout<<"size:"<<m_size<<" index:"<<index<<endl; //cout<<cur->val<<endl; //cout<<cur->next->val<<endl; while(index--) { cur=cur->next; // cout<<cur->val<<endl; } cout<<cur->val<<endl; return cur->val;//判断过index的范围在链表size的有效范围,因此cur必定有值,无需再判是否为空指针。 } void addAtHead(int val) { //先创建一个新节点 ListNode *newNode=new ListNode(val); newNode->next=dummyHead->next;//newnode相当于插入在dummyhead和第一个节点之间 dummyHead->next=newNode;//注意,将新加的节点放在虚拟节点的后面 m_size+=1; } void addAtTail(int val) { //先创建一个新节点 ListNode*newNode=new ListNode(val); ListNode *cur=dummyHead; //找到链表尾节点 while(cur->next!=nullptr){ cur=cur->next; } //将新节点串到尾节点上 cur->next=newNode; //更新链表大小 m_size+=1; } void addAtIndex(int index, int val) { ListNode *newNode=new ListNode(val); ListNode *cur=dummyHead; if(index>m_size)//若index大于size,直接返回 { return; } //找到第index个节点的前一个节点 while(index--) { cur=cur->next; } //令cur下一个节点指向新节点,同时新节点的下一个节点指向cur原始的下个节点 // ListNode *tmp=cur->next;//若index=size,该值为空指针,也满足。 // cur->next=newNode; // newNode->next=tmp; newNode->next=cur->next; cur->next=newNode; // cout<<"增加后的链表"<<dummyHead->next->val<<cur->val<<cur->next->val<<endl; //更新链表size m_size+=1; } void deleteAtIndex(int index) { if(index>=m_size||index<0)//注意可以取到等号 { return; } ListNode *cur=dummyHead;//当前节点先指向头节点 //cout<<cur->next->val<<endl; //找到index的位置 while(index--) { cout<<cur->next->val<<endl; cur=cur->next; } //cur指向index前一个元素 ListNode*tmp=cur->next; cur->next=cur->next->next; delete tmp; //更新size m_size=m_size-1; //cout<<m_size+10<<endl; } private: int m_size; ListNode *dummyHead; };
4)总结
链表定义时初始化了一个虚拟头节点,数据为0,这个节点不计入index中。返回链表index个元素的时候下标从0开始,从第一个有效节点开始算,虚拟头节点不算。index与数组下标类似。
在第index位置插入元素时,需要把当前节点(从虚拟节点开始)先移动到index的前一个节点,移动次数为index次,数不清次数时可以画图。
设计到链表index时,一定要先判断index是否在有效范围内,与0,size之间的关系是否有效,若无效直接返回。
多个函数调试出错时可以通过打日志的方式记录,判断到底是执行哪个函数时出错,哪个循环内出问题。
- get(index):获取链表中第
-
反转链表
1)题目描述
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
2)解题思路
单链表反转的本质是链表节点的插入。输出链表初始只有虚拟节点和头节点,将头节点后面的节点(作为当前节点)依次插入虚拟节点和新头节点之间,即可完成链表的反转。
关键在于每次插入完需要更新头节点和当前节点到底是哪一个。
3)代码实现
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head==nullptr)//若链表为空,返回
{
cout<<0<<endl;
return 0;
}
//创建一个虚拟头节点
ListNode* dummyHead=new ListNode(0);//创建虚拟头节点需要在堆区创建
ListNode*cur=head;
//将第一个节点链接在虚拟头节点后面
dummyHead->next=head;
//将cur->next节点插入dummyHead和head之间
while(cur->next!=nullptr)
{
//在虚拟头节点和头节点之间插入第二个节点
ListNode *tmp=cur->next->next;
//cout<<"q下一个节点值"<<q->next->val<<endl;
dummyHead->next=cur->next;
cur->next->next=head;
//完成插入后更新head节点和cur->next节点
cur->next=tmp;//将cur->next移到tmp处
head=dummyHead->next;//将head移到更新的head处
// cout <<dummyHead->next->val<<endl;
}
return head;
}
};
4)总结
1.链表一定要定义好head节点,dummyHead节点,cur节点究竟指向哪一个节点,否则在循环中容易绕晕
2.创建虚拟头节点时需要在堆区new出来,不能直接调用有参构造函数创建,否则会报错。下图写法错误,错误原因不是很清楚。
struct ListNode {
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
//这种创建节点方法错误,错误原因是啥??
ListNode* dummyHead(0);//这是个匿名对象吗??
//这种写法编译器不报错
auto p=ListNode(5);
//正确的写法
ListNode* dummyHead=new ListNode(0);
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
//这种创建节点方法错误,错误原因是啥??
ListNode* dummyHead(0);//这是个匿名对象吗??
//这种写法编译器不报错
auto p=ListNode(5);
//正确的写法
ListNode* dummyHead=new ListNode(0);