第一章:链表


链表的测试文件其实不需要太复杂,随便就写一个abcde五个节点就行,主要看能不能跑,你自己的测试输入肯定没有人家leetcode玩的花,交给leetcode测试就好


为什么会有这一系列文章: 虽然本站是2025年才搭建完,但是这一系列笔记是我刚上大学时候用obsidian记的笔记,当时还并没有图床,项目文件全部保存在本地,因此也用了许多obsidian里面的特性(比markdown和latex混用,还有莫名奇妙的跳转其实这个跳转的功能挺好使,待我研究研究怎么在halo上实现跳转到固定章节的功能) 因此有一些只有obsidian里面才有的特性,部分文章的部分如果看的不是很清楚的话还是请各位大脑渲染一下,鞠躬


推荐算法竞赛网站:USACO Training Gateway 给初高中做的


链表逆序

#easy

主要为单链表数据结构

题目如下:206. 反转链表 - 力扣(LeetCode)

主要思路

一次遍历链表节点,每次遍历一个节点即逆置一个节点

同时不要忘记备份

解决方法如下


struct ListNode* reverseList(struct ListNode* head){

    struct ListNode* head1 = head;//指向老的头节点指针

    struct ListNode* new_head = NULL;//建立新的头节点指针

    while(head1!= NULL)

    {

        struct ListNode* temp = head1->next;//给原先的节点的下一个节点进行备份

        head1->next =new_head;//交换次序

        new_head = head1;//移动新节点至原先节点处

        head1 = temp;//移动老节点

  

    }

    return new_head;//返回的是新的头节点

  

}

链表逆序PLUS

#medium

这下从之前的全部逆序变成部分的逆序

92. 反转链表 II - 力扣(LeetCode)

关键是找到4个节点

注意如果m = 1的话头节点就被改变,返回的头节点就不应该是最开始的头节点,最好的办法是建立一个result根据结果来判断result的值

解决方法如下


struct ListNode* reverseBetween(struct ListNode* head, int left, int right){

    int revers_len = right - left +1;//计算要被逆序的数值

    struct ListNode* pre_head = NULL;//被逆序之前的指针

    struct ListNode * result = head;//返回的结果指针,默认是head

    while(head&&--left){ //把指针控制到被逆序的附近

        pre_head = head;

        head = head->next;

    }

    struct ListNode* revers_tail = head;//指向被逆序的第一个指针,也是逆序完成之后的最后一个指针

    struct ListNode* new_head = NULL;//参考上一题的逆序

    while(head&&revers_len){

        struct ListNode*  temp = head->next;

        head->next = new_head;

        new_head = head;

        head = temp;

        revers_len--;

    }

    revers_tail->next = head;//再逆序完成之后,head指针并不是刚好停留再完成的最后一个指针,而是最后一个的下一个,因为最后一次循环head还会往前走

    if(pre_head){//如果指针为空说明是从头开始逆序的

        pre_head->next = new_head;//正常连接,此时result就是head

  

    }

    else{

        result = new_head;//result直接等于new_head

    }

return result;

  

}

求链表交点

#easy

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

这道题有两个方法,其中一个方法简单但是时间复杂度和空间复杂度都高

方法1

使用set容器 #set的相关知识

set容器只在C++语言里面有

  • set容器的相关知识

  • set.find()返回一个迭代器,如果没有再里面找到对应元素的话则返回的迭代器指向set的最后一个元素

所以思路1的方法就出来了

解决办法如下


class Solution {

public:

    ListNode getIntersectionNode(ListNode headA, ListNode *headB) {

        std::set<ListNode*> node_set;//设置查找集合node_set

        while(headA){

            node_set.insert(headA);//把headA的每一个元素都插入node_set

            headA = headA->next;//遍历链表A

        }

        while(headB){

        if(node_set.find(headB)!=node_set.end())//当在node_set里面找到第一个出现在node_set里面的节点的时候

        {

            return headB;

        }

        headB = headB->next;//遍历链表B

    }

    return NULL;

    }

};
方法2

由于set的时间复杂度是nlogn 空间复杂度是n

主要原因是我不会C++

这就有了第二个方法

思路如下

也就是说先找到长的链表,然后移动指针让两个链表一样长即可

BTW这个是_C语言_

代码如下


struct ListNode getIntersectionNode(struct ListNode headA, struct ListNode *headB) {

    int len_a = get_list_lenth(headA);

    int len_b = get_list_lenth(headB);

    if(len_a>len_b){//如果A比B长就前移A

        headA = forword_longlist(len_a,len_b,headA);

    }

    else{//反之亦然

         headB = forword_longlist(len_b,len_a,headB);

    }

    while(headA&&headB){//同时遍历两个链表

        if(headA==headB){

            return headA;//当相等的时候返回相同的链表

        }

        headA= headA->next;

        headB = headB->next;

    }

    return NULL;//全部遍历完成之后返回空

}

int get_list_lenth(struct ListNode*head);

int get_list_lenth(struct ListNode*head)//得到链表的长度,你应该看得懂

