8.算法与数据结构——指针与链表

链表简介

我们知道,如果申请一块儿连续的存储来储存数据的话,一旦遇到插入和删除操作,这个时候后续所有数据都需要向前或者向后移动,这种时间上的开销甚至可能达到O(N)。
所以我们想到用不连续的存储,即用链表来存储,每个链表节点中,含有两部分,第一部分是值部分,第二部分是指针部分,通常记作NEXT指针,NEXT指针指向下一个节点的的位置。如果其是最后一个节点,NEXT则指向NULL。
我们来看一下leetcode默认的链表表示方法:

struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};

这是一种结构体,类型名为ListNode,其内包含一个值 val,和一个ListNode类型的指针NEXT,指向下一个节点。再往下就是其构造函数。

节点的插入与删除

  • 删除:比如删除第三个元素,我们首先需要修改第二个元素的指针,使其指向第四个元素,然后回收第三个元素的内存。但是我们要注意一下这些问题:如果某一个元素本身不存在或者存在很多个怎么办?如果这个要被删除的元素在第一个或者最后一个怎么办?
  • 插入:比如在23之间插入,使用malloc调用从系统中得到一个新的单元,然后调整元素2指向要插入的元素,要插入的元素再指向元素3即可。同理,我们要注意如果这个位置在第一个和最后一个怎么办?

出现的问题

  1. 表的起始端进行插入或者删除的时候,是一种很特殊的情况,因为其改变了表起始的节点
  2. 对当前节点进行疏忽的操作会造成表的丢失,比如内存或指针出错。

解决方法:

1.哑节点(虚拟节点)/表头:
我们在第一个位置之前(即位置0)多留一个节点,使其指向当前链表的头节点。这样即使我们删除了整个链表,表头这个节点仍然存在。
2.尽量处理当前节点的下一个节点,而非当前节点本身
这个想法和我们做dp动态规划的时候指针标识相似,我们回忆一下,我们做dp的时候,大多时候dp【0】都是一个没有实际用处却需要初始化的对象,因为我们理想的从第一个数开始处理,但是创建的数组或者vector却是从0开始的。所以很多人在写dp的时候,会如下这样写,每次处理上一个节点。

for(int i=1...)
{
	dp[i-1]....
}

同样的,我们在进行链表遍历的时候,每次可以对i+1进行操作。

链表的基本操作:

一.链表翻转——206. 反转链表

链表翻转最主要的内容就是改变指针指向,
注意
如果他本身就是张空表,我们直接返回即可。

非递归写法:

我们需要两个指针,一个指向当前节点prev,一个指向下一个节点next。
每次先获取当前节点的next,把他记录下来(为什么要先做这一步,你可以想象一下, 如果我们成功的把某一根箭头反转了,那么原本下一个位置依靠于当前节点的next,如果我们改变了)
主要步骤:初始化,改向,右移,初始化

  1. 告诉当前节点(head)pre在哪
  2. 开始循环,先保存下一个节点next,避免翻转后丢失下一个节点的地址
  3. 改向,修改head->next
  4. 为下一次更改做准备,先告诉其pre在哪,再把当前节点右移(即next变成了head)
  5. 循环结束的时候,链表已经翻转,此时返回第一个节点
