[算法]力扣刷题-初级算法 - 链表(一) [删除链表中的节点] [删除链表的倒数第 N 个结点] [反转链表]

先总结下链表这一数据结构在代码中的操作特点:

因为线性表的各个元素在内存中是连续的,而链表的各个元素在内存中是不连续的,是通过next、prev指针找到前后相邻的元素,因此链表操作与线性表操作最大的区别在于:
1. 链表无法直接定位到某个元素。
线性表中可以通过index直接找到第n个元素,而链表中只能通过指针遍历的方式找。

2. 链表增删元素不需要修改其他元素的位置。
由于线性表在内存中是连续存储的,因此想要增加或删除某一元素而保持其他元素顺序不变,必须要把修改元素之后的所有元素进行前一或后移操作。
而链表中的元素在内存中是不连续存储的,因此增删时无需修改前后的元素的存储位置,只要修改前后的两个节点的next、prev指针即可。

3. 链表操作需要注意断链的问题。
由于链表的前后节点之间是通过next、prev指针进行连接的,因此如果指针操作有误,没有将指针原来所指向的节点地址保留,就会发生断链的问题,
将无法找到之后的链表了。



237. 删除链表中的节点

请编写一个函数,用于 删除单链表中某个特定节点 。在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问 要被删除的节点 。
题目数据保证需要删除的节点 不是末尾节点 。
示例 1: 输入:head = [4,5,1,9], node = 5 输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9
链接:https://leetcode-cn.com/problems/delete-node-in-a-linked-list

思路:“死的那个是你,你是你哥哥”

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
void deleteNode(struct ListNode* node) 
{
    node->val = node->next->val;
    node->next = node->next->next;
}

在这里插入图片描述

19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例 1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例 2:
输入:head = [1], n = 1
输出:[]
进阶:你能尝试使用一趟扫描实现吗?
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/xn2925/

思路:
最开始的思路比较简单,因为题目中对于链表长度做了限制:30。
可以先将整个链表遍历一遍,通过数组将所有节点存储起来,等删除时直接通过索引找到该节点执行删除即可。

struct ListNode* removeNthFromEnd(struct ListNode* head, int n)
{
    struct ListNode* node_map[30] = {0}; 
    struct ListNode* head_temp = head;
    char node_count = 0;
    for(; head_temp != NULL; head_temp=head_temp->next)
    {
        node_map[node_count++] = head_temp;
    }
    if(node_count-n == 0)
    {
        if(node_count == 1)
        {
            return NULL;
        }
        else
        {
            node_map[0]->val = node_map[1]->val;
            node_map[0]->next = node_map[1]->next;
        }
    }
    else
    {
        node_map[node_count-n-1]->next = node_map[node_count-n]->next;
    }
    
    return node_map[0];
}

但实际写出来发现,代码中包含了太多对于边界值的特殊处理。 进一步优化代码:做一个假的链表头,next指向真实的链表头,从假链表头开始遍历做处理,返回假链表头的next节点指针,就不需要对删除头节点、只有一个节点等边界情况做特殊处理了:
struct ListNode* removeNthFromEnd(struct ListNode* head, int n)
{
    struct ListNode* node_map[31] = {0};
    struct ListNode head_node = {0};
    head_node.next = head;
    
    char node_count = 0;
    for(struct ListNode* head_temp = &head_node; head_temp != NULL; head_temp=head_temp->next)
    {
        node_map[node_count++] = head_temp;
    }

    node_map[node_count-n-1]->next = node_map[node_count-n]->next;
    return node_map[0]->next;
}

当然,因为数组依旧存在,优化前后内存占用都很拉跨。
在这里插入图片描述

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
链接:https://leetcode-cn.com/problems/reverse-linked-list

思路:
最开始的想法是遍历,但是把for写出来以后发现遍历的方式每次需要对当前节点、当前节点->next、当前节点->next->next进行操作,而且对于边界(开头和结尾)的处理会比较麻烦,直接放弃换递归。

递归的方法就比较简单了,结束条件是node->next==NULL,也就是到了链表的最后一个节点,将该节点地址作为反转后的链表头逐级返回。
同时每一级传入当前节点和前一个节点,并将当前节点的next指针指向前一个节点。
时间复杂度O(n)
空间复杂度O(n)

struct ListNode* reverseList_recursion(struct ListNode* prev_node, struct ListNode* node)
{
    if(node->next == NULL)
    {
        node->next = prev_node;
        return node;
    }
    else
    {
        struct ListNode* head = reverseList_recursion(node, node->next);
        node->next = prev_node;
        return head;
    }
}

struct ListNode* reverseList(struct ListNode* head)
{
    if(head == NULL)
    {
        return NULL;
    }
    return reverseList_recursion(NULL, head);
}

在这里插入图片描述
发现内存占用比较拉跨,原因是因为递归导致的调用栈占用,可以优化成尾递归:

struct ListNode* reverseList_recursion(struct ListNode* prev_node, struct ListNode* node)
{
    if(node->next == NULL)
    {
        node->next = prev_node;
        return node;
    }
    else
    {
        struct ListNode* node_next = node->next;
        node->next = prev_node;
        return reverseList_recursion(node, node_next);
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

___NULL___

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值