提示:DDU,供自己复习使用。欢迎大家前来讨论~
链表
进入链表的学习
一、链表的基础理论
1. 链表的分类
- 单链表
- 双链表
- 循环链表
链表与数组的区别:链表在内存中是不连续的,数组在内存中连续存放
2. 链表的定义
//单链表
struct ListNode{
int x; //节点上存储的数据
ListNode *next; //指向下一个节点的指针
ListNode(int x) : val(x), next(null){}; //节点的构造函数
}
如果使用默认的构造函数,在初始化的时候不能直接给变量赋值
//采用自己的构造函数初始化节点
ListNode* head = new ListNode(6);
//采用默认的构造函数
ListNode* head = new ListNode();
head->val = 6;
3. 链表的操作
二、题目
题目一:203.移除链表元素
1.在原链表上进行操作
在原始的链表中进行删除,要==区分是否头节点==两种情况,分别进行操作。
使用 tmp 的原因:
- 安全删除:在删除节点之前,需要先保存节点的地址,因为一旦执行 head = head->next;,原始头结点的地址就会丢失,无法进行删除操作。
- 避免内存泄漏:删除节点是为了释放不再使用的内存,如果不使用 tmp 来保存节点的地址,直接执行 delete head; 会导致在更新 head 指针后无法释放原头结点的内存,从而造成内存泄漏。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
//删除头节点
while(head != NULL && head->val == val){
ListNode* tmp = head;
head = head->next;
delete tmp;
}
//删除非头节点
//cur 是遍历链表的中所有位置(区别head),帮助定位和删除链表中特定值的节点
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;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
2.使用虚拟头节点(dummyHead)
使用虚拟头节点(dummyHead),可以统一删除头节点和非头节点的操作。
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)
题目二:707.设计链表
练习使用虚拟头节点。
链表的增删改查,需注意:
- 所有的操作都要定义虚拟节点
- 节点的index是从0开始,判断index的合法性。
- 添加和删除节点的时候,因为单链表只能往后,不能往前,所以定义一个curent节点,添加和删除都是对应current->next这个节点的位置。
class MyLinkedList {
public:
//定义链表节点结构体
struct LinkedNode {
int val; //这是一个放数值的变量,要和下面的构造保持一致。
LinkedNode *next;
LinkedNode(int x) : val(x), next(nullptr){}
};
//初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0);
_size = 0;
}
//获取第n个节点的数值
int get(int n){
if(n<0 || n>_size-1){
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(n--){
cur=cur->next;
}
return cur->val;
}
//在头节点前插入新的节点
void addAtHead(int x){
LinkedNode* newNode = new LinkedNode(x);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
//在第n个位置插入新的节点
void addAtIndex(int index, int x){
if(index > _size) return;
if(index<0) index = 0;
LinkedNode* newNode = new LinkedNode(x);
LinkedNode* cur = _dummyHead;
while(index--){
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
//删除第n个位置的节点
void deleteAtIndex(int index){
if(index>=_size || index<0){
return ;
}
LinkedNode* cur = _dummyHead;
while(index--){
cur = cur->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
tmp=nullptr; //tmp不主动赋值,他会是一个随机值,可能是乱指的野指针。
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead->next; // 从虚拟头结点的下一个节点开始遍历
while (cur != nullptr) {
cout << cur->val << " "; // 打印当前节点的值
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};
题目三:206.反转链表
思路:不需要重新定义一个链表,改变链表next的指向就可以了。
- 双指针操作,必须要掌握。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* cur = head;
ListNode* pre = NULL;
ListNode* tmp; //需要一个临时的节点记录后移。
while(cur){
tmp = cur->next;
cur->next = pre;
pre = cur; //顺序不能反
cur = tmp;
}
return pre;
}
};
- 时间复杂度: O(n)
- 空间复杂度: O(1)
- 递归的写法,有些抽象,但和双指针的思想是一致的。
class Solution {
public:
ListNode* reverse(ListNode* pre, ListNode* cur){
if(cur==NULL) return pre;
ListNode* tmp;
tmp = cur->next;
cur->next = pre;
return reverse(cur, tmp);//递归下一轮
}
ListNode* reverseList(ListNode* head) {
//和双指针的思路一致
return reverse(NULL, head);
}
};
- 时间复杂度: O(n), 要递归处理链表的每个节点
- 空间复杂度: O(n), 递归调用了 n 层栈空间
总结
- 使用虚拟头节点
- 反转链表中的双指针思想和递归方法