算法学习--链表

引言:为什么进行链表的学习?

  • 考察能力独特:链表能很好地考察应聘者对指针操作、内存管理的理解和运用能力,还能检验代码的鲁棒性,比如处理链表的插入、删除操作时对边界条件的处理。
  • 数据结构基础:链表是很多复杂数据结构和算法的基础,如图算法中的邻接表存储结构就用到了链表,在操作系统的内存管理中也常利用链表来管理空闲内存块等。

1 方法论

核心技巧                       适用场景                             时间复杂度优化                  经典例题 ───────────────────────────────────────────────────────────

快慢指针       链表环检测、找中间节点等      O(n) 复杂度下高效处理    环形链表 Ⅱ (142)         
虚拟头节点    头部插入/删除等边界简化操作   不改变复杂度但简化逻辑  删除链表的倒数第 N 个节点 
  递归         链表反转、合并有序链表等     利用递归特性简化操作逻辑  合并两个有序链表 (21)      

*设计链表

 你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。
#define MAX(a, b) ((a) > (b) ? (a) : (b))

typedef struct {
    struct ListNode *head;
    int size;
} MyLinkedList;

struct ListNode *ListNodeCreat(int val) {
    struct ListNode * node = (struct ListNode *)malloc(sizeof(struct ListNode));
    node->val = val;
    node->next = NULL;
    return node;
}

MyLinkedList* myLinkedListCreate() {
    MyLinkedList * obj = (MyLinkedList *)malloc(sizeof(MyLinkedList));
    obj->head = ListNodeCreat(0);
    obj->size = 0;
    return obj;
}

int myLinkedListGet(MyLinkedList* obj, int index) {
    if (index < 0 || index >= obj->size) {
        return -1;
    }
    struct ListNode *cur = obj->head;
    for (int i = 0; i <= index; i++) {
        cur = cur->next;
    }
    return cur->val;
}

void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
    if (index > obj->size) {
        return;
    }
    index = MAX(0, index);
    obj->size++;
    struct ListNode *pred = obj->head;
    for (int i = 0; i < index; i++) {
        pred = pred->next;
    }
    struct ListNode *toAdd = ListNodeCreat(val);
    toAdd->next = pred->next;
    pred->next = toAdd;
}

void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
    myLinkedListAddAtIndex(obj, 0, val);
}

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
    myLinkedListAddAtIndex(obj, obj->size, val);
}

void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index < 0 || index >= obj->size) {
        return;
    }
    obj->size--;
    struct ListNode *pred = obj->head;
    for (int i = 0; i < index; i++) {
        pred = pred->next;
    }
    struct ListNode *p = pred->next;
    pred->next = pred->next->next;
    free(p);
}

void myLinkedListFree(MyLinkedList* obj) {
    struct ListNode *cur = NULL, *tmp = NULL;
    for (cur = obj->head; cur;) {
        tmp = cur;
        cur = cur->next;
        free(tmp);
    }
    free(obj);
}

2 快慢指针

 2.1 介绍

溯找前驱;链表超长且环大时,快指针环内多圈追赶,有性能损耗。快慢指针是链表操作的高效技巧,通过设置快、慢两个指针,快指针每次移动两步,慢指针每次移动一步,利用二者速度差达成特定目标。

  • 适用场景
    • 链表环检测:精准判断链表有无环,有环时能定位环入口,如在复杂数据结构中排查循环引用隐患。
    • 找中间节点:像归并排序前期需快速平分链表,快慢指针遍历一次即可定位中点,为后续有序处理奠基。
    • 判断奇偶节点:依快慢指针最终位置,轻松判别节点奇偶性,辅助特殊逻辑,如奇偶位交替变换。
  • 时间复杂度优化原理:利用速度差,无环时快指针率先触尾结束遍历;有环时快指针必在环内 “追上” 慢指针,单次 O (n) 遍历就解决问题,找中点同理,快到尾时慢恰在中点。
  • 选择策略:但凡涉及链表节点位置判断,像找特定节点、查环,尤其单次遍历需多元位置信息,优先启用。
  • 局限性:单向链表中,快慢指针难以直接回

2.2 练习

(1)环形链表Ⅱ

环形链表 IIhttps://leetcode.cn/problems/c32eOV/

给定一个链表,返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。如果链表无环,则返回 null

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head) {

    struct ListNode* fast = head;
    struct ListNode* slow = head;
  while(fast != NULL && fast->next != NULL) {
    slow = slow->next;
    fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直⾄相遇
if (slow == fast) {

    struct ListNode* index1 = fast;
    struct ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
 }
return index2; // 返回环的⼊⼝
 }
 }
return NULL;
}

  (2)相交链表

 相交链表https://leetcode.cn/problems/intersection-of-two-linked-lists/

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

图示两个链表在节点 c1 开始相交

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {

    struct ListNode * pA = headA;
    struct ListNode * pB = headB;

    int lengthA = 0;
    int lengthB = 0;

    while(headA != NULL){
        lengthA ++;
        headA = headA->next;
    }

      while(headB != NULL){
        lengthB ++;
        headB = headB->next;
    }

    if(lengthA < lengthB){
      for(int i = 0;i < abs(lengthA - lengthB);i++){
            pB = pB->next;
        }
    }else{
      for(int i = 0;i < abs(lengthA - lengthB);i++){
            pA = pA->next;
        }
    }

    while(pA != NULL && pB != NULL){

        if(pA == pB){
            return pA;
        }

        pA = pA->next;
        pB = pB->next;
    }

    return NULL;
    
}

