链表基础知识
分类
单链表
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。链表的入口节点称为链表的头结点也就是head。
双链表
每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表既可以向前查询也可以向后查询。
循环链表
链表首尾相连。循环链表可以用来解决约瑟夫环问题。
链表的存储
链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
C/C++的定义链表节点方式及初始化
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
通过自己定义构造函数初始化节点:
ListNode* head = new ListNode(5);
使用默认构造函数初始化节点:
ListNode* head = new ListNode();
head->val = 5;
链表的操作
删除节点
只要将C节点的next指针 指向E节点就可以了。在C++里最好是再手动释放这个D节点,释放这块内存。
添加节点
链表断链的时候如果后面还需要该被断开的节点,应当用指针临时存储一下。
链表的增添和删除都是O(1)操作,也不会影响到其他节点。
注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。
链表与数组对比
链表的插入/删除时间复杂度是O(1)是因为已经知道前一个节点的情况下,如果单纯给一个链表删除特定元素,那么需要遍历+删除,时间复杂度就是O(n)了。
题目
203.移除链表元素
!虚拟头节点法
/**
* 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; // 将虚拟头结点指向head,这样方面后面做删除操作
// 上面两句可以直接换成 :
// ListNode* dummyHead = new ListNode(0,head);
ListNode* cur = dummyHead;
while (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;
}
};
- 关于链表定义和实例化:
根据上面代码注释部分可以看出,链表定义首先必须包含:一个数值变量
int val和一个指针变量ListNode *next。其次可以有三种重构方式,在实例化时:
ListNode* dummyHead = new ListNode():表示该节点值为0,指向NULL;
ListNode* dummyHead = new ListNode(5):表示该节点值为5,指向NULL;
ListNode* dummyHead = new ListNode(5,head):表示该节点值为0,指向head。
- 关于cur指针:
cur是个ListNode类型的指针。
不能直接用head节点进行后续的操作,如果head节点的值就是要删的值,用head节点操作没法删除自己,必须要找到head的前一个节点。因此也解释了为什么
ListNode* cur = dummyHead;
- C++最好要释放内存:
语法即 delete XXX;
分头节点和非头节点的方法
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
// 注意这里不是if,因为有可能后面都是要删除的val:
// 例如链表为[1,1,1,2,3,4,5],val=1,则删除头节点这个操作要执行3次
while (head != NULL && head->val == val) {
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
ListNode* cur = head;
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;
}
};
707.设计链表(增删改查)
class MyLinkedList {
//这三句不能放在struct ListNode的前面,否则ListNode* myhead;无法执行
// private:
// int size;
// ListNode* myhead;
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() {
//定义虚拟头节点
myhead = new ListNode(0);
//!!!链表长度
size = 0;
}
//得到链表中index的值
int get(int index) {
// 获取到第index个节点数值,如果index是非法数值直接返回-1,
//!!注意index是从0开始的,第0个节点就是头结点
//因此假如size=8,但索引只能取到7,所以index>=8是无效的
if(index >= size || index<0){
return -1;
}
//从index=0开始,即从头节点开始
ListNode *cur = myhead ->next;
while(index--){
cur = cur->next;
}
return cur->val;
}
//在链表头添加节点
void addAtHead(int val) {
ListNode * newnode = new ListNode(val);
//注意:myhead是虚拟头节点
//把新插入的节点指向 myhead节点 所指向的节点,即head节点
newnode->next = myhead -> next;
//再把myhead指向这个新插入的头节点
myhead ->next = newnode;
//链表长度+1
size++;
}
//在链表尾添加节点
void addAtTail(int val) {
ListNode * newnode = new ListNode(val);
ListNode* cur = myhead;
//让cur指向最后一个节点
while(cur->next!=nullptr){
cur = cur -> next;
}
//结束循环,则代表cur已经指向最后一个节点了
//此时把cur的下一个节点指向newnode,即代表在尾部添加节点
cur->next = newnode;
size++;
}
//在链表的index处添加节点
void addAtIndex(int index, int val) {
//判断index和链表长度关系,index超过链表长,则不返回任何
if(index > size){return;}
//index小于0在头部插入,即把index置为0
if(index < 0) index = 0;
ListNode * newnode = new ListNode(val);
ListNode* cur = myhead;
//index不为0
while(index--){
cur = cur->next;
}
//如果index=0或者while循环结束
newnode->next = cur->next;
cur -> next = newnode;
size++;
}
//删除链表index处的节点
void deleteAtIndex(int index) {
//如果index无效,则不做操作
if(index >= size || index<0){
return;
}
ListNode *cur = myhead;
while(index--){
cur = cur -> next;
}
ListNode * temp = cur->next;//用于释放内存
cur->next = cur->next->next;
delete temp;
size--;
}
// 打印链表
void printLinkedList() {
ListNode* cur = myhead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int size;
ListNode* myhead;
};
//用于测试
// MyLinkedList* obj = new MyLinkedList();
// int param_1 = obj->get(index);
// obj->addAtHead(val);
// obj->addAtTail(val);
// obj->addAtIndex(index,val);
// obj->deleteAtIndex(index);
206.反转链表
链表一定要分清节点和指针的概念。 new ListNode()是真实存在的一个节点
head = new ListNode() 相当于 head指针指向了一个真实的节点
node = head, 相当于node和head同时指向了这个真实的节点
ListNode *cur = head;相当于定义了cur指针,指向了head。
双指针法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//定义三个指针
ListNode *pre = nullptr;//pre指向空
ListNode *cur = head;//cur指向head节点
ListNode* temp; // 用于保存cur的下一个节点
//循环结束cur会指向null,pre指向最后的节点
while(cur!=nullptr){
temp = cur->next; //保存cur的下一个节点
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur; //把pre指向 cur所指向的节点
cur = temp;//把cur指向 temp所指向的节点
}//当cur指向最后一个节点,再进入while循环后,cur最终指向null
return pre;//所以最后应该返回pre,而不是cur
}
};
递归法
根据双指针的思路可以写出递归的方法:
- 定义递归反转的函数reverse(),根据双指针法,要反转的是cur和pre两个指针,即在第一次递归中的head和NULL,则传入的为reverse(NULL, head);
- 在reverse()函数中,递归的终止条件是cur指向了最后的null,按照题目要求返回反转后的头节点,即pre
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
//递归的终止条件是cur指向了最后的null,按照题目要求返回反转后的头节点,即pre
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);
}
};
总结
今日学习时长 3+1h