数据结构:链表的一些经典的OJ题目,环形链表问题

简介: 数据结构:链表的一些经典的OJ题目,环形链表问题

写在前面

本篇为本人学习链表的过程中遇到的典型OJ题,于是整理出来分享思路和便于后续重新学习,每个标题均可跳转至对应习题,大多为Leetcode


链表OJ调试技巧

Leetcode中只能看到函数体,不能看到链表的具体情况,因此调试存在困难,自己搭建链表又过于繁琐,这里介绍一种很方便的链表调试技巧

原理如下

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
struct ListNode
{
  int val;
  struct ListNode* next;
};
int main()
{
  struct ListNode* n1 = (struct ListNode*)malloc(sizeof(struct ListNode));
  assert(n1);
  struct ListNode* n2 = (struct ListNode*)malloc(sizeof(struct ListNode));
  assert(n2);
  struct ListNode* n3 = (struct ListNode*)malloc(sizeof(struct ListNode));
  assert(n3);
  struct ListNode* n4 = (struct ListNode*)malloc(sizeof(struct ListNode));
  assert(n4);
  struct ListNode* n5 = (struct ListNode*)malloc(sizeof(struct ListNode));
  assert(n5);
  n1->val = 1;
  n2->val = 2;
  n3->val = 3;
  n4->val = 4;
  n5->val = 5;
  n1->next = n2;
  n2->next = n3;
  n3->next = n4;
  n4->next = n5;
  n5->next = NULL;
}

我们只需要创建出每一个节点,接着把这些节点都连在一起,手动创建一个链表,链表中需要多少个元素自己搭建,这样可以便于调试,解决OJ中遇到的问题~~

介绍完调试的方法,正式总结链表中的OJ题目


移除链表元素

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

1.遍历,是val的删掉

一种暴力解决的方法,图示如下

代码实现如下

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* prev=NULL;
    struct ListNode* cur=head;
    while(cur)
    {
        if(cur->val==val)
        {
            if(prev)
            {
                prev->next=cur->next;
                free(cur);
                cur=prev->next;
            }
            else
            {
                cur=head->next;
                free(head);
                head=cur;
            }
        }
        else
        {
            prev=cur;
            cur=cur->next;
        }
    }
    return head;
}

2.遍历链表,把不是val的拿下来尾插

图解过程如下:

代码如下所示

struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* cur=head;
    struct ListNode* newnode = NULL;
    struct ListNode* tail = NULL;
    while(cur)
    {
        if(cur->val!=val)
        {
            if(tail==NULL)
            {
                newnode=tail=cur;
            }
            else
            {
                tail->next=cur;
                tail=tail->next;
            }
            cur=cur->next;
        }
        else
        {
            struct ListNode* del=cur;
            cur=cur->next;
            free(del);
        }
    }
    if(tail)
        tail->next=NULL;
    return newnode;
}

反转链表

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

输入:head = [1,2,3,4,5]

输出:[5,4,3,2,1]

下面介绍两种思路,均能解决问题

1.改变节点指向

定义三个节点(思考为什么定义三个而不是两个?)而后通过指针遍历把cur指向的节点方向指向前一个,然后通过迭代到末尾,当cur为空退出循环

代码实现如下

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
        return NULL;
    struct ListNode* prev=NULL;
    struct ListNode* cur=head;
    struct ListNode* next=head->next;
    while(cur)
    {
        cur->next=prev;
        prev=cur;
        cur=next;
        if(next)
            next=next->next;
    }
    return prev;
}

2.将每一个节点都头插

是一个新方法,只需要把新创建一个头,然后把每一个节点都头插进来即可,下面为原理图

代码实现如下

struct ListNode* reverseList(struct ListNode* head)
{
  struct ListNode* cur = head;
  struct ListNode* rhead = NULL;
  while (cur)
  {
    struct ListNode* next = cur->next;
    //头插
    cur->next = rhead;
    rhead = cur;
    //迭代
    cur = next;
  }
  return rhead;
}

关于代码简洁性:

代码简洁程度取决于特殊情况的处理,例如,假设这里把next定义在while循环外,那么假设参数为NULL,就需要对特殊情况做处理,此时代码就多了if条件,整体来说简洁程度就降低了

简洁程度需要对特殊情况的处理足够好,才能足够精炼解决问题


链表的中间节点

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

1.遍历链表求长度

代码实现如下

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* plist=head;
    int flag=0;
    while(plist)
    {
        while(plist->next!=NULL)
        {
            plist=plist->next;
            flag++;
        }
        break;
    }
    flag+=1;
    flag=flag/2+1;
    for(int i=1;i<flag;i++)
    {
        head=head->next;
    }
    return head;
}

2.快慢指针法

定义两个指针,一个指针是慢指针,每次走一步;一个指针是快指针,每次走两步。当快指针走到末尾时,慢指针走的位置恰好为整个链表的一半

相比起思路1来说,时间复杂度只是O(N),从时间复杂度来讲,这个方法更优

具体实现原理如下图所示

代码实现如下

struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* slow=head;
    struct ListNode* fast=head;
    while(fast!=NULL && fast->next!=NULL)
    {
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}

链表中倒数第K个节点

描述

输入一个链表,输出该链表中倒数第k个结点。

输入:1,{1,2,3,4,5}

输出:{5}

1.遍历求长度,根据k求结果

代码思路和上题十分类似,十分简单

