链表算法讲解

链表基础概念

链表类型

1.单链表:
每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思),单链表中的指针域只能指向节点的下一个节点。
在这里插入图片描述
2.双链表
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表:既可以向前查询也可以向后查询。
在这里插入图片描述
3.环形链表
环形链表:首尾相连
在这里插入图片描述

链表存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
在这里插入图片描述

链表定义

c++默认生成构造函数,但是存在问题,就是你使用默认构造初始化节点,不能直接给变量赋值:

ListNode* head = new ListNode();
head->val = 5;

所以:我们首要自己定义链表

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

定义链表后,就可以直接对链表初始化节点,可以直接赋值:

ListNode* head = new ListNode(5);

链表删除和添加

删除:就是讲该链表上个节点指向下个节点头节点,就把该节点掠过
在这里插入图片描述
添加:往里插入节点
在这里插入图片描述

链表和数组性能分析

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。
在这里插入图片描述

双指针技巧解决链表问题

合并两个有序链表

这是最基本的链表技巧,力扣第 21 题「合并两个有序链表
思路代码可视化图:
在这里插入图片描述
思路一:递归
目前你就明白一个事,只要调用这个 mergeTwoLists 函数你就可以讲两个链表合并,如何合并我们不管,默认这函数就只做这一件事,就对了。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if(list1 == nullptr) return list2;
        if(list2 == nullptr) return list1;
        if(list1->val < list2->val)
        {
        	// 所以此处,我们用list1->next 来接收合并后的链表接在后面
            list1->next = mergeTwoLists(list1->next,list2);
            return list1;
        }
        list2->next = mergeTwoLists(list1,list2->next);
        return list2;
    }
};

思路二:常规思路while循环遍历
就是对两个连边逐层遍历,比较他们的值

lass Solution {
public:
        if(list1 == nullptr) return list2;
        if(list2 == nullptr) return list1;
        // 虚拟头节点
        ListNode* cur= new ListNode(0);
        ListNode *p = cur;
        while(list1 != nullptr && list2 != nullptr)
        {
            if(list1->val >= list2->val )
            {
                p->next = list2;
                list2 = list2->next;
            }
            else
            {
                p->next = list1;
                list1 = list1->next;
            }
            p = p->next;
        }
        p->next = list1 != nullptr ? list1 : list2;
        ListNode *result = cur->next;
        return result;
    }
};

代码中还用到一个链表的算法题中是很常见的「虚拟头结点」技巧,也就是 cur节点。
什么时候需要创造虚拟右节点呢?:
如果需要创造新链表,则创建虚拟头节点。比如:将一个链表拆分为两条链表。

单链表分解

直接看下力扣第 86 题「分隔链表」:
思路:
将大于等于x值的,我们放入一个链表,小于x的放入宁一个链表,这样再将两个链表结合起来

class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode * dumppy1 = new ListNode(0);
        ListNode * dumppy2 = new ListNode(0);
        ListNode *p1 =  dumppy1;
        ListNode *p2 =  dumppy2;
        ListNode *p = head;
        while(p != nullptr)
        {
            if(p->val >= x)
            {
                p2->next = p;
                p2 = p2->next;
            }
            else
            {
                p1->next = p;
                p1 = p1->next;
            }
            //
            ListNode *temp = p->next;
            p->next =nullptr;
            p = temp;
            // head = head->next;
            //p = p->next;
        }
        p2->next = nullptr;
        p1->next = dumppy2->next;
        return dumppy1->next;
    }
};

该该代码中,最重要的一点就是将节点提取出来,进行排序:

            ListNode *temp = p->next;
            p->next =nullptr;
            p = temp;

不断开,最终结果将会包含一个环形链表,这样断开属于是将链表提取出来了。
思考迷惑:如果我直接用head这个代表节点就不会有什么其他问题

合并k个有序链表(该题涉及后续的(优先级队列问题)完全不会) 后续在回头查看

力扣第 23 题「合并K个升序链表」:

单链表的倒数第k个节点

