优选算法【专题八:链表】

目录

1、链表类算法题常用技巧:

2、2.两数相加

3、24.两两交换链表中的节点

4、143.重排链表

5、23.合并K个升序链表

6、25.K个一组翻转链表


1、链表类算法题常用技巧:

【技巧】

1、画图!!!!!--->直观 + 形象 + 便于我们理解

2、引入虚拟头节点

链表类型的算法题一般都是不带头节点的单向链表,一般都是从第一个位置开始就已经存储有效数据了。 这类链表一般需要考虑很多的边界情况,所以给链表引入头节点,这个节点不存储数据,只是起到一个哨兵的作用。 好处:1)便于处理边界情况 2)方便我们对链表进行操作

3、不要吝啬空间,大胆去定义变量。

直接定义一个指针,就不用去担心语句的执行顺序,因为节点已经被保存起来了,不用害怕丢失,这样写就会很方便。

4、快慢双指针

特别好用在:判断链表是否有环,或者找链表环的入口、找链表倒数第n个节点

【链表中的常用操作】

1、创建一个新节点new

2、尾插:先定义一个变量指向尾节点,tail->next = 新节点 tail 指向新节点,方便下一次尾插(初始化尾指针的时候尾指针指向最后一个节点)

3、头插 先来一个虚拟头节点,然后让新节点的next指向虚拟头节点的next(不用担心虚拟头节点的next是一个null,是null不会影响,新节点指向空也是ok的),然后让虚拟头节点的next指向新节点即可 【头插其实可以直接完成逆序链表这道题】

只需定义一个cur,再完成头插即可实现逆序链表

【注意链表题一定要特别注意空节点!!!!!】

2、2.两数相加

题目解析:

2、4、3

5、6、4

因为是逆序存储的所以:342+465=807

逆序存储,返回708

这个题逆序其实是方便我们操作了,因为我们是从最低位开始相加的,刚好对应链表的2、5

【可以去牛客网去做链表相加Ⅱ这道题】-----相似

先来一个虚拟头节点(newhead),再初始化一个t用来存储两数加的结果,把cur1的值加到t中,把cur2的值加到t中,然后我求出t的个位,创建一个新节点,把这个数放进去,链接到虚拟头结点的后面。

