目录
写在前面
链表
链表的概念和结构
链表的概念
链表的分类
单向链表和双向链表
带头链表和不带头链表
循环链表和非循环链表
常用
链表题目
LeetCode 203 之删除某值节点
LeetCode 206 之 反转链表
LeetCode 876 之链表的中间结点
牛客网 链表中的倒数第k 个结点
LeetCode 21 之合并链表
链表在面试题和笔试题中出现的频率相当的高,所以我们应该着重的复习链表和经典的链表相关题目。
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
注意:
链表在逻辑上连续,但是在物理层面不一定连续。
现实的结点一般是从堆上申请出来的。
从堆上申请的空间不一定连续。
按照方向来分,或者说是按照结点中存储的逻辑上其他相邻结点的地址来分。
如果有一个头节点,就是带头链表,带头链表处理一些链表相关题目表现很好。
判断标准是首位是否相连。
无头单向非循环链表: 结构简单,但是一般不单独用来存储数据,而是作为哈希桶、图的临界表等其他复杂数据结构的子结构。笔试题中出现颇多。
带头双向循环链表:
结构最复杂,一般用于单独存储数据。虽然结构复杂,但是因为带头而且循环,处理起来比较简单。
题目描述:给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
题目思路:我们只需要遍历一遍链表,然后遇到需要删除的节点直接修改前一个节点的指向即可。这样就要求我们记住前一个节点。
铺垫:
首先我们先回顾一下如何删除节点:找到节点的前一个节点,然后进行删除操作。
if(next->val == val){ node->next = next->next; }
虚拟头节点:在单向链表中添加虚拟头节点是一个常用而且好用的技巧。
ListNode* Head = new ListNode;
我的解法1:常规思路,首先看头节点,确保头节点的val不等于val。然后再去遍历后面的链表。
/** * 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) { if(head == nullptr) return nullptr; ListNode* Head = head; while(head->val == val){ if(head->next == nullptr){ return nullptr; } head = head->next; Head = head; } ListNode* next = head->next; while(next){ if(next->val == val){ head->next = next->next; } else{ head = head->next; } if(head == nullptr){ next = nullptr; }else{ next = head->next; } } return Head; } };
我的解法2:添加虚拟头节点,直接遍历数组即可。
class Solution { public: ListNode* removeElements(ListNode* head, int val) { if(head == nullptr) return nullptr; ListNode* Head = new ListNode; Head->next = head; ListNode* next = Head->next; ListNode* node = Head; while(next){ if(next->val == val){ node->next = next->next; } else{ node = next; } if(node == nullptr){ next = nullptr; }else{ next = node->next; } } return Head->next; } };
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
思路:需要熟悉如何进行节点的反转,因为后一个节点要指向前一个节点,所以我们需要提前保存前一个节点;同时因为修改指向后无法找到下一个节点,所以需要提前保存。
铺垫:熟悉一下节点的反转操作
ListNode* prev, curr, next; next = curr->next; curr->next = prev; //反转操作 prev = curr; curr = next;
/** * 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* reverseList(ListNode* head) { if(!head) return nullptr; ListNode* prev = nullptr; ListNode* curr = head; ListNode* next = curr->next; while(curr){ next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; } };
给你单链表的头结点 head
,请你找出并返回链表的中间结点。 如果有两个中间结点,则返回第二个中间结点。
思路:首先最容易想到的方法应该是首先遍历一遍整个链表,获取到总共的结点个数,随后再遍历找到中间结点。这种方法非常好实施;
但是同时也可以使用双指针的方法解,因为中间结点相较于最终结点存在一个明显的二倍关系,可以采用快慢指针,快指针一步走两个结点,慢指针一步走一个结点,当快指针指向nullptr 时,慢指针指向的就恰好是中间结点。
我的解法1:
/** * 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* middleNode(ListNode* head) { if(!head) return nullptr; int num = 0; ListNode* begin = head; while(begin){ num++; begin = begin->next; } num /= 2; while(num--){ head = head->next; } return head; } };
我的解法2:
/** * 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* middleNode(ListNode* head) { if(!head) return head; ListNode* pfast = head; ListNode* pslow = head; while(pfast && pfast->next){ pfast = pfast->next; pslow = pslow->next; if(pfast) pfast = pfast->next; //判定当pfast 不为空时再继续。 } return pslow; } };
两种解法相比,第一种比较常规,第二种应用快慢指针,但是实际上,性能方面没有较大差别。
描述:输入一个链表,输出该链表中倒数第k个结点。
示例1 输入:
1,{1,2,3,4,5}
返回值:
{5}
思路:最常规的思路就是遍历找到总共的链表个数n,第n-k+1 个就是要找的倒数第k 个结点。不过需要处理几个细节:(1)pListHead 本身为空可以直接返回。(2)k <= 0 直接返回。(3)k >= 链表中结点的个数,直接返回。 不过本题目仍然可以按照快慢指针的方法去做。想要的最终结果是当快指针走到尽头的时候,慢指针恰好走到倒数第k 个结点上,这并不难做到,只需要让快指针先走k 步即可。
我的解法1:
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } };*/ #includeclass Solution { public: ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) { if(!pListHead || k <= 0) return nullptr; int num = 0; ListNode* head = pListHead; while(head){ num++; head = head->next; } if(num < k) return nullptr; head = pListHead; int times = num - k; while(times--){ head = head->next; } return head; } };
我的解法2:(我们还是对上面的一些特殊情况进行了优先处理)
/* struct ListNode { int val; struct ListNode *next; ListNode(int x) : val(x), next(NULL) { } };*/ class Solution { public: ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) { if(!pListHead || k <= 0) return nullptr; ListNode* pfast = pListHead; ListNode* pslow = pListHead; //pfast 先走k 步,如果走k步还没到就走到尽头了,说明k > 链表中结点个数,返回空 for(int i = k; i > 0; i--){ pfast = pfast->next; if(!pfast && i == 1) return pslow; else if(!pfast) return nullptr; } while(pfast){ pfast = pfast->next; pslow = pslow->next; } return pslow; } };
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:升序排列的方法就是比较两个链表的结点中val 的大小,取小的一个尾插到新链表中。注意在这题中可以使用虚拟头结点来简化过程。
我的解法1:
class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode* pcurr1 = list1; ListNode* pcurr2 = list2; ListNode* head = new ListNode; ListNode* newCurr = head; while(pcurr1 && pcurr2){ if(pcurr1->val > pcurr2->val){ newCurr->next = pcurr2; pcurr2 = pcurr2->next; }else{ newCurr->next = pcurr1; pcurr1 = pcurr1->next; } newCurr = newCurr->next; } //来判断到底是谁终止了,直接修改newCurr 到未终止的结点的指向即可。 newCurr->next = (pcurr1 == nullptr) ? pcurr2 : pcurr1; return head->next; } };
我的解法2: 递归如果 list1 或者 list2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断 list1 和 list2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。
class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { if(!list1) return list2; else if(!list2) return list1; else if(list1->val < list2->val){ list1->next = mergeTwoLists(list1->next, list2); return list1; }else{ list2->next = mergeTwoLists(list1, list2->next); return list2; } } };