寻找某个节点,最简单方法:
1.正序遍历,for循环。
2.寻找倒数k个,正序遍历也可以,比如 n - k + 1 也属于for循环(该方法,你得首先遍历链表知道链表的n,这样会大大增加复杂度)。
3.只需要遍历一次就可以到达倒数第几个***(双指针法)***
a.首先,p1先移动k次
在这里插入图片描述
b.然后再p2,p1移动 n-k 步,这样刚刚p2只想倒数k节点
在这里插入图片描述
代码逻辑模板:

// 返回链表的倒数第 k 个节点
ListNode* findFromEnd(ListNode* head, int k) {
    ListNode* p1 = head;
    // p1 先走 k 步
    for (int i = 0; i < k; i++) {
        p1 = p1 -> next;
    }
    ListNode* p2 = head;
    // p1 和 p2 同时走 n - k 步
    while (p1 != nullptr) {
        p2 = p2 -> next;
        p1 = p1 -> next;
    }
    // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
    return p2;
}

比如力扣题库力扣第 19 题「删除链表的倒数第 N 个结点」:
这道题就完美运用了上文的双指针法:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        if (head == nullptr || n <= 0) {
            return nullptr;  // 无效的输入,返回空指针
        }

        ListNode* fast = head;
        ListNode* slow = head;

        // 将 fast 移至第 n+1 个节点
        while (n-- && fast != nullptr) {
            fast = fast->next;
        }

        if (fast == nullptr) {
            // 处理 n 等于链表长度的情况
            return head->next;
        }

        // 移动 fast 到链表末尾,同时 slow 保持在倒数第 n+1 个节点
        while (fast->next != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }
        // 删除倒数第 n 个节点
 
        slow->next = slow->next->next;
        

        return head;
    }
};

其中,得注意这个状况,就事倒数 n 刚好是头节点问题

        if (fast == nullptr) {
            // 处理 n 等于链表长度的情况
            return head->next;
        }

单链表的中点

思路:
1.最常规的方法,就是for遍历,先求出链表长度 n ,然后再 for 遍历找到 n/2 的位置。
2.本章节主要讲解的是双指针的问题,直接使用双指针链表,快慢指针。
快慢指针:fast = fast+2 , slow = slow+1 这样永远fast是slow两倍,当 fast 指向末尾节点 null 时,此时 slow 则为中间指针。
在这里插入图片描述

比如力扣题库第 876 题「链表的中间结点」:
我先给出最常规求解方法,代码如下:
在这里插入图片描述

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        int n = 0;
        ListNode *cur  = head;
        while(cur!=nullptr)
        {
            n++;
            cur = cur->next;
        }
        int k = 0;
        cur = head;
        while(k<n/2)
        {
            k++;
            cur = cur->next;
        }
        return cur;
    }
};

下文代码是通过双指针写法:
该部分注意的一点就是,奇偶状况,fast到底是运行到末尾,末尾上一个。比如下图,fast指向末尾了,他都不存在 fast = fast->next->next;,所以此处的判断的稍微注意!

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        //双指针写法
        if(head == nullptr)
        {
            return head;
        }
        //快慢指针定义
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast!=nullptr && fast->next != nullptr)
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
};

判断链表是否包含环

思路:
方法1 :
和上文找寻中节点一样。
每当慢指针 slow 前进一步,快指针 fast 就前进两步。
如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。
比如:再该途中,只要存在环,fast 和 slow 指针迟早会相遇,只是相遇节点不一定是环的起点
在这里插入图片描述
判断是否有环模板代码:

bool hasCycle(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;
}

那我们应该怎么找到起点?在上文我们可以找到相遇点:
在这里插入图片描述
该图我们可以看到 fast 始终是比 slow 快 k 步,当相遇时 slow 和 fast 都指向相遇点这个节点
在这里插入图片描述
所以,找到相遇点,我们再 让 slow 指向 head 头节点,head 和 fast 再一起一步一步往后移,则下次他两相遇则再环起点

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode *fast = head;
        ListNode *slow = head;
        if(head == NULL)
        {
            return NULL;
        }
        while(fast->next != NULL && fast->next->next != NULL)
        {
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast)
            {
            // 此处就是查找环起点
                while(fast != head)
                {
                    fast = fast->next;
                    head = head->next;
                }
                return fast;
            }
        }
        return NULL;
    }
};

