先总结下链表这一数据结构在代码中的操作特点:
因为线性表的各个元素在内存中是连续的,而链表的各个元素在内存中是不连续的,是通过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);
}
}