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. 合并两个有序链表

在这里插入图片描述
创建两个指针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 
  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值