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 输出:[]提示:
- 列表中的节点数目在范围
[0, 104]
内1 <= Node.val <= 50
0 <= val <= 50
解法一:在原链表中实现删除
思路:要移除一个结点,那么就必须找到它的前驱点,改变其前驱点的指向,来删除这一个元素,如果要删除的结点为头结点,那么就要更新头结点的位置,再释放掉原来的结点
实现过程:
1.特殊情况,先判断头结点的是不是我们要删除的结点,如果头结点是我们要删除的结点,那么只要头结点不为空,我们就不断更新头结点的位置,由于是使用C语言来实现的,因此需要手动地去释放点原头结点所占用的内存空间。
2.让一个指针pre指向链表的头,使用这个指针来遍历链表,如果查找到要删除的结点,则进行链表删除操作,否则更新当前指针的位置,不断遍历链表
3.返回头结点
注意:
1.不能让指针pre指向头结点的下一个结点,这样我们就无法查找到要删除的元素的前驱点
2.在遍历链表的过程中,指针pre不能为空,并且pre->next也不能为空,因为pre->next才是我们需要删除的结
代码实现:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val) {
//定义一个指针,如果判断头结点是需要删除的结点,则通过这个指针去释放原头结点占用的内存
struct ListNode *pt;
while(head && head->val == val) //只要头结点不为空并且头结点存储的是我们要删除的值
{
pt = head;
head = head->next; //更新头结点的位置
free(pt); //释放原头结点的空间
}
//让一个新的变量等于链表头,使用该变量来遍历链表,不能使用头结点,如果更改了头结点会导致链表无法访问
//让变量指向链表头是便于查找要删除的结点的前驱的,因此不能指向head->next
struct ListNode *pre = head;
while(pre != NULL && pre->next != NULL)//只要头结点不为空,且头结点的下一个结点不为空,就遍历链表
{
if(pre->next->val == val) //如果查找到要删除的结点
{
struct ListNode *pr = pre->next; //记录下该结点,便于释放内存
pre->next = pre->next->next; //删除该结点
free(pr);
}
else
{
pre = pre->next; //没有查找到就继续遍历链表
}
}
return head;
}
解法二:使用虚拟头结点删除
思路:在头结点之前创造一个虚拟的头结点nul,使用一个新指针指向该结点,然后通过该结点来遍历链表,此时就能直接找到要删除结点的前驱点,也就不用考虑前驱点无头结点的情况。
实现过程:
1.创造一个虚拟头结点nul,让其指向头结点,定义一个遍历链表的指针p指向nul
2.遍历该链表,查找要删除的结点
3.返回头结点的指针
注意:
此时我们要让p指向虚拟头结点而不是指向头结点,因为虚拟头结点的设立是为了便于查找要删除的结点的头结点,我们实际要删除的结点应该是p->next,因此要注意到p->next也不能指向空,否则就是在操作空指针。
因为是使用C实现,所以一定要记得释放删除结点的内存空间。
代码:
struct ListNode* removeElements(struct ListNode* head, int val) {
//创造一个虚拟头结点,让该结点指向链表头
struct ListNode *nul = malloc(sizeof(struct ListNode));
nul->next = head;
//定义一个变量来遍历链表
struct ListNode *p = nul;
//遍历链表
while(p->next != NULL)
{
if(p->next->val == val)
{
struct ListNode *del = p->next;
p->next = p->next->next;
free(del);
}
else
{
p=p->next;
}
}
return nul->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
的节点。示例:
输入 ["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"] [[], [1], [3], [1, 2], [1], [1], [1]] 输出 [null, null, null, null, 2, null, 3] 解释 MyLinkedList myLinkedList = new MyLinkedList(); myLinkedList.addAtHead(1); myLinkedList.addAtTail(3); myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3 myLinkedList.get(1); // 返回 2 myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3 myLinkedList.get(1); // 返回 3提示:
0 <= index, val <= 1000
- 请不要使用内置的 LinkedList 库。
- 调用
get
、addAtHead
、addAtTail
、addAtIndex
和deleteAtIndex
的次数不超过2000
。
解法一:使用单链表
思路:
为单链表设置一个虚拟头结点,便于链表的插入和删除结点时,易于找到要插入和删除位置的前驱点,便于插入和删除链表的操作
获取链表的第n个结点
注意:n是从0开始的,n也不能大于链表的长度(已经超出了尾结点),要注意该位置的边界条件
实现:
首先判断n的值是否符合条件,如果不符合,直接退出
遍历链表,返回位置n结点的值
代码:
int myLinkedListGet(MyLinkedList* obj, int index) {
//obj为虚拟头结点,让遍历链表指针指向头结点
MyLinkedList *head = obj->next;
//遍历链表查找位置n的结点,只要链表不为空就一直循环
for(register int i = 0 ; head ; i++)
{
//查找到了则返回n位置结点的值
if(i == index)
{
return head->val;
}
else //否则更新循环指针的位置
head = head->next;
}
//返回值为-1代表没有查到到n位置
return -1;
}
在链表头部插入结点
实现:
1.创造一个新结点,为新结点分配内存空间
2.为该结点赋值
3.将该结点插入到虚拟头结点与原头结点中,注意更改头指针的位置
代码:
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
//创造新结点,并为其分配内存空间,进行初始化
MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
node->val = val;
//将新的头结点插入到愿头结点与虚拟头结点之间的位置作为新的头结点
node->next = obj->next;
//让虚拟头结点的指针域指向新的头结点
obj->next = node;
}
在链表尾部插入结点
实现:
1.创造一个新结点,为该结点分配内存空间
2.遍历链表,查找到链表的最后一个结点
3.最后一个结点指向新要插入的结点,让新插入的结点指向为空
代码:
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
//开始遍历,找到链表的末尾结点
MyLinkedList *p = obj;
while(p->next)
p = p->next;
//为该节点分配内存空间,并为其赋值
MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
node->val = val;
node->next = NULL;
//让原本尾结点指向新插入的结点,将该结点链接到链表上
p->next = node;
}
在链表的第n个位置插入结点
实现:
1.创造一个新结点,为其分配内存空间
2.遍历链表,查找到n位置结点,并且要注意保留其的前驱点
3.在前驱点与n位置上的结点之间插入新结点
注意:
如果n的位置特殊,例如n为0,那么可以直接调用上面的函数,在头部插入
下图插入结点时步骤1和2不能更改顺序,否则会丢失n位置的结点,导致无法访问
代码:
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
//如果要插入位置为0,则直接调用在头结点插入新结点的函数,完成后退出程序
if(index == 0)
{
myLinkedListAddAtHead(obj,val);
return;
}
//如果n不为0,开始遍历查找链表
MyLinkedList *p = obj->next;
for(register int i = 1 ; p ; i++)
{
//查找到则将结点插入链表,并退出程序
if(i == index)
{
MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
node->val = val;
node->next = p->next;
p->next = node;
return;
}
else //否则更新p指针的位置
p = p->next;
}
}
删除链表的第n个结点
实现:
1.需要先对n的合法性进行判断,如果n的位置不合法,则直接退出
2.需要遍历链表,查找到n位置结点的前驱点
3.实现删除结点操作,不要忘记手动去释放掉原n位置结点的内存
注意:
在删除结点时,不要忘记保留要删除结点的后继结点,否则会导致后继结点的丢失
代码:
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
//如果n为0,那么要删除的结点为头结点
if(index == 0)
{
//指针del指向头结点
MyLinkedList *del = obj->next;
//不能对空指针进行操作
if(del)
{
obj->next = del->next;
free(del);
}
return;
}
//要删除的位置不为头结点,从头开始遍历
MyLinkedList *p = obj->next;
//当前结点不能为空,并且当前结点的下一个结点也不能为空
for(register int i = 1 ; p && p->next ; i++)
{
//查找到index位置
if(i == index)
{
//记录下该位置,便于后序释放内存
MyLinkedList *del = p->next;
//不能对空指针进行操作
if(del)
{
p->next = del->next;
free(del);
}
return;
}
else // 更新p指针位置,用来判断链表是否为空
p = p->next;
}
}
清空链表
实现:
依次遍历链表,只要链表不为空,则释放掉当前节点的内存空间,删除该结点
代码:
void myLinkedListFree(MyLinkedList* obj) {
//只要链表不为空,依次从虚拟头结点开始释放内存
while(obj)
{
MyLinkedList *del = obj;
obj = obj->next;
free(del);
}
}
完整代码
//定义单链表的结点
typedef struct MyLinkedList {
int val;
struct MyLinkedList *next;
} MyLinkedList;
MyLinkedList* myLinkedListCreate() {
MyLinkedList *head = (MyLinkedList *)malloc(sizeof(MyLinkedList));
head->next = NULL;
return head;
}
int myLinkedListGet(MyLinkedList* obj, int index) {
//obj为虚拟头结点,让遍历链表指针指向头结点
MyLinkedList *head = obj->next;
//遍历链表查找位置n的结点,只要链表不为空就一直循环
for(register int i = 0 ; head ; i++)
{
//查找到了则返回n位置结点的值
if(i == index)
{
return head->val;
}
else //否则更新循环指针的位置
head = head->next;
}
//返回值为-1代表没有查到到n位置
return -1;
}
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
//创造新结点,并为其分配内存空间,进行初始化
MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
node->val = val;
//将新的头结点插入到愿头结点与虚拟头结点之间的位置作为新的头结点
node->next = obj->next;
//让虚拟头结点的指针域指向新的头结点
obj->next = node;
}
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
//开始遍历,找到链表的末尾结点
MyLinkedList *p = obj;
while(p->next)
p = p->next;
//为该节点分配内存空间,并为其赋值
MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
node->val = val;
node->next = NULL;
//让原本尾结点指向新插入的结点,将该结点链接到链表上
p->next = node;
}
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
//如果要插入位置为0,则直接调用在头结点插入新结点的函数,完成后退出程序
if(index == 0)
{
myLinkedListAddAtHead(obj,val);
return;
}
//如果n不为0,开始遍历查找链表
MyLinkedList *p = obj->next;
for(register int i = 1 ; p ; i++)
{
//查找到则将结点插入链表,并退出程序
if(i == index)
{
MyLinkedList *node = (MyLinkedList *)malloc(sizeof(MyLinkedList));
node->val = val;
node->next = p->next;
p->next = node;
return;
}
else //否则更新p指针的位置
p = p->next;
}
}
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
//如果n为0,那么要删除的结点为头结点
if(index == 0)
{
//指针del指向头结点
MyLinkedList *del = obj->next;
//不能对空指针进行操作
if(del)
{
obj->next = del->next;
free(del);
}
return;
}
//要删除的位置不为头结点,从头开始遍历
MyLinkedList *p = obj->next;
//当前结点不能为空,并且当前结点的下一个结点也不能为空
for(register int i = 1 ; p && p->next ; i++)
{
//查找到index位置
if(i == index)
{
//记录下该位置,便于后序释放内存
MyLinkedList *del = p->next;
//不能对空指针进行操作
if(del)
{
p->next = del->next;
free(del);
}
return;
}
else // 更新p指针位置,用来判断链表是否为空
p = p->next;
}
}
void myLinkedListFree(MyLinkedList* obj) {
//只要链表不为空,依次从虚拟头结点开始释放内存
while(obj)
{
MyLinkedList *del = obj;
obj = obj->next;
free(del);
}
}
206.反转链表
题目描述:
给你单链表的头节点
head
,请你反转链表,并返回反转后的链表。示例 1:
输入:head = [1,2,3,4,5] 输出:[5,4,3,2,1]示例 2:
输入:head = [1,2] 输出:[2,1]示例 3:
输入:head = [] 输出:[]提示:
- 链表中节点的数目范围是
[0, 5000]
-5000 <= Node.val <= 5000
思路:
改变原链表中的指针,让其结点的next指针直接指向该结点的前一个结点,更新头指针的位置,就能在原链表中实现链表的反转。
为什么不使用一个新的链表逆序存储原链表的值呢?
答:这样会造成内存空间的浪费
解法一:双指针
思路:
需要定义两个指针,一个指针用来记录当前结点的位置,另一个指针用来记录当前结点反转之后的指向,不断的改变链表中结点的指向,直到节点全部实现反转,如下图所示
实现:
1.先对指针进行初始化,pre为当前结点要指向的位置,因此要初始化为NULL,而cur指针需要指向链表的当前位置,而链表从头开始遍历,因此cur要初始化为head
2.遍历结束的要求应该是到达原结点的尾结点,尾结点也要进行反转,即当cur指向空时,遍历链表的操作就应该结束,否则就会对空指针进行操作
3.在改变结点的指向时,需要使用一个临时的指针p来记录当前结点的后继结点,否则就会造成后面的结点丢失,无法访问(需要在反转之前保存下来)
4.整体后移指针pre和cur,即pre移动到cur,cur移动到p的位置,这步操作的顺序不能更改,否则会造成pre移动不到cur的正确位置
5.此时新链表的头结点应该为pre
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head) {
typedef struct ListNode node;
//使用两个指针,一个指向当前结点下一步要指向的位置,一个指向当前结点
node *pre = NULL;
node *cur = head;
//只要没有超出链表的尾部
while(cur)
{
//记录下当前结点的后序结点,防止当前结点的后继结点丢失
node *p = cur->next;
//改变当前结点的指向
cur->next = pre;
//更新两个指针的位置,让其后移
//下面步骤不能更改,否则会导致pre无法到达正确的位置
pre = cur;
cur = p;
}
//返回新的头结点
return pre;
}
解法二:递归
思路:
逻辑与上述的双指针解法一致
实现:
1.需要定义一个函数,专门使用来反转链表,需要传入的参数应该为当前结点的位置和当前结点反转后要指向的位置
2.使用一个主函数,在主函数调用翻转链表的函数,只要cur不为空,则递归遍历,否则返回pre(新链表的头)
3.实现结点的反转操作
4.再调用递归来更新pre和cur的值
代码:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode *rev(struct ListNode *pre , struct ListNode *cur)
{
//如果当前位置为空,则代表pre的位置刚好为原链表的尾,新链表的头
//此时也意味着所有链表的结点都已经实现反转
if(cur == NULL)
return pre; //返回新链表的头
//记录当前结点的下一个结点,防止当前结点的后继结点丢失
struct ListNode *p = cur->next;
//改变当前结点的指向,实现反转结点
cur->next = pre;
//递归调用
//形参pre代表当前结点要反转的指向,则实参传递的应该为更新后的pre的位置,即pre = cur;
//形参cur代表当前结点的位置,实参传递的应该为更新后的cur值,即cur = p;
return rev(cur,p);
}
struct ListNode* reverseList(struct ListNode* head) {
return rev(NULL,head);
}
练习总结
本次练习掌握了多种对链表的操作方法,如对于链表增删操作使用虚拟头结点更便于找到要删除的前驱点,也能够在原链表中进行操作,对链表操作时也能够对边界条件做出判断,主要的收获就是基本掌握的双指针的用法以及虚拟头结点用法。
但在此次练习中,在设计链表时,没有去使用双链表来进行操作,需要在下次练习时使用双链表的方式去设计链表,对与递归的使用还是不太熟练,对于链表的操作需要多去应用。