/**
 * struct ListNode {
 *  int val;
 *  struct ListNode *next;
 * };
 */
/**
 * 
 * @param pListHead ListNode类 
 * @param k int整型 
 * @return ListNode类
 */
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    // write code here
    struct ListNode* cur=pListHead;
    struct ListNode* prev=pListHead;
    int flag=0;
    while(cur)
    {
        cur=cur->next;
        flag++;
    }
    if(k>flag)
    {
        return NULL;
    }
    for(int i=0;i<flag-k;i++)
    {
        prev=prev->next;
    }
    return prev;
}

2.仿照上题,快慢指针的方法解决

和上题相比,略有不同的一点是,上面的快指针每次走的是一样的,而对于这个题来说,假设K=4,那么就令快指针每次走四格,其余思路和上题类似

总结原理:这里既然要求的是倒数第k个,那么就令fast指针和slow指针之间差k个,然后两个一起走,当fast指针到末尾的时候,slow所在的位置就是fast位置减去它们一开始的距离差,也就是倒数k个的位置

整体思路是十分巧妙的,其实原理和方法1类似,都是找到最后一个元素,然后再找前面倒数的元素,但相比起来使用快慢指针的方法可以让时间复杂度缩短到最少

代码实现如下

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    struct ListNode* fast=pListHead;
    struct ListNode* slow=pListHead;
    for(int i=0;i<k;i++)
    {
        if(fast)
            fast=fast->next;
        else
         return NULL;
    }
    while(fast)
    {
        fast=fast->next;
        slow=slow->next;
    }
    return slow;
}

这里思考

第一次走k步和走k-1步有什么区别?


链表分割问题

题目描述

现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针

由于题目描述中提到,不能改变原来的数据顺序,那么也就意味着只能尾插,因为尾插不会破坏数据顺序

于是,我们要做的实际上就是把小于val的单独拿出来,大于等于val的单独拿出来,创建两个节点让拿出来的这两份数据分别进行尾插,最后再实现链表的合并,即可解决这个问题

代码的实现也相对简单,代码实现如下

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        struct ListNode* lesshead,*lesstail,*greaterhead,*greatertail;
        lesshead=lesstail=(struct ListNode*)malloc(sizeof(struct ListNode));
        greaterhead=greatertail=(struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode* cur=pHead;
        while(cur)
        {
            if(cur->val<x)
            {
                lesstail->next=cur;
                lesstail=lesstail->next;
            }
            else 
            {
                greatertail->next=cur;
                greatertail=greatertail->next;
            }
            cur=cur->next;
        }
        //链表合并
        lesstail->next=greaterhead->next;
        greatertail->next=NULL;
        pHead=lesshead->next;
        free(lesshead);
        free(greaterhead);
        return pHead;
    }
};

环形链表

环形链表是链表中的一类题,先看环形链表的基本题型

  1. 如何判断一个链表是否带环?
  • 可以使用快慢指针解法,快指针一次走两步,慢指针一次走一步,如果slow指针和fast指针在环中相遇,就说明链表带环,否则快指针独自走完就说明不带环
  1. 快指针一次走两步,慢指针一次走一步,这样的情况下快慢指针一定会相遇吗?
  • 是的,一定会相遇,fast一次走两步,slow一次走一步,假设进环后它们之间相差的距离为N,那么它们两个每走一次,之间的距离就会少1,因此最后一定会相遇
  1. 快指针一次走三步,慢指针一次走一步,这样的情况下快慢指针一定会相遇吗?
  • 不一定,这和它们之间的初始距离还有环的长度有关,假设进环的时候它们之间的距离为N,那么快慢指针每走一次,它们两个之间的距离就少2,如果这里的N为偶数,就会相遇,如果N为奇数,在此次追击中会错过,此时会进入下一次追击,假设环的长度为C,那么此时它们之间的距离为C-1,那么C-1如果为偶数快慢指针还能相遇,如果C-1为奇数快慢指针无法相遇
  1. 慢指针一次走一步,快指针一次走n步(n>2)可以吗?
  • 假设快指针每次走三步,慢指针每次走一步,此时快指针先进环,慢指针后进环,假设慢指针进环的时候,快指针的位置如图所示,则此时按照上述方法进行绕环移动,每次快指针走三步,慢指针走一步,永远不会相遇,快指针刚好把慢指针套圈了,因此不可以
  • 只有快指针走两步,慢指针走一步才可以,因为环的最小长度是1,即使套圈了两个指针也在相同的位置

  1. 给定一个带环的链表,如何求链表开始入环的第一个节点?
  • 思路:一个指针从相遇点走,一个指针从起始点走,两个指针会在入口相遇
  • 假设快指针一次走两步,慢指针一次走一步,那么快指针走的路程是慢指针走的路程的两倍,从起始点到进环口的距离设为L,进环口到相遇点之间的距离为X,环的长度为C,fast进入环后绕环n圈(假设初始为1)
  • 慢指针走的路程为L+X,快指针走的路程为L+X+n* C,因此化简后L=n* C-X通常n为1,因此L=C-X,因此从上述结论成立

相关文章
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
220 4
|
9月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
343 30
|
9月前
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
443 25
|
9月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
10月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
488 5
|
11月前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
12月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
353 5
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
1018 9
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
287 59
|
5月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
118 0
栈区的非法访问导致的死循环(x64)

热门文章

最新文章

下一篇
oss云网关配置