Day3 链表01
一、链表基础
-
什么是链表?
- 链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
- 链接的入口节点称为链表的头结点也就是head。
- 单链表、双链表、循环链表
-
链表的存储方式
- 链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
-
链表的定义
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
二、移除链表元素
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
1、原链表删除元素
对于在原链表中删除元素,要注意头节点以及非头节点的区别
-
删除的如果是头节点,首先头节点不能为空;其次,需要用到
while
而不是if
,因为可能出现数组为[1, 1, 1, 1],需要删除的元素val=1
的情况。这种情况下,一直删除的都是头节点,所以删除头节点是一个持续的过程; -
删除的如果是非头节点,则直接用到如下图思路
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) { // 注意这里不是if
//注意自己释放创建的指针
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;
}
};
2、虚拟头节点
如果我们想节约代码,将头节点以及非头节点归为一类,只需要设置一个虚拟头节点dummyHead
,这样我们原来的头节点也变为了普通节点,不需要单独考虑;
注意ListNode* cur = dummyHead
,这个地方我们要设置 cur = dummyHead
,因为如果cur = head
,那么操作head的时候,他就又不是一个普通的节点了,类似于法一里面的ListNode* cur = head
,是一样的道理。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向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;
}
}
return dummyHead->next;
}
};
三、设计链表
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
示例:
输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]
解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
myLinkedList.get(1); // 返回 3
五个函数的思路:
- 获取链表第index个节点的值:循环遍历;
- 在头节点前添加节点:即在虚拟头节点和头节点之间添加节点;
- 在尾部添加节点:遍历,当
cur
指向尾节点之后,通过cur->next
添加节点; - 任意添加节点:
- index<0,则添加到为头节点;
- index=length,添加到尾部后;
- index>length,return;
- 即0<=index<=length时,通过遍历cur到index节点的前一个节点,来添加;
- 删除任意节点:循环遍历。
class MyLinkedList {
public:
//定义链表结构体
struct LinkNode{
int val;
LinkNode* next;
LinkNode(int val):val(val), next(nullptr){}
};
MyLinkedList() {
dummyNode = new LinkNode(0);
length = 0;
}
int get(int index) {
LinkNode* cur = dummyNode;
if(index > (length - 1) || index < 0) return -1;
else{
for(int i = 0; i <= index; i++){ // 这里将循环次数改为 <= index
cur = cur->next;
}
}
return cur->val;
}
void addAtHead(int val) {
LinkNode* add = new LinkNode(val);
LinkNode* cur = dummyNode->next;
dummyNode->next = add;
add->next = cur;
length++;
}
void addAtTail(int val) {
LinkNode* add = new LinkNode(val);
LinkNode* cur = dummyNode;
while(cur->next != nullptr)
{
cur = cur->next;
}
cur->next = add;
length++;
}
void addAtIndex(int index, int val) {
if(index > length) return;
if(index < 0) index = 0;
LinkNode* add = new LinkNode(val);
LinkNode* cur = dummyNode;
while(index--){
cur = cur->next;
}
LinkNode*temp = cur->next;
cur->next = add;
add->next = temp;
length++;
}
void deleteAtIndex(int index) {
if(index < 0 || index >= length) return; // 添加对 index 有效性的检查
LinkNode* cur = dummyNode;
for(int i = 0; i < index; i++)
{
cur = cur->next;
}
if (cur->next != nullptr) {
cur->next = cur->next->next;
length--;
}
}
private:
LinkNode* dummyNode;
int length;
};
四、反转链表
1、双指针
- 首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
- 开始反转,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
- 先移动pre节点到cur节点的位置。
- 再移动cur节点,指向tmp节点。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
};
2、递归
-
return reverse(cur,temp);
- 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
pre = cur
;cur = temp
;
-
return reverse(NULL, head);
- 和双指针法初始化是一样的逻辑
ListNode* cur = head
;ListNode* pre = NULL
;
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}
};
五、总结
今天的题目有些其实之前做过,但是今天再做感觉就是脑子会了,手不会,链表有些基础知识也有些忘了;递归还不太理解。