代码随想录算法训练营第三天 | 203. 移除链表元素,707.设计链表,206. 反转链表 [链表篇]

文章详细解析了LeetCode中的三个链表题目:移除链表元素、设计单/双链表及反转链表,介绍了删除节点的方法,包括头节点和非头节点的处理,并提供了参考代码和实例分析。
摘要由CSDN通过智能技术生成

LeetCode 203. 移除链表元素

题目链接:203. 移除链表元素
文章讲解:代码随想录#203. 移除链表元素
视频讲解:手把手带你学会操作链表 | 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
输出:[]

思路

对于原链表的删除,需要区分删除头节点与删除其他节点,这两者的删除方式不同的。

  • 删除非头节点时,
    就是将要删除的节点的前一个节点指向要删除的节点的下一个节点,如图(图片借用代码随想录)所示:
    删除非头节点
    伪代码如下
// 假如当前遍历到cur节点,想要删除cur后面的节点
struct ListNode *cur = head;
if (cur != NULL && cur->next != NULL) {
    struct ListNode *tmp = cur->next; // 将要删除的节点存入临时变量,方便后面释放内存
	cur->next = cur->next->next; // 当前节点指向下下一个节点
	free(tmp);
}

总之,要删除链表的非头节点都是通过前一个节点来删除当前节点的,但头结点没有前一个节点。

  • 删除头节点时,
    需要将头节点移动到下一个节点就可以。
    删除头节点
    伪代码如下
// 删除头节点head
if (head != NULL) {
    struct ListNode *tmp = head; // 将要删除的头节点存入临时变量,方便后面释放内存
	head = head->next; // 当头节点指向下一个节点
	free(tmp);

删除头节点和删除非头节点的区别在于,删除非头节点时是通过cur来删除cur->next,而删除头节点时是直接将head指向下一个节点,仔细观察可知,这两块代码的处理逻辑是相同。
所以可以构造一个虚拟节点指向头节点head,这样就可以按照统一的方式删除节点了,具体可以参考下面代码。

参考代码

struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* dummynode = (struct ListNode*)malloc(sizeof(struct ListNode));
    dummynode->next = head;
    struct ListNode* cur = dummynode; // 构建个虚拟节点指向head

    while (cur->next != NULL) {
        if (cur->next->val == val) { // 可以通过当前节点找到下一个节点
            struct ListNode* tmp = cur->next; // 保存下一个节点
            cur->next = cur->next->next; // 指向下下一个节点
            free(tmp); // 释放下一个节点,不能释放当前节点,否则就找不到下一个节点了
        } else {
            cur = cur->next;
        }
    }
    head = dummynode->next;
    free(dummynode);
    return head;
}

总结

  1. 不会就画图,模拟链表删除
  2. 需要先将要删除的节点存入临时变量中,然后移动节点,再释放该节点
  3. 一定要注意cur=head还是cur=head->next,以及各种循环的判断条件

LeetCode 707.设计链表

题目链接:707.设计链表
文章讲解:代码随想录#707.设计链表
视频讲解:帮你把链表操作学个通透!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 的节点。

示例1

输入
[“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

思路

参考视频或者文章讲解,主要就是考查链表的基本操作

参考代码

typedef struct {
    int val;
    struct ListNode *next;
}MyLinkedList;

MyLinkedList* myLinkedListCreate() {
    MyLinkedList* head = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    head->next = NULL;
    return head;
}

int myLinkedListGet(MyLinkedList* obj, int index) {
	if (index < 0 || obj == NULL) {
		return -1; // 异常情况需要考虑
	}

    MyLinkedList *cur = obj->next; // index=0,取非头节点的第一个节点
    for (int i = 0; cur != NULL; i++){
        if (i == index){
            return cur->val;
        }
        else{
            cur = cur->next;
        }
    }
    return -1;
}

void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
	if (obj == NULL) {
		return; // 异常情况需要考虑
	}

    MyLinkedList* node = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    node->val = val;
    node->next = obj->next; // 新节点指向非头节点的第一个节点
    obj->next = node; // 头节点指向新节点
}

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
	if (obj == NULL) {
		return; // 异常情况需要考虑
	}

    MyLinkedList* tmp = obj;
    MyLinkedList* node = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    node->val = val;
    node->next = NULL;

    while (tmp->next != NULL) {
        tmp = tmp->next; // 找到最后一个节点
    }
    tmp->next = node; // 最后一个节点指向新节点
}

