代码随想录Day3->链表 203.移除链表元素 707.设计链表 206.反转链表
链表
特点
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景
链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理
各个节点分布在内存的不同地址空间上,通过指针串联在一起。
定义
// 单链表
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) {}
};
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
这是C++中的初始化列表语法,用于在构造函数中初始化成员变量。
ListNode(int x)是一个构造函数,它接受一个整数参数x。val(x)和next(NULL)是初始化列表
不定义构造函数,C++默认生成一个构造函数
//通过自己定义构造函数初始化节点:
ListNode* head = new ListNode(5);
//使用默认构造函数初始化节点:
ListNode* head = new ListNode();
head->val = 5;
类型
- 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点
双链表既可以向前查询也可以向后查询。 - 循环链表:链表首尾相连。
循环链表可以用来解决约瑟夫环问题
203.移除链表元素
给你一个链表的头结点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的结点,并返回 新的头结点
让结点next指针直接指向next的next一个结点,不需要再增设一个指针q
p->next = p->next->next;
注意:p是指向结点的指针,是p->next不是p++
内存管理
C++需要手动释放内存
ListNode * tmp = p;
//操作
delete tmp;
区别在于头结点的处理:
- 不设头指针,头结点单独处理
- 设头指针(虚拟结点,初值为0)指向头结点,相同处理
不设头指针
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if (head == NULL)
return NULL;
// 删除头结点
while (head != NULL && head->val == val) // 注意这里不是if
{
ListNode * tmp = head;
head = head->next;
delete tmp;
}
ListNode * p = head;
// 删除非头结点
while (p != NULL && p->next != NULL)
{
if (p->next->val == val)
{
ListNode * tmp = p->next;
p->next = p->next->next;
delete tmp;
}
else
{
p = p->next;
}
}
return head;
}
};
头结点单独处理,注意这里是while而不是if,因为head移动到next之后的值仍然是等于val,需要继续删,应该是一个循环
while (head != NULL && head->val == val)
{
ListNode * tmp = head;
head = head->next;
delete tmp;
}
注意处理其他结点的时候,while的判断条件是p != NULL && p->next != NULL,必须要加p->next != NULL,因为下面if的判断条件就是p->next->val == val,需要p->next != NULL
增设头指针
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head
ListNode* p = dummyHead;
while (p != NULL && p->next != NULL)
{
if (p->next->val == val)
{
ListNode* tmp = p->next;
p->next = p->next->next;
delete tmp;
}
else
p = p->next;
}
head = dummyHead->next;
delete dummyHead; // 删除虚拟头结点
return head;
}
};
总结
- 定义结点时,可以加构造函数,参数列表不同函数重载
- 删除结点时,看有没有头指针,增设的虚拟头结点,要记得释放
- 如果循环中出现了p->next,循环判断条件中也要有p->next
相关题目
707.设计链表
实现 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 x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
// 链表定义
MyLinkedList() {
m_dummyHead = new ListNode(0, NULL);
m_size = 0;
}
// 按下标索引
int get(int index) {
if (index > (m_size - 1) || index < 0) {
return -1;
}
ListNode* p = m_dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
p = p->next;
}
return p->val;
}
// 头插法
void addAtHead(int val) {
ListNode* tmp = new ListNode(val);
tmp->next = m_dummyHead->next;
m_dummyHead->next = tmp;
m_size++;
}
void addAtTail(int val) {
ListNode* tmp = new ListNode(val, NULL);
ListNode* p = m_dummyHead;
/*for (int i = 0; i < m_size; i++)
{
p = p->next;
}*/
while (p->next != nullptr) // 下面要用p->next,判断条件p->next != NULL
p = p->next;
p->next = tmp;
m_size++;
}
void addAtIndex(int index, int val) {
if (index > m_size)
return;
if (index < 0)
index = 0;
// 这段不加也行,在尾部加相同操作,无需单独拎出来说
if (index == m_size)
{
addAtTail(val); // 这里调用了addAtTail(val)就不要再m_size++了
return;
}
ListNode* tmp = new ListNode(val, NULL);
ListNode* p = m_dummyHead;
/*for (int i = 0; i < index; i++)
{
p = p->next;
}*/
while (index--)
{
p = p->next;
}
tmp->next = p->next;
p->next = tmp;
m_size++;
}
void deleteAtIndex(int index) {
if (index >= m_size || index < 0)
return;
ListNode* p = m_dummyHead;
/*for (int i = 0; i < index; i++)
{
p = p->next;
}*/
while (index--)
{
p = p->next;
}
ListNode* tmp = p->next;
p->next = p->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp = nullptr;
m_size--;
}
// 打印链表
void printLinkedList() {
ListNode* p = m_dummyHead;
while (p->next != nullptr) {
cout << p->next->val << " ";
p = p->next;
}
cout << endl;
}
// 链表属性
private:
int m_size;
ListNode* m_dummyHead;
};
- 尾插法判断条件常用p->next != nullptr,而不是size
- 关于下标的判断条件`常用while (index–),而不是size
- NULL:在C++中,NULL是一个宏,通常被定义为整数0。这是从C语言中继承过来的,因为在C语言中没有专门表示空指针的关键字,通常使用0来表示空指针。
nullptr:这是C++11引入的一个新关键字,专门用来表示空指针。nullptr是一种特殊类型的字面量,它可以被转换为任意其他指针类型,而且这种转换是安全的。
NULL实际上是整数,所以在某些情况下,使用NULL可能会产生意想不到的结果,特别是在函数重载的场景。例如,如果你有一个接受int参数的函数和一个接受指针参数的函数,然后你传入NULL,编译器实际上会选择接受int参数的版本,因为NULL是整数。而如果你传入nullptr,编译器会选择接受指针参数的版本,因为nullptr是指针类型,优先使用nullptr来表示空指针,因为它更安全,更能表达你的意图 - delete命令指示释放了tmp指针原本所指的那部分内存,被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针,如果之后的程序不小心使用了tmp,会指向难以预想的内存空间,所以delete tmp之后要接着tmp = nullptr
- 进行增减的时候,一定要注意m_size的变化
delete tmp;
tmp = nullptr;
相关题目
206.反转链表
反转一个单链表
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表
代码
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* p = head;
ListNode* q = NULL;
ListNode* tmp;
while (p != NULL)
{
tmp = p->next;
p->next = q;
q = p;
p = tmp;
}
head = q;
return head;
}
};
- p是遍历指针,q始终指向p的前一个,逆转变成p的后一个,tmp暂存p->next
- while (p != NULL)等价于while §,遍历指针一定要分清p != NULL还是p->next != NULL
总结
双指针+暂存指针可以实现逆转而无需重新建链表