代码随想录算法训练营 Day3 关于链表
关于链表
主播平时打比赛的时候都是用数组去模拟链表,搞得我对于链表的概念不是很清晰。这里直接引用代码随想录中的原文解释概念。
链表的类型
单链表
链表是一种通过指针串联在一起的线性结构,(单链表的)每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
如图所示:

双链表
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表既可以向前查询也可以向后查询。
如图所示:

循环链表
循环链表,顾名思义,就是链表首尾相连,可以用来解决约瑟夫问题。

链表的存储方式
链表的存储方式
了解完链表的类型,再来说一说链表在内存中的存储方式。
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
如图所示:

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
链表的定义
竞赛当中,单链表一般是用一个\(nxt[N]\)模拟指针,然后用\(e[N]\)来表示这个节点存放的元素。
// 单链表的版本
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}
// 双链表的版本
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
下面是结构体的写法:
// 单链表
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) {}
};
// 双链表
struct DList {
int val; // 节点储存的元素
DList *next; // 指向下一个节点的指针
DList *prev; // 指向上一个节点的指针
// 构造函数:一般常用的两种类型
DList () : val(0), next(nullptr), prev(nullptr) {}
DList (int _val) : val(_val), next(nullptr), prev(nullptr) {}
};
结构体里面的析构函数如果没有的话没办法赋初值,最好还是要加上。
数组模拟的链表操作如果感觉很难理解,可以试着画一个图。
链表的操作
删除节点
删除D节点,如图所示:

只要将C节点的next指针 指向E节点就可以了。如果是C++的话记得要释放一下这个节点的内存。
添加节点
如图所示:

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
性能分析
再把链表的特性和数组的特性进行一个对比,如图所示:

上文当中关于链表的概念以及解释图均出自《代码随想录》中“链表的理论基础”一节,这里只加入了用数组模拟的模板。
Leetcode 203.移除链表元素
题目链接:https://leetcode.cn/problems/remove-linked-list-elements/description/
题目描述:给你一个链表的头节点 \(head\) 和一个整数 \(val\) ,请你删除链表中所有满足 \(Node.val == val\) 的节点,并返回 新的头节点 。
题目解析:这个题目主要的点在于“虚拟头节点的使用”,这里只给出虚拟头节点的AC代码。
/**
* Definition for singly-linked list.
* 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) {
//新建一个虚拟头节点
ListNode* dummyNode = new ListNode(0, head);
// 从虚拟头节点开始进行删除
ListNode* cur = dummyNode;
// 不断查找下一个 直到链表的尾部
while (cur->next != nullptr) {
// 分类讨论:
// 如果下一个节点的值是val,那么需要删除。
// 我们先用tmp存一下这个点,便于释放内存,然后更新我们的cur->next
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
// 反之,直接更新就好了
cur = cur->next;
}
}
// 这个时候的头节点不一定是一开始的头节点,所以更新一下这个head
// 不要忘记删除虚拟头结点释放内存
head = dummyNode->next;
delete dummyNode;
return head;
}
};
也可以递归求解:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 基础情况:空链表
if (head == nullptr) {
return nullptr;
}
// 递归处理
if (head->val == val) {
ListNode* newHead = removeElements(head->next, val);
delete head;
return newHead;
} else {
head->next = removeElements(head->next, val);
return head;
}
}
};
Leetcode 707. 设计链表
题目链接:https://leetcode.cn/problems/design-linked-list/description/
题目描述:你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:\(val\) 和 \(next\) 。\(val\) 是当前节点的值,\(next\) 是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 \(prev\) 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 \(MyLinkedList\) 类:
\(MyLinkedList()\) 初始化 \(MyLinkedList\) 对象。
\(int get(int index)\) 获取链表中下标为 \(index\) 的节点的值。如果下标无效,则返回 -1 。
\(void addAtHead(int val)\) 将一个值为 \(val\) 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
\(void addAtTail(int val)\) 将一个值为 \(val\) 的节点追加到链表中作为链表的最后一个元素。
\(void addAtIndex(int index, int val)\) 将一个值为 \(val\) 的节点插入到链表中下标为 \(index\) 的节点之前。如果 \(index\) 等于链表的长度,那么该节点会被追加到链表的末尾。如果 \(index\) 比长度更大,该节点将 不会插入 到链表中。
\(void deleteAtIndex(int index)\) 如果下标有效,则删除链表中下标为 \(index\) 的节点。
题目解析:这个题目就是要我们去实现链表的五个接口。
class MyLinkedList {
public:
// 定义链表节点结构体
struct ListNode {
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int val) : val(val), next(nullptr) {}
ListNode(int val, ListNode *next) : val(val), next(next) {}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new ListNode(0);
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > _size - 1 || index < 0) {
return -1;
}
ListNode* cur = _dummyHead->next;
while (index --) {
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
ListNode* newNode = new ListNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size ++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
ListNode* newNode = new ListNode(val);
ListNode* cur = _dummyHead;
while (cur->next != nullptr) {
cur = cur->next;
}
cur->next = newNode;
_size ++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if (index > _size) return;
if (index < 0) index = 0;
ListNode* newNode = new ListNode(val);
ListNode* cur = _dummyHead;
while (index --) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size ++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
ListNode* cur = _dummyHead;
while (index --) {
cur = cur->next;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp = nullptr;
_size --;
}
// 打印链表
void printLinkedList() {
ListNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
ListNode* _dummyHead;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
这里的代码基本是参考《代码随想录》中敲的。还有一种双链表的写法,但是我对于链表不很熟,就先这样吧。
Leetcode 206. 反转链表
题目链接:https://leetcode.cn/problems/reverse-linked-list/description/
题目描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
题目解析:如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
只需要改变链表的next指针的指向,直接将链表反转,而不用重新定义一个新的链表,如图所示:


这里我们使用《代码随想录》中的图进行解释。
双指针法的意思就是,我们先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
然后开始反转,把cur->next用tmp存一下。
Q:为什么要保存一下这个节点呢?
A:因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
然后开始跑循环,继续移动pre和cur指针。
最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。
/**
* Definition for singly-linked list.
* 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* reverseList(ListNode* head) {
//双指针的写法
ListNode* cur = head;
ListNode* pre = nullptr;
while (cur) {
ListNode* nxt = cur->next;
cur->next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
};
这道题还可以使用递归和递推的思路分别去写,本质上是头插法和尾插法的区别,可以看一下这道题题解灵神的代码。

浙公网安备 33010602011771号