方法二:
该方法,只要遍历一遍,不需要双指针法,如果有环,肯定 for 遍历的时候会首先再环起点相遇:

class Solution{
public:
    ListNode *detectCycle(ListNode *head) {
        unordered_set<ListNode *> visited;
        while(head != NULL)
        {
        	//查找是否已经插入该节点,查询到了,则直接返回
        	if(visited.count(head))
        	{
        		return head;
        	}
        	//没查询到,则插入该节点
        	visited.insert(head);
        	head = head->next;
        }
        return NULL;
};

两链表是否相交

链表相交,我们明白一个点,如果相交了,等于他们后续节点都会一摸一样,理解到这个地方,这件事就简单了。
我们只需要像下图这样,从末尾开始对齐,因为判断相交嘛,就代表后续节点数肯定相同,然后再分别 curA 和 curB 一个一个往后遍历,看时候节点相等,遇到相等节点,则代表相交了
在这里插入图片描述
还有个思路如下:(但是我不推荐,这样容易把自己绕进去)
我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。
如果这样进行拼接,就可以让 p1 和 p2 同时进入公共部分,也就是同时到达相交节点 c1:
在这里插入图片描述
倘若至始至终没有交点,则属于 c1 当作空指针吗,最后返回null

// 求链表的交点
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {
    // p1 指向 A 链表头结点,p2 指向 B 链表头结点
    ListNode *p1 = headA, *p2 = headB;
    while (p1 != p2) {
        // p1 走一步,如果走到 A 链表末尾,转到 B 链表
        if (p1 == nullptr) p1 = headB;
        else               p1 = p1->next;
        // p2 走一步,如果走到 B 链表末尾,转到 A 链表
        if (p2 == nullptr) p2 = headA;
        else               p2 = p2->next;
    }
    return p1; // 返回交点
}

递归解决链表

单个链表整体反转

链表递归模板
再这个代码中,我们就想清楚一件事,当我调用了 reverseList 函数他就会执行反转操作,怎么执行,我不理会

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        //递归做法
        // 此处 head == nullptr 的原因,是防止 head 就是为空节点 
        if(head == nullptr || head->next == nullptr)
        {
            return head;
        }
        ListNode* last =  reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return last;
    }
};

下面是递归流程图:
1.初始链表
在这里插入图片描述
2.开始进行递归操作
ListNode* last = reverseList(head->next);
在这里插入图片描述
执行完成,链表则成为下图:
last 属于一步一步指向末尾
在这里插入图片描述
3.head->next->next = head;
在这里插入图片描述
4.最后一步将原本 head-> = nullptr; 保证链表最后指向为空
在这里插入图片描述

反转链表前N个节点

反转前N个节点,意思如下图:
在这里插入图片描述
代码模板:

ListNode* successor = nullptr;

// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode* reverseN(ListNode* head, int n) {
    if (n == 1) {
        successor = head->next;
        return head;
    }
    ListNode* last = reverseN(head->next, n - 1);
    head->next->next = head;
    head->next = successor;
    return last;
}

1.我们此处判定是 n == 1 时结束,因为 n == 1 时,此处就是他自己这个节点。
2.上文整体反转时,我们将,head->next = nullptr; , 但是因为此处我们只反转了前 n 个,所以此处我们需将后续节点加在后面,所以我们上文代码设立了 successor 记录不反转的节点,这行代码的作用就是如此: successor = head->next;
在这里插入图片描述

反转链表的其中一部分

比如力扣的92题「反转链表 II」:
该题的思路就是,将中间反转部分进行转化:
在这里插入图片描述
我们把中间部分挑剔出来:
在这里插入图片描述
这样后面部分就化解成为了上文的反转前 n 个节点链表问题

class Solution {
public:

ListNode * success = nullptr;

    ListNode* reverseBetween(ListNode* head, int left, int right) {
        if(left == 1)
        {
            return trverse(head,right);
        }
        head->next = reverseBetween(head->next,left-1,right-1);
        return head;

    }

    // 该函数主要是用于链表反转
    ListNode* trverse(ListNode*head,int n)
    {
        if(n == 1)
        {
            success =  head->next;
            return head;
        }
        ListNode *last = trverse(head->next,n-1);
        head->next->next = head;
        head->next = success;
        return last;
    }
};

