Week1Day4B:链表的应用【2023 安全创客实践训练|笔记】

内容为武汉大学国家网络安全学院2022级大一第三学期“996”实训课程中所做的笔记,仅供个人复习使用,如有侵权请联系本人,将与15个工作日内将博客设置为仅粉丝可见。
 


链表的演示


双向链表的演示


链表的实现

实现链表,一般有两种方式:用指针实现的链表、用数组实现的链表。

指针实现版:

类型定义及结点构造
struct node {
    struct node *next;
    int d;
};
struct node *newnode(int d) {
    struct node *p = (struct node *)malloc(sizeof(struct node));
    p->d = d;
    p->next = NULL;
    return p;
}

struct node *head = newnode(1);
插入元素
// p is the location before insertion
struct node *x = newnode(data);
x->next = p->next;
p->next = x;
删除元素
// p is the location before deletion
p->next = p->next->next;
删除表头
head = head->next;
想一想

上面的操作是否有内存泄漏?如何避免?

数组实现版:

struct node {
    int d, next;
} pool[1000];
int tot = 0;
int newnode(int d) {
    pool[tot].d = d;
    pool[tot].next = -1;
    return tot++;
}
int head = newnode(1);
插入元素
// p is the location before insertion
int x = newnode(data);
pool[x].next = pool[p].next;
pool[p].next = x;
删除元素
// p is the location before deletion
pool[p].next = pool[pool[p].next].next;
删除表头
head = pool[head].next;

常见链表问题

基于链表的定义和插入、删除操作思想,有很多常见的链表问题,接下来会为你介绍其中的几个最经典的例子,及其解法和示例代码。

统计链表中的结点个数

对于给定的链表,统计其中一共有多少个结点。

思路:仿照前面插入和删除操作中的 while 循环,从表头head开始遍历,直到NULL结束。时间复杂度为 O(n)。

示例代码如下:

int get_length(ListNode *head) {
    int count = 0;
    while (head != NULL) {
        count++;
        head = head->next;
    }
    return count;
}
找出倒数第 K 个结点

对于给定的链表,找出倒数第 k 个结点的元素。

思路 1:先统计出链表中的结点个数 n,之后从前往后找到第 n−k+1 个结点。时间复杂度为 O(n)。

示例代码如下:

ListNode* get_kth(ListNode *head, int k) {
    int n = get_length(head);
    if (k <= 0 || k > n) {
        return NULL;
    }
    int count = 0;
    for (int i = 0; i < n - k; ++i) {
        head = head->next;
    }
    return head;
}

思路 2:先让一个指针nextk指向链表第 k 个元素,然后让headnextk同时向后移动,直到nextk为空。时间复杂度为 O(n)。

示例代码如下:

ListNode* get_kth(ListNode *head, int k) {
    if (k <= 0 || k > n) {
        return NULL;
    }
    ListNode *nextk = head;
    for (int i = 0; i < k - 1; ++i) {
        nextk = nextk->next;
    }
    while (nextk != NULL) {
        head = head->next;
        nextk = nextk->next;
    }
    return head;
}

链表翻转

将给定的链表进行翻转。例如,一个链表中的元素分别为 1,2,3,4,5,则翻转后的结果为 5,4,3,2,1。

思路 1:新建一个链表,每次把原链表中的第一个元素放到新链表的头部,并把这个元素从原链表中删掉。空间复杂度为 O(n),时间复杂度为 O(n)。

示例代码如下:

ListNode* reverse(ListNode *head, int k) {
    ListNode *new_list = NULL;
    while (head != NULL) {
        ListNode *node = (ListNode*)malloc(ListNode);
        node->val = head->val;
        new_list = insert(new_list, 0, node);
        head = head->next;
    }
    return new_list;
}

想一想,能不能把空间复杂度优化到 O(1) 呢?先思考一下再翻到下一页吧。

思路 2:我们可以不借助额外 O(n) 的空间就可以将这个链表翻转。我们以链表 a,b,c,d,e 举例,讲一下算法的过程:

a -> b -> c -> d -> e

第一步,让 a 的next指向 c,再让 b 的next指向表头 a,把表头修改为 b:

b -> a -> c -> d -> e

接下来,让 a 的next指向 d,再让 c 的next指向表头 b,把表头修改为 c:

c -> b -> a -> d -> e

第三步,让 a 的next指向 e,再让 d 的next指向表头 c,把表头修改为 d:

d -> c -> b -> a -> e

最后,让 a 的nextNULL,再让 e 的next指向表头 d,把表头修改为 e:

e -> d -> c -> b -> a

算法的时间复杂度为 O(n),空间复杂度为 O(1)。


链表环的处理

当然,链表也不一定都是呈现“一条链”的状态的。对于如下的链表:

