代码随想录算法训练营第三天| 链表理论基础 、 203.移除链表元素、 707.设计链表 、 206.反转链表

链表理论基础

链表的定义:

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域,指针域指向下一个节点,最后一个节点的指针域指向NULL。

链表的入口结点被称为链表的头节点。

定义链表结点的代码:

struct ListNode{
    int val; //数据域
    int ListNode*next;//指针域
    ListNode(int x):val(x),next(NULL){}//初始化
};

代码中的初始化方式是构造函数的初始化列表,构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。上面的代码等于:

我也不知道对不对

struct ListNode{
    int val; //数据域
    int ListNode*next;//指针域
    ListNode(int x)
    {
        ListNode->val = x;
        ListNode->next = NULL;
    }
};

链表的类型:

1、单链表

上面的就是单链表

2、双链表

双链表和单链表的区别就是,单链表只有一个指针域,指向下一个结点。而双链表有两个指针域和一个数据域,一个指针域指向下一个结点,一个指针域指向上一个结点。所以双链表可以向前查找也可以向后查找。

3、循环链表

循环链表和单链表的区别是,单链表最后一个结点的指针域指向NULL,而循环链表最后一个结点的指针域则指向头结点,从而形成一个环。

链表的存储方式:

链表在内存中不是连续分布的。链表是通过指针域的指针链接内存中的各个结点。

所以链表中的结点在内存中不是连续分布的,而是散乱分布在内存中的某些地址。

链表的操作:

1、删除结点

链表的删除操作,只需要将要删除结点的前一个结点的指针域指向要删除结点的下一个结点。非常的方便,但是进行删除操作的时候要先查找到该结点,由于链表不是存储在一片连续空间中,所以查找只能顺着头节点慢慢找。

2、增加结点

增加结点需要知道结点要插入的位置,将该结点的指针域指向该位置的后一个结点,将该位置的前一个结点的指针域指向要增加的结点。

链表和数组的区别

数组存储在一片连续的地址中,所以其查找和修改操作非常方便时间复杂度为O(1),但是其删除操作会比较麻烦,因为数组的删除其本质是从后往前覆盖,其时间复杂度为O(n)。

链表存储在一片散乱分布的地址中,其通过指针将各个结点链接。所以其删除和增加操作会比较方便只需要更改一个或者两个指针的指向就行,其时间复杂度为O(1)。但是其查找操作会比较麻烦,因为其存储在的地址不连续,只能通过指针从头到尾查找,其时间复杂度为O(n)。

力扣题目链接:203. 移除链表元素

这题需要设置一个虚拟头节点dummyhead来统一操作数组中的元素,也就是可以设置一个结点指向头节点,返回的时候直接返回dummyhead->next就行。如果没有虚拟头结点的话,对于头节点和其后续结点就要分开进行处理。

还需要一个位置指针cur,其指向虚拟头结点,用cur去寻找链表中值和val相等的结点的前一个结点,也就是cur->next->val ==val 的时候,进行结点的删除操作。为什么是与val值相等的结点的前一个结点,而不是该节点,是因为,如果cur指向该节点的话,就找不到其前一个结点了,结点的删除操作是该结点的前一个结点指向该结点的后一个结点。然后需要注意的是,我们可以设置一个临时指针其指向要被删除的结点,到时候好释放内存。

循环终止条件,可以明显的看出来,cur->next==NULL的时候结束循环。具体过程如下图:

其代码如下:

/**
 * 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* removeElements(ListNode* head, int val) 
    {
        ListNode*dummyhead = new ListNode(0);
        dummyhead->next = head;
        ListNode*cur = dummyhead;
        while(cur->next!=NULL&&cur!=NULL)
        {
            if(cur->next->val ==val)
            {
                ListNode*temp = cur->next;
                cur->next = cur->next->next;
                delete temp;
            }
            else
            {
                cur = cur->next;
            }
        }
        return dummyhead->next;
    }
};

力扣题目链接:707. 设计链表

这个题目很长,需要做的操作也很多,但是都比较基础。要注意的是,我们平时练习力扣题目,题目都是默认定义好了链表结点,而该题是需要你自己去定义一个链表结点,并且初始化。

初始化链表:

首先我们要做的肯定是先定义一个创建一个链表结点的结构体,具体方法在上面‘链表基础知识’有说。然后就是题目的第一个要求,也就是初始化链表,需要注意的是,我们需要自己在类中定义头节点和链表表长,即Listnode*dummyhead;int Listsize;。然后初始化操作,也就是Listnode = new Listnode(0); Listsize = 0;。

给定一个索引下标index,获得该下标中结点的值:

给定一个索引下标index,要我们去获得该下标中结点的值。这题没什么难度,只需要我们知道链表的查找方式,链表不是存储在一片连续的空间,所以查找一个链表需要从头节点一个个往后找。需要注意的是,我们需要先对index进行一个判断,防止index是负数或者index的大小超出数组下标,也就是,index<0||index>=Listsize;这里需要注意,因为链表的下标是从0开始,也就是说真正头结点的下标为0,所以链表最后一个元素的下标为Listsize-1,所以我们的条件可以为>=。

我们可以设置一个位置指针也就是cur,令它指向链表的头节点,注意是真正的头节点而不是虚拟头结点,所以cur = dummyhead->next;。我们可以去移动位置指针而定位下标为index的区域。这里可以使用一个while循环来实现,while循环的条件我们可以设置为index--,这样的话我们可以循环index次,从而使cur指向index下标所在的结点。cur指针的移动也就是cur = cur->next;,最后直接返回cur的数据域就行。

头插法:

将一个值为val的结点插入到链表中第一个元素之前,该链表会成为链表新的头节点。

这个题由于我们设置了虚拟头节点,所以实现起来会非常简单。只需要先创建一个新的结点,将val赋值给它,然后将它的指针域指向真正的头节点,然后将虚拟头节点的指针域指向它。注意这个顺序不能变,如果先将虚拟头节点指向它,会导致无法找到真的头结点。最后不要忘记,将链表的长度+1.

尾插法:

将一个值为val的结点插入到链表中的最后一个元素。

还是很简单,首先我们需要一个位置指针cur,然后使其移动到最后一个元素。然后直接将cur->next指向新结点就行。

如何将cur指向最后一个元素,我们可以使用一个while循环来实现,循环的条件可以设置为cur->next!=NULL。最后不要忘记,将链表的长度+1.

给定一个index,将一个值为val的结点插入到下标为index的结点之前:

这题和上面尾插差不多。先得判断index是否有效,注意:index<0,我们要令其为0(我也不知道为啥)。插入思路为:设置一个位置指针,将其指向虚拟头节点。然后移动cur,将其移动到下标为index-1的位置。因为它的要求是插入到下标为index的结点之前,所以我们不能将其移动到下标为index的位置。将其移动到下标为index的位置后,我们直接进行插入操作,也就是将创建的新结点指向cur->next,然后将cur->next指向新结点。

怎么将cur移动到index-1的位置。由于我们将cur设置为指向虚拟头节点,所以我们可以使用一个while循环来实现,循环条件为index--。这样的话就算index=0,也不会被影响。最后不要忘记,将链表的长度+1.

给定一个下标index,下标有效,删除链表中下标为index的结点:

这题还是先判断index的有效性,这里index<0是无效。

然后设置一个位置指针cur,移动cur到下标为index-1的位置,因为要进行删除操作必须找到前一个结点。找到之后直接进行删除操作就行了。需要注意的是:我们可以建立一个临时指针temp用来指向要删除的结点,到时候好释放内存。删除操作:cur->next=cur->next->next;。最后不要忘记,将链表的长度-1。

注意:在删除临时指针temp的时候,除了delete temp; 还得将temp = nullptr; 卡哥的解释为

     //delete命令指示释放了temp指针原本所指的那部分内存,
        //被delete后的指针temp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
        //如果不再加上一句tmp=nullptr,temp会成为乱指的野指针
        //如果之后的程序不小心使用了temp,会指向难以预想的内存空间

本题具体代码如下:

class MyLinkedList {
public:
    struct ListNode{
        int val;
        ListNode*next;
        ListNode(int x): val(x),next(NULL){}
    };

    MyLinkedList() {
        dummyhead = new ListNode(0);
        Listsize = 0;
    }
    
    int get(int index) {
        
        if(index<0||index>=Listsize)
        {
            return -1;
        }
        ListNode*cur = dummyhead->next;
        while(index--)
        {
            cur = cur->next;
        }
        return cur->val;
    }
    
    void addAtHead(int val) {
        
        ListNode* newnode = new ListNode(val);
        newnode->next = dummyhead->next;
        dummyhead->next = newnode;
        Listsize++;
    }
    
    void addAtTail(int val) {
        ListNode*newnode = new ListNode(val);
        ListNode*cur = dummyhead;
        while(cur->next != NULL)
        {
            cur= cur->next;
        }
        cur->next = newnode;
        Listsize++;
    }
    
    void addAtIndex(int index, int val) {
        if(index>Listsize)
        {
            return;
        }
        if(index<0)
        {
            index = 0;
        }
        ListNode* newnode = new ListNode(val);
        ListNode*cur = dummyhead;
        while(index--)
        {
            cur= cur->next;
        }
        newnode->next = cur->next;
        cur->next = newnode;
        Listsize++;

    }
    
    void deleteAtIndex(int index) {
        if(index>=Listsize||index<0)
        {
            return;
        }
 
        ListNode*cur = dummyhead;
        while(index--)
        {
            cur=cur->next;
        }
        ListNode*temp = cur->next;
        cur->next = cur->next->next;
        delete temp;
        temp = nullptr;
        Listsize--;
    }
    int Listsize;
    ListNode*dummyhead;
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList* obj = new MyLinkedList();
 * int param_1 = obj->get(index);
 * obj->addAtHead(val);
 * obj->addAtTail(val);
 * obj->addAtIndex(index,val);
 * obj->deleteAtIndex(index);
 */

力扣题目链接:206.反转链表

双指针法:

双指针法的思路就是,定义两个指针,一个指针pre先指向NULL,一个指针cur指向头节点。

还需要一个临时指针,保存cur->next的位置,不然指针cur的指向反转之后会丢失cur->next的位置。

反转指针的操作为cur->next=pre;,反转之后将pre和cur往前移动,也就是pre=cur,cur=temp,重复操作,知道cur==NULL结束循环。并且返回pre。具体过程如下。

代码如下:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode*pre =NULL;
        ListNode*cur = head;
        while(cur)
        {
            ListNode*temp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};

递归法:

递归法的思路如下,递归结束的条件还是cur==NULL,可以定义一个递归函数,函数的参数为pre和cur两个指针,操作还是双指针法的操作。但是用递归的方式代替了双指针的移动。

递归法代码如下:

class Solution {
public:
    ListNode*reverse(ListNode*pre,ListNode*cur)
    {
        if(cur==NULL)
        {
            return pre;
        }
        ListNode*temp = cur->next;
        cur->next = pre;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        return reverse(NULL,head);
    }
};

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值