方法二:迭代法
思路和前文一样,依旧属于是将中间部分挑出来进行前N个反转迭代

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
    ListNode *result = new ListNode();
    result->next = head;
    ListNode* before  = result;
    ListNode* pre = head;
    ListNode* cur = head->next;

    //此处属于是将前几个不反转的给挑剔出来
    for(int i =1; i<left;i++)
    {
        //确定前面不反转的最后一个节点
        before = pre ;
        pre = cur;
        cur = cur->next;
    }

    //下面函数则是属于是前 n 个链表反转
    while(cur != nullptr && left<right)
    {
        left++;
        ListNode *temp = cur->next;
        cur->next = pre;
        pre = cur;
        cur = temp;
    }
    before->next->next = cur;
    before->next = pre ;
    return result->next;
    }
};

如何k个一组反转链表(迭代法反转)

单链表反转问题

此处主要是讲迭代方法
如下图框架,一点一点往后遍历
在这里插入图片描述

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode * cur = head;
        ListNode * pre = nullptr;
        while(cur != nullptr)
        {
            ListNode *temp = cur->next;
            cur->next = pre ;
            pre = cur;
            cur = temp;
        }
        return pre;
	}
};

当然,上文我们学习了递归,那也可以用递归的方式进行解决,回顾递归知识点:
下文代码,我们在递归时反复运用,加深记忆了。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if(head == nullptr || head->next == nullptr)
        {
            return head;
        }
        ListNode * last = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return last;
	}
};

反转链表a到b个节点

此处和上文全部反正没什么区别,上文无非就时 b == nullptr 属于是末尾节点,所以只需要将上文代码稍加修改即可,属于 cur != b ;

// 反转区间 [a, b) 的元素,注意是左闭右开
ListNode* reverse(ListNode* a, ListNode* b) {
    ListNode *pre, *cur, *nxt;
    pre = nullptr; cur = a; nxt = a;
    // while 终止的条件改一下就行了
    while (cur != b) {
        nxt = cur->next;
        cur->next = pre;
        pre = cur;
        cur = nxt;
    }
    // 返回反转后的头结点
    return pre;
}

l流程图如下:
在这里插入图片描述
该模板代码,我们只是得到了 a 到 b 之间的链表反转,并没有进行完整拼接,比如当 b = 4 时,我们 return pre 得到的结果如下,只有前半部分,上文代码并没有整体结合起来。
在这里插入图片描述

反转k个一组链表

思路:一步一步分解为上文 节点 a 到节点 b 的反转
流程图:
在这里插入图片描述

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if(head == nullptr)
        {
            return head;
        }
        ListNode *b = head;
        for(int i = 0 ;i<k;i++)
        {
            // 下面这个if用处在于判断是否超过k 个,不超过之直接输出,不用反转;
            if(b == nullptr)
            {
                return head;
            }
            b = b->next;
        }
        ListNode *result = reverse(head,b);
        
        head->next = reverseKGroup(b,k);
        return result;
    }

    ListNode * reverse(ListNode *a, ListNode *b)
    {
        ListNode *pre = nullptr;
        ListNode * cur = a;
        while(cur != b)
        {
            ListNode *temp = cur->next;
            cur->next = pre ;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

该部分属于反转临界条件:

            // 下面这个if用处在于判断是否超过k 个,不超过之直接输出,不用反转;
            if(b == nullptr)
            {
                return head;
            }

回文问题链表

什么时回文?
回文就是对称,正着读和倒着读都是一样的

寻找回文串

核心思想: 从中心往两段进行扩展

// 在 s 中寻找以 s[left] 和 s[right] 为中心的最长回文串
string palindrome(string s, int left, int right) {
    // 防止索引越界
    while (left >= 0 && right < s.length()
            && s[left] == s[right]) {
        // 双指针,向两边展开
        left--;
        right++;
    }
    // 返回以 s[left] 和 s[right] 为中心的最长回文串
    return s.substr(left + 1, right - left - 1);
}

while 循环就是属于从中间往两边找最大回文子串。
如果是奇数,则只需要传入一个中心点(传入两个既同时指向一个中心点为起始点),偶数,则需要入上文传入两个。

判断回文串

思路和上文恰好相反
双指针从两段向中间逼近:
只要属于左右存在不相等,则说明它不是回文

bool isPalindrome(string s) {
    //一左一右两个指针相向而行
    int left = 0, right = s.length() - 1;
    while (left < right) {
        if (s[left] != s[right]) { //判断左右指针对应字符是否相等
            return false;
        }
        left++;
        right--;
    }
    return true;
}

判断回文单链表

力扣第 234 题「回文链表」:
思路一:以数组形式进行判断
回文嘛,前后相等即可,我们可以把链表值存入 vector 容器,进行前后对比:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vals;
        while (head != nullptr) {
            vals.emplace_back(head->val);
            head = head->next;
        }
        for(int i=0,j = vals.size()-1;i<j;i++,j--)
        {
            if(vals[i] != vals[j])
            {
                return false;
            }
        }
        return true;
    }
};

