一. 链表理论基础
文章链接:https://programmercarl.com/%E9%93%BE%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html
1.链表的定义
链表是一种线性数据结构,可以通过指针将各个数据串联在一起,形成一个整体。
2.链表的结构
通过上述定义,可以很轻易地看出指针的每个节点至少由两个部分组成:数据域和指针域。数据域用于存放数据,指针域用于存放指向下一个节点的指针,最后一个节点的指针域指向null(空指针)。通过上一个节点的指针域,我们可以得到下一个节点的位置,因此,所有节点就线性地串起来了。示意图如下:
3.链表的类型
(1)单链表
刚刚展示的就是单链表的结构,即单个指针域,也指单向连接。
(2)双链表
即双向链表,意味着这类链表的每个节点有两个指针,前指针和后指针,分别指向前一个节点和后一个节点,但仍然只有一个数据域。双端要么一个没有前指针,要么一个没有后指针。
(3)循环链表
循环,意味着一个圈,也就表明该类链表首尾相连(也可以说没有明确的头尾),像是串珠子做手链一样。 一般在解决约瑟夫环问题时会使用该类链表。
4.链表的存储方式
和数组在内存中的连续存储方式不同,链表在内存中一般不是连续分布的。因为节点之间的串联是通过指针实现的,只要知道下一个节点的地址,就可通过地址找到下一个节点,实现逻辑连接,但节点与节点之间的物理地址不一定连续。
所以链表中的节点是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
由此图可以很明显地看出,该链表各个节点在内存中不是连续存放的。
5.链表的实现
面试的时候,经常会要求手写链表,因此必须掌握手写链表的代码。
struct ListNode{
int val;//定义数据域
ListNode *next; //定义指针域,注意这里的指针数据类型是ListNode *,不是int *
//因为指针指向的下一个节点是整个ListNode,而不单单是它存储的int类型数据
ListNode(int x) : val(x), next(NULL){} //构造函数,可以赋初值。
};
注意,C++会默认生成一个构造函数,但是这个构造函数不可以进行初始化。
6.链表的操作
链表元素的增删比数组要简单地多,因为它在内存中不是连续存储的,因此增删节点不会改变后面节点的位置,没有数组增删后面元素移位的过程。
(1)增加节点
只需要将待插入位置的前一个节点指针指向的位置改为需插入节点的地址,并且将新节点的指针域指向原下一个节点即可。
(2)删除节点
删除节点比增加节点更简单,只需要将待删除节点的前一个节点的指针直接指向待删除节点的下一个节点的地址即可,即将上一个节点的指针赋值为待删除节点的指针的值。 除此之外,还需要释放待删除节点所占的内存(C++而言)。
(3)查找
链表的增删看着似乎很简单,但其实一般的增删题都是要先找到待增删的位置,而查找对链表来说非常麻烦。比如,我要删除第五个节点,在数组中可以直接访问第五个节点a[4]的位置,但在链表中,你需要从头节点开始,根据指针一步一步地访问过去(根本原因还是因为链表在内存中不是顺序存储的)。因此链表的查找操作是O(n).
总结:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
二. LeetCode203.移除链表元素
题目链接&&文章讲解&&视频讲解:https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html
状态:已解决
1.思路
链表的删除没有太大的思维难度,本质就是让待删除节点的上一个节点直接指向待删除节点的下一个节点,但有一些细节的地方需要去琢磨:
(1)设立cur指针代表当前指针,那么当前指针究竟是指向待删除节点,还是待删除节点的前一个节点?
如果cur指向待删节点,
其实也可以再设立一个指针代表上一个节点,跟随cur一起后移,但是比较麻烦。
如果cur指向待删节点的上一个节点:
更直接方便。
(2)选择cur是待删节点的上一个节点,如果待删节点是头节点,而头节点没有上一个节点,又该怎么统一代码?
新增一个虚拟头节点,这样原链表的所有节点就都可以按照统一的方式进行移除了。
示例:
(3)如果选择C++编写程序,删除节点的时候还需要释放该节点的内存,避免未删除导致新建变量使用了保留原数据的内存。
2.代码实现
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode * dummyNode = new ListNode(0);
dummyNode->next = head;
ListNode * cur = dummyNode;
while(cur->next != nullptr){
if(cur->next->val == val){
ListNode * temp = cur->next;
cur->next = cur->next->next;
delete temp;
}else{
cur = cur->next;
}
}
return dummyNode->next;
}
};
时间复杂度:O(n)
空间复杂度:O(1)
三. LeetCode707.设计链表
题目链接&&文章讲解&&视频讲解:https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html
状态:已解决
1.思路
这道题主要考察链表的综合运用,整体思维难度不大,主要是还是一些细节之处的处理。
为了统一代码,依旧是做了虚拟头节点的处理。
(1)cur的值取上一个节点的地址还是待处理节点的地址?
对于删除、增加(中间节点)这种需要获取前一个节点地址的操作,cur都是指向上一个节点,对于获取、增加头尾节点这种要么不涉及上一个节点,要么无真正前驱,要么无真正后继的操作,cur都是指向待处理节点的。
(2)新增一个节点,两条赋值语句(index节点的上一个节点指向新节点,新节点指向index节点)的顺序是怎样的(针对单链表)?
假如是这个顺序,那么当C指向新节点F时(C->next = F),C与D之间的连接就不存在了,因为C->next的值改变了,我们没法找到原先C指向的下一个节点D的地址了,F也就没法指向D节点了,因此,我们必须先让F指向D,再让C指向F。
(3)index判断是否越界
操作之间index判断是否越界的条件是不同的,对于添加中间节点,index是大于size;对于删除节点,index是大于等于size。
2.代码实现:
#include<iostream>
using namespace std;
class MyLinkedList {
public:
struct ListNode{
int val;
ListNode *next;
ListNode(int x) : val(x),next(NULL){}
};
MyLinkedList() {
dummyHead = new ListNode(0);
size = 0;
}
int get(int index) {
if(index >= size || size < 0) return -1;
ListNode * cur = dummyHead->next;
while(index--){
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
ListNode * newNode = new ListNode(val);
ListNode * cur = dummyHead;
newNode->next = dummyHead->next;
dummyHead->next = newNode;
size++;
}
void addAtTail(int val) {
ListNode * newNode = new ListNode(val);
ListNode * cur = dummyHead;
while(cur->next != NULL){
cur = cur->next;
}
cur->next = newNode;
size++;
}
void addAtIndex(int index, int val) {
if(index > size) return;
ListNode * newNode = new ListNode(val);
ListNode * cur = dummyHead;
while(index--){
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
size++;
}
void deleteAtIndex(int index) {
if(index >= size || index < 0 ) return;
ListNode * cur = dummyHead;
while(index--){
cur = cur->next;
}
ListNode * tmp = cur->next;
cur->next = tmp->next;
delete tmp;
tmp = NULL;
size--;
}
private:
int size = 0;
ListNode * dummyHead;
};
时间复杂度:涉及index的相关操作为O(index),其余为O(1)。
空间复杂度:O(n)。
四. LeetCode206.反转链表
题目链接&&文章讲解&&视频讲解:https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html
状态:已解决
1.思路
开始想的是,从尾部开始反转,但是细想后发现要是这样处理,链表就要从尾往头遍历,单链表做不到。因此,只能从头往尾遍历。并且不能只用一个指针。为什么呢?可能有人觉得让cur指向待处理节点的上一个节点不就好了,跟之前一样,但待处理节点翻转后,cur就没办法继续往尾部走了。如图。
实则还需要一个指针代表现在正在处理的节点,一共三个指针,分别代表:已经处理的节点、正在处理节点、即将处理的节点,这样才能保证遍历能够一直进行下去。
2.代码实现
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode * cur = head;
ListNode * forwardNode = nullptr;
while(cur != NULL){
ListNode * temp = cur;//temp为待处理节点
cur = cur->next;//更新下一个节点,避免后续赋值结束无法获得下一个节点地址了。
temp->next = forwardNode;//反转
forwardNode = temp; //处理后的节点变成新的待处理节点的上一个节点
}
return forwardNode;
}
};
时间复杂度: O(n)
空间复杂度: O(1)