一、链表理论基础
1. 什么是链表
什么是链表?链表是一种通过指针串联在一起的线性结构,每一个结点由两部分组成,一个是数据域(存放数据),另一个是指针域(存放指向下一个节点的指针),最后一个结点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头节点,也就是 head。
如图所示:
2. 链表的类型
(1) 单链表
- 单链表
上面说的就是单链表
(2)双链表
- 双链表
单链表中的节点只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表既可以向后查询,也可以向前查询。
如图所示:
(3)循环链表
循环链表,顾名思义,就是将链表的首尾相连。
循环链表可以用来解决 约瑟夫环 问题。
3. 链表的存储方式
数组在内存中的分布是连续的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中的各个节点,组成的一串不连续的存储空间。
所以链表中的节点在内存中不是连续分布的,而是散乱分布在内存中的某个地址上,分配机制取决于操作系统的内存管理。
如图所示:
这个链表的起始节点为2,终止节点为7,各个节点分布在内存的不同地址空间上,通过指针串联在一起。
4. 链表的定义
链表节点的定义,看似简单,但实际上很多人面试的时候都写不好。
这是因为平时在刷 LeetCode 的时候,链表的节点都默认定义好了,直接用就行了,所以大家都没有注意到链表的节点是如何定义的。
而在面试的时候,我们是需要自己手写链表的。
以下是 C/C++定义链表节点的方式:
// 单链表class ListNode{int val; // 数据域ListNode *next; // 指针域ListNode(int x) : val(x), next(null) {} // 节点的构造函数}
链表的定义可以不写构造函数吗?答案是可以的,C++会帮我们生成一个默认构造函数。
但是这个构造函数不会初始化任何成员变量。
举个例子:
// 自己写了构造函数ListNode *head = new ListNode(5);
// 使用默认构造函数ListNode * head = new ListNode();head->val = 5;
如果不定义构造函数,使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!
5. 链表的操作
(1)删除节点
删除节点D,如下:
只要将 C 节点的 next 指针指向 E 节点就可以了。
那有人会说了,D节点不是依然存留在内存里吗?只不过是没有在这个链表里而已。
确实是这样,所以在C++里最好是再手动释放这个D节点,释放这块内存。
其他的语言,例如Java、Python,就有自己的内存回收机制,不再需要程序员手动释放了。
(2)添加节点
如图所示:
可以看出链表的增添和删除都是 O(1) 操作,也不会影响到其他节点。
但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点,然后通过next指针进行删除操作,查找的时间复杂度是 O(n)。
6. 性能分析
链表的特性和数组的特性的比较,如图所示:
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删,适合数据量不固定,频繁增删,较少查询的场景。
二、移除链表元素
1. 移除链表元素
(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* removeElements(ListNode* head, int val) {// 如果删除的是头节点while(head != nullptr && head->val == val){ListNode* temp = head;head = head->next;delete temp; // 释放内存}// 如果删除的不是头节点ListNode* curptr = head;while(curptr != nullptr && curptr->next != nullptr){if(curptr->next->val == val){ListNode* temp = curptr->next;curptr->next = curptr->next->next;delete temp;}else{curptr = curptr->next;}}return head;}};
(2)无法访问头节点
方法:和下一个节点交换
删除链表中的节点的常见的方法是定位到待删除结点的上一个结点,修改上一个结点的 next 指针,使其指向待删除节点的下一个节点,即可完成删除操作。
这道题中,传入的参数 node 为要被删除的节点,无法定位到该节点的上一个节点。注意到要被删除的节点不是链表的末尾节点,因此 node.next 不为空,可以通过对 node 和 node.next 进行操作实现删除节点。
在给定节点 node 的情况下,可以通过修改 node 的 next 指针的指向,删除 node 的下一个节点。但是题目要求删除 node,为了达到删除 node 的效果,只要在删除节点之前,将 node 的节点值修改为 node.next 的节点值即可。
例如:给定链表 4 —> 5 —> 1 —> 9,要被删除的节点值是 5,即链表中的第2个接待你。可以通过如下两步操作实现删除节点的操作。
- 将第2个节点的值修改为第3个节点的值,即将节点5的值修改为1,此时链表如下:
4 —> 5 —> 1 —> 9
- 删除第3个节点,此时链表如下:
4 —> 1 —> 9
至此,题解。
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/class Solution {public:void deleteNode(ListNode* node) {// 因为题目给的是要删除当前的节点,所以获取不到当前节点的前一个结点// 但是题目又说,被删除的结点不是尾节点,所以我们想到当前将下一个节点的赋给当前要删除的节点,然后删掉下一个节点,这样就等价于删除了当前节点// 因为这样,即使题目要删除的是头节点,我们通过交换,删除的是下一个节点,所以不用分情况讨论ListNode* temp = node->next;node->val = node->next->val;node->next = node->next->next;delete temp; // 释放内存}};
2. 设计链表
(1)设计一个完整的链表
写这道题主要是对链表还不是很熟悉,而且卡在一个地方,就是在编译器写的时候是可以使用内部类的,但是在力扣平台上不知道为什么用不了,所以写了结构体,当然这个结构体也可以提到类的外部。其他的都没什么大问题,多写就行了。
class MyLinkedList {public:// 链表节点struct LinkedNode{ // 定义链表节点的结构int val;LinkedNode* next;LinkedNode(int val) : val(val), next(nullptr){}};MyLinkedList() { // 构造函数_dummyHead = new LinkedNode(0);_size = 0; // 虚拟头节点不算入真正链表的结点}// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点int get(int index) {if(index < 0 || index > _size - 1){ // 索引无效return -1;}LinkedNode* curptr = _dummyHead; // 虚拟头指针赋给当前需要操作的指针while(index--){curptr = curptr->next;}return curptr->next->val;}void addAtHead(int val) {LinkedNode* newHead = new LinkedNode(val); // 新的头节点newHead->next = _dummyHead->next;_dummyHead->next = newHead;_size++;}void addAtTail(int val) {LinkedNode* newTail = new LinkedNode(val); // 新的尾节点LinkedNode* curptr = _dummyHead;while(curptr->next != nullptr){ // 定位到尾节点curptr = curptr->next;}curptr->next = newTail;_size++;}// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点// 如果index大于链表的长度,则返回空void addAtIndex(int index, int val) {if(index > _size){return;}LinkedNode* newNode = new LinkedNode(val);LinkedNode* curptr = _dummyHead;while(index--){ // 找到要插入位置的前一个节点curptr = curptr->next;}newNode->next = curptr->next;curptr->next = newNode;_size++;}void deleteAtIndex(int index) {if(index < 0 || index > _size - 1){return;}LinkedNode* curptr = _dummyHead;while(index--){curptr = curptr->next;}LinkedNode* temp = curptr->next;curptr->next = curptr->next->next;delete temp;_size--;}// 打印链表void display(){LinkedNode* cur = _dummyHead->next;while(cur != nullptr){cur = cur->next;cout << cur->val << " ";}cout << endl;}private:LinkedNode* _dummyHead; // 虚拟头节点int _size; // 链表的节点数,即链表的大小};/*** Your MyLinkedList object will be instantiated and called as such:* MyLinkedList* obj = new MyLinkedList();* int param_1 = obj->get(index);* obj->addAtHead(val);* obj->addAtTail(val);* obj->addAtIndex(index,val);* obj->deleteAtIndex(index);*/
扩展:可以试试用 双链表 来解,双链表需要开 一个虚拟头节点 和 一个虚拟尾节点,同时需要一个 _size 来记录链表的大小,我们通过 _size - index 更接近 虚拟头 还是 虚拟尾,来确定从那边开始操作,这样会让程序的效率提高。
3. 反转链表
(1)反转整个链表
① 迭代的思想
206. 反转链表 - 力扣(LeetCode)
思路:
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的 next 指针的指向,直接将链表反转,而不用重新定义一个新的链表,如图所示:

之前链表的头节点是元素1,反转之后头节点就是元素5,这里并没有添加或者删除节点,仅仅是改变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){ListNode* temp; // 保存下一个节点的指针ListNode* pre = nullptr; // pre指向空指针ListNode* cur = head; // cur指向头指针while(cur != nullptr){ // cur需要遍历一遍指针temp = cur->next; // 先将下一个节点保存再tempcur->next = pre; // 实现第i个节点的反转// 更新pre和cur指针,因为最后我们要通过pre来访问反转后的链表pre = cur;cur = temp;}return pre;}};
② 递归思想
力扣有位大牛 labuladong 对递归反转链表总结的很好https://mp.weixin.qq.com/s/5wz_YJ3lTkDH3nWfVDi5SA
递归的思想相对迭代思想,稍微有点难以理解,处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。
处理看起来比较困难的问题,可以尝试化整为零,把一些简单的解法进行修改,解决困难的问题。
值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是O(n),但是迭代解法的空间复杂度是O(1),而递归解法需要堆栈,空间复杂度是O(n)。但是用递归写的代码看起来更加简洁、优美。考虑效率的话还是使用迭代算法更高。
/*** 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 == nullptr){return head;}if(head->next == nullptr){ // 递归出口,如果要反转的节点只剩下一个,则直接返回本节点return head;}ListNode* last = reverseList(head->next);head->next->next = head; // 让下一个节点指向它的上一个节点head->next = nullptr; // 上一个节点指向空return last; // 递归完成后last指针指向原来链表的最后一个节点,也就是反转后的第一个节点}};
(2)回文链表
垃圾算法,时间复杂度 O(3n),空间复杂度 O(n)
/*** 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:bool isPalindrome(ListNode* head) {ListNode* temp;ListNode* pre = nullptr;ListNode* cur = head; // cur指针用来遍历链表// 开了一个新链表,垃圾算法来着ListNode* newHead = new ListNode();ListNode* ptr = newHead;while(cur != nullptr){ListNode* newNode = new ListNode();newNode->val = cur->val;ptr->next = newNode;ptr = ptr->next;cur = cur->next;}cur = head;ptr = newHead->next;while(cur != nullptr){temp = cur->next; // 指向下一个节点cur->next = pre;// 更新pre和curpre = cur;cur = temp;}while(pre != nullptr && ptr != nullptr){if(pre->val != ptr->val){return false;}pre = pre->next;ptr = ptr->next;}return true;}};
时间复杂度 O(n),空间复杂度 O(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* reverse(ListNode* head){ListNode* temp = nullptr;ListNode* pre = nullptr;ListNode* cur = head; // cur指针用来遍历链表while(cur != nullptr){temp = cur->next; // temp保存cur下一个节点的指针域cur->next = pre; // 让cur指向nullptrpre = cur; // 更新pre和curcur = temp;}return pre; // pre才是反转后链表的头节点}bool isPalindrome(ListNode* head) {if(head == nullptr || head->next == nullptr){return true;}ListNode* slow = head; // 慢指针,每次移动一个位置,用来记录链表的中间位置ListNode* fast = head; // 快指针,每次移动两个位置,走完链表,让slow能够走到链表的中间位置ListNode* pre = head; // pre指向slow的前一个位置,用来分割链表while(fast != nullptr && fast->next != nullptr){ // 目的是让slow能够指向链表的中间位置pre = slow; // pre指向slow的前一个位置slow = slow->next;fast = fast->next->next;}pre->next = nullptr; // 将链表前半部分断开ListNode* cur1 = head; // 链表前半段ListNode* cur2 = reverse(slow); // 链表后半段while(cur1 != nullptr){ // 两个链表开始比较,cur1段链表的长度一定小于cur2,所以使用cur1作为循环的长度if(cur1->val != cur2->val){return false;}cur1 = cur1->next;cur2 = cur2->next;}return true;}};
4. 交换链表中的节点
(1)两两交换链表中的节点
思路:
一定要画图。
情况一:
情况二:
先定义一个快指针和一个慢指针,指向交换节点前的链表的头(也就是节点1),因为题目不允许修改节点的值,所以只能改变节点指针域的指向以达到交换节点的效果。首先要考虑清楚链表的结点数量是偶数个还是奇数个?偶数个意味着每两个节点都需要交换,那么 slow->next = fast->next->next->next;如果是奇数个,那么 slow->next = fast->next->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* swapPairs(ListNode* head) {if(head == nullptr || head->next == nullptr){ // 如果是空链表或只有一个节点,那么直接返回return head;}ListNode* slow = head;ListNode* fast = head;head = head->next;while(fast && fast->next){ // fast或fast->next为空,表明没有节点或只有一个节点,这时不再需要交换了fast = fast->next->next;slow->next->next = slow;if(fast == nullptr){ // 如果fast节点不存在,那么slow->next则需要指向一个空指针,不然链表陷入死循环slow->next = nullptr;}else{if(fast->next == nullptr){ // 如果 fast->next不存在,那么表明链表的个数是奇数,最后一个节点不需要交换(因为没有)slow->next = fast;}else{ // 链表节点是偶数个slow->next = fast->next;}}slow = fast;}return head;}};
5. 删除链表的倒数第N个节点
19. 删除链表的倒数第N个节点 - 力扣(LeetCode)
我的解法
时间复杂度O(n)
控件复杂度O(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* removeNthFromEnd(ListNode* head, int n) {ListNode* dummyHead = new ListNode(-1); // 虚拟头节点dummyHead->next = head;ListNode* cur = head;int size = 0; // 记录链表的大小while(cur != nullptr){cur = cur->next;size++;}int moveNumber = size - n; // 链表正向移动的节点数cur = dummyHead; // cur指针指向虚拟头节点while(moveNumber > 0){cur = cur->next;moveNumber--;}cur->next = cur->next->next; // 删除指定的节点return dummyHead->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:int cur = 0;ListNode* removeNthFromEnd(ListNode* head, int n) {if(head == nullptr){ return nullptr; }// 这里为什么要用 head->next 来接收递归的返回值呢?很巧妙的,当递归到最后一层时,递归开始往回走// 它会返回最后一个节点(形参head->next),即相当于把 head->next 返回给 head->next,这样子head->next也会跟着往前走// 知道走到链表的第一个节点的位置。head->next = removeNthFromEnd(head->next, n);cur++;if(cur == n){ return head->next; } // 如果当前的节点是要删除的节点,就返回它的下一个节点,即我们忽略要删除的节点return head;}};
6. 链表相交
面试题 02.07. 链表相交 - 力扣(LeetCode)
思路:
简单来说,就是求两个链表相交的指针,注意这里的相交指的并不是链表节点的值相同,而是指针相同。
我们可以求出两个链表的长度,然后求出他们的长度差 num,通过这个长度差,让更长的链表先移动 num个节点数,然后这个时候他们就在同一起跑线上了,我们可以同时让他们往后移动,判断这两个指针是否相等就行了,至此题解。
复杂度分析:
时间复杂度O(n + m)
空间复杂度O(1)
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/class Solution {public:ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {ListNode* A_head = headA;ListNode* B_head = headB;int A_size = 0; // 记录链表A和链表B的长度int B_size = 0;while(A_head != nullptr){ // 记录A的大小A_head = A_head->next;A_size++;}while(B_head != nullptr){ // 记录B的大小B_head = B_head->next;B_size++;}A_head = headA;B_head = headB;if(A_size < B_size){ // 不管A和B谁更长,最终我们都让A_head指向更长的那条链表swap(A_size, B_size);swap(A_head, B_head);}int num = A_size - B_size; // A、B链表节点的数量差while(num > 0){ // 链表A先移动 num 个节点A_head = A_head->next;num--;}while(A_head != nullptr){if(A_head == B_head){ return A_head; }A_head = A_head->next;B_head = B_head->next;}return nullptr;}};
7. 环形链表
(1)环形链表II
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/class Solution {public:ListNode *detectCycle(ListNode *head) {if(head != NULL && head->next != NULL){ListNode* cur = head;ListNode* slow = head; // 慢指针ListNode* fast = head; // 快指针while(fast != NULL && fast->next != NULL){ // 若fast指针或它的下一个指针为空,表明链表有尽头,即没有环fast = fast->next->next; // fast指针每次移动两个位置slow = slow->next; // slow指针每次移动一个位置if(slow == fast){ // 如果相遇表示链表存在环ListNode* index1 = fast;ListNode* index2 = head;while(index1 != index2){index1 = index1->next;index2 = index2->next;}return index2;}cur = cur->next;}}return NULL;}};
(2)环形链表
/*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/class Solution {public:bool hasCycle(ListNode *head) {if(head != NULL && head->next != NULL){ListNode* slow = head;ListNode* fast = head;while(fast && fast->next){ // 边界问题需要注意fast = fast->next->next; // fast指针一次走两步slow = slow->next; // slow指针一次走一步if(slow == fast){return true;}}}return false;}};
