今日重点内容:
1.熟悉用虚拟头结点来移除节点
2.练习链表5个常规操作
3.使用双指针法和递归法来反转链表
链表理论基础
链表:存储每一个元素和下一个元素的地址
结点:含数据域和指针域
链表类型单链表、双向链表和循环链表(约瑟夫环)
头指针:指向链表第一个结点的指针
单链表由表头唯一确定,所以可以用头指针来代表整个链表
空表:头结点的指针域为空
链表节点定义:
// 单链表
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;
203.移除链表数组
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
1.直接使用原来链表进行删除操作
移除头结点时向后移一位即可,其他节点则找到删除结点的前一个节点,断开连接,指向删减节点的下一位,再free删除节点。常规操作。
/**
* 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(head!=NULL&&head->val==val)
{
ListNode*temp=head;
head=head->next;
delete temp;
}
//删除元素非头结点
ListNode*cur=head;
while(cur!=NULL&&cur->next!=NULL)
{
if(cur->next->val==val)
{
ListNode*temp=cur->next;
cur->next=cur->next->next;
delete temp;
}
else cur=cur->next;
}
return head;
}
};
易错点,分不清删除节点时用free还是delete。
在C++中,delete和free都可以用来释放动态分配的内存。虽然它们都能够完成内存释放的功能,但这两者之间有着很多区别,如下所示:
1. 对象类型
delete是C++语言自带的运算符,对于类对象进行删除时调用类的析构函数。
free是C标准库函数,仅释放空间,不会释放一个类(或者结构体)对象的空间。2. 内存碎片
delete在释放内存后,能够确保内存内容被释放,并合并成一块可用的空间。在大规模内存管理、长时间运行时程序表现更为优秀。
free只是简单地将内存地址修改为空闲状态,并不能保证合并成一块可用的空间。如果频繁地执行 malloc 和 free,容易产生大量小块内存碎片,从而导致不利于程序运行效率的问题。3. 参数类型
delete
都是以指针为参数。
free
是以地址值为参数。4. 行为保证
delete
具有行为保证(即无论是否出错都会表现一致),能够确保释放内存不会对其他对象造成影响。
free
不具备任何行为保证,释放后的地址空间中仍可能存在原来数据的残留物,甚至可能覆盖其他已经被释放的对象。
2.设计一个虚拟头结点来移除节点
好处是可以统一所有节点的处理方法。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyhead=new ListNode(0);
dummyhead->next=head;//创建一个指向头结点的虚拟头结点
ListNode*cur=dummyhead;//一个指向当前节点的结点
while(cur->next!=NULL)//遍历链表
{
if(cur->next->val==val)
{
ListNode*temp=cur->next;
cur->next=cur->next->next;
delete temp;
}
else cur=cur->next;
}
return dummyhead->next;//注意这里返回的是真正的头结点
}
};
也可以用递归法。
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
的节点。
单链表
class MyLinkedList {
public:
//定义链表节点结构体
struct Linkednode{
int val;
Linkednode*next;
Linkednode(int val):val(val),next(nullptr){}
};
//初始化链表
MyLinkedList() {
Lsize=0;
dummyhead=new Linkednode(0);//虚拟头结点,非真正的头结点
}
//找到对应下标节点的值
int get(int index) {
if(index>Lsize-1||index<0)//检查下标是否合法
return -1;
Linkednode*cur=dummyhead->next;
while(index--)//从index=0开始找,因此cur指向头结点
{
cur=cur->next;
}
return cur->val;
}
//头插法
void addAtHead(int val) {
Linkednode*newnode=new Linkednode(val);
newnode->next=dummyhead->next;
dummyhead->next=newnode;//这里要注意顺序,一定要先建立新节点和下个节点的连接,再和上一个节点建立
Lsize++;
}
//尾插法
void addAtTail(int val) {
Linkednode*cur=dummyhead;
Linkednode*newnode=new Linkednode(val);
while(cur->next!=NULL)
{
cur=cur->next;
}
cur->next=newnode;
Lsize++;
}
//在第index个的节点前插入一个节点
//如果index小于等于0,都是头插入为头结点,若等于链表长度则是新的尾节点,大于链表长度则返回空
void addAtIndex(int index, int val) {
Linkednode*cur=dummyhead;
Linkednode*newnode=new Linkednode(val);
if(index<0)index=0;
if(index>Lsize)return;
while(index--)cur=cur->next;
newnode->next=cur->next;
cur->next=newnode;
Lsize++;
}
//删除第index个节点
void deleteAtIndex(int index) {
//注意验证下标合法性
if(index<0||index>Lsize-1)return;
Linkednode*cur=dummyhead;//注意!删除第几个节点,cur就指向它前一个,这个和寻找那里指向不一样
while(index--)
{
cur=cur->next;
}
Linkednode*temp=cur->next;//这里指向删除节点就行,注意定义一个指针就行
cur->next=cur->next->next;
delete temp;
temp=NULL;
Lsize--;
}
private:
int Lsize;//链表长度
Linkednode*dummyhead;
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
新知,删除节点那里,最后一步要把temp指针置空原因
//delete命令指示释放了tmp指针原本所指的那部分内存, //被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后, //如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针 //如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
本题需要注意cur是指向虚拟头结点还是头结点,这样就不会搞错循环次数和index范围了。
结合画图比较好理解。
双链表待补充。。
206.反转链表
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]
双指针法
思路:定义一个pre指针先指向null,然后cur指向头结点,额外一个临时指针保存cur的下一个节点。然后cur指向pre,然后pre和cur都下移一位。然后给一个循环过程遍历链表,当cur为空时就完成了链表的反转。(挺好理解)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode*pre=NULL;
ListNode*cur=head;
ListNode*temp;
while(cur){
temp=cur->next;//先保存下一个位置,因为等会cur转向时要斩断和next的连接
cur->next=pre;
pre=cur;
cur=temp;
}
return pre;//最后pre指向的就是头结点
}
};
递归法
模拟的就是双指针法的操作过程,只要把while循环的一次次操作弄成递归操作的就行
区别就是反转操作让reverse函数来完成。然后reverselist调用。
传入新的cur和pre时,注意是cur移动到temp,pre移动到cur的位置。
class Solution {
public:
ListNode*reverse(ListNode*cur,ListNode*pre)
{
if(cur==NULL)return pre;
ListNode*temp=cur->next;
cur->next=pre;
return reverse(temp,cur);
}
ListNode* reverseList(ListNode* head) {
return reverse(head,NULL);
}
};
总结:由于之前看过印度老哥的视频,所以思路是有点印象的,理解上不会太困难,不过代码还有很多小细节需要注意。