链表基础
链表是啥呢
我的理解是:就是一个个珠子(数据)用指针串在一起,每个节点包括两个结构,一个是data,另一个是指针(指向下一个数据,连在一起嘛)
链表的入口节点成为链表的头结点也就是head
链表的类型
单链表
就是普通的链表,刚刚说的就是单链表,是单方向的,无法从下一个数据找到上一个数据
双链表
与单链表相比,不同的是双链表的节点的结构分为三个,一个数据不变,另外两个指针,一个还是指向下一个节点,但是呢,另一个节点指向上一个节点,优点就是可以访问上一个节点了
如图所示:
循环链表
循环链表,就是把链表的首部和尾部连接起来,尾部的指针不再只想null,而是指向头节点
链表的存储方式
我们知道数组的存储方式是连续存储的,比如int类型的数组arr,arr[0]的内存地址是100,arr[1]的内存地址就是104(一个int类型的数组占4个字节)
而链表不是,链表的每个节点是散乱分布的,所以只有通过指针才能访问到下一个节点,分配机制取决于操作系统的内存管理。
如图
这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。
链表的定义
链表的定义需要用结构体(struct)手写
struct ListNode{
int val;//节点上存储的元素
ListNode *next;//指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
//这就是一个构造函数,用于构造节点
};
对这个ListNode(int x) : val(x), next(NULL) {} 的解释
ListNode(int x) : val(x), next(NULL) 是 ListNode 结构体的构造函数,用来创建 ListNode 对象的实例。这里的构造函数使用了初始化列表语法,这是C++语言的特性。
来分解一下这个构造函数的各个部分:
- ListNode(int x)
:这是构造函数的声明。它表明这个构造函数接受一个整数类型的参数 x。
2.:
:冒号表示接下来是成员初始化列表的开始。
3.val(x)
:这表示 ListNode 的 val 成员被初始化为构造函数接收的参数 x。
4.next(NULL)
:这表示 ListNode 的 next 成员被初始化为 NULL(在现代C++中通常使用nullptr代替NULL)。NULL是一个宏,通常定义为((void*)0),表示空指针。这意味着新创建的 ListNode 的 next 指针指向空,即没有指向任何下一个节点。
5.{}
:大括号表示构造函数体的开始和结束。在这个例子中,构造函数体是空的,因为所有的工作都已经在初始化列表中完成了。
ListNode(int x) : val(x), next(NULL) 是一个构造函数,它的特殊之处在于构造函数没有返回类型并且其名称与类(或结构体)的名称相同。在这个例子中,ListNode 是构造函数的名称,也是其所属结构体的名称。这就是为什么看起来这个函数没有名称,因为它的名称实际上与结构体的名称一致。
构造函数是一个特殊的成员函数,它在对象被创建时自动调用。它的主要目的是初始化对象的成员变量。在结构体或类被实例化时,相应的构造函数就会被调用,执行必要的初始化操作。
对于 ListNode 结构体来说,当你创建一个新的 ListNode 实例时,比如 ListNode node(5);,ListNode 的构造函数就会被调用,node.val 被设置为传递给构造函数的值 5,而 node.next 被初始化为 NULL,表示这是链表的末尾或者该节点尚未连接到下一个节点。
初始化节点
这个是按自己定义构造函数初始化节点:
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)。