考研408数据结构----链表题型解析(C语言描述)

一. 总览

        链表是算法设计题的重点考查章节,由于链表的算法题的代码量相对较少,又具有一定的算法设计技巧,因此适合笔试考查。

二. 链表初始化

以下内容为 双向链表的初始化,后续的题目中的代码实现都是基于此处的。若后续题目要求为 循环单链表、循环双向链表等等,读者自行增删以下的初始化代码即可

#include <iostream>
using namespace std;

typedef struct node
{
    int data;
    struct node* next;

    struct node* pre;
}Node, *LinkList;

LinkList initList()
{
    LinkList l = (Node*)malloc(sizeof(Node));
    if (l == NULL)
    {
        printf("分配内存失败");
        return NULL;
    }
    else
    {
        l->next = NULL;
    }
    return l;
}

三. 题型拆分剖析

1. 删除结点-----------基础操作

void del(LinkList l, int a, int b)
{
    Node* pr = l;
    Node* p = l->next;
    while (p != NULL)
    {
        if (p->data > a && p->data < b)
        {
            Node* temp = p;
            p = p->next;
            pr->next = p;
            free(temp); // 释放内存
            temp = NULL;
        }
        else {
            pr = p;
            p = p->next;
        }
    }
}

指针temp : 只是一个变量,存储的是一个内存地址,占用的是 栈内存
temp 指向的内存 : 是malloc申请的 堆内存,这部分内存需要手动释放
                            (比如 malloc(sizeof(Node)) 或 new Node ) 

free(temp)的作用 : 释放 temp 指向的 堆内存,而不是释放指针变量本身

2. 两个链表交集--- 归并思想

归并思想:通过双指针同步遍历多个有效序列,在遍历过程中比较元素大小and合并结果,从而将时间复杂度优化到 O(m+n)(m和n分别为两个序列的长度)


算法设计:

1. 采用归并思想,设置两个工作指针,对两个链表进行归并扫描,保留交集,其余结点释放

2. 当一个链表遍历完毕后,释放另一个表中的其余结点。

void Get_Intersection(LinkList A, LinkList B)
{
    Node* pa = A->next;
    Node* pb = B->next;
    Node* temp = NULL;
    Node* pc = A;    // 用于构建结果链表
    while (pa != NULL && pb != NULL)
    {
        if (pa->data == pb->data)
        {
            pc->next = pa;  // 删除 非交集结点
            pc = pa;        // 移动pc 指向交集结点  
            pa = pa->next;
            temp = pb;      // 暂存pb  用来释放b中的交集结点
            pb = pb->next;
            free(temp);
        }
        else  if (pa->data < pb->data)
        {
            temp = pa;
            pa = pa->next;
            free(temp);
        }
        else
        {
            temp = pb;
            pb = pb->next;
            free(temp);
        }
    }    
// 循环结束,必然满足pa == NULL或 pb == NULL(即至少一个链表已遍历完)
    
    while (pa != NULL)
    {    // 假设B先遍历完,此时A中还有剩余节点,释放A中剩余的结点
        temp = pa;
        pa = pa->next;
        free(temp);
    }
    while (pb != NULL)
    {    // 假设A先遍历完,此时B中还有剩余节点,释放B中剩余的结点
        temp = pb;
        pb = pb->next;
        free(temp);
    }

    pc->next = NULL;  // 主循环while执行后,pc移动到了末尾
    free(B);          // 释放B的头节点
}

3. 连续子序列-------字符匹配

bool isContinuousSunsequence(LinkList  A, LinkList B)
{
    Node* pa = A->next;
    Node* pb = B->next;
    Node* start = NULL;
    while (pa != NULL)
    {
        if (pa->data == pb->data)
        {
            if (start == NULL)
            {
                start = pa;    // 记录起始匹配位置
            }
            pb = pb->next;
            if (pb == NULL)  // B 已遍历完毕
            {
                return true;   
            }
        }
        else
        { 
            pb = B->next;    // 重置 pb 位置
            if (start != NULL)
            {
                //代码执行前,start指向 匹配成功的结点
                pa = start->next; 
                start = NULL;
                continue;  // 跳转到 while(pa != NULL),不会移动pa
            }
        }
        pa = pa->next;
    }
    return false;
}