cpp 复制代码
/**
 * 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;
            }
            //此时,cur1+cur2已经加完了,就把这个数的个位尾插到结果链表中
            prev->next = new ListNode(t % 10);//只把个位放进来
            prev = prev->next;//尾指针往右移动 方便下一个位置进来
            t /= 10;//把个位干掉剩下的就是进位
        }
        prev = newhead->next;//先存一下,再释放
        delete newhead;//释放内存防止内存泄露
        return prev;
    }
};

3、24.两两交换链表中的节点

cpp 复制代码
/**
 * 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;
        //*next = cur->next cur指针必须存在,又因为cur指向的是head所以先给head判空,*nnext = next->next,所以next也不能为空,如果next为空那么链表中就只有一个数head,也就不用交换 --保证不会出现空指针的解引用
        ListNode* prev = newhead, *cur = prev->next, *next = cur->next, *nnext = next->next; 
        while(cur && next)//cur和cur都不等于空
        {
            //交换节点
            prev->next = next;
            cur->next = nnext;
            next->next = cur;
            //修改指针---按照原来指针从前到后的位置重新定义指针
            prev = cur; //注意顺序
            cur = nnext;
            if(cur) next = cur->next;//cur为空的时候不能解指针
            if(next) nnext = next->next;
        }
        cur = newhead->next;
        delete newhead;
        return cur;

    }
};

4、143.重排链表

题意:输出第一个,倒数第一个,第二个,倒数第二个这样输出

算法思想就是进行模拟

1、找到链表的中间节点 (使用快慢指针)

2、把后面的部分逆序(头插法)

3、合并两个链表----之前做过题合并两个有序链表----(使用双指针)

分类讨论:当链表元素有奇数个的时候,slow指针刚好就是指向链表中间的,但是当元素个数为偶数个的时候,slow指向中间偏右的位置。

我们需要对后半部分的链表进行反转操作:1、将slow->next(也就是slow之后进行反转)

2、包含slow所指向的节点一起反转,这两种对于奇数个和偶数个都是适用的。

只是,第二种方式对于奇数个元素时,要把head指向slow(head的下一个指针)要拆开,因为我们要将链表分为两部分。这样拆其实会发生错误,解决这种错误的方法就是给链表加一个虚拟头节点。

第二步:反转--->头插----虚拟头节点

第三步:合并--->尾插---虚拟头节点+尾指针

cpp 复制代码
/**
 * 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后面的节点插到head2的后面
        slow->next = nullptr;//因为slow指向第一个链表的最后一个元素,所以让slow的next为空,就可以把链表断开
        while(cur)
        {
            //进行头插操作
            ListNode* next = cur->next;//先把cur的下一个位置保存一下,防止丢失
            cur->next = head2->next;
            head2->next = cur;
            cur = next;//让cur继续向后移动
        }
        //3、合并两个链表--使用双指针 -- 创建一个虚拟头节点然后把节点都放在虚拟头节点的后面
        ListNode* ret = new ListNode(0);
        ListNode* prev = ret;//合并两个链表需要尾插,所以需要一个尾指针 一直尾插到prev的后面就可以了
        ListNode* cur1 = head, *cur2 = head2->next;//用两个指针分别指向两个分开的链表的第一个有效元素
        while(cur1) //仅需判断cur1不为空即可 --- cur1比较长
        {
            //放入第一个链表的元素
            prev->next = cur1;
            cur1 = cur1->next;
            prev = prev->next;
            //放入第二个链表的元素 
            if(cur2) //因为第二个链表比第一个链表短,所以要防止cur2为空的情况,不为空才向后挪动
            {
                prev->next = cur2;
                cur2 = cur2->next;
                prev = prev->next;
            }
        }
        //释放所有new出来的节点
        delete head2;
        delete ret;
    }
};

5、23.合并K个升序链表

利用优先级队列:

创建一个小根堆,使用k个指针,先指向第一个节点,然后把这些元素全放进小根堆当中,拿出堆顶的元素,这个堆顶元素肯定是输入的k个链表中某个节点,如果这个节点的后面还有节点就放到堆里。

cpp 复制代码
/**
 * 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 
{
    //实现小根堆的比较函数
    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;
    }
};

6、25.K个一组翻转链表

1、先求出需要逆序多少组:n = 长度/k

2、重复n次,长度为k的链表的逆序即可----链表逆序用头插法

但是需要注意:

当反转完一组之后,进入下一组的时候,我们不能把下一组的元素放到上一组的前面(因为我们在利用头插来反转),所以当我们进入每组的反转循环的时候,就要先记录一下第一个节点,然后当这组反转完之后,把下组放在这个节点的后面开始头插。

cpp 复制代码
/**
 * 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;//cur从head位置开始
        for(int i = 0; i < n; i++)//n次
        {
            //每次头插之前要记录第一个节点 就是下一次头插的前驱
            ListNode* tmp = cur;
            for(int j = 0; j < k; j++)//长度为k
            {
                //头插之前先用一个变量记录一下cur的下一个位置
                ListNode* next = cur->next;
                cur->next = prev->next;
                prev->next = cur;
                cur = next;
            }
            //第一组反转完之后,跟新prev就知道下一次头插的前驱 就是头插之前记录的
            prev = tmp;
        }
        //两个for循环之后就剩下后面不成一组的,就不需要反转
        prev->next = cur;//把不反转的接上
        cur = newhead->next;//我们把结果放在newhead的后边,但是newhead需要进行释放,所以先把他的next存到cur,返回cur即可 虚拟头节点的下一个进行返回
        delete newhead;
        return cur;
    }
};
相关推荐
Prince-Peng7 小时前
技术架构系列 - 详解Redis
数据结构·数据库·redis·分布式·缓存·中间件·架构
码农水水7 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
-Try hard-8 小时前
数据结构:链表常见的操作方法!!
数据结构·算法·链表·vim
wengqidaifeng8 小时前
数据结构---顺序表的奥秘(下)
c语言·数据结构·数据库
嵌入小生0078 小时前
单向链表的常用操作方法---嵌入式入门---Linux
linux·开发语言·数据结构·算法·链表·嵌入式
千谦阙听8 小时前
数据结构入门:栈与队列
数据结构·学习·visual studio
定偶8 小时前
MySQL知识点
android·数据结构·数据库·mysql
dazzle8 小时前
Python数据结构(十二):插入排序详解
数据结构·python·排序算法
希望有朝一日能如愿以偿9 小时前
力扣每日一题
数据结构·算法·leetcode
草履虫建模9 小时前
力扣算法分析 27.移除元素
java·开发语言·数据结构·后端·算法·leetcode·排序算法