算法——链表

链表常用技巧

  1. 画图分析!!!!!!!!!!——直观形象,便于理解、大多数都是模拟
  2. 引入虚拟头结点(哨兵位)
    1. 典型的就是在第一个节点传空指针,此时我们如果解引用,程序直接崩掉

image.png

  1. 我们一般选择创建哨兵位头结点,这里我们哨兵位不存储数据。解释传入空链表,这里的newhead只需要特判一下即可image.png

  2. 不要吝啬空间,大胆定义变量。

    1. 我们经常会遇到这种题目,将cur插入到prev后面。途中四句是正确的顺序,在考试题目中经常会遇到将这四句顺序打乱的情况。其实我们只需要保证前两句先执行即可(五角星标志的),这样是为了防止prev找不到next。如果先执行后两句,那么prev链接cur之后,就找不到next节点

image.png

  2. 我们也可以定义next指针,这样就不需要考虑下面四句语句的执行顺序

image.png

  1. 链表快慢双指针,典型题目有:判环;找链表中环的入口;找链表中倒数第n个节点

链表中常用操作

  1. 创建一个新节点 new
  2. 尾插:先定义一个指针tail指向最后一个位置,让tail->next 指向新节点,然后将尾结点转移到新节点上。image.png
  3. 头插:让新节点的next指向newhead->next,此时如果之前给的链表为空,让新节点next指向空,不会发生访问空节点的报错。然后让newhead->next指向新节点。如果怕链表断开,可以新定义一个next指针。我们也可以利用这个方法完成逆序链表

image.pngimage.png

两数相加

两数相加

题目解析

image.png
这里给我们逆序存储链表反而有利于我们做题,我们相加时是从最低位开始加,如果题里给我们正常顺序存储的链表的话,我们还需要自己逆置。

算法原理

解法:模拟两数相加的过程

  • 这里创建一个newhead用于链接最终结果。如果这里我们不用newhead链接,我们需要先把两个链表头结点相加然后存储新的head用来记录结果(因为最后要返回链表),然后再继续往后相加。这样我们操作就多了一步。

image.png

  • 用一个变量t来标记我们的进位,然后定义两个指针模拟加法。只要cur1、cur2不为空,就把他们指向的数字移到t中,然后把t中个位数字提出来(因为数字相加可能变为两位数,牵扯到进位),然后把new一个节点7,链接到到我们的newhead后面。同理…

image.pngimage.png

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) 
    {
    ListNode* cur1 = l1, *cur2 = l2;
    ListNode* newhead = new ListNode(0); // 创建⼀个虚拟头结点,记录最终结果
    ListNode* prev = newhead; // 尾指针
    int t = 0; // 记录进位
while(cur1 || cur2 || t)
{
 // 先加上第⼀个链表
    if(cur1)
    {
    t += cur1->val;
    cur1 = cur1->next;
    }

// 加上第⼆个链表
    if(cur2)
    {
    t += cur2->val;
    cur2 = cur2->next;
    }
  prev->next = new ListNode(t % 10);
  prev = prev->next;
  t /= 10;
}
    prev = newhead->next;
    delete newhead;
    return prev;
    }
};

两两交换链表中的节点

两两交换链表中的节点

题目解析

  • 给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

image.png

算法原理

解法:循环迭代:

  • 这里同样引入一个虚拟节点newhead链接链表。如果不引入头结点,在我们交换前两个节点的时候,因为前面没有节点。所以我们要先定义一个变量找到最终要返回的头结点。因为前半部分1、2两个节点是不需要将修改将前面的指针指向2,但是后面部分3、4需要交换过后,让前面位置的指针指向4链接。这时需要写两段代码分别写两种情况。image.png
  • 不要吝啬我们的空间,定义四个指针,紫色情况是交换1、2,绿色情况是交换3、4image.png
  • 当3、4交换完成时,四个指针移动如下情况:
    • 当链表是奇数个节点,这时cur有可能访问空指针,所以我们需要终止循环image.png
    • 当链表是偶数个节点时,next指向空,需要终止循环。image.png

所以终止循环条件:cur或者next有一个指向空的时候,就终止循环

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
    if(head == nullptr || head->next == nullptr) return head;
    ListNode* newHead = new ListNode(0);
    newHead->next = head;
    ListNode* prev = newHead, *cur = prev->next, *next = cur->next, *nnext =next->next;
    while(cur && next)
    {
// 交换结点
    prev->next = next;
    next->next = cur;
    cur->next = nnext;
// 修改指针
    prev = cur; // 注意顺序
    cur = nnext;
    if(cur) next = cur->next;
    if(next) nnext = next->next;
    }
    cur = newHead->next;
    delete newHead;
    return cur;
    }
};

重排链表

重排链表

题目解析

  • 第一个、倒数第一个、第二个、倒数第二个image.png

