单链表算法题(二)(超详细版)

前言 : 通过算法题 , 学习解决问题的思路 , 再面对类似的算法题时 , 能快速定位解决方案

一 . 链表的回文结构

链表的回文结构 : 链表的回文结构_牛客题霸_牛客网

思路一 :

创建新链表, 对原链表进行反转,结果存储在新链表中,然后遍历新旧链表进行比较

思路二 :

因为牛客网里给了 ---> 链表的长度小于等于900 的这个条件 , 所以我们可以创建一个新数组,遍历链表,把链表里的值存储在数组中,然后定义两个变量(left,right),分别指向数组的最左端和最右段,然后进行对比,相等的时候,left++,right--

1 . 创建数组

2 . 遍历链表,把 链表结点的值 存到数组中

3 . 创建两个变量 (left = 0 , right = i-1)

4 . left 与 right 往中间走 ,相等继续往中间走,都相等返回true , 不相等时返回false

思路三 :

1 . 找到中间结点**(快慢指针)**

2 . 反转以中间结点为头的链表**(三指针法)**

3 . 遍历【原链表】和【以中间结点为头】的链表

思路二代码:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
public:
    bool chkPalindrome(ListNode* A) {
        // write code here
        int arr[900] = {0};
        int i = 0;
        ListNode* pcur = A;
        //遍历链表,赋值给数组
        while(pcur){
            arr[i++] = pcur->val;
            pcur = pcur->next;
        }
        int left = 0;
        int right = i-1;
        while(left < right)
        {
            if(arr[left] != arr[right])
            {
                return false;
            }
            left++;
            right--;
        }
        return true;;

    }
};

思路三代码 : 快慢指针和三指针法在单链表算法题(一)(超详细版)-CSDN博客有详细说明

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class PalindromeList {
  public:
     ListNode* middleNode(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
    ListNode* reverseList( ListNode* head) {
    if(head == NULL)
    {
        return head;
    }
    ListNode* n1,*n2,*n3;
    n1 = NULL;
    n2 = head;
    n3 = head->next;
    while(n2)
    {
        n2->next = n1 ;
        n1 = n2 ;
        n2 = n3 ;
        if(n3)
            n3 = n2->next;
    }
    return n1;
    }

    bool chkPalindrome(ListNode* A) {
       //1.找到中间结点
       ListNode* mid = middleNode(A);

       //2.反转以中间结点为头的链表
       ListNode* right = reverseList(mid);
       
       //3. 遍历左右链表
       ListNode* left = A;
       while(right)
       {
        if(left->val != right->val)
        {
            return false;;
        }
        left = left->next;
        right = right->next;
       }
        return true;
    }
};

二 . 相交链表

相交链表 : . - 力扣(LeetCode)

思路 :

1 . 求两个链表的长度

2 . 计算两个链表的长度差

3 . 找到大/小链表,让大链表先走 长度差步

4 . 遍历两个链表,比较是否存在相同的结点

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //计算两个链表长度
    ListNode* pa = headA , *pb = headB;
    int sizeA = 0,sizeB = 0;
    while(pa){
        ++sizeA;
        pa = pa->next;
    }
    while(pb)
    {
        ++sizeB;
        pb = pb->next;
    }
    //计算长度差 -- 绝对值(可以使用函数abs)
    int gap = abs(sizeA - sizeB);

    //找大小链表 -- 让大链表走gap 步
    ListNode* longList = headA;
    ListNode* shortList = headB;
    if(sizeA < sizeB){
        longList = headB;
        shortList = headA;
    }
    while(gap--)
    {
        longList = longList->next;
    }
    //遍历链表 -- 找相交结点
    //存在相交结点
    while(longList){
        if(longList == shortList){
            return longList;
        }
        longList = longList->next;
        shortList = shortList->next;
    }
    //不存在相交结点
    return NULL;

}

三 . 环形链表I

环形链表I : . - 力扣(LeetCode)

思路 : 快慢指针

慢指针每次走一步,快指针每次走两步 ,两个指针从链表起始的位置开始运行;若链表带环 ,快指针在环内追逐 ,快慢指针会相遇 ; 若不带环 ,快指针率先走到链表的末尾 ;

这道题重在证明思路 :

1 ) 为什么慢指针每次走一步,快指针每次走两步,快慢指针会相遇?会遇不上吗?

2 ) 快指针能不能一次走3步,走4步,...n步?

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {
    ListNode* slow = head;
    ListNode* fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        //快慢指针相遇,存在环
        if(slow == fast)
        {
            return true;
        }
    }
    //没相遇,不存在环
    return false;
}

3.1 证明一:

1 ) 为什么慢指针每次走一步,快指针每次走两步,快慢指针会相遇?会遇不上吗?

fast 一次走两步 , slow 一次走一步 , 如果链表中存在环 , 那么快指针先进入环 ,假设slow也走完入环前的距离为N(最大距离),在接下来的追逐过程中,每追击一次,他们之间的距离缩小一步

追击过程中 fast 和slow 的变化 :

N - 1 => N - 2 => N-3 => ...... => 2 => 1 => 0 (相遇了)

3.2 证明二 :

2 ) 快指针能不能一次走3步,走4步,...n步?

按照上面的分析,慢指针每次走一步,快指针每次走三步,此时快慢指针的最大距离为N,接下来的追逐中,每追逐一次,他们之间的距离缩小2步

追逐过程中fast和slow之间的距离变化:

分析 :

1 . 如果N是偶数 , 第一轮就追上了

2 . 如果N是奇数 , 第一轮追不上,错过了,距离变为-1,即C-1 , 进入新的一轮追击

1) C-1如果是偶数,那么下一轮就追上了

