什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
接下来说一下链表的几种类型:
刚刚说的就是单链表。
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表,顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
了解完链表的类型,再来说一说链表在内存中的存储方式。
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
如图所示:
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
接下来说一说链表的定义。
链表节点的定义,很多同学在面试的时候都写不好。
这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。
而在面试的时候,一旦要自己手写链表,就写的错漏百出。
这里我给出C/C++的定义链表节点方式,如下所示:
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。
但是这个构造函数不会初始化任何成员变量,下面我来举两个例子:
通过自己定义构造函数初始化节点:
ListNode* head = new ListNode(5);
使用默认构造函数初始化节点:
ListNode* head = new ListNode();
head->val = 5;
所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!
删除D节点,如图所示:
只要将C节点的next指针 指向E节点就可以了。
那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。
是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。
如图所示:
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
再把链表的特性和数组的特性进行一个对比,如图所示:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
相信大家已经对链表足够的了解,后面我会讲解关于链表的高频面试题目,我们下期见!
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
struct ListNode {
int val; // 节点上存储的元素
ListNode *prev; // 指向前一个节点的指针
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), prev(nullptr), next(nullptr) {} // 节点的构造函数
};
突然想到多级指针,AI解答
指针指向指针是一种多级指针的概念,也叫作指向指针的指针。顾名思义,它是指一个指针变量存储了另一个指针变量的地址。这样的指针可以被用来访问、修改甚至创建其他指针变量。
以下是一个示例代码:
int main() {
int number = 42;
int *ptr1 = &number; // 指针ptr1指向number
int **ptr2 = &ptr1; // 指针ptr2指向ptr1
// 使用指针进行访问
printf("number: %d\n", *ptr1); // 输出: number: 42
printf("number: %d\n", **ptr2); // 输出: number: 42
// 修改值
*ptr1 = 24;
printf("number: %d\n", *ptr1); // 输出: number: 24
printf("number: %d\n", **ptr2); // 输出: number: 24
return 0;
}
在上面的示例中,我们首先创建了一个整数变量 number
和一个指向 number
的指针变量 ptr1
。然后,我们使用 &
符号获取 ptr1
的地址,并将其存储在另一个指针变量 ptr2
中。
通过使用多级指针,我们可以通过一个额外的间接层级来访问和修改所指向的数据。在示例中,*ptr1
和 **ptr2
都可以访问并修改 number
的值。
多级指针在某些情况下很有用,比如在函数参数中传递指针的指针,或者动态分配内存等场景中。
方法一(手动删除头节点):
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
while(head !=nullptr && head->val == val) //注意这里不是if,因为下一个节点也可能需要删除
{
ListNode* tmp = head;
head = head->next;
delete tmp; //手动释放内存
}
ListNode* curhead = head;
while(curhead != nullptr && curhead->next!=nullptr)
{
if(curhead->next->val == val)
{
ListNode* tmp = curhead->next;
curhead->next = curhead->next->next;
delete tmp; //手动释放内存
}
else
{
curhead=curhead->next;
}
}
return head;
}
};
方法二(虚拟头节点):
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
/*
*虚拟头节点
*/
ListNode* dummyhead = new ListNode();
dummyhead->next = head;
ListNode* curhead = dummyhead;
while(curhead != nullptr && curhead->next!=nullptr)
{
if(curhead->next->val == val)
{
ListNode* tmp = curhead->next;
curhead->next = curhead->next->next;
delete tmp; //手动释放内存
}
else
{
curhead=curhead->next;
}
}
//return dummyhead->next; 这样写有问题,我们没有delete dummyhead
head = dummyhead->next;
delete dummyhead;
return head;
}
};
class MyLinkedList {
public:
struct LinkedNode{
int val;
LinkedNode* next;
//构造函数
LinkedNode() : val(0),next(nullptr){}
LinkedNode(int x) : val(x), next(nullptr){}
LinkedNode(int x, LinkedNode* next) : val(x),next(next){}
};
private:
int size;
LinkedNode* DummyHead;
public:
MyLinkedList() {
//构造函数赋值成员变量
DummyHead = new LinkedNode(); //通过构造函数直接赋值val为0
size = 0;
}
int get(int index) {
if(index>(size - 1) || index<0) //index范围[0,size-1] 超过size-1或者小于0 直接返回
{
return -1;
}
LinkedNode* cur = DummyHead->next;
while(index--) //index = 0为头节点,index=size-1为尾节点,后自减:先取值再减少,index为0退出循环
{
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
LinkedNode* AddedNode = new LinkedNode(val);
AddedNode->next = DummyHead->next; //新增节点尾部先连接
DummyHead->next = AddedNode; //然后dummyhead尾部连接新增节点
size++; //链表长度+1
}
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = DummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
size++; //链表长度+1
//下面代码出错,问题不明2023-12-15 2:35
/* LinkedNode* AddedNode = new LinkedNode(val);
LinkedNode* cur = DummyHead;
while(size--) //遍历找到尾巴
{
cur = cur->next;
}
cur->next = AddedNode;
size++;*/
//问题找到,我们不能直接操作size,要新建临时变量,2023-12-15 3:16
/* LinkedNode* AddedNode = new LinkedNode(val);
LinkedNode* cur = DummyHead;
int temp = size;
while(temp--) //遍历找到尾巴
{
cur = cur->next;
}
cur->next = AddedNode;
size++;*/
}
void addAtIndex(int index, int val) {
if(index > size) return;
if(index < 0) index = 0;
LinkedNode* AddedNode = new LinkedNode(val);
LinkedNode* cur = DummyHead; //局部变量,不要移动DummyHead!!!!!
while(index--) //遍历找到插入位置,第n个节点前一个
{
cur = cur->next;
}
AddedNode->next = cur->next;//新增节点尾部先连接
cur->next = AddedNode;//然后cur尾部连接新增节点
size++; //链表长度+1
}
void deleteAtIndex(int index) {
if (index >= size || index < 0) {
return;
}
LinkedNode* cur = DummyHead;
while(index--) //遍历找到删除位置,第n个节点前一个
{
cur = cur->next;
}
LinkedNode* tmp = cur->next; //先储存起来,之后手动清理内存
cur->next = cur->next->next;
delete tmp;
tmp =nullptr;
size--;
}
};
null
和 nullptr
区别:
null
和 nullptr
都可以表示空指针,但它们是不同的概念。
null
是 C++ 标准库中提供的宏定义,它被定义为 0 或者 (void*)0。在 C++98 的标准中,null
被定义为 0,它可以被隐式地转换为任意指针类型。这种隐式转换可能会导致一些不安全的编程错误,例如再分配空指针时可能会触发未定义的行为。因此,在 C++11 的标准中,它被弃用了。
nullptr
是 C++11 引入的新关键字,它是一个特殊的空指针常量,可以被转换为任意指针类型,但不能转换为整数类型。它具有与字面量更加一致的语义,可以避免一些潜在的编程错误,因此更推荐使用。
在实际编程中,如果你需要表示空指针,应该尽量使用 nullptr
,以避免隐式转换和意外的错误。如果你必须使用 null
,则必须格外小心,确保不会引入不必要的错误。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* tmp;
ListNode* cur = head;
ListNode* pre = nullptr;
while(cur) {//cur为nullptr退出循环
tmp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};