{

    int len = 0;

    struct ListNode* p = head;

    while(p){

        len++;

        p = p->next;

    }

    return len;

}

 struct ListNode forword_longlist(int long_len,int short_len,struct ListNode head);

struct ListNode forword_longlist(int long_len,int short_len,struct ListNode head)//把长链表的头节点前移

{

    int delta = long_len-short_len;

    while(head&&delta){

    head = head->next;

    delta--;

    }

    return head;//返回链表位置

}

链表求环

#medium

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

BTW这道题是链表求环的简化版141. 环形链表 - 力扣(LeetCode)

方法1

同样是用set容器 我同样不会C++

BTW保存的是地址而不是数据域别忘了

解决办法如下


class Solution {

public:

    ListNode detectCycle(ListNode head) {

        std :: set <ListNode*> node_set;  //设置node_set

        while(head){

            if(node_set.find(head)!= node_set.end()){

               return head;//返回环的第一个节点

  

            }

            node_set.insert(head);//节点数据全部插入node_set

            head = head->next;

        }

        return NULL;//没有遇到环就返回NULL

  

    }

};
方法2

方法二的空间和时间复杂度都低于方法1,而且不用C++

[[链表,栈,队列,堆的补充内容#快慢指针求环条件分析|代码优化]]

下面是思路

通过计算发现绿色区域和黄色区域长度永远相等

BTW这道题对NO.141来说只要相遇就可以说是有环存在了

  • 在这个时候,只需要让两个指针分别从head和meet同时出发就能在环的起点相遇

解决方法如下


struct ListNode detectCycle(struct ListNode head) {

    struct ListNode* fast = head ;

    struct ListNode* slow = head;//快慢指针

    struct ListNode * meet  = NULL; //相遇的指针,初始化为空

    while(fast){//链表无环且节点为偶数个,这里跳出循环

        slow = slow->next;//slow 和fast都先各走一部

        fast = fast->next;

        if(!fast)//链表无环,且节点个数为奇数个,这里返回

        {

            return NULL;

        }

        fast = fast->next;//快指针再走一步

        if(fast == slow)

        {

            meet = slow;//把相遇的meet指针定义到slow或者fast上

            break;

        }

    }

    if(meet == NULL){

        return NULL;

  

    }

    while(head&&meet){

        if(head == meet){//如果两个指针相遇

            return head;

  

        }

        head=head->next;

        meet = meet->next;

    }

    return NULL;

}

这里是分割线,然后是下节课


链表划分

#medium

86. 分隔链表 - 力扣(LeetCode)

已知链表头的head与数值X,将所有小于X的节点放在大于或者等于X的节点前

思路:巧用临时头节点

相当于把大于的放在一个链表,小于的放在一个链表,然后再连一起

解决方法如下


struct ListNode* partition(struct ListNode* head, int x){

    struct ListNode less_head = {0,NULL};

    struct ListNode more_head = {0,NULL};//设置两个临时头节点

    struct ListNode* less_ptr = &less_head;

    struct ListNode* more_ptr = &more_head;//设立临时指针

    while(head){

        if(head->val<x){//如果节点小于x,则改节点插入less_ptr后

        less_ptr->next = head;

        less_ptr = head;

        }

        else

        {//否则插入到more_ptr后面

           more_ptr->next = head;

           more_ptr =head;

        }

        head = head->next; //遍历链表

    }

    less_ptr->next = more_head.next;//less表尾部与more的头部相连

    more_ptr->next = NULL;//置空尾部节点指针

    return less_head.next;

  

}

复杂的链表的深度拷贝

#hard

138. 复制带随机指针的链表 - 力扣(LeetCode)

已知一个复杂的链表,节点中有一个指向本链表任意某个

节点的随机指针,也可以为空,求这个链表的深度拷贝

哎呀就是除了next指针之外还有一个random在这里瞎几把乱指

  • 深度拷贝:构造生成一个完全新的链表,即使原链表损毁,新链表仍可独立使用

需要用到的知识 #STLmap的使用 又tmd是C++

  • 映射

  • 地址

vector的使用 #vector的使用

vector 是 C++的一个顺序容器,它可以存储任意类型的动态数组。(包括链表)

push_back():在 vector 的末尾添加一个元素。

. pop_back():删除 vector 末尾的元素。

insert():在 vector 的指定位置插入一个或多个元素。

erase():删除 vector 中的一个或多个元素。

new 是 C++ 中用于动态分配内存的运算符。它会在堆上分配一块内存,大小为指定类型的大小,并返回一个指向该内存块的指针。

 std::vector<Node*> node_vec; 这句话就相当于定义了一个里面都是Node * 的空间

 node_vec.push_back(new Node(ptr->val)) 这句话就相当于在vector末尾开辟一个Node的节点,里面的val被初始化为ptr->val

map可以将一个元素的值映射为另一个元素的值

在这个例子里面,我们将节点的地址映射为节点的位置,这样我们可以通过节点的地址来找到对应节点的位置

通过这个方法,我们可以得到以下思路:

解决思路如下


class Solution {

public:

    Node* copyRandomList(Node* head) {

        std::map<Node* ,int> node_map;//地址到节点的map

        std::vector<Node*> node_vec; //使用vector来储存节点位置访问地址

        Node* ptr = head;

        int i = 0;

        while(ptr){//将新链表节点push进入node_vec生成了对应新链表节点位置到地址的map

        node_vec.push_back(new Node(ptr->val));

        node_map[ptr] = i;

        ptr = ptr->next;//遍历原始列表

        i++;

        }//i记录节点位置

        node_vec.push_back(0);

        ptr = head;

        i = 0; //再次遍历原始链表,链接新链表的next指针,random指针

        while(ptr){

            node_vec[i]->next = node_vec[i+1];

            if(ptr->random){//当random指针不空时

                int id = node_map[ptr->random];//根据node_map确认

            node_vec[i]->random = node_vec[id];

            }

            ptr = ptr->next;//链接新链表至random指针

            i++;

        }

        return node_vec[0];

    }

};

排序链表的合并(上)

#easy

21. 合并两个有序链表 - 力扣(LeetCode)

已知两个_已排序链表头节点指针l1和l2,将两个链表合并,合并后依然有序_ 返回合并后的头节点

思路

解决方法如下


struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){

    struct ListNode temp_head = {0};//设置临时头节点

    struct ListNode *pre = &temp_head;//使用pre指针

    while(list1&&list2){//l1和l2不空的时候进行比较

        if(list1->val < list2->val){//将pre与较小的节点进行连接

            pre->next = list1;

            list1 = list1->next;

  

        }

        else

        {

            pre->next = list2;

            list2 = list2->next;

  

        }

        pre=pre->next;//pre指向新连接的节点

    }

        if(list1){//如果l1有剩余

            pre->next = list1 ;//l1接到pre后

        }

        if(list2){

            pre->next = list2;

        }

    return temp_head.next;

}

排序链表的合并(下)

#hard

23. 合并 K 个升序链表 - 力扣(LeetCode)

从两个变成k个

传入的是所有链表头节点的vector ListNode* mergeKlists(std::vector<ListNode*> &lists)

方法1——暴力合并

不用看了,绝壁超时

方法2——先排序后相连

注意sort函数需要一个判断函数

方法2的解决办法


 bool cmp(const ListNode*a,const ListNode*b){

     return a->val < b->val;

 }

class Solution {

public:

    ListNode* mergeKLists(vector<ListNode*>& lists) {

        std::vector<ListNode*>node_vec;

        for(int i =0;i<lists.size();i++){

            ListNode* head = lists[i]; //遍历k个链表,将节点全部添加至Node_vec

            while(head){

                node_vec.push_back(head);

                head = head->next;

  

            }

        }

        if(node_vec.size() == 0){

            return NULL;  //根据节点数值进行排序

  

        }

        std::sort(node_vec.begin(),node_vec.end(),cmp);

        for(int i = 1;i<node_vec.size();i++){

 //连接新的链表

        node_vec[i-1]->next = node_vec[i];

        }

        node_vec[node_vec.size()-1]->next = NULL;

        return node_vec[0];

    }

};

其node_vec.size()返回的是node_vec里面节点的数量

值得一提的是,在这一段代码里面,sort函数会把里面所有的节点进行整体排序,但不会改变连接关系举个例子,假设有两个链表,第一个链表为 1->4->5,第二个链表为 2->3->6。在将这两个链表中的所有节点添加到 node_vec 向量后node_vec 的内容为 [1, 4, 5, 2, 3, 6]。

用 std::sort(node_vec.begin(),node_vec.end(),cmp); 对 node_vec 进行排序。排序后node_vec 的内容变为 [1, 2, 3, 4, 5, 6]。但是,这并不会改变原来两个链表中节点之间的连接关系。也就是说,第一个链表仍然是 1->4->5,第二个链表仍然是 2->3->6。

方法3——分治后相连 #分治思想

详解在[[第四章:递归,回溯和分治#分治算法的归并排序|分治算法]]

最优的算法

方法3的解决办法


class Solution {

public:

    ListNode* mergeKLists(vector<ListNode*>& lists) {

        if(lists.size() == 0){

            return NULL ; //如果lists为空,则返回NULL

  

        }

        if(lists.size() == 1){

            return lists[0];

        }

        if(lists.size() == 2){//如果只有两个list,调用两个list merge函数

            return mergeTwoLists(lists[0],lists[1]);

        }

        int mid = lists.size()/2;

        std::vector<ListNode*>sub1_lists;//拆分为两个子lists

        std::vector<ListNode*>sub2_lists;

        for(int i =0;i<mid;i++){

            sub1_lists.push_back(lists[i]);

        }

        for(int i =mid;i<lists.size();i++){

            sub2_lists.push_back(lists[i]);

        }

        ListNode* l1 = mergeKLists(sub1_lists);

        ListNode* l2 = mergeKLists(sub2_lists);

    return mergeTwoLists(l1,l2);//分治处理

    }

};

关于其中mergeTwolists函数参考[[第一章:链表#排序链表的合并(上)|双有序链表合并]]

实际上分治的思想就是把大问题拆分成小问题,然后用递归的思想来把小问题的结果加起来

好耶,下课了