4. 访问频度域-------多循环条件嵌套

注:将题目中的 pred 改为 pre

Node* Locate(LinkList L, int x)
{
    Node* p = L->next;
    Node* target = NULL;
    // 查找数据为 x 的结点
    while (p != NULL)
    {
        if (p->data == x)
        {
            target = p;
            target->freq++;
            break;
        }
        p = p->next;
    }
    // 未找到目标结点,返回NULL
    if (target == NULL)
    {
        return NULL;
    }
    // 向前调整节点位置,保持 freq 递减 && 最近访问在前(频度相同)
    // 从当前节点的前驱 开始比较
    // 循环条件解析:前指针不等于头指针 && 保持 freq 递减 || 若频度相同,最近访问在前   
    p = target->pre;
    while (p != L && (target->freq > p->freq || (p->freq == target->freq && p->next != target)))
    {
        target->pre = p->pre;
        p->pre->next = target;
        p->pre = target;
        p->next = target->next;
        if (target->next->pre != NULL)
        {
            target->next->pre = p;
        }
        target->next = p;
        // 继续向前
        p = target->pre;
    }
    // 返回找到的结点地址
    return target;
}

5. 多次头插法-------成环与断环

成环与断环:将单链表首尾相连,以便确认尾节点

void move_node(LinkList l,int k)
{
    // 计算n的个数
    int n = 1;  // while 循环内代码执行顺序特殊,n从1开始 
    Node* p = l->next;
    while (p->next != NULL)
    {
        p = p->next;
        n++;
    }
    // 成环
    p->next = l;
    // 寻找新首元节点
    for (int i = 1; i <= n - k; i++)
    {
        p = p->next;
    }
    // 断环
    l = p->next;        // l 指向新首元结点
    p->next = NULL;     // 断环
}

6. 链表有无环-------快慢双指针

环检测算法(Floyd判圈法):需要 慢指针每次走一步,快指针每次走两步


数学推导

设 头节点到环入口的距离为 a ,环入口到 快慢指针 第一次相遇点的距离为 b,相遇点到环入口的距离为 c

因此 环的总长度为 b+c

当两者第一次相遇时:慢指针走过的距离为: a+b ;快指针:2*(a+b)

深入考虑:快指针在达到相遇点前,已经绕环至少一圈,因此 总距离可以表示为

a+b+k*(b+c) (k是快指针绕环的圈数)(k >= 1)

2*(a+b) = a+b+k*(b+c)

a+b = k*(b+c)

a = k*(b+c) - b

a = (k-1)*(b+c) + c :快指针从相遇点出发,走c步,必然到达换环入口


a = (k-1)*(b+c) + c

即 慢指针从起点 走a步 = 快指针从相遇点出发,走c步(此时,即第一次相遇后,快慢指针每次都走一步)


整体分析

第一次相遇前:快指针每次走两步,慢指针每次走一步

第一次相遇后:慢指针被重置到头节点,快指针保留在相遇点,此后均以每次1步的速度移动,直至第二次相遇,此时的相遇点,即为环入口

Node* detect_Cycle(LinkList l)
{
    Node* slow = l->next;
    Node* fast = slow->next;
    while (fast != NULL && fast->next != NULL)  
    {
        if (slow == fast)
        {
            slow = l->next;  // 重置慢指针
            while (slow != fast)
            {
                slow = slow->next;
                fast = fast->next; // 相遇后,每次走一步
            }
            return slow;
        }
        slow = slow->next;
        fast = fast->next->next;
    }
    return NULL;
}

7. 比较最大和-------反转指针指向

