【数据结构与算法】链表2W字终极无敌总结(二)

简介: 【数据结构与算法】链表2W字终极无敌总结(二)

4. 链表成环问题


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


由于有扩展问题,我们先解决题目:


给你一个链表的头节点 head ,判断链表中是否有环。


如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false


示例 1:

微信图片_20230224165115.png


输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

微信图片_20230224165122.png


输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

微信图片_20230224165126.png


输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

提示:

  • 链表中节点的数目范围是 [0, 104]
  • -105 <= Node.val <= 105
  • pos-1 或者链表中的一个 有效索引


进阶: 你能用 O(1)(即,常量)内存解决此问题吗?


对于成环问题,如果是环,那么链表迭代的过程中不会截止,但是我们不能根据是否截止进行判断是不是环,这样只会运行超时,因此,需要采用一定的特殊技巧:


利用快慢指针,即快指针一次走两步,慢指针一次走一步,当慢指针进入环后,转化思想为快指针追赶慢指针:根据相对运动,每次移动快指针都会离慢指针更进一步,这就使得二者一定会在圈中相遇。即为真。

如果不是环,快指针一定先走到末端。


bool hasCycle(struct ListNode *head) {
    if(head == NULL)
        return false;
    struct ListNode* slow = head,*fast = head;
    while(fast && fast->next)//一次走两步,防止出现野指针需要判断两个条件
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return true;
        }
    }
    return false;
}


【扩展问题】


为什么快指针每次走两步,慢指针走一步可以?

假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在满指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。


微信图片_20230224165520.png

快指针一次走3步行吗?


不一定。慢指针在进圈后,快指针以相对慢指针两个单位两个单位的追赶,如果N为奇数(N代表两个指针之间的距离),距离就会变成:N-2,N-4,……3,1,-1。当变成-1时,又是一个新的开始,此时二者相距C-1个长度,C为环的周长,如果c-1是奇数,那么就永远不会相遇,因此不一定。

快指针一次走M步行吗?


对此,可与一次走三步的类似,需要看N与C的关系。

慢指针一次走N步,快指针一次走M步行吗?(M>N)


只要是在环中,并且M比N大1个单位,那么就可以认为快指针相对慢指针靠近一步,这样相当于遍历所有可能性,一定会相遇。


总结:只要fast比slow快一步,无论他们一次走几个单位,都一定可以相遇。


4.2返回入环的第一个结点


对于这种类型的,先证明一下无疑是最好的学习方式:


微信图片_20230221175402.png


假设进环前的长度是L,假设环的长度是C,假设入口点到相遇点距离为X


1.公式推导:

fast走的距离 = 2*slow走的距离;


slow走的距离:L+X;


fast走的距离:L+N*C+X;(fast转了N圈,N>=1)


注: 为什么slow走的不是L+n*C+X呢? 即为什么slow在圈里一定走了不到一圈就相遇了呢?我们知道当slow刚刚进圈时slow与fast之间的距离一定小于C-1,fast一次走两步,slow一次走一步,距离逐渐减小,即一定走了小于C-1次就会相遇,因此推出此时slow走了不到一圈。


即:根据二倍关系:2(L+X) = L+X+N*C,即L = N * C - X;进一步得出:

L = ( N − 1 ) ∗ C + C − X L = (N-1)*C+C-X

L=(N−1)∗C+C−X


结论:一个指针A从头开始走,一个指针B从相遇点开始走,他们会在入口点相遇。


2.转化成相交问题

当我们通过快慢指针找到相遇点记录下来以后,可以想象把此相遇节点与下一节点断开,记录下一个节点为链表B的头,并记录起始位置为链表A的头,这样通过相交链表的方法,就能求得入环的第一个节点,也就是链表的第一个交点


微信图片_20230221175519.png


那么我们可以尝试解决这道题目:


142. 环形链表 II


给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。


如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。


不允许修改 链表。

示例 1:

微信图片_20230224165650.png


输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。


示例2:


微信图片_20230224165654.png


输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。


示例 3:


微信图片_20230224165657.png


输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。


提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

进阶: 你是否可以使用 O(1) 空间解决此题?


  • 公式推导法:


struct ListNode* hasCycle(struct ListNode *head) {
    if(head == NULL)
        return NULL;
    struct ListNode* slow = head,*fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return slow;
        }
    }
    return NULL;
}
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* meet = hasCycle(head);
    if(meet == NULL)
        return NULL;
    struct ListNode* begin = head;
    while(1)
    {
        if(begin == meet)
        {
            return begin;
        }
        begin = begin->next;
        meet = meet->next;
    }
    return NULL;
}

微信图片_20230224165911.png


相交法:


struct ListNode* hasCycle(struct ListNode *head) {
    if(head == NULL)
        return NULL;
    struct ListNode* slow = head,*fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            return slow;
        }
    }
    return NULL;
}
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    if(headA == NULL ||headB == NULL)
        return NULL;
    struct ListNode* curA = headA, *curB = headB;
    int lenA = 1;
    //找尾节点
    while(curA)
    {
        curA = curA->next;
        ++lenA;
    }
    int lenB = 1;
    while(curB)
    {
        curB = curB->next;
        ++lenB;
    }
    struct ListNode* longList = headA, *shortList = headB;
    if(lenA<lenB)
    {
        longList = headB;
        shortList = headA;
    }
    //长的链表先走差距步
    int gap = abs(lenA-lenB);
    while(gap--)
    {
        longList = longList->next;
    }
    while(longList!=shortList)
    {
        longList = longList->next;
        shortList  = shortList->next;
    }
    return longList;
}
struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode* meet = hasCycle(head);
    if(meet == NULL)
        return NULL;
    struct ListNode* newheadB = meet->next;
    meet->next = NULL;//必须断开,否则在求相交链表在求长度时会死循环。
    struct ListNode* newheadA = head;
    struct ListNode* newmeet = getIntersectionNode(newheadA,newheadB);
    meet->next = newheadB;//恢复环
    return newmeet;
}

微信图片_20230224170008.png

相信这个代码大家已经看出来了,复用的两个函数不正是判断是否有环的函数和相交链表的函数吗?只不过判断是否有环的函数的返回值稍微改了一下,因此,只要掌握思路,写出的代码一定是有联系的。

5. 复制带随机指针的链表


138. 复制带随机指针的链表

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。


构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。


例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。


返回复制链表的头节点。


用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:


val:一个表示 Node.val 的整数。

random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。

你的代码 只 接受原链表的头节点 head 作为传入参数。


示例 1:


微信图片_20230224170056.png


输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]


示例 2:


微信图片_20230224170100.png


输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例 3:

微信图片_20230224170104.png


输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]


提示:


0 <= n <= 1000

-104 <= Node.val <= 104

Node.random 为 null 或指向链表中的节点。

这是一道有挑战性的题目。因此,我把他单拿出来放在这里。这道题本身的难度在于random指针,通过常规暴力的方法,我们需要的是将每一个random指针的位置给记录下来,从而当处理拷贝链表的过程中,再利用双层循环将每个特定的位置的random指向这个拷贝链表相对应的位置,但是为什么不能根据val值从而链接呢?因为val的值本身是允许重复出现的,只有通过具体位置才能锁定,因此需要创建数组来记录位置(思路清晰,实现起来繁琐,自己也是想了好久),下面的代码实现就是这样的思路:


1.暴力解决


/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
struct Node* BuySLTNode(struct Node* node)
{
    struct Node* newnode = (struct Node*)malloc(sizeof(struct Node));
    newnode->next = NULL;
    newnode->random = NULL;
    newnode->val = node->val;
    return newnode;
}
struct Node* copyRandomList(struct Node* head) {
    //创建复制的链表
    struct Node* newhead = (struct Node*)malloc(sizeof(struct Node));
    newhead->next = NULL;
    struct Node* ntail = newhead;
  struct Node* cur = head;
    while(cur)
    {
        ntail->next = BuySLTNode(cur);
        ntail = ntail->next;
        cur = cur->next;
    }
   // 记录节点个数,
    cur = head;
    int n = 1;
    while(cur)
    {
        n++;
        cur = cur->next;
    }
    cur = head;
    //将位置记录到count数组里,count数组的每一个元素记录该节点random指针指向的位置
    struct Node* repcur = cur;
    int* count = (int*)calloc(n,sizeof(int));//开辟数组记录每一个random指向的数据的位置
    int i = 0;
    while(cur)
    {
        int order = 0;
        while(repcur)
        {
            if(cur->random == NULL)
            {
                count[i++] = n-1;
                break;
            }
            if(cur->random == repcur)
            {
                count[i++] = order;
                break;
            }
            order++;
            repcur = repcur->next;
        }
        repcur = head;
        cur = cur->next;
    }
    i = 0;
    //通过之前的数组找到复制之后的位置
    struct Node* newcur = newhead->next;
    struct Node* newcur1 = newhead->next;
    while(newcur)
    {
        int j=0;
        while(newcur1)
        {
           if(j == count[i])
           {
               newcur->random = newcur1;
               break;
           }
           j++;
           newcur1 = newcur1->next;
        }
        newcur1 = newhead->next;
        newcur = newcur->next;
        i++;
    }
    free(count);
    return newhead->next;
}


