链表 & 设计链表
List 链表
我们知道,数组存储在计算机中占据的是一段连续的内存,所以之前我们在删除某个元素的时候,会对后面所有元素进行前移,以达到一种类似覆盖的模式。
因为内存资源是很宝贵的东西,计算机中有个内存管理器在为我们分配内存,如果我们要存储数据,我们或许会使用数组进行存储,但是数组是连续的,所以如果存满了还要继续存的时候,不是不要给我们分配新的内存来存储数据,这时候,他要把原来数组中的数据进行拷贝,然后把原来的数组大小改大一点。
现在,我向内存管理器请求内存存储我的数据,如果我单独请求得到的存储内存地址很大程度上是不连续的,如果我想要将这些数据联系起来,对于数组来说,很简单,因为他是连续的。但如何将不连续的数据块给联系起来呢?为了解决这个问题,就搞出来个链表的数据结构。
链表有两个部分,一部分存储数据,一部分存储指向下一块数据的地址,如果存储地址的部分为null则表示结尾。链表入口也称为head。

我们定义一个节点Node
struct Node {
int data; // 节点存储的数据
Node *next; // 指向下一个节点的指针
Node(int x):value(x),next(nullptr){} // 节点构造函数
}
移除链表元素
删除链表中等于给定值 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 输出:[]
/**
* 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) {
while(head != nullptr && head->val == val){
ListNode* temp = head;
head = head->next;
delete temp;
}
ListNode* cur = head;
while(cur != nullptr){
if(cur->next && cur->next->val == val){ //指向不为空,且是目标值
ListNode* temp = cur->next;
cur->next = temp->next;
delete temp;
} else {
cur = cur->next;
}
}
return head;
}
};
以上是直接使用原来的链表进行删除操作
如果说链表是这样的[1,1,1,1],val = 1,这样目标值就等于头节点中的val,这种情况需要单独考虑,必须循环删除,这里需要使用一个临时变量temp来存放头节点head,然后将head指向下一处,清空原内存。
删除非头节点的情况下:因为链表是一个一个类似寻宝一样去遍历节点的,所以需要先定义当前的节点cur,当前节点非空的情况下,判断该节点指向的下一个节点cur->next,如果该节点非空且该节点的值cur->next->val是要删除的目标值的话,那么就将cur节点指向的地址改为cur->next->next,最后清理一下内存;当cur->next 不是目标值,正常移动。
虚拟节点 / 哨兵节点
这里采用一种新的方法来解决头节点是目标值的问题,我们创建一个虚拟节点,让虚拟节点的指向区域等于头节点,然后开始遍历搜寻,这样就不用担心头节点是目标值的问题了。
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyhead = new ListNode(0,head); // 堆上创建一个节点
ListNode* cur = dummyhead;
while(cur != nullptr){
ListNode* temp = cur->next;
if(temp && temp->val == val){
cur->next = temp->next;
delete temp;
} else {
cur = temp;
}
}
head = dummyhead->next; // 这里需要注意了,结束的时候头节点返回应该是dummyhead->next,但是我们开辟了堆上的内存,需要将其清理,所以我们需要提取真实头节点,释放哨兵后返回
delete dummyhead;
return head;
}
当时突发奇想,能不能在栈上创建一个节点对象,然后取地址,用这样方法是不是不用手动delete释放了
ListNode* removeElements(ListNode* head, int val) {
// 栈上创建哨兵节点对象
ListNode dummyhead(0, head);
ListNode* cur = &dummyhead; // 取栈对象地址
while(cur != nullptr){
ListNode* temp = cur->next;
if(temp && temp->val == val){
cur->next = temp->next;
delete temp;
} else {
cur = temp;
}
}
head = dummyhead.next;
return head;
// return dummyhead.next; // 这里需要注意了,离开了这个函数作用域后,dummyhead就没了,会有野指针访问的风险
}
需要注意的点是,如果外部需要这个函数的返回值,如果返回dummyhead.next是有风险的。
设计链表
没啥可说的,实现单链表
实现 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的节点。
我们需要设计5个函数,分别是获取、首插、尾插、索引插入、索引删除。
class MyLinkedList {
public:
struct Node {
int val;
Node* next;
Node():val(0),next(nullptr){};
Node(int val):val(val),next(nullptr){};
Node(int val, Node* next):val(val),next(next){};
};
Node* dummyhead = new Node(0); // 定义了一个哨兵节点
int size = 0; // 列表长度
MyLinkedList(){}
~MyLinkedList(){ // 析构函数
Node* cur = dummyhead;
while(cur != nullptr){
Node* temp = cur;
cur = cur->next;
delete temp;
}
}
// 这里你需要注意索引和长度是有区别的,索引只能取到[0,size)
// 比如size = 2, 对应索引 0, 1,
// 所以当index >= size 都是无效的
int get(int index) { // get获取函数,超出索引返回 -1
if(index < 0 || index >= size) return -1;
Node* cur = dummyhead->next; // 从真实的头节点开始
for(int i = 0; i < index; ++i){
cur = cur->next;
}
return cur->val;
}
// 首插,很简单,让哨兵节点指向新的节点就行, 新节点中的指向指为原来哨兵节点指向,当然别忘了++size
void addAtHead(int val) {
Node* new_node = new Node(val, dummyhead->next);
dummyhead->next = new_node;
++size;
}
// 尾插,并不困难,遍历节点,找到某个节点指向区域是nullptr,就是最后一个了
void addAtTail(int val) {
Node* cur = dummyhead;
while(cur->next != nullptr){ // 不为空,继续找,直到找到的是nullptr
cur = cur->next;
}
cur->next = new Node(val); // 修改最后一个节点中的指向
++size;
}
// 按索引进行插入
void addAtIndex(int index, int val) {
if(index > size) return; // 如果索引大于长度就返回
else if(index == size) { // 索引 == size 还是举个例子 比如size = 2,
addAtTail(val); // 那么索引就是0,1 这时候插入节点就应该是尾插
return;
} else if (index <= 0){ // 索引 <= 0 首插
addAtHead(val);
return;
}
Node* pre = dummyhead; // 这里我们并没有从真实节点开始,而是通过虚拟节点
for(int i = 0; i < index; ++i){ // 这样遍历得到节点需要插入位置的前面 pre
pre = pre->next; // [1,3] 在index = 1 处插入2 -> [1,2,3]
} // 从虚拟节点开始, pre 是指向1的
pre->next = new Node(val, pre->next); // 创建一个新节点,将pre的指向指向新节点,新节点的指向指向原来pre的指向
++size;
}
// 索引删除
void deleteAtIndex(int index) {
if(index >= size || index < 0) return; // 前置判断
Node* pre = dummyhead; // 依旧从虚拟开始
for(int i = 0; i < index; ++i){
pre = pre->next;
}
// 需要删除后面的一个节点
Node* temp = pre->next;
pre->next = temp->next;
delete temp;
--size;
}
};

浙公网安备 33010602011771号