来源:代码随想录
1.理论基础
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
(1)类型
单链表只有一个指针域,只能指向下一个节点。
双链表有两个指针域,一个指向下一个节点,一个指向上一个节点。既可以向前查询也可以向后查询。
循环链表:首尾相连,可以用来解决约瑟夫环问题。
(2)存储方式
链表在内存中的分布不是连续的,它通过指针域的指针链接在内存中的各个节点。
(3)定义
//单链表,用结构体来定义一个链表节点
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* head = new ListNode(5);
使用默认构造函数初始化节点:
ListNode* head = new ListNode();
head->val=5;
如果不定义构造函数、直接使用默认构造函数的话,在初始化的时候不能直接给变量赋值。
(4)数组和链表的性能对比分析
数组的元素可以由元素下标跳着访问,但链表元素不行,必须要按照指针顺序找到这个元素。所以查找的时间复杂度是O(n)。
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
2.移除链表元素 203
移除链表元素的方法:将指向它的指针修改为指向它下一个节点的指针。注意移除后要从内存中将废弃节点删掉。
移除头结点:将头结点指针向后移动一个即可。
移除头结点有两种方法:(1)直接用原来的链表进行删除操作,则需要单独处理头结点;(2)设置一个虚拟头结点再进行删除操作,这样原链表的所有节点就可以按照统一的方式进行移除。
方法(1):
如果不释放废弃节点的内存:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//删除头结点
while(head!=NULL && head->val==val)
//注意这里不是if,因为删掉这个头结点之后新的头结点可能还=val
{
head=head->next;
//把头结点的指针域里的指针也就是指向头结点下一个节点的指针赋给指向头结点的指针head
//此时head就不指向头结点,而是直接指向下一个节点,那么就把原来的头结点从链表中去除了
}
//删除非头结点
ListNode* current=head; //因为头结点不能随便动,所以重新定义一个指针,赋值head指针
while(current!=NULL && current->next!=NULL) //链表中至少有两个节点
{
if(current->next->val==val) //下一个节点就是要删除的节点
{
current->next=current->next->next;
//把指向下一个节点的指针,直接改成下一个节点指向下下一个节点的指针,就把下一个节点从链表中去除了
}
else //下一个节点也不是要删除的节点
{
current=current->next;
//把指向下一个节点的指针赋值给指向这个节点的指针,就往后走了一个
}
}
return head;
}
};
释放废弃节点内存:
//删除头结点
while(head!=NULL && head->val==val)
{
ListNode* temp=head; //重新定义一个临时指针,让它来指向头结点
//不重新定义的话会错误释放新的头结点
head=head->next;
delete temp;
//delete释放new分配的单个对象指针指向的内存
//则这句释放了temp指针指向的内存空间,现在temp=head
//所以就释放了原来head指向的内存空间,也就从内存中删除了头结点
}
//删除非头结点
ListNode* current=head;
while(current!=NULL && current->next!=NULL)
{
if(current->next->val==val)
{
ListNode* temp=current->next;
current->next=current->next->next;
delete temp;
//释放了temp指针指向的内存空间,也就是释放了current->next指向的节点
}
else
{
current=current->next;
}
}
方法(2):
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead=new ListNode(0,head); //定义一个空节点作为虚拟头结点
//自身指针指向为dummyHead,下一个next指针为head
ListNode* cur=dummyHead; //虚拟头结点不能乱动,所以重新定义一个指针
while(cur->next!=NULL) //第一次循环的时候,这个next指的是真正的头结点
{
if(cur->next->val==val)
{
ListNode* temp=cur->next;
//结束后要释放掉cur->next这个节点,而一会儿这个值会被修改,所以先把它存一下,以防错误释放
cur->next=cur->next->next;
delete temp;
}
else
{
cur=cur->next; //不是要删除的节点,所以向后移一个
}
}
head=dummyHead->next;
//真正的头结点,永远是虚拟节点的下一个节点
delete dummyHead;
return head;
}
};
3.设计链表 707
重点:链表节点不能跳跃访问,必须从头节点开始一个一个访问过去。
class MyLinkedList {
public:
//定义链表节点结构体,最后不要忘了加分号
struct LinkedNode
{
int val;
LinkedNode* next;
//LinkedNode(){}
LinkedNode(int val):val(val),next(nullptr){}
};
//初始化链表
MyLinkedList() {
dummyHead=new LinkedNode(0); //创建一个虚拟头结点
size=0;
}
//获取第index个节点的值,如果index是非法值直接返回-1
int get(int index) {
if(index>=size || index<0)
{
return -1;
}
//要从(虚拟)头结点挨个访问到第index个节点
//因为是访问到第index个节点,所以从虚拟头结点的下一个节点,也就是真正的头结点开始
//挨个访问到刚好index减到0的地方,就是需要的那个节点
LinkedNode* cur=dummyHead->next;
while(index--)
{
cur=cur->next;
}
return cur->val;
}
//在链表头部插入节点,插入完成后,新插入的节点为链表的新节点
void addAtHead(int val) {
//实现方法为修改指针
//创建一个新的节点
LinkedNode* newnode=new LinkedNode(val);
newnode->next=dummyHead->next; //先让新节点的指针为原来指向真正头结点的指针
dummyHead->next=newnode; //让虚拟头结点指向newnode,则newnode为新的头结点
size++;
}
//在链表尾部插入新节点,注意链表不能跳跃式访问,必须从头结点一个一个访问过去
void addAtTail(int val) {
LinkedNode* newnode=new LinkedNode(val);
LinkedNode* cur=dummyHead;
while(cur->next!=nullptr)
{
cur=cur->next; //下一个不为空的话就一直往下走,这样结束的时候就访问到了最后一个节点
}
cur->next=newnode; //把newnode给最后一个节点的下一个
size++;
}
//在第index个节点之前插入值为val的新节点
//所以要循环访问至第index-1个节点
void addAtIndex(int index, int val) {
if(index>size) return; //index大于链表长度,直接返回,等于链表长度则在链表尾部插入
if(index<0) index=0; //index<0,则在链表头部插入节点
LinkedNode* newnode=new LinkedNode(val);
//因为是访问到第index-1个节点,所以从虚拟头节点开始
LinkedNode* cur=dummyHead;
while(index--)
{
cur=cur->next;
}
newnode->next=cur->next;
cur->next=newnode;
size++;
}
//删除第index个节点,index大于等于链表长度直接返回
void deleteAtIndex(int index) {
if(index>=size || index<0)
{
return;
}
LinkedNode* cur=dummyHead;
while(index--) //循环到index前一个节点
{
cur=cur->next;
}
LinkedNode* temp=cur->next; //现在cur是index前一个节点
cur->next=cur->next->next;
delete temp;
size--;
}
//打印链表
void PrintList()
{
LinkedNode* cur=dummyHead; //从头开始
while(cur->next!=nullptr) //到尾结束
{
cur=cur->next;
cout<<cur->val<<" ";
}
cout<<endl;
}
private:
int size;
LinkedNode* dummyHead;
};
4.反转链表.206
(1)双指针法
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; //待会用来存放cur->next这个节点
ListNode* pre=NULL;
ListNode* cur=head;
while(cur) //循环到cur为空为止
{
temp=cur->next; //保存原来的下一个节点
cur->next=pre; //反转
pre=cur;
cur=temp; //往下走一个,直到cur->next=NULL为止,此时就已经到尾部了
}
return pre; //现在pre是头结点,cur已经空了
}
};
(2)递归法,没有很懂
class Solution {
public:
ListNode* reverse(ListNode* pre, ListNode* cur)
{
if(cur==NULL) return pre; //这其实已经到最后了
ListNode* temp=cur->next;
cur->next=pre; //反转了
//下来需要更新
return reverse(cur,temp);
//这两句相当于:
//pre=cur; cur=temp;
}
//在这里面初始化就行
ListNode* reverseList(ListNode* head) {
return reverse(NULL,head);
}
};
5.两两交换链表中的节点24
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead=new ListNode(0);
dummyHead->next=head;
ListNode* cur=dummyHead;
while(cur->next && cur->next->next) //当这两个都不为空
{
ListNode* temp1=cur->next;
ListNode* temp2=cur->next->next->next; //令指向cur->next->next->next这个节点的指针为temp2
cur->next=cur->next->next; //步骤1
cur->next->next=temp1; //步骤2
cur->next->next->next=temp2; //步骤3
//步骤3看起来像是cur->next->next->next=cur->next->next->next,两边一样
//实际上左边是第cur->next->next个节点指向下一个节点的指针,而右边应该是第cur->next->next->next个节点
//所以先保存一下指向第cur->next->next->next个节点的指针,然后赋给第cur->next->next个节点指向下一个节点的指针
cur=cur->next->next; //向后移动两位,准备下一轮交换
}
return dummyHead->next;
}
};
6.删除链表的倒数第n个节点19
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyHead=new ListNode(0);
dummyHead->next=head;
ListNode* slow=dummyHead;
ListNode* fast=dummyHead;
//第一步 先让fast走n+1步,走到要删除的第n个节点之前再往前一个节点
while(n--)
{
fast=fast->next;
}
fast=fast->next; //要再走一个,才到第n个节点之前的一个节点
//第二步 slow和fast同时移动,直到fast=NULL
//此时可以保证slow停在要删除的第n个节点之前
while(fast!=NULL)
{
slow=slow->next;
fast=fast->next;
}
//第三步 删除slow的下一个节点
slow->next=slow->next->next;
return dummyHead->next;
//头节点可能变了,但虚拟头节点一直没变
}
};
7.链表相交 面试题02.07
curA指向链表A的开头,curB指向链表B的开头
(1)获得两个链表的长度和长度的差
(2)让curA移动到和curB使两个链表尾部对齐的位置(只有尾部对齐了才可以开始比较)
(3)再移动curA与curB,并比较二者是否相等:若相等,返回相等的节点下标,没有相等的则返回空(注意相交是链表节点的指针相等,不是值相等)
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA=headA;
ListNode* curB=headB;
int lenA=0, lenB=0;
//求链表A的长度
while(curA!=NULL)
{
lenA++;
curA=curA->next;
}
//求链表B的长度
while(curB!=NULL)
{
lenB++;
curB=curB->next;
}
//令curA curB回到头节点
curA=headA;
curB=headB;
//保证A始终是最长的那个链表
if(lenB>lenA)
{
swap(lenA,lenB);
swap(curA,curB);
}
//求长度的差
int gap=lenA-lenB;
//让A和B的尾部对齐
while(gap-- && curA!=NULL)
{
curA=curA->next;
}
//对齐后开始比较,并一起移动
while(curA!=NULL)
{
//if(curA->val==curB->val)
//注意相交是链表节点的指针相等,不是值相等
if(curA==curB)
{
return curA;
}
curA=curA->next;
curB=curB->next;
}
//没有交点,返回空
return NULL;
}
};
8.环形链表 142
两个关键点:
(1)判断链表是否有环
可以定义两个指针slow和fast。令fast走两步,slow走一步,则如果有环的话,在环内相当于fast是一步一步靠近slow的,所以二者一定会在环内相遇。即:相遇必有环。
(2)有环的话,如何找到环的入口
重新定义两个指针,一个指针从头节点出发,一个从相遇的节点出发,各走一步,下一次相遇的地方就是环的入口节点。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast=head;
ListNode* slow=head;
while(fast!=NULL && fast->next!=NULL) //没有环的话,fast走到链表结尾就结束循环了
{
slow=slow->next; //slow走一步
fast=fast->next->next; //fast走两步
if(slow==fast) //相遇,说明有环
{
ListNode* index1=fast; //一个指针从相遇的节点开始
ListNode* index2=head; //另一个指针从头节点开始
while(index1!=index2) //两个指针一步一步移动,直到相遇,结束循环
{
index1=index1->next;
index2=index2->next;
}
return index1; //相遇的地方就是环的入口节点
}
}
return NULL; //fast走到最后也没有fast=slow,说明没有环
}
};