也可以直接双容器,不过和上文是一个性质,反而增加冗余度:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        vector<int> vals;
        while (head != nullptr) {
            vals.emplace_back(head->val);
            head = head->next;
        }
        vector<int>que;
        que = vals;
        reverse(que.begin(),que.end());
        vector<int>res;
        res = que;
        for(int i = 0;i<vals.size();i++)
        {
            if(res[i] != vals[i])
            {
                return false;
            }
        }
        return true;
    }
};

思路二:双指针链表法:
1.快慢指针,left = left +1 right = right + 2
在这里插入图片描述

2.当 right 指向末尾或者 null 时(分奇偶),left 指向为后半部分节点
![在这里插入图片描述](https://img-blog.csdnimg.cn/82a2d1ed98914c9489d6dff49d5dd80c.pn

3.后半部分节点反转(此处为奇数,left +1 )
在这里插入图片描述

4.将反转后的链表和原始链表进行对比,相等,则代表时回文
在这里插入图片描述

下文代码属于回顾前文知识点结合,链表反转和双指针:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if(head == nullptr)
        {
            return true;
        }
 
        ListNode *fast = head;
        ListNode *slow = head;
        while(fast!=nullptr &&fast->next!=nullptr)
        {
            fast = fast->next->next;
            slow = slow->next;
        }
        //此处判断奇偶问题,奇数则需要 + 1
        if(fast != nullptr)
        {
            slow = slow->next;
        }
        slow = reverseList(slow);
        fast = head;
        // 此处则是反转后代码和原始代码的对比
        while(slow != nullptr)
        {
            if(slow->val != fast->val)
            {
                return false;
            }
            slow = slow->next;
            fast = fast->next;
        }
        return true;
    }
public:
// 链表反转代码
     ListNode* reverseList(ListNode* head) {
        ListNode *cur = head;
        ListNode *pre = nullptr;
        while(cur != nullptr)
        {
            ListNode *temp = cur->next;
            cur->next =pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

但是上文代码怕坏了原始链,弥补的话:只需要将前文节点 left->next = reverseList(slow)

在这里插入图片描述链表反转部分:可以参考二叉树
二叉树遍历:

void traverse(TreeNode* root) {
    // 前序遍历代码
    traverse(root->left);
    // 中序遍历代码
    traverse(root->right);
    // 后序遍历代码
}

同样,链表也兼具递归结构:
如果时正序,则只需要将代码放在前序遍历位置
如果是倒叙,则只需要将代码放在后续遍历位置

void traverse(ListNode* head) {
    // 前序遍历代码
    //print(head->val);
    traverse(head->next);
    // 后序遍历代码
    //print(head->val);
}

代码如下:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
    ListNode* left = head; // 将 left 移到 isPalindrome 函数中
    return traverse(head, left);
}
    bool traverse(ListNode* right, ListNode*& left) { // 注意传递 left 的引用
        if (right == nullptr) return true;
        bool res = traverse(right->next, left);
        // 后序遍历代码
        res = res && (right->val == left->val);
        left = left->next;
        return res;
    }
};

其中这行代码:

        res = res && (right->val == left->val);

这行代码的作用是将之前的结果 res 与当前节点值的比较结果相与,确保之前的结果为true,并且当前节点的值也相等,才能保持整体的回文性质。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值