【数据结构】【学习笔记】链表
链表
当用户需要不确定地添加数据时,使用数组就不能很好的解决需求,这时候就需要用到链表。
⚙️结构
链表是离散的数据结构,链表中每个节点都会有指向下一个元素的指针next,即链表中的每一项都包含着在何处能找到下一项的信息。
并且需要一个头指针来永远地指向链表中第1项的地址。
🔎特性
由上面的结构可以知道,插入链表的时间复杂度是 \(O(1)\),寻找链表中的某个值的时间复杂度是 \(O(N)\)。
比较
数组:内存连续,缓存局部性(CPU Cache Locality)好,顺采极快。
链表:节点散落在内存各处,缓存局部性差,CPU 预取容易失效。
🤓实现
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) {}
};
❔题目
1. 删除排序链表中的重复元素
描述

思路
- 因为已经排序了,所以重复的元素都是黏在一起的,
- 直接找前一个是否相同,相同则移除即可,
- 注意:假如需要移除,则不要将指针往后跳,这样子会跳过对下一个值的检查。
实现
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
ListNode* cur = head;
while(cur != nullptr && cur->next != nullptr){
if(cur->val == cur->next->val){
// ListNode* temp = cur->next->next;
// cur->next->next = nullptr;
// cur->next = temp;
// 下面这个方法在刷题会很慢
ListNode* toDelete = cur->next; // 1. 先把要删的节点存下来
cur->next = cur->next->next; // 2. 重新接线
delete toDelete; // 3. 释放内存
}else{
cur = cur->next;
}
}
return head;
}
};
function deleteDuplicates(head: ListNode | null): ListNode | null {
// 因为排序了,所以直接看上一个是不是相等就行了
let cur = head;
while(cur !== null && cur.next !== null){
if(cur.next.val === cur.val){
// 当后一个等于前一个,移除此后一个
// JS/TS 中直接跳过即可,GC 会自动回收中间那个无引用的节点
// let temp = cur.next.next;
// cur.next.next = null;
// cur.next = temp;
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return head;
};
public class Solution {
public ListNode DeleteDuplicates(ListNode head) {
ListNode cur = head;
while(cur != null && cur.next != null){
if(cur.val == cur.next.val){
// C# 的 GC 使用的是 可达性分析算法(Tracing GC)
// ListNode temp = cur.next.next;
// cur.next.next = null;
// cur.next = temp;
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return head;
}
}
2. 奇偶链表
描述

思路
使用链表双指针(快慢/前后指针)+穿针引线。
- 一个指针指着奇链表,一个指针指着偶链表;
- 像穿针引线一样:
- 先奇链表的子节点接上跳过一个元素的元素,并更新奇链表的位置;
- 然后偶链表的子节点也接上现奇链表的子节点(因为奇链表的位置已经更新了),并更新偶链表的位置;
- 注意:
- 由于最后需要将偶链表接到奇链表的尾部,所以需要记录偶链表的头;
- 由于偶指针是快指针,所以循环条件应该使用偶指针而不是奇指针。
实现
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
if(head == nullptr || head->next == nullptr){
return head;
}
ListNode* odd = head, * even = head->next;
ListNode* evenH = even;
while(even != nullptr && even->next != nullptr){
odd->next = even->next;
odd = odd->next;
even->next = odd->next;
even = even->next;
}
odd->next = evenH;
return head;
}
};
function oddEvenList(head: ListNode | null): ListNode | null {
if(head === null || head.next === null){
return head;
}
let odd = head, even = head.next;
const evenHead = even;
while(even !== null && even.next !== null){
// 穿针引线
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
};
public class Solution {
public ListNode OddEvenList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode odd = head, even = head.next;
ListNode evenH = even;
while(even != null && even.next != null){
odd.next = odd.next.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenH;
return head;
}
}
3. 反转链表
描述

思路
- 迭代法
以三指针的方式,左指针永远指着最后的头部,而右指针继续往右走进行调换,为了调换准确,需要应该指针暂时存放右指针原本的子节点。
- 递归法
实现
// 迭代法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if(head == nullptr || head->next == nullptr){
return head;
}
ListNode* newH = head, * cur = head->next;
newH->next = nullptr;
while(cur != nullptr){
ListNode* temp = cur->next;
cur->next = newH;
newH = cur;
cur = temp;
}
return newH;
}
};
// 迭代法
function reverseList(head: ListNode | null): ListNode | null {
if(head === null || head.next === null){
return head;
}
let l = head, r = head.next;
l.next = null;
while(r !== null){
let temp = r.next;
r.next = l;
l = r;
r = temp;
}
return l;
};
// 递归法
public class Solution {
public ListNode ReverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
// 2. 递:一直递归到最深处,拿到原链表的最后一个节点
// 这里的 newHead 在归的过程中永远不会变,一直就是原链表的最后一个节点
ListNode newHead = ReverseList(head.next);
// 3. 归:从倒数第二个节点开始,逐层往回反转指针
head.next.next = head;
head.next = null;
return newHead;
}
}
具体类别
单链表 (Singly Linked List)
仅包含 next 指针,节省指针内存,无法反向遍历。
双向链表 (Doubly Linked List)
- 包含 prev 和 next,可双向遍历;
- 可在时间复杂度为\(O(1)\) 删除已知节点;
- 每个节点多占一个指针的空间。
循环链表 (Circular Linked List)
- 尾节点 next 指向头节点。不同之处在于我们需要将循环链表尾部节点的next引用指向头部节点。
- 适合处理环形轮询任务
- 容易写死循环,需注意终止条件
- 有单向和双向两种
- 单向

- 双向

- 单向
跳表 (Skip List)
- 多层索引 + 链表,实际上就是二维链表,链接的方向向前(也就是向右)或向下。
- 查找复杂度降到 \(O(\log N)\)
- 空间换时间

引用
[1] 删除排序链表中的重复元素——力扣
[2] 奇偶链表——力扣
[3] 反转链表——力扣
[4] 史蒂芬·普拉达.C Primer Plus (第6版) 中文版【最新修订版】[M].微信读书版
[5] 洛伊安妮·格罗纳.学习JavaScript数据结构与算法 (第3版)[M].微信读书版
[6] 布拉德利·米勒,戴维·拉努姆.Python数据结构与算法分析 (第2版)[M].微信读书版
注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

浙公网安备 33010602011771号