void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
    if (index < 0 || obj == NULL) {
		return; // 异常情况需要考虑
	}

    if (index == 0) {
        myLinkedListAddAtHead(obj, val);
        return;
    }
    
    MyLinkedList* tmp = obj->next; // 指向非头节点的第一个节点
    for (int i = 1; tmp != NULL; i++) { // 退出条件:遍历完链表
        if (i == index) { // 如果index存在,则在它后面插入新的节点
            MyLinkedList* node = (MyLinkedList*)malloc(sizeof(MyLinkedList));
            node->val = val;
            node->next = tmp->next;
            tmp->next = node;
            return;
        } else {
            tmp = tmp->next; // 继续遍历
        }
    }
}

void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index < 0 || obj == NULL) {
		return; // 异常情况需要考虑
	}

    if (index == 0) {
        MyLinkedList* tmp = obj->next;
        if (tmp != NULL) {
            obj->next = tmp->next;
            free(tmp);
        }
        return;
    }

    MyLinkedList* cur = obj->next;
    for (int i = 1; cur != NULL && cur->next != NULL; i++) {
        if (i == index) {
            MyLinkedList* tmp = cur->next;
            if (tmp != NULL) {
                cur->next = tmp->next;
                free(tmp);
            }
            return;
        } else {
            cur = cur->next;
        }
    }
}

void myLinkedListFree(MyLinkedList* obj) {
	if (obj == NULL) {
		return;
	}

    while(obj != NULL){
        MyLinkedList *tmp = obj;
        obj = obj->next;
        free(tmp);
    }
}

总结

  1. 这几个接口函数覆盖了链表的基本操作,需要多加练习。
  2. index为0时,比较疑惑,到底是指向头节点呢,还是第一个节点呢,需要确认。

LeetCode 206. 反转链表

题目链接:206. 反转链表
文章讲解:代码随想录#206. 反转链表
视频讲解:帮你拿下反转链表 | LeetCode:206.反转链表 | 双指针法 | 递归法

题目描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例1

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例2

输入:head = [1,2]
输出:[2,1]

思路

算是比较简单的一道题,用双指针进行遍历,最重要的要保存tmp指针用来保存下一个节点。
使用两个指针pre和cur,初始化时将pre指向NULL,cur指向第一个节点。
从前到后开始遍历cur指针,每遍历一次cur时,将cur的下一个节点先保存到tmp变量中,
然后将cur指向pre实现指针的反转,接着移动pre指向cur,移动cur指向tmp…
直到cur为NULL时,说明整个链表已经遍历完了,此时pre指向原来链表的最后一个节点,所以返回pre即可。

可以查看代码随想录的动画,比较生动形象。

参考代码

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode *pre = NULL; // 双指针 pre 和 cur
    struct ListNode *cur = head;
    struct ListNode *tmp = NULL; // tmp用来向后遍历

    while (cur) {
        tmp = cur->next;
        cur->next = pre; // 反转
        pre = cur; // pre、cur向后遍历
        cur = tmp;
    }
    return pre;
}

总结

这道题也可以使用递归来实现,代码如下:

struct ListNode* reverseList(struct ListNode* pre, struct ListNode* cur) {
	if (cur == NULL) {
		return pre; // 递归函数一定要有边界条件,当遍历到cur为NULL时,说明pre已经指向了原链表的最后一个节点了
	}
	
	struct ListNode* next = cur->next; // 向后遍历
	cur->next = pre; // 反转
	return reverseList(cur, next);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值