(3)删除链表的倒数第N个结点 

删除链表的倒数第 N 个结点https://leetcode.cn/problems/SLwz0R/

给定一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* removeNthFromEnd(struct ListNode* head, int n){

 // 创建虚拟头节点,方便处理删除头节点的情况
    struct ListNode* dummy = (struct ListNode*)malloc(sizeof(struct ListNode));
    dummy->val = 0;
    dummy->next = head;
    struct ListNode* first = dummy;
    struct ListNode* second = dummy;
    // 让 first 指针先移动 n + 1 步
    for (int i = 0; i <= n; i++) {
        first = first->next;
    }
    // 同时移动 first 和 second 指针,直到 first 指针到达链表末尾
    while (first != NULL) {
        first = first->next;
        second = second->next;
    }
    // 此时 second 指针指向要删除节点的前一个节点
    struct ListNode* temp = second->next;
    second->next = second->next->next;
    free(temp);
    // 获取新的头节点
    struct ListNode* newHead = dummy->next;
    free(dummy);
    return newHead;
}

3 虚拟头节点

    3.1 介绍    

虚拟头节点是链表操作的辅助 “锚点”,不存实质数据,占位简化逻辑。

  • 适用场景
    • 头部插入删除:常规操作头节点需额外边界判断,虚拟头使插入、删除逻辑统一,降低出错概率。
    • 链表初始化:为空链表开篇或构建新结构 “打头阵”,后续添加节点顺理成章。
    • 多链表操作:合并多链表时,提供统一起始,梳理合并流程,代码结构更清晰。
  • 时间复杂度优化原理:统一头部操作,规避重复判断,多次操作下稳定耗时,提升整体效率。
  • 选择策略:频繁头部操作或统一管理多链表,又或需明确起始且简化头节点处理时,它是首选。
  • 局限性:占用额外空间存储虚拟节点;少量头部操作时,引入虚拟头会使代码繁杂,得不偿失。

   3.2 练习

(1)移除链表元素

移除链表元素https://leetcode.cn/problems/remove-linked-list-elements/

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val) {
    
    struct ListNode dummy;
    dummy.next = head;
    struct ListNode* current = &dummy;
    while (current->next) {
        if (current->next->val == val) {
            struct ListNode* temp = current->next;
            current->next = current->next->next;
            free(temp);
        } else {
            current = current->next;
        }
    }
    return dummy.next;
}
struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* phead = NULL;
    struct ListNode* ptail = NULL;
    struct ListNode* pcur = head;
    while(pcur)
    {
        if(pcur->val != val)
        {
            if(phead == NULL)
            {
                phead = ptail = pcur;
            }
            else
            {
                ptail->next = pcur;
                ptail = pcur;
            }
        }
        pcur = pcur->next;
    }
    if(ptail)
        ptail->next = NULL;
    return phead;
}

4 递归 

 4.1 介绍

 递归是基于自身定义问题求解的策略,将链表大问题拆解为同构子问题处理。

  • 适用场景
    • 链表反转:从尾到头逐节点反转,递归贴合天然顺序,代码简洁直观。
    • 合并有序链表:对比、拼接子链表,递归处理层次分明,轻松融合多链表。
    • 链表遍历操作:深度优先遍历修改节点值等,递归按链表结构递进,无需复杂循环。
  • 时间复杂度优化原理:分解难题,各子问题独立递归求解,省却多层嵌套循环,顺链表结构操作,削减冗余步骤。
  • 选择策略:操作有递归特性,子问题相似,如反转、合并场景;追求代码精简、逻辑通透且时间要求适度时优先考虑。
  • 局限性:长链表易引发栈溢出,因递归需栈存调用状态;空间占用多,函数调用开销也会拖慢性能。

4.2 练习 

(1)反转链表

反转链表https://leetcode.cn/problems/UHnkqh/

给定单链表的头节点 head ,请反转链表,并返回反转后的链表的头节点。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* reverseList(struct ListNode* head){

 struct ListNode *prev = NULL;
    struct ListNode *current = head;
    struct ListNode *nextNode;
    while (current != NULL) {
        // 保存当前节点的下一个节点
        nextNode = current->next;
        // 将当前节点的 next 指针指向前一个节点
        current->next = prev;
        // 更新前一个节点为当前节点
        prev = current;
        // 更新当前节点为之前保存的下一个节点
        current = nextNode;
    }
    // 最后 prev 指向反转后链表的头节点
    return prev;
}

(2)合并两个有序链表

合并两个有序链表https://leetcode.cn/problems/merge-two-sorted-lists/

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    
     // 创建虚拟头节点
    struct ListNode dummy = {0, NULL};
    struct ListNode* tail = &dummy;
    // 遍历两个链表,比较节点值并合并
    while (list1 && list2) {
        if (list1->val < list2->val) {
            tail->next = list1;
            list1 = list1->next;
        } else {
            tail->next = list2;
            list2 = list2->next;
        }
        tail = tail->next;
    }
    // 将剩余的节点连接到新链表尾部
    if (list1) {
        tail->next = list1;
    }
    if (list2) {
        tail->next = list2;
    }
    return dummy.next;
}

学习时间 2025.02.11 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值