今天任务:学习链表理论基础
链表的类型
链表的存储方式
链表的定义
链表的操作
性能分析
学习文档:代码随想录 (programmercarl.com)
链表的类型:单链表、双链表、循环链表(区别就在于其结构不同)
链表是一种常用的数据结构,通过指针串联在一起,相对于数组有以下几方面优点:
-
动态大小:链表的大小是动态的,可以在运行时根据需要进行扩展或缩减。而数组的大小在声明时就固定了,不能动态改变。
-
内存利用率:链表不需要像数组那样预先分配一块连续的内存空间,因此可以更有效地利用内存,尤其是在内存碎片较多的情况下。
-
插入和删除操作搞笑:在链表中,插入和删除节点通常只需要改变指针,而不需要移动其他元素。这使得链表在插入和删除操作上比数组更高效,因为数组需要移动插入点或删除点之后的所有元素。
-
不需要初始化大小:在创建链表时,不需要指定链表的大小,可以根据需要逐步构建链表。
-
空间分配:链表的节点可以在需要时单独分配,这意味着即使链表很大,也不需要一次性分配大块内存,从而减少了内存的浪费。
-
灵活的数据结构:链表可以很容易地构建成其他复杂的数据结构,如双向链表、循环链表
等,这些结构可以支持更复杂的操作。
链表也有其缺点,比如访问元素时需要从头开始遍历,导致访问时间较长;指针的额外存储空间可能会增加内存的开销;以及由于指针的存在,可能会导致程序的复杂性增加。
链表的存储方式
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
链表的定义
单链表的定义 (特别注意,在面试中可能需要自己定义链表)
// 单链表
struct ListNode {int val; // 节点上存储的元素ListNode *next; // 指向下一个节点的指针ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
链表的基本操作:删除、添加
删除节点
删除D节点,如图所示:
只要将C节点的next指针 指向E节点就可以了。注意此时D节点依旧留在内存中,只不过是没有在这个链表中而已,在使用C++最好手动释放这个D节点,释放这块内除。
添加节点
可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)
性能分析
Leetcode: 203.移除链表元素
题目描述:
给你一个链表的头节点 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.删除头节点
2.删除非头节点
完整代码:
/*** 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 因为删除有可能需要一直删 不止一个val// 如果val是头节点 直接将head = head->next;即可while(head!=NULL && head->val == val) {ListNode* tmp = head;head = head->next;delete tmp;}ListNode* cur = head;// 如果不是头节点,需要cur->next = cur->next->next; 这样就删除了cur->next这个节点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;}
};
使用虚拟头节点:
使用一个虚拟头节点,可以统一逻辑来删除链表节点
/*** 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) {// 设置一个虚拟头节点ListNode* dummyHead = new ListNode(0);// 将虚拟头节点设置为这个链表的头节点dummyHead->next = head;// 从虚拟头节点开始遍历ListNode*cur = dummyHead;// 统一删除节点逻辑 都是删除非头节点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;}}// 重新设置头节点head = dummyHead->next;delete dummyHead;return head;}
};
Leetcode: 707.设计链表
题目描述
你可以选择使用单链表或者双链表,设计并实现自己的链表。单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 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
的节点。
解题思路
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目
可以继续使用上面的操作,使用虚拟头节点来操作。
class MyLinkedList {
public:// 定义链表节点的结构体struct LinkedNode {int val;LinkedNode* next;LinkedNode(int val) : val(val), next(nullptr){}};// 初始化链表 这里定义的头节点是一个虚拟头节点 而不是真正的链表头节点MyLinkedList() {_dummyhead = new LinkedNode(0);// 一个整型变量,用于存储链表中实际节点的数量_size = 0;}// 获取链表第index个节点的数值int get(int index) {// 如果索引无效(即超出链表范围),返回 -1if(index > (_size - 1) || index < 0) {return -1;}// 从虚拟头节点的下一个节点开始遍历,直到到达指定索引的节点,然后返回该节点的值LinkedNode* cur = _dummyhead->next;while(index--) {cur = cur->next;}return cur->val;}// 在链表最前面插入一个节点 ,插入完成后,新插入的节点为链表新的头节点void addAtHead(int val) {LinkedNode* newnode = new LinkedNode(val);// 注意这里的顺序不能改变 统一插入的赋值顺序 先将新头节点插入在head之前 然后将虚拟头节点依旧放在最前面newnode->next = _dummyhead->next;_dummyhead->next = newnode;_size++;}// 在链表最末尾插入节点void addAtTail(int val) {LinkedNode* newnode = new LinkedNode(val);LinkedNode* cur = _dummyhead;// 先将cur指向最后一个节点 判断条件cur->next != NULLwhile(cur->next != NULL) {cur = cur->next;}cur->next = newnode;_size++;}// 在第index个节点之前插入一个新节点 使用虚拟头节点就可以方便处理index为0 插入头节点的情况void addAtIndex(int index, int val) {if(index > _size) {return;}LinkedNode* newnode = new LinkedNode(val);LinkedNode* cur = _dummyhead;while(index--) {cur = cur->next;}newnode->next = cur->next;cur->next = newnode;_size++;}//删除第index个节点void deleteAtIndex(int index) {//当 index 等于链表长度时,cur->next 将为 nullptr,因此不能访问 cur->next->nextif(index >= _size || index < 0) {return;}LinkedNode* cur = _dummyhead;// 注意index是从0开始的 这样刚好指向index前一个节点while(index--) {cur = cur->next;}LinkedNode* tmp = cur->next;// 删除index节点cur->next = cur->next->next;delete tmp;_size--;}private:int _size;LinkedNode* _dummyhead;
};
总结:要用意识去使用虚拟头节点,对于一些边界条件判断不到位!
Leetcode: 206.反转链表
题目描述
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
解题思路
1.依次遍历链表 依次反转次序
/*** 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* reverseList(ListNode* head) {ListNode* former = NULL;ListNode* mid = head;ListNode* latter = NULL;while(mid != NULL) {// 保存mid的下一个节点,因为接下来要改变mid->next的指向了latter = mid->next;mid->next = former;former = mid;mid = latter;}// 注意最后一次while循环 将latter赋给了mid 所以former是反转链表后的头节点return former;}
};
2.递归法
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
关键是初始化的地方, 可以看到双指针法中初始化 cur = head,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);}};