如果你使用如下代码遍历链表的话:

while (head != NULL) {
    printf("%d\n", head->val);
    head = head->next;
}

 

这个循环会一直循环下去,也就是说,这个链表和我们前面学到的不太一样,具有一个特性:有环。

对于有环的链表,有一些常见的问题,接下来会逐个为大家介绍思路。

 

判断链表是否有环

给定一个链表,判断其中是否有环。

思路 1:顺序遍历链表中的每个元素,判断链表中后面是否会再次出现该元素。时间复杂度为 O(n^2)。

示例代码如下:

int has_ring(ListNode *head) {
    while (head != NULL) {
        ListNode *now = head->next;
        while (now != NULL && now != head) {
            now = now->next;
        }
        if (now == head) {
            return 1;
        }
        head = head->next;
    }
    return 0;
}

 

思路 2:可以设定两个指针,一个“速度”为 1,一个“速度”为 2。速度指的是指针每次向后移动的次数。一旦之后出现两个指针指向同一个位置的情况,则说明链表中包含环;而如果其中某个指针指向了NULL,则说明链表无环。

对于上面这个链表,初始ptr1ptr2都指向 4。

第一步,ptr1指向 5,ptr2指向 8;

第二步,ptr1指向 8,ptr2指向 5;

第三步,ptr1ptr2都指向 1,说明链表有环,结束算法。

留一个思考题:为什么选择 1 和 2 呢?这个组合是不是最优的呢?

 

环和柄的长度

对于包含环的链表来说,有两个长度常常会需要计算:环的长度和柄的长度。环的长度是指环上的结点个数,而柄的长度指的是从链表的第一个结点开始,直到第一个进入环的结点为止的结点个数。

例如上面这个链表,环的长度为 3,柄的长度为 2。

环的长度

仿照判断链表是否有环的思路 2,我们在ptr1ptr2相遇后让两个指针继续按原速度移动,直到再次相遇,两次相遇之间的移动次数就是环的长度。

第一次相遇时,ptr1ptr2都指向 1。第一步,ptr1指向 5,ptr2指向 8。

第二步,ptr1指向 8,ptr2指向 5。

第三步,ptr1ptr2都指向 1。一共经历了三步,所以环的长度是 3。

为什么是正确的呢?这是个很经典的“追及问题”:首先,速度为 1 和速度为 2 的指针同向移动,如果速度为 2 的指针从后面超过速度为 1 的指针,一定会在某个元素相遇,而不会“错过”。这样的话,两次相遇之间,ptr2ptr1移动的位置差就是环的长度,也因此,ptr1的移动次数就是环的长度。


双链表问题

在链表相关的算法问题中,同时在两个链表上进行操作是一类经常出现的题目。总的来说,一共有两类:一类是将在两个数组上的算法迁移到链表中的题目;一类是两个链表成环、交叉等基于链式结构的题目。

对于第一类题目,考察的往往是二分、分治等算法,结合单链表基本的插入、删除、遍历操作,对链表考察的部分难度不大,这里也就不再进行额外的讲解了,大家如果做这类题目遇到困难,应该先去完成“数组版”的题目,再将它改写成“链表版”的。

第二类题目最典型的例子就是一系列关于两个链表交叉的题目,接下来,会为你介绍其中几个较为经典的题目及其思路和解法。

判断无环链表是否交叉

两个链表交叉,意味着两个链表有公共点,这说明着什么呢?

我们看到,上图中的两个链表在 2 处相遇后,就完全重合了,这是由于链表的链式结构的性质决定的。因此,我们可以从两个链表的表头分别遍历到表尾,比较一下两个表尾是否相等,如果相等则说明两个链表交叉。

给出算法的 C++ 示例代码如下:

int intersect(ListNode *head1, ListNode *head2) {
    while (head1 != NULL) {
        head1 = head1->next;
    }
    while (head2 != NULL) {
        head2 = head2->next;
    }
    return head1 == head2;
}

如果已知两个链表交叉,要找出其中的交叉点,可以先算出两个链表head1head2的长度,不妨设为 l1,l2。如果 d=l1−l2≥0,则让head1先向后移动 d 次,然后让两个指针同时向后移动,直到两个指针相等;如果 d=l2−l1>0,则让head2先向后移动 d 次,然后让两个指针同时向后移动,直到两个指针相等。两个指针第一次相等时,就是两个链表的交叉点。


打印锯齿矩阵

  • 时间限制:1000ms
  • 空间限制:131072K
  • 语言限制:C语言

锯齿矩阵是指每一行包含的元素个数不相同的矩阵,比如:

3 5 2 6 1
2 3 4
1 6 2 7

初始时矩阵为空,读入 m 对整数 (x,y),表示在第 x 行的末尾加上一个元素 y。