算法原理

**解法:模拟——**其实很像两个链表合并,一个是正向的1、2,一个是倒向的链表4、3。恰好是原始链表的前半部分,和后半部分逆序。

所以我们的策略是:先找到链表的中点,然后把后面部分的链表逆序;然后把前面的部分和后面的部分一个一个交互链接,合并一起即可。image.png

  1. 找到链表的中间节点(⼀定要画图考虑 slow 的落点在哪⾥ )

    1. 利用快慢双指针。这里注意一下节点个数是奇数和偶数的情况。slow=slow->next;fast=fast->next->next;我们的停止循环的条件是当fast或fast->next为空。如下图分别是节点个数为奇数和偶数的情况。此时观察slow落在中点的情况:
      1. 我们可以让slow->next部分直接翻转。这里我们可能会有疑惑,难道不应该让slow后面的部分整体翻转吗?这是这道题的特殊性,我们还是用1234进行举例子,我们发现重排前和重拍后发现2和3的链接部分是不变的,所以我们可以把slow指向的这个节点大胆给第一个链表。重排后发现是不影响结果的。

image.png

  2. 我们也可以让slow所指的开始翻转(红色笔标注的)。此时无非是把slow所指的节点丢给后半部分,逆序合并后发现结果并不影响![image.png](https://img-blog.csdnimg.cn/img_convert/77049b1c441cb9b3336de5f341cee1c9.png)
  1. 用头插法,如果这里使用头插法,建议使用第一个方法(slow->next翻转)。因为最终是断开成两个链表,
    1. 如果从slow所指的位置开始逆置,会发现箭头所指的位置指针无法断开。有可能出现意想不到的错误;

image.png

  2. 当我们选择slow->next位置开始翻转,我们只需让slow所指的位置置空即可,

image.png

  3. 如果我们只能用slow位置开始翻转的情况时,我们可以引入一个虚拟头结点newhead(哨兵位),去链接head,然后在快慢指针找中间节点,之前是slow位置,就会变成slow->next,这时候也能将两个链表断开。
  1. 把slow后面部分逆序——头插法(使用虚拟头结点)
    • 把slow后面的节点,头插到新建的头(哨兵位)结点后面。当让cur头插到newhead后面时,先让cur->next指向空(就是newhead2后面的节点),然后newhead2->next指向cur,此时newhead2就与null(原先后面的节点)断开
    • 当newhead2后面有节点时,按照上面的操作我们会发现,newhead2会与后面的断开了,所以在刚开始头插时我们要定义一个next指针标记newhead2后面的节点。当执行完上面操作后,让cur=nextimage.png
  2. 合并两个链表
    1. 我们是创建一个虚拟节点,让他们都尾插在这后面
    2. cur1相较于cur2会长一些,所以循环判空条件为cur1为空。

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    void reorderList(ListNode* head) 
    {
    // 处理边界情况
    if(head == nullptr || head->next == nullptr || head->next->next == nullptr)
    {
    	return ;
    }
    
 // 1. 找到链表的中间节点 - 快慢双指针(⼀定要画图考虑 slow 的落点在哪⾥)
    ListNode* slow = head, *fast = head;
   
    while(fast && fast->next)
    {
     slow = slow->next;
     fast = fast->next->next;
    }

// 2. 把 slow 后⾯的部分给逆序 - 头插法
    ListNode* head2 = new ListNode(0);
    ListNode* cur = slow->next;
    slow->next = nullptr; // 注意把两个链表给断开   
    while(cur)
    {
        ListNode* next = cur->next;
        cur->next = head2->next;
        head2->next = cur;
        cur = next;
    }

// 3. 合并两个链表 - 双指针
    ListNode* ret = new ListNode(0);
    ListNode* prev = ret;
    ListNode* cur1 = head, *cur2 = head2->next;
    while(cur1)
    {
// 先放第⼀个链表
    prev->next = cur1;
    cur1 = cur1->next;
    prev = prev->next;

// 再放第⼆个链表
        if(cur2)
        {
        prev->next = cur2;
        prev = prev->next;
        cur2 = cur2->next;
        }
    }
delete head2;
delete ret;
}
};

合并k个升序链表

合并k个升序链表

题目解析

image.png

算法原理

**解法一:暴力解法:**逐个合并.其中n是链表的长度,k为链表的个数image.png

解法二:优先级队列做优化
我们可以直接定义k个指针,仿照合并k个有序链表,把这些链表中较小的头结点放到newhead后面,放完之后右移,继续比较各个节点最小的,把最小的连接在newhead后面即可。这样只需要遍历一遍链表即可。O(n*k)是理想状态下的时间复杂度,我们还要找出k个节点中最小的节点。这里我们可以利用优先级队列

