【数据结构】【学习笔记】链表

链表

当用户需要不确定地添加数据时,使用数组就不能很好的解决需求,这时候就需要用到链表。

⚙️结构

链表是离散的数据结构,链表中每个节点都会有指向下一个元素的指针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. 删除排序链表中的重复元素

描述

image

思路

  • 因为已经排序了,所以重复的元素都是黏在一起的,
  • 直接找前一个是否相同,相同则移除即可,
  • 注意:假如需要移除,则不要将指针往后跳,这样子会跳过对下一个值的检查。

实现

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. 奇偶链表

描述

image

思路

使用链表双指针(快慢/前后指针)+穿针引线。

  • 一个指针指着奇链表,一个指针指着偶链表;
  • 像穿针引线一样:
    • 先奇链表的子节点接上跳过一个元素的元素,并更新奇链表的位置;
    • 然后偶链表的子节点也接上现奇链表的子节点(因为奇链表的位置已经更新了),并更新偶链表的位置;
  • 注意
    • 由于最后需要将偶链表接到奇链表的尾部,所以需要记录偶链表的头;
    • 由于偶指针是快指针,所以循环条件应该使用偶指针而不是奇指针。

实现

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. 反转链表

描述

image

思路

  • 迭代法

以三指针的方式,左指针永远指着最后的头部,而右指针继续往右走进行调换,为了调换准确,需要应该指针暂时存放右指针原本的子节点。

  • 递归法

实现

// 迭代法
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引用指向头部节点。
  • 适合处理环形轮询任务
  • 容易写死循环,需注意终止条件
  • 有单向和双向两种
    • 单向
      image
    • 双向
      image

跳表 (Skip List)

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

引用

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


注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

posted @ 2026-07-22 21:23  SEHOD  阅读(11)  评论(0)    收藏  举报