2) C-1如果是奇数,那么就追不上

总结一下追不上的条件 :N是奇数,C是偶数 (证明是否存在N为奇数,C为偶数的情形)

假设 :

环的周长为C,头结点到slow的长度为L , slow 走一步 ,falst 走三步 , 当slow 指针 入环后,slow和fast 指针在环中开始进行追逐 , 假设此时的fast 已经绕环 n 周

同理 : 对快指针走 4 , 5 ... 步最终也会相遇,证明方法同上!

typedef struct ListNode ListNode;
bool hasCycle(struct ListNode* head) {
    ListNode *slow, *fast;
    slow = fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        int n = 3; // fast每次⾛三步
        while (n--) {
            if (fast->next)
                fast = fast->next;
            else
                return false;
        }
        if (slow == fast) {
            return true;
        }
    }
    return false;
}

提示 :

虽然已经证明了 , 快指针无论走多少步都可以满足在带环链表中相遇 , 但是在编写代码的时候,会有额外的步骤引入**,涉及快慢指针的算法题中,通常习惯使用快指针每次走两步,慢指针走一步的方式。**

四 . 环形链表II

环形链表II :. - 力扣(LeetCode)

主要思路 : 快慢指针的思想

(快指针 fast 每次走两步 , 慢指针 slow 每次走一步)

创建两个指针(快慢指针) , 如果链表带环 , 存在相遇结点 , 此时的相遇结点到入环结点的距离与头结点到入环结点的距离 相同 !

1 . 创建指针变量(快慢指针)

2 . 判断快慢指针走到的结点是否相同 ---> 相同(带环)

3 . 创建指针变量(pcur)(初始化为头结点)

4 . pcur 与 slow 不相等时 , 两个指针一直往后走

5 . 返回相等时的结点

证明 : 为什么带环链表中 , 相遇点到入环结点的距离 等于 头结点到入环结点的距离?

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 typedef struct ListNode ListNode; 
struct ListNode *detectCycle(struct ListNode *head) {
    ListNode* fast = head,*slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        //相遇点
        if(slow == fast)
        {
            ListNode* pcur = head;
            while(slow != pcur)
            {
                pcur = pcur->next;
                slow = slow->next;
            }
            //找到入环点了
            return pcur;
        }
    }
    return NULL;
}

4.1 证明

证明 : 为什么带环链表中 , 相遇点到入环结点的距离 等于 头结点到入环结点的距离?

( 如下图 )

假设 : 头结点到入环结点的距离为 L , 相遇结点为M , 环的长度为X , 链表如果带环 , 在相遇点 , 快指针到入环结点的距离为 R-L

1 . 求出相遇时 , 快慢指针所走的路径长度

fast = L + X + nR

slow = L + X

注 : 当慢指针进入环的时候 , 快指针可能已经在环中绕了n 圈 , n 至少为 1 (因为快指针先进环 ,先走到 M的位置 , 最后在M的位置和慢指针相遇)

2 . 快指针 = 2 * 慢指针

fast = 2 * slow

L + X + nR = 2 * ( L + X )

nR = L + X

( n-1 )R + R - X = L

( n 为 1, 2 , 3 , 4 .... , n 的大小取决于环的大小 , 环越小n 越大)

极端情况下 , 假设 n = 1 , 此时 : L = R - X

即 : 一个指针从链表起始结点运行 , 一个指针从相遇结点绕环 , 每次走一步 , 两个指针最终会在入口结点相遇

五 . 随机链表的复制

随机链表的复制 : . - 力扣(LeetCode)

难点 : 深拷贝 --> 重新申请一块空间存储一样的结点 ---> 可是如果创建新链表 , 但是random 指向的结点还没有创建好 , 会报错,非法访问空间

所以我们可以在原链表中 , 先把值进行拷贝 , 然后此时random 指向的结点是存在的 ,通过遍历拷贝的结点 和 原链表的结点把random的指向处理好 , 最后断开与原链表的结点的连接即可

1 ) 拷贝结点

2 ) 置random

3 ) 断开连接

注意 : 当链表为空时 ,直接返回head

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
typedef struct Node Node;
Node* buyNode(int x)
{
    Node* node = (Node*)malloc(sizeof(Node));
    node->val = x ;
    node->next = node->random = NULL;
    return node;
}
void AddNode(Node* head)
{
    Node* pcur = head;
    while(pcur)
    {
        Node* next = pcur->next;
        Node* newnode = buyNode(pcur->val);
        //尾插
        newnode->next = next;
        pcur->next = newnode;
        pcur = next;
    }
}

void SetRandom(Node* head)
{
    Node* pcur = head;
    while(pcur)
    {
        Node* copy = pcur->next;
        if(pcur->random)
            copy->random = pcur->random->next;
        pcur = copy->next;
    }
}
struct Node* copyRandomList(struct Node* head) {
	if(head == NULL)
    {
        return head;
    }
    //1.拷贝结点
    AddNode(head);
    //2.置randow
    SetRandom(head);
    //3.断开链表
    Node* pcur = head;
    Node* newHead,*newTail;
    newHead = newTail = pcur->next;
    while(newTail->next)
    {
        pcur = newTail->next;
        newTail->next = pcur->next;
        newTail = pcur->next;
    } 
    return newHead;
}

写算法题目时 , 如果暂时没有解题思路可以先画画图 , 打开新大陆!

相关推荐
Theodore_10226 分钟前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou31 分钟前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
‘’林花谢了春红‘’2 小时前
C++ list (链表)容器
c++·链表·list
----云烟----2 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024062 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic3 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it3 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康3 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神3 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
搬砖的小码农_Sky3 小时前
C语言:数组
c语言·数据结构