算法设计思想:

  1. 利用双指针法,先通过遍历链表找到中间位置,将链表后半部分反转
  2. 同时遍历 原链表前半部分 和 反转后的后半部分,计算和,比较最大和
  3. 最后可选择恢复链表结构(若有需求)
    int maxTwinSum(LinkList l, int n)
    {       
        //  1.找中间节点
        Node* slow = l->next;
        Node* fast = l->next;
        while (fast != NULL && fast->next != NULL)
        {
            slow = slow->next;
            fast = fast->next->next;
        }   
        // 此时 slow 指向 后半部分第一个结点,fast指向NULL
        
        //  2.反转后半链表(反转的是指针,并非旋转后半链表)
        Node* prev = NULL;
        Node* curr = slow;
        Node* nextNode = NULL;
        while (curr != NULL)
        {
            nextNode = curr->next;  // 保存下一个结点
            curr->next = prev;      // 反转当前结点指针
            prev = curr;            // 移动 prev 到当前结点
            curr = nextNode;        // curr 指向下一个结点
        }  
        // 此时curr指向为NULL,prev指向最后一个结点
    
        // 3.计算比较最大和
        Node* p1 = l->next;       // 前半部分指针
        Node* p2 = prev;          // 后半部分指针
        int maxSum = 0;
        int currentSum = 0;
        while (p2 != NULL)
        {
            currentSum = p1->data + p2->data;
            if (currentSum > maxSum)
            {
                maxSum = currentSum;
            }
            p1 = p1->next;
            p2 = p2->next;
        }
        return maxSum;
    }

    8. 删除结点-----------空间换时间

    题目分析:

    1. 时间复杂度尽可能高效:建立辅助数组
    2. |data| <= n : data的取值范围为 [0,n] ,共 n+1 种可能 ,因此数组的大小可设置为 n+1
    3. 数组的下标 对应 |data| ,数组下标对应的值 为 0 表示未出现,1 表示已出现。此操作实现 以 O(1)时间 判断节点是否需要删除,避免重复遍历
    typedef struct node
    {
        int data;
        struct node* link;
    }Node, *LinkList;
    
    void del_node(LinkList head, int n)
    {
        int* p = (int*)malloc(sizeof(int) * (n + 1));
        for (int i = 0; i < n + 1; i++)
        {
            p[i] = 0;
        }
        Node* prev = head;
        Node* curr = head->link;
        while (curr != NULL)
        {
            if (p[abs(curr->data)] == 1)
            {
                prev->link = curr->link;
                free(curr);
                curr = prev->link;
            }
            else
            {
                p[abs(curr->data)] = 1;
                prev = curr;
                curr = curr->link;
            }
        }
        free(p);
        p = NULL;
    }
    // 时间复杂度:O(m)
    // 空间复杂度:  O(n)

    9. 两种思想混合-----双指针与反转

    算法设计:

    1. 通过快慢指针法 找到 中间节点
    2. 将后半段原地逆置
    3. 交叉连接结点
    void rearrange(LinkList l)
    {
        // 寻找链表中间结点
        Node* slow = l;
        Node* fast = l;
        while (fast->next != NULL)
        {
            slow = slow->next;
            fast = fast->next;
            if (fast->next != NULL)
            {
                fast = fast->next;
            }
        }
    
    
    
        // second 为后半段链表 首元结点
        Node* second = slow->next;
        // 分割链表
        slow->next = NULL;
        // 后半段指针逆置
        Node* temp = NULL;
        Node* prev = NULL;
        Node* curr = second;
        while (curr != NULL)
        {
            temp = curr->next;
            curr->next = prev;
            prev = curr;
            curr = temp;
        }
        // curr 为NULL,prev 指向反转后的首元节点
    
    
    
        // 合并两个链表
        Node* p = l->next;
        Node* q = prev;
        Node* next_p;
        Node* next_q;
        while (p != NULL && q != NULL)
        {
            next_p = p->next;
            next_q = q->next;
            p->next = q;
            q->next = next_p;
            p = next_p;
            q = next_q;
        }
    }
    // 时间复杂度 O(n)

    四. 总结

    常用的算法设计技巧,仅供参考:头插法、尾插法、反转、归并、快慢指针

    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
    红包 添加红包
    表情包 插入表情
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值