微信图片_20230224170349.png


2.在链表本身进行拷贝


再次本着无优解博客不写的原则,既然暴力的解决了,那为什么还要有优解呢? emm……这是一个很痴呆的问题,有好的方法,谁会不愿意用呢?那下面,我们就来说说这个美妙的方法:


上文提到了,难点在于random指针的拷贝,但我们又根据原链表很清晰的知道random指向的位置,因此,我们就要靠着原链表,在原链表的基础上进行改造:


/

微信图片_20230224170417.png

在此基础上进行改造:

微信图片_20230224170420.png


在每一个节点的后方,拷贝一个与该节点一模一样的节点(当然地址肯定不一样喽)即图中的copy节点插入到原链表,这样就可以对random指针进行下面操作:


copy->random = cur->random->next;//后者代表着仍然是copy的节点,因为有指向next


这样就可以完美的解决random指针的问题啦。

因此应该进行以下步骤:

  • 1.复制节点插入原链表中,并对copy的random进行赋值(关键操作)。
  • 2.将copy的节点拿出来尾插编程新的链表。
  • 3.在第二步的同时将原链表恢复原状
/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
struct Node* copyRandomList(struct Node* head) {
    //1.插入copy节点
    struct Node* cur = head;
    struct Node* copy = NULL;
    struct Node* next = NULL;
    while(cur)
    {
        next = cur->next;
        copy = (struct Node*)malloc(sizeof(struct Node));
        copy->val = cur->val;
        //连接到原链表
        cur->next = copy;
        copy->next = next;
        //迭代往后走
        cur = next;
    }
    //2.更新copy->random
    cur = head;
    while(cur)
    {
        copy = cur->next;
        if(cur->random == NULL)
            copy->random = NULL;
        else
            copy->random = cur->random->next;
        //迭代往后走
        cur = cur->next->next;
    }
    //将其copy的节点尾插,并还原原链表
    cur = head;
    struct Node* copyhead = NULL;
    struct Node* copytail = copyhead;
    while(cur)
    {
        copy = cur->next;
        next = copy->next;
        if(copyhead == NULL)
        {
            copyhead = copytail = copy;
        }
        else
        {
            copytail->next = copy;
            copytail = copytail->next;
        }
        //重新还原原链表
        cur->next = next;
        //迭代
        cur = next;
    }
    return copyhead;
}

微信图片_20230224170557.png

6. 双向带头循环链表


6.1函数实现