/**
 * 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* reverseList(ListNode* head) 
    {
        //head表示当前节点,pre表示上一个节点,next表示下一个节点
        if(!head) return head;//head为表头的指针,指向第一个节点,如果其为null(即0),那么这个表只有一个表头(空表),我们返回即可
        ListNode *pre,*next;
        pre=nullptr;//为什么我们要初始化pre,因为第一个节点的pre翻转过后就是最后一个节点的指向,即nullptr
        while(head)
        {
            next=head->next;//next先把下一个地址存下来,避免改向后丢失
            head->next=pre;//更改当前节点的next指针指向前一个元素(或者空)
            //现在改向已经完成,接下来右移即可,即接下来需要初始化下一个节点的pre和下一个节点自己在哪
            pre=head;//下一个节点的pre就是当前的节点head
            head=next;//注意不能先改head,如果先改head,下一个节点必然找不到当前节点next了
        }
        //注意,我们此时已经翻转了链表,需要返回的是表头的指针。即最后一次循环的pre
        return pre;

    }
};

递归写法
递归写法在函数参数中得先告诉其pre为多少

ListNode* reverseList(ListNode *head,ListNode *pre=nullptr)
{
//递归写法先考虑循环结束时条件,即head指向了空
if(!head) return pre;

//每次记录下个节点,改向
ListNode *next=head->next;
head->next=pre;
return reverseList(next,head);//告诉下一个节点pre是当前的head,自己的位置在当前的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* reverseList(ListNode* head) 
    {
        if(head==nullptr ||head->next==nullptr)//如果head本身是空或者是尾部节点,返回
        {
            return head;
        }
        ListNode* newhead=reverseList(head->next);//一直递归到5返回,即在第4层的时候,得到了第5层返回回来的新的头节点,注意当前是第4层
        head->next->next=head;
        head->next=nullptr;
        return newhead;

    }
};

二.链表合并——21. 合并两个有序链表

8.算法与数据结构——指针与链表_第1张图片
创建两个指针dummy和node,两个开始都指向表头,最后dummy在表头,node指在表尾。
思路时分两个阶段,
第一阶段l1和l2任意一个都没走完。
第二阶段走完了其中一个。

第一阶段只需要比较每次l1和l2的大小,然后谁小就让node-next指向谁,随后node和小的都后移。知道某一条走完了。
这时比如说l1走完了,那么l1此时必定指向空,此时我们只需要让node->next指向非空的那一个就行,l1?判定为0,所以我们选l2.
非递归写法

/**
 * 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* mergeTwoLists(ListNode* l1, ListNode* l2) 
    {
        ListNode *dummy=new ListNode(),*node=dummy;//dummy作为表头,node作为最后一个节点
        while(l1 &&l2)//先并完某一条
        {
            if(l1->val <= l2->val)
            {
                node->next=l1;
                l1=l1->next;
            }
            else
            {
                node->next=l2;
                l2=l2->next;
            }
            node=node->next;
        }
        node->next=l1 ? l1:l2;//剩下的指向哪个就接着指哪个
        return dummy->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* mergeTwoLists(ListNode* l1, ListNode* l2) 
    {
        if(!l1) return l2;
        else if(!l2) return l1;
        
        if(l1->val > l2->val)//l1>l2,即从l2指向l1
        {
            l2->next=mergeTwoLists(l1,l2->next);
            return l2;
        }
        //else
            l1->next=mergeTwoLists(l1->next,l2);
            return l1;
        
    }
};

三.链表交换操作——24. 两两交换链表中的节点

8.算法与数据结构——指针与链表_第2张图片
每次记录123节点为q1 q2 q3
递归写法:
我们首先确定递归的终止条件,就是q1已经为空或者q1已经到了4(即q1->next为空)
每次让q2->next=q1 即完成了2指向1,到最后返回q2就是头节点即要的结果。
那么q1指向谁呢
q1指向递归返回的下一个调整了顺序的头节点,即指向4(所以q1->next=swapxx()这个括号里应该是下一个q1,即此时的q3)

/**
 * 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 || !head->next) return head;
        ListNode *q1=head,*q2=q1->next,*q3=q2->next;
        q2->next=q1;
        q1->next=swapPairs(q3);
        return q2;
    }
};

非递归写法:

/**
 * 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 || !head->next) return head;
        ListNode *dummy=new ListNode();
        dummy->next=head;
        ListNode *t=dummy;
        while(t->next && t->next->next)//注意这里为什么要加t->next->next,因为如果他的个数是奇数,比如最后多了一个5,也不需要交换就直接结束了
        {
            ListNode *q1=t->next,*q2=q1->next;
            t->next=q2;
            q1->next=q2->next;
            q2->next=q1;
            t=q1;
        }
        return dummy->next;
        
    }
};

四.判断链表是否相交——160. 相交链表

8.算法与数据结构——指针与链表_第3张图片

我们想象一下,如果两个链表长度相同,那么他们以相同的速度循环移动(即到了尾部就回到对方的头节点,一定要回到对方的头节点,否则他们的间距不会改变),必定能在交点碰见。
如果长度不同呢?
我们来看一下示例一:链1的长度为2,链2的长度为3,链3(假设右半部分记为链3,其并不是一个单独的链表)的长度为3.

  1. A4 B5
  2. A1 B0
  3. A8 B1
  4. A4 B8//此时B落后A一个身位,但是由于A短1,我们在此刻就能知道此后的某一刻他们必定能碰见
  5. A5 B4
  6. A5 B5//A回到B 的头节点,而B跑到末尾节点
  7. A0 B4//B回到A 的头节点
  8. A1 B1
  9. A8 B8//此时相遇

共移动8次,由此我们推出:
记左上链长A,左下链长B,右链长C
必定在A+B+C次移动之后能相遇。(注意特殊情况如果有一方为空就一定相遇不了了)

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 
    {
        if(!headA ||!headB) return NULL;
        ListNode *a=headA,*b=headB;
        while(a!=b)
        {
            a=a? a->next:headB;
            b=b? b->next:headA;
        }
        return a;
        
    }
};

五.链表排序

插入排序——147. 对链表进行插入排序

8.算法与数据结构——指针与链表_第4张图片

/**
 * 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* insertionSortList(ListNode* head) 
    {
        if(!head ||!head->next) return head;

        ListNode *dummy=new ListNode();
        dummy->next=head;

        ListNode *cur=head->next,*last=head;
        while(cur)
        {
           if(cur->val>=last->val)
           {
               last=last->next;
           }
           else
           {
               ListNode *pp=dummy;
               while(cur->val>pp->next->val)
               {
                   pp=pp->next;
               }
               //将cur插入到pp和pp->next之间
               last->next=cur->next;
               cur->next=pp->next;
               pp->next=cur;
           }
            cur=last->next;
        }
        return dummy->next;



    }
};

归并排序

  1. 找到中点(快慢指针)
  2. 分别排序两个子链(直到每个长度为一)
  3. 合并子链
  4. 递归

注意最好把链断开。避免无限递归。

/**
 * 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* sortList(ListNode* head) 
    {
        if(!head || !head->next) return  head;//为空或者只有一个
        auto slow=head,fast=head;
        while(fast->next &&fast->next->next)
        {
            slow=slow->next;
            fast=fast->next->next;
        }
        //把链断开
        fast=slow->next;
        slow->next=nullptr;

        return mergelist(sortList(head),sortList(fast));
    }

    ListNode* mergelist(ListNode* head1,ListNode* head2)//合并两个有序链表
    {
        ListNode* dummy=new ListNode();
        ListNode* temp=dummy,*temp1=head1,*temp2=head2;
            while(temp1 &&temp2)
            {
                if(temp1->val<temp2->val)
                {
                    temp->next=temp1;
                    temp1=temp1->next;
                }
                else
                {
                    temp->next=temp2;
                    temp2=temp2->next;
                }
                temp=temp->next;
            }
            temp->next=temp1? temp1:temp2;
        
        return dummy->next;
    }
};

练习

2.两数相加

8.算法与数据结构——指针与链表_第5张图片
8.算法与数据结构——指针与链表_第6张图片

我们的主要思想很简单,先统计双方长度len1 len2,然后补全短的一方
注意示例三想告诉我们,最后的长度len,并不一定等于两者中的一个,因为进位的话会多一位

/**
 * 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 *dummy1=l1,*dummy2=l2;
        //先统计每个链表的长度
        int len1=getlen(l1),len2=getlen(l2);
        int len=len1-len2;
        //补0
        if(len>0)//len1长,在l2后面补0
        {
           while(l2->next)
           {
               l2=l2->next;
           }

            for(int i=1;i<=len;++i)//补len1-len2个
            {
                l2->next=new ListNode(0); 
                l2=l2->next;
            }

        }
        
        else if(len<0)//len2长,在l1后面补
        {
            while(l1->next)
            {
                l1=l1->next;
            }
            for(int i=1;i<=len2-len1;++i)//补len2-len1个
            {
                l1->next=new ListNode(0); 
                l1=l1->next;
            }
        }
        
        
        ListNode *first=dummy1,*last;
        int last_add=0;
        while(dummy1 &&dummy2)
        {
            int sum=dummy1->val +dummy2->val+last_add;
            dummy1->val=(sum%10);
            last_add=sum>9? 1:0;
            last=dummy1;
            dummy1=dummy1->next;
            dummy2=dummy2->next;    
        }
        //如果最后溢出了还有进位
        if(last_add)
        {
            last->next=new ListNode(1);
            last=last->next;
            
        }
        return first;
    }
    
    int getlen(ListNode *l)
    {
        int len=0;
        while(l)
        {
            ++len;
            l=l->next;
        }
        return len;
    }
};

快慢指针——234. 回文链表

8.算法与数据结构——指针与链表_第7张图片

解法一:把链表赋值给数组
过于low,不说了

/**
 * 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:
    bool isPalindrome(ListNode* head) {
        if(!head) return false;
        vector<int> num;
        while(head)
        {
            num.push_back(head->val);
            head=head->next;
        }
        int n=num.size(),l=0,r=n-1;
        bool ans=true;
        while(ans &&l<r)
        {
            if(num[l]==num[r])
            {
                ++l,--r;
            }
            else
                ans=false;
        }
        return ans;

    }
};

解法二:翻转后半部分然后判断
需要如下几个辅助函数:
1.判断中点在哪
2.翻转后半部分链表
3.比较两个链表

怎么找中点在哪?你可能想先算出他的长度l,然后再找到一半。但是这样遍历了两次链表。我们在想有没有遍历一次就能找到中点的方法。
快慢指针
定义fast 和slow两个指针,fast指针每次走两步,而slow指针每次走一步。我们来简单验证一下这个猜想对不对。
假设节点有偶数个,比如6个
f 1 3 5
s 1 2 3
此时fast->next->next为空就得终止,终止时slow指向的就是中点。

假设节点有奇数个,比如5个
f 1 3 5
s 1 2 3
此时fast->next为空就得终止

综上while循环的条件得是

while(fast->next && fast->next->next)
{	
	fast=fast->next->next;
	slow=slow->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:
    bool isPalindrome(ListNode* head) {
        if(!head) return false;
        ListNode *mid=findmid(head);//第一步,找出中点
        ListNode *rhead=reverselistnode(mid->next);//第二步,以mid的下一个节点为头节点翻转后续节点
        //第三步,比较两个链表
        while(head && rhead)
        {
            if(head->val!=rhead->val)
                return false;
            head=head->next;
            rhead=rhead->next;

        }
        return true;
    }
    ListNode* findmid(ListNode *head)
    {
        ListNode *fast=head,*slow=fast;
        while(fast->next &&fast->next->next)
        {
            fast=fast->next->next;
            slow=slow->next;
        }
        return slow;
    }
    ListNode *reverselistnode(ListNode *head)
    {
        ListNode *pre=nullptr,*next;
        while(head)
        {
            next=head->next;
            head->next=pre;
            pre=head;
            head=next;
        }
        return pre;
    }
};

快慢指针——19. 删除链表的倒数第 N 个结点

8.算法与数据结构——指针与链表_第8张图片

/**
 * 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* removeNthFromEnd(ListNode* head, int n) 
    {
        //定义双指针相隔n-1,每次以步长1移动。
        ListNode *fast,*slow,*dummy=new ListNode();
        dummy->next=head;
        if(!head->next)//如果只有一个节点,又因为我至少要删除一个节点,所以返回空指针
        {
            delete head;
            return nullptr;
        }
        //右移fast指针,初始化其位置
        fast=head,slow=dummy;
        int k=n-1;
        while(k!=0)
        {   
            fast=fast->next;
            --k;
        }

        //双方均以步长为1右移
        while(fast &&fast->next)
        {
            fast=fast->next;
            slow=slow->next;
        }
        //删除slow的下一个节点
        ListNode *need_re=slow->next;
        slow->next=slow->next->next;
        delete need_re;
        return  dummy->next;


    }
};

83. 删除排序链表中的重复元素

8.算法与数据结构——指针与链表_第9张图片
注意重复节点要回收内存,因为C++不支持自动处理垃圾

/**
 * 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* deleteDuplicates(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode *q=head;
        while(q && q->next)
        {
            if(q->val == q->next->val)
            {
                ListNode *n=q->next;
                q->next=n->next;
                delete n;
            }
            else
                q=q->next;
        }
        return head;
    }
};

328. 奇偶链表

8.算法与数据结构——指针与链表_第10张图片

/**
 * 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* oddEvenList(ListNode* head) 
    {
        if(!head ||!head->next ||!head->next->next) return head;
        ListNode *q=head,*p=head->next,*s=p;
        bool flag=true;
        while(p&&p->next)
        {
            if(flag)//奇数
            {
                q->next=p->next;
                q=q->next;
                flag=false;
            }
            else//偶数
            {
                p->next=q->next;
                p=p->next;
                flag=true;
            }
        }
        q->next=s;
        return head;
    }
};

2022.4.1加更 23. 合并K个升序链表

8.算法与数据结构——指针与链表_第11张图片
思路一:
既然对于每个小list,我们每次都需要找出开头最小的元素。
可以使用小根堆/优先队列( priority_queue, greater< int > >),把所有val都加进去,然后每次只要取堆顶元素加进去就ok。

/**
 * 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) 
    {
        if(!lists.size()) return nullptr;
        priority_queue<int,vector<int>,greater<int> > minHeap;//小根堆
        for(ListNode *x:lists)//把所有节点的val都加入小根堆
        {
            while(x)
            {
                minHeap.push(x->val);
                x=x->next;
            }
        }
        ListNode *dummy = new ListNode(),*x=dummy;
        while(!minHeap.empty())
        {
            int val = minHeap.top();
            minHeap.pop();
            x->next=new ListNode(val);
            x= x->next;
        }
        return dummy->next;
        
    }
};

你可能感兴趣的:(算法,链表,算法)