1.链表基础知识:
根据代码随想录学习,做的笔记和算法心得
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思),链表的入口节点称为链表的头结点也就是head。
三种:单链表,双链表,循环链表。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表 既可以向前查询也可以向后查询。
循环链表,就是链表首尾相连,循环链表可以用来解决约瑟夫环问题。
链表中的节点在内存中不是连续分布的 ,
C/C++的定义链表节点方式
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
c++删除链表节点需要手动释放该节点。
链表的增添和删除都是O(1)操作,也不会影响到其他节点。
要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
2.移除链表元素(203题)
题目描述:删除链表中等于给定值 val 的所有节点。
链表节点需要去手动删除,删除下的节点
链表的一般解法1.直接在原来的链表进行删除操作2.设置一个虚拟头节点进行删除操作
删除链表元素的操作本质就是将节点的next指针直接指向下一个节点的下一个节点,(直接链表操作)其他节点根据前一个节点删除当前节点,头节点:将头节点向后移动一位即可。设置虚拟头节点可以不用上述操作。
直接链表操作法:
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) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
while(head != NULL && head->val == val){//判断头节点是否是删除的值
ListNode* tmp = head;//记录下头节点
head = head->next;//头节点移动下一个节点即可
delete tmp;//删除头节点
}
ListNode* cur = head;//非头节点操作
while(cur != NULL && cur->next != NULL){
if(cur->next->val == val){
ListNode* tmp = cur->next;//和处理头节点类似
cur->next = cur->next->next;
delete tmp;
}else{
cur = cur->next;//相当于循环体
}
}
return head;
}
};
创建一个虚拟头节点就可按照统一的方式进行处理,注:虚拟头节点的下一个节点才是头节点,
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyhead = new ListNode(0);//创建一个虚拟头节点以有参构造的形式
dummyhead->next = head;//将虚拟头节点接入进来
ListNode* cur = dummyhead;//定义当前节点位置
while(cur->next != NULL){//到整个链表结束
if(cur->next->val == val){
ListNode* tmp = cur->next;//定义一个临时存储变量接收
cur->next = cur->next->next;//指向下一个位置
delete tmp;
}else{
cur = cur->next;//相当于 ++操作
}
}
head = dummyhead->next;//虚拟头节点下一个才是头节点
delete dummyhead;//删除虚拟头节点
return head;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
3.设计链表(707题)
题目描述:
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
这个题包含的全是链表的基本操作,很好的练习对链表的基本操作,
class MyLinkedList {
public:
struct Linknode{//定义链表节点结构体
int val;//数据域
Linknode* next;//指针域
Linknode(int val):val(val),next(nullptr){}//有参构造
};
MyLinkedList() {//初始化链表
_dummyhead = new Linknode(0);//设置虚拟头节点
_size = 0;
}
int get(int index) {//获取下标的值
if(index > (_size-1) || index < 0){//定义下标无效的情况
return -1;
}
Linknode* cur = _dummyhead->next;//设置当前节点是头节点
while(index--){//下标进行循环
cur =cur->next;//循环体相当于++操作
}
return cur->val;//返回得到的值
}
void addAtHead(int val) {//在头插入值
Linknode* newnode = new Linknode(val);//创建一个新的节点
newnode->next = _dummyhead->next;//连接链表的左侧
_dummyhead->next = newnode;//再将链表的右侧连接起来
_size++;//整体大小+1
}
void addAtTail(int val) {//在尾插入值
Linknode* newnode = new Linknode(val);//创建一个新的节点
Linknode* cur = _dummyhead;//定义当前节点,为找到最后节点遍历做基础
while(cur->next != nullptr){//循环条件找到最后的节点位置
cur = cur->next;//循环体,
}
cur->next=newnode;//最后节点位置的下一个将插入元素放入
_size++;
}
void addAtIndex(int index, int val) {//将一个值为val的节点加入到Index处
if(index > _size){//下标无效
return;
}
Linknode* newnode = new Linknode(val);//新建立节点
Linknode* cur = _dummyhead;//当前节点
while(index--){
cur = cur->next;
}
newnode->next = cur->next;//找到index位置,先链接后面再链接前面
cur->next = newnode;//
_size++;
}
void deleteAtIndex(int index) {//删除链表Index的节点
if(index >= _size || index < 0){//下标无效情况
return;
}
Linknode* cur = _dummyhead;//当前节点
while(index--){//
cur = cur->next;
}
Linknode* tmp = cur->next;//找到当前index位置的节点
cur->next = cur->next->next;//链接后面
delete tmp;//删除节点
_size--;
}
void printLinkedList(){//打印节点值
Linknode* cur = _dummyhead;//当前节点
while(cur->next != nullptr){//知道整个链表结束
cout<<cur->next->val<<"";
cur = cur->next;
}
cout<<endl;
}
private:
int _size;
Linknode* _dummyhead;
};
4.反转链表(206题)
题目要求:反转一个单链表,不申请额外的内存空间。
改变链表的指针指向即可
双指针解法:定义一个快指针,一个慢指针,快指针指向头节点,慢指针指向空,使用tmp保持临时节点。
class Solution {
public:
ListNode* reverseList(ListNode* head) {//双指针方法
ListNode* fast = head;//定义一个快指针指向头节点
ListNode* tmp;//临时节点,接入快指针的位置
ListNode* slow = nullptr;//定义一个慢指针指向空
while(fast){//遍历整个链表
tmp = fast->next;//首先断开后面的链表,并且用一个指针接受位置,更新下一个快指针的位置
fast->next = slow;//链接前面的节点
slow = fast;//更新慢指针到快指针位置
fast = tmp;//快指针在向后移动到达临时接受的指针位置
}
return slow;//新的头节点也就是慢指针的位置
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
递归解法:注:是根据两个节点去写反转的实现到整个链表
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){//定义两个链表节点进行操作
if(cur == nullptr)return pre;//如果快指针为0,返回慢指针
ListNode* tmp = cur->next;//临时指针接收快指针下一个位置
cur->next = pre;//反转
return reverse(cur,tmp);//递归下一个函数,相当于cur是新的pre,tmp是新的cur变相链表指针向后移动
}
ListNode* reverseList(ListNode* head) {
return reverse(nullptr,head);//从头到尾反转
}
};
- 时间复杂度: O(n), 要递归处理链表的每个节点
- 空间复杂度: O(n), 递归调用了 n 层栈空间
5. 两两交换链表中的节点(24题)
题目描述:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
模拟一下反转链表即可实现,定义循环条件这两个交换的节点都不为空时才可以循环,且循环条件也有顺序,不可以随意替换
注:要断的地方需要用临时节点保存一下
class Solution {
public:
ListNode* swapPairs(ListNode* head) {//
ListNode* dummyhead = new ListNode(0);//设置虚拟头节点
dummyhead->next = head;//将虚拟头节点接入整个链表中
ListNode* cur = dummyhead;//定义当前节点是虚拟头节点开始
while(cur->next!=nullptr&&cur->next->next!=nullptr){//确保两个节点不为空,为空不做处理
ListNode* tmp = cur->next;//设置一个临时的指针接收头节点
ListNode* tmp1 = cur->next->next->next;//设置第二个临时接收第二次的头节点位置
cur->next = cur->next->next;//头节点指向2号位置
cur->next->next = tmp;//2号的下一个指向1号位置也就是第一个临时节点
cur->next->next->next = tmp1;//一号位置下一个指向第二次节点指向位置
cur = cur->next->next;//走到相当于2号位置
}
return dummyhead->next;//返回头节点
delete dummyhead;
}
};
- 时间复杂度:O(n)
- 空间复杂度:O(1)
6.删除链表的倒数第N个节点(19题)
双指针解法:删除倒数第n个节点,让快指针先走n步,快指针应该向后移动一个节点,为了慢指针可以删除操作,之后让快指针和慢指针同时移动,直到快指针为null为止,然后删除slow指向的节点即可。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyhead = new ListNode(0);
dummyhead->next = head;
ListNode* fast = dummyhead;//定义快慢指针都指向虚拟头节点
ListNode* slow = dummyhead;
while(n-- && fast != nullptr){//先让快指针走n个节点并且确保快指针不为空
fast = fast->next;
}
fast = fast->next;//快指针再前一步,因为删除操作需要前一个节点信息,所以这为了帮助slow指向删除节点的上一个节点
while(fast != nullptr){//快指针循环条件知道空指针为止
fast = fast->next;//快慢指针一起移动
slow = slow->next;
}
ListNode* tmp = slow->next;//定义一个临时的节点接收需要删除的节点
slow->next = slow->next->next;//删除节点,指向需要删除节点的下个节点
delete tmp;//释放节点内存
return dummyhead->next;//返回头节点
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
7. 链表相交(面试题 02.07)
链表相交求的是两个链表交点节点的指针,
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* curA = headA;//定义两个指针分别指向头节点
ListNode* curB = headB;
int lenA = 0,lenB = 0;//定义每个链表的长度值
while(curA){//计算A链表长度
lenA++;
curA = curA->next;
}
while(curB){//计算B链表长度
lenB++;
curB = curB->next;
}
curA = headA;//重新将指针指向头节点位置
curB = headB;
if(lenA<lenB){//让lenA是最大值的链表
swap(curA,curB);
swap(lenA,lenB);
}
int gap = lenA-lenB;//计算两个链表的差值
while(gap--){//将A 链表移动到与b链表尾部一个位置的位置
curA = curA->next;
}
while(curA){//循环条件直到A链表为空
if(curA == curB){//找到两个链表相等的位置返回A链表
return curA;
}
curA = curA->next;//否则两个链表一起向后移动
curB = curB->next;
}
return NULL;
}
};
实际上就是首先定义两个节点,并且对两个链表求长度,得到差值,然后再经过长度大的链表移动差值之后,对比两个链表的值,如果相等就返回。
- 时间复杂度:O(n + m)(取决两个链表的长度)
- 空间复杂度:O(1)
8.环形链表II(142题)
题目描述:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。不允许修改给定的链表。
两个任务首先判断是否有环,找到入环的入口,判断是否有环采用的是双指针的做法,快指针每次走两个节点,慢指针每次走一个节点,为何能相遇,对于slow来说是一个节点速度慢慢的逼近,设头节点到入环节点的节点数为x,在环内相遇的地方距离入口为y,剩余的节点环内为z,slow指针走了x+y节点,fast指针走了x+n(y+z)+y,可以得到一个等式2(x+y)=x+n(y+z)+y,我们需要x值,所以可以整理可得x=(n-1)(y+z)+z,说明一个问题假设两个指针一个从头节点出发,另一个在相遇点出发,每次移动一个节点,两个指针相遇时候则就是入环地址。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow = head;//设置两个指针一个快指针一个慢指针都指向头节点
ListNode* fast = head;
while(fast != nullptr && fast->next != nullptr){//循环条件
slow = slow->next;//慢指针一次走一个节点
fast = fast->next->next;//快指针一次走两个节点
if(slow == fast){//如果快慢指针相遇
ListNode* index1 = fast;//记录快指针的位置
ListNode* index2 = head;//说明一个指针从头节点出发,另一个指针从相遇位置出发,最后相遇的点就是环的入口
while(index1 != index2){//循环条件
index1 = index1->next;
index2 = index2->next;
}
return index2;//返回入口地址也是两个指针相遇的地址
delete index1;
delete index2;
}
}
return nullptr;//
delete slow;
delete fast;
}
};
总结:
链表基础知识,在空间不是连续存在的,删除节点需要手动释放空间,第一个节点是头指针,最后一个是空指针,记住链表节点的结构体,
移除链表元素,采用虚拟头节点方法将指针指向删除节点的下一个指针域即可,不要忘记释放节点
设计链表:首先定义链表节点的结构体,数据域,指针域,有参构造,删除节点,以及打印,加值删除值,
反转链表:双指针方法实现,快指针指向头节点,慢指针指向空指针,注意断开连接时需要找个临时节点接入断开节点的下一个节点,每次两个指针向前移动。
两两交换链表:其实就是模拟反转的过程,但是需要采用两个临时节点接入,而且注意顺序,循环条件的顺序也要注意,其他就是模拟一下链表的反转操作流程。
删除链表的倒数第N个节点:双指针解法:思想非常好,先让快指针跑n+1个节点,这里为什么是n+1节点,因为慢指针需要操作删除节点,
链表相交:一个长链表和一个短链表,计算两个链表的长度,计算差值,让长链表移动差值之后,两个链表一起遍历如果相等返回数值。
环形链表:有两个点一个是如何判断有环,还有一个是环的入口在哪,判断有环的思想是定义两个指针,快慢指针快指针每次走两个节点,慢指针每次走一个节点,如果相遇则有环,并且用指针记录下来,另一个指针从头节点开始,这两个指针每次走一个节点最后相遇就是环的入口。真是妙极了!!!!!!!!