// 2、带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
    LTDataType _data;
    struct ListNode* next;
    struct ListNode* prev;
}ListNode;
// 创建返回链表的头结点.
ListNode* ListCreate();
// 双向链表销毁
void ListDestory(ListNode* plist);
// 双向链表打印
void ListPrint(ListNode* plist);
// 双向链表尾插
void ListPushBack(ListNode* plist, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* plist);
// 双向链表头插
void ListPushFront(ListNode* plist, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* plist);
// 双向链表查找
ListNode* ListFind(ListNode* plist, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的结点
void ListErase(ListNode* pos);

与单链表不同的是,这里无论是什么函数传的都是一级指针,原因是此结构是带头的,即哨兵位,哨兵位的数据域不存储,并且传进去的哨兵位为真正哨兵位的形参,但是其next和prev分别记录了实际节点的地址,因此,这里都用一级指针完全可以解决。


顺序表与链表的优异


微信图片_20230221175934.png


7. 总结


这篇文章呕心沥血,是我目前写的时间最长的文章,不过到这里也就收尾了,本篇文章侧重的是实现链表所需要注重的细节及经过链表oj强训所带来的一系列的新的逻辑与方法,相信读到这里的你对于链表的掌握会上升一个非常大的台阶,即便这样,仍要多加训练,因此,将我的链表oj仓库放在这里以便大家食用(以后刷的链表也会在这里),一起加油哇!


相关文章
|
8天前
|
算法 Java
Java数据结构与算法:双向链表
Java数据结构与算法:双向链表
|
8天前
|
算法 Java
Java数据结构与算法:循环链表
Java数据结构与算法:循环链表
|
8天前
|
算法
【数据结构与算法 刷题系列】求带环链表的入环节点(图文详解)
【数据结构与算法 刷题系列】求带环链表的入环节点(图文详解)
|
8天前
|
算法
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
【数据结构与算法 刷题系列】判断链表是否有环(图文详解)
|
8天前
|
算法
【数据结构与算法 经典例题】随机链表的复制(图文详解)
【数据结构与算法 经典例题】随机链表的复制(图文详解)
|
4天前
|
算法 安全 数据库
基于结点电压法的配电网状态估计算法matlab仿真
**摘要** 该程序实现了基于结点电压法的配电网状态估计算法,旨在提升数据的准确性和可靠性。在MATLAB2022a中运行,显示了状态估计过程中的电压和相位估计值,以及误差随迭代变化的图表。算法通过迭代计算雅可比矩阵,结合基尔霍夫定律解决线性方程组,估算网络节点电压。状态估计过程中应用了高斯-牛顿或莱文贝格-马夸尔特法,处理量测数据并考虑约束条件,以提高估计精度。程序结果以图形形式展示电压幅值和角度估计的比较,以及估计误差的演变,体现了算法在处理配电网状态估计问题的有效性。
|
1天前
|
数据采集 存储 算法
基于BP算法的SAR成像matlab仿真
**摘要:** 基于BP算法的SAR成像研究,利用MATLAB2022a进行仿真。SAR系统借助相对运动合成大孔径,提供高分辨率图像。BP算法执行回波数据预处理、像素投影及图像重建,实现精确成像。优点是高精度和强适应性,缺点是计算量大、内存需求高。代码示例展示了回波生成、数据处理到插值显示的全过程。
|
8天前
|
机器学习/深度学习 自然语言处理 算法
m基于深度学习的OFDM+QPSK链路信道估计和均衡算法误码率matlab仿真,对比LS,MMSE及LMMSE传统算法
**摘要:** 升级版MATLAB仿真对比了深度学习与LS、MMSE、LMMSE的OFDM信道估计算法,新增自动样本生成、复杂度分析及抗频偏性能评估。深度学习在无线通信中,尤其在OFDM的信道估计问题上展现潜力,解决了传统方法的局限。程序涉及信道估计器设计,深度学习模型通过学习导频信息估计信道响应,适应频域变化。核心代码展示了信号处理流程,包括编码、调制、信道模拟、降噪、信道估计和解调。
30 8
|
10天前
|
算法
基于GA遗传优化的混合发电系统优化配置算法matlab仿真
**摘要:** 该研究利用遗传算法(GA)对混合发电系统进行优化配置,旨在最小化风能、太阳能及电池储能的成本并提升系统性能。MATLAB 2022a用于实现这一算法。仿真结果展示了一系列图表,包括总成本随代数变化、最佳适应度随代数变化,以及不同数据的分布情况,如负荷、风速、太阳辐射、弃电、缺电和电池状态等。此外,代码示例展示了如何运用GA求解,并绘制了发电单元的功率输出和年变化。该系统原理基于GA的自然选择和遗传原理,通过染色体编码、初始种群生成、适应度函数、选择、交叉和变异操作来寻找最优容量配置,以平衡成本、效率和可靠性。
|
2天前
|
算法 vr&ar
基于自适应波束成形算法的matlab性能仿真,对比SG和RLS两种方法
```markdown - MATLAB2022a中比较SG与RLS自适应波束成形算法。核心程序实现阵列信号处理,强化期望信号,抑制干扰。RLS以其高效计算权重,而SG则以简单和低计算复杂度著称。[12345] [6666666666] [777777] ```