输出最终的锯齿矩阵。

输入格式

第一行输入两个整数 n,m (1≤n,m≤10000),其中 n 表示锯齿数组的行数,m 表示插入的元素总数。

接下来一共 m 行,每行两个整数 x,y (1≤x≤n,0≤y≤10000),表示在第 x 行的末尾插入一个元素 y。

输出格式

一共输出 n 行,每行若干个用空格分隔的整数。如果某行没有任何元素,则输出一个空行

格式说明

输出时每行末尾的多余空格,不影响答案正确性

样例输入
3 12
1 3
2 2
2 3
2 4
3 1
3 6
1 5
1 2
1 6
3 2
3 7
1 1
样例输出
3 5 2 6 1
2 3 4
1 6 2 7

任务提示:请用链表完成本题,每行可以看成是一个链表,每次在尾部添加一个元素。

标程与题解

每行可以看成是一个链表,每次在尾部添加一个元素。

#include <stdio.h>

int main() {
    int data[10000];
    int next[10000];
    int head[10000];
    int n, m, x, y, tot = 0;
    scanf("%d%d", &n, &m);
    memset(next, -1, sizeof(next));
    memset(head, -1, sizeof(head));
    for (int i = 0; i < m; i++) {
        scanf("%d%d", &x, &y);
        x--;
        if (head[x] == -1) {
            head[x] = tot;
            data[tot] = y;
            tot++;
        } else {
            int now = head[x];
            while (next[now] != -1) {
                now = next[now];
            }
            next[now] = tot;
            data[tot] = y;
            tot++;
        }
    }
    for (int i = 0; i < n; i++) {
        int now = head[i];
        while (now != -1) {
            printf("%d ", data[now]);     
            now = next[now];
        }
        printf("\n");
    }
    return 0;
}

我的答案

#include <stdio.h>
#include <stdlib.h>

#define MAXN 10010

typedef struct Node {
    int data;
    struct Node *next;
} Node;

Node *create_node(int data) {
    Node *node = (Node *)malloc(sizeof(Node));
    node->data = data;
    node->next = NULL;
    return node;
}

void insert(Node *head, int data) {
    Node *p = head;
    while (p->next != NULL) {
        p = p->next;
    }
    p->next = create_node(data);
}

void print_list(Node *head) {
    Node *p = head->next;
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);

    Node *heads[MAXN];
    for (int i = 1; i <= n; i++) {
        heads[i] = create_node(-1);
    }

    for (int i = 0; i < m; i++) {
        int x, y;
        scanf("%d%d", &x, &y);
        insert(heads[x], y);
    }

    for (int i = 1; i <= n; i++) {
        print_list(heads[i]);
    }

    return 0;
}

求链表环的长度

  • 时间限制:1000ms
  • 内存限制:32867K
  • 语言限制:C语言

求出单链表中环的长度。我们已经为你定义好了链表结点。具体的可以参考注释部分。

/**
 * 已经定义的 ListNode 结点
 * typedef struct Node {
 *     int val;
 *     struct Node *next;
 * } ListNode;
 */
int linkedListCycleLength(ListNode *head) {
    
}

传入的原始的链表的头指针。如果有环,返回环中结点的个数,否则返回 0。链表长度不大于 2000。

格式说明

输出时每行末尾的多余空格,不影响答案正确性

样例输入
6
8 13 11 20 2 8
2
样例输出
5

我的答案

int linkedListCycleLength(ListNode *head) {
    if (head == NULL || head->next == NULL) return 0;
    
    ListNode *slow = head;
    ListNode *fast = head;
    
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            int count = 1;
            slow = slow->next;
            while (slow != fast) {
                count++;
                slow = slow->next;
            }
            return count;
        }
    }
    return 0;
}

找出两个链表的交点

  • 时间限制:1000ms
  • 内存限制:32867K
  • 语言限制:C语言

给出两个单链表,两个链表可能会相交。如下图,两个单链表在结点 2 相交。保证两个链表都不存在环。

我们已经为你定义好了链表结点。具体的可以参考注释部分。

/**
 * 已经定义的 ListNode 结点
 * typedef struct Node {
 *     int val;
 *     struct Node *next;
 * } ListNode;
 */
ListNode* findIntersectionListNode(ListNode *head1, ListNode *head2) {
    
}

传入两个原始链表的头指针,返回交点的指针,如果两个链表没有交点那么返回NULL。链表总长度不大于 20000。

样例输入
5 3 2
样例输出
Accept

我的答案

ListNode* findIntersectionListNode(ListNode *headA, ListNode *headB) {
    ListNode *pA = headA, *pB = headB;
    while (pA != pB) {
        pA = pA ? pA->next : headB;
        pB = pB ? pB->next : headA;
    }
    return pA;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值