把k个节点丢入小根堆里,经过排序后,堆顶元素即为最小的元素,链接到newhead后面后,将移动后下一个节点放入小根堆里。这里时间复杂度O(nklogk)
image.png

解法三:分治——递归
流程:

  1. 特判,如果题⽬给出空链表,⽆需合并,直接返回;
  2. 返回递归结果。


递归函数设计:

  1. 递归出⼝:如果当前要合并的链表编号范围左右值相等,⽆需合并,直接返回当前链表;
  2. 应⽤⼆分思想,等额划分左右两段需要合并的链表,使这两段合并后的⻓度尽可能相等;
  3. 对左右两段分别递归,合并[l,r]范围内的链表
  4. 再调⽤mergeTwoLists函数进⾏合并(就是合并两个有序链表)

时间复杂度:一共k个链表,每个链表里面有n个节点,每一层我们都平均分,即高度为logk,每一个节点都执行了logk次合并(这棵树的高度次)image.png

代码实现

class Solution
{
struct cmp
 {
    bool operator()(const ListNode* l1, const ListNode* l2)
    {
       return l1->val > l2->val;
    }
 };

public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
    // 创建⼀个⼩根堆
 priority_queue<ListNode*, vector<ListNode*>, cmp> heap;

// 让所有的头结点进⼊⼩根堆
    for(auto l : lists)
        if(l) heap.push(l);

// 合并 k 个有序链表
ListNode* ret = new ListNode(0);
ListNode* prev = ret;
while(!heap.empty())
 {
    ListNode* t = heap.top();
    heap.pop();
    prev->next = t;
    prev = t;
    if(t->next) heap.push(t->next);
 }

prev = ret->next;
delete ret;
return prev;
}
};
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution
{
public:
    ListNode* mergeKLists(vector<ListNode*>& lists)
    {
        return merge(lists, 0, lists.size() - 1);
    }

    ListNode* merge(vector<ListNode*>& lists, int left, int right)
    {
        if(left > right) return nullptr;
        if(left == right) return lists[left];

    // 1. 平分数组
    int mid = left + right >> 1;

    // [left, mid] [mid + 1, right]
    // 2. 递归处理左右区间
    ListNode* l1 = merge(lists, left, mid);
    ListNode* l2 = merge(lists, mid + 1, right);

    // 3. 合并两个有序链表
    return mergeTowList(l1, l2);
    }

    ListNode* mergeTowList(ListNode* l1, ListNode* l2)
    {
        if(l1 == nullptr) return l2;
        if(l2 == nullptr) return l1;
        
        // 合并两个有序链表
        ListNode head;
        ListNode* cur1 = l1, *cur2 = l2, *prev = &head;
        head.next = nullptr;
    while(cur1 && cur2)
    {
        if(cur1->val <= cur2->val)
        {
            prev = prev->next = cur1;
            cur1 = cur1->next;
        }

        else
        {
            prev = prev->next = cur2;
            cur2 = cur2->next;
        }
    }

    if(cur1) prev->next = cur1;
    if(cur2) prev->next = cur2;
    return head.next;
    }
};

k个一组翻转链表

k个一组翻转链表

题目解析

链表中的节点每k个一组进行翻转,如果剩余的链表不够k个,则不翻转image.png

算法原理

解法:模拟

  1. 先求出需要逆序多少组n:遍历链表一遍 n=链表节点个数/k
  2. 重复n次长度为k的链表的逆序
    1. 利用头插法逆序,创建一个newhead。头插时,cur->next=head->next;head->next = cur;此时右边节点(2)会丢失,需要先用一个next指针记录一下节点,然后头插。头插完之后cur++,直至头插3之后,这一组完成。

image.png

  1. 进行下一组头插时,需要将4头插到1的后面,所以需要一个指针标记一下1的位置,这个位置是每一组开始头插的第一个位置。所以每组第一个位置开始头插的时候,要标记好开始头插的位置(tmp),还需要定义个prev标记将要头插位置的前驱,每当一组头插完,该进行下一组时,让prev更新到tmp位置。image.pngimage.png

代码实现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
    // 1. 先求出需要逆序多少组
    int n = 0;
    ListNode* cur = head;
    while(cur)
    {
        cur = cur->next;
        n++;
    }
    n /= k;

 // 2. 重复 n 次:⻓度为 k 的链表的逆序即可
    ListNode* newHead = new ListNode(0);
    ListNode* prev = newHead;
    cur = head;
    for(int i = 0; i < n; i++)
    {
        ListNode* tmp = cur;
        for(int j = 0; j < k; j++)
        {
        ListNode* next = cur->next;
        cur->next = prev->next;
        prev->next = cur;
        cur = next;
        }

        prev = tmp;
    }

// 把不需要翻转的接上
	prev->next = cur;
	cur = newHead->next;
	delete newHead;
	return cur;
	}
};
  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

裙下的霸气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值