Bootstrap

Day3||203.移除链表数组|707.设计链表|206.反转链表

今日重点内容: 

1.熟悉用虚拟头结点来移除节点

2.练习链表5个常规操作

3.使用双指针法和递归法来反转链表

链表理论基础

链表:存储每一个元素和下一个元素的地址

结点:含数据域和指针域

链表类型单链表、双向链表和循环链表(约瑟夫环)

头指针:指向链表第一个结点的指针

单链表由表头唯一确定,所以可以用头指针来代表整个链表

空表:头结点的指针域为空

链表节点定义:

// 单链表
struct ListNode {
    int val;  // 节点上存储的元素
    ListNode *next;  // 指向下一个节点的指针
    ListNode(int x) : val(x), next(NULL) {}  // 节点的构造函数
};

或自定义:

ListNode* head = new ListNode(5);

使用默认构造函数初始化节点:

ListNode* head = new ListNode();
head->val = 5;

203.移除链表数组

题目:203. 移除链表元素 - 力扣(LeetCode)

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

 1.直接使用原来链表进行删除操作

移除头结点时向后移一位即可,其他节点则找到删除结点的前一个节点,断开连接,指向删减节点的下一位,再free删除节点。常规操作。

/**
 * 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) {
        //若删除元素是头结点
       while(head!=NULL&&head->val==val)
       {
        ListNode*temp=head;
        head=head->next;
        delete temp;
        }

        //删除元素非头结点
        ListNode*cur=head;
        while(cur!=NULL&&cur->next!=NULL)
        {
            if(cur->next->val==val)
            {
                ListNode*temp=cur->next;
                cur->next=cur->next->next;
                delete temp;
            }
            else cur=cur->next;
        }
        return head;


    }
};

易错点,分不清删除节点时用free还是delete。

在C++中,delete和free都可以用来释放动态分配的内存。虽然它们都能够完成内存释放的功能,但这两者之间有着很多区别,如下所示:

1. 对象类型
delete是C++语言自带的运算符,对于类对象进行删除时调用类的析构函数。
free是C标准库函数,仅释放空间,不会释放一个类(或者结构体)对象的空间。

2. 内存碎片
delete在释放内存后,能够确保内存内容被释放,并合并成一块可用的空间。在大规模内存管理、长时间运行时程序表现更为优秀。
free只是简单地将内存地址修改为空闲状态,并不能保证合并成一块可用的空间。如果频繁地执行 malloc 和 free,容易产生大量小块内存碎片,从而导致不利于程序运行效率的问题。

3. 参数类型

  • delete都是以指针为参数。

  • free是以地址值为参数。  

4. 行为保证

  • delete具有行为保证(即无论是否出错都会表现一致),能够确保释放内存不会对其他对象造成影响。

  • free不具备任何行为保证,释放后的地址空间中仍可能存在原来数据的残留物,甚至可能覆盖其他已经被释放的对象。

内容来自:面试题:delete和free区别_free和delete-CSDN博客

 2.设计一个虚拟头结点来移除节点

好处是可以统一所有节点的处理方法。

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
    ListNode* dummyhead=new ListNode(0);
    dummyhead->next=head;//创建一个指向头结点的虚拟头结点
    ListNode*cur=dummyhead;//一个指向当前节点的结点
    while(cur->next!=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.设计链表

 题目:707. 设计链表 - 力扣(LeetCode)

一道比较综合的题目,涵盖了链表的常见操作。

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

 单链表

class MyLinkedList {
public:
    //定义链表节点结构体
    struct Linkednode{
        int val;
        Linkednode*next;
        Linkednode(int val):val(val),next(nullptr){}

    };
      //初始化链表
    MyLinkedList() {
       Lsize=0;
       dummyhead=new Linkednode(0);//虚拟头结点,非真正的头结点

    }
    //找到对应下标节点的值
    int get(int index) {
      if(index>Lsize-1||index<0)//检查下标是否合法
      return -1;
      Linkednode*cur=dummyhead->next;
      while(index--)//从index=0开始找,因此cur指向头结点
      {
        cur=cur->next;
      }
      return cur->val;

    }
    //头插法
    void addAtHead(int val) {
    Linkednode*newnode=new Linkednode(val);
    newnode->next=dummyhead->next;
    dummyhead->next=newnode;//这里要注意顺序,一定要先建立新节点和下个节点的连接,再和上一个节点建立
    Lsize++;
    }
    //尾插法
    void addAtTail(int val) {
        Linkednode*cur=dummyhead;
        Linkednode*newnode=new Linkednode(val);
        while(cur->next!=NULL)
        {
            cur=cur->next;
        }
        cur->next=newnode;
        Lsize++;

    }
    //在第index个的节点前插入一个节点
    //如果index小于等于0,都是头插入为头结点,若等于链表长度则是新的尾节点,大于链表长度则返回空
    void addAtIndex(int index, int val) {
        Linkednode*cur=dummyhead;
        Linkednode*newnode=new Linkednode(val);
        if(index<0)index=0;
        if(index>Lsize)return;
        while(index--)cur=cur->next;

        newnode->next=cur->next;
        cur->next=newnode;
        Lsize++;

    }
    //删除第index个节点
    void deleteAtIndex(int index) {
        //注意验证下标合法性
        if(index<0||index>Lsize-1)return;
        Linkednode*cur=dummyhead;//注意!删除第几个节点,cur就指向它前一个,这个和寻找那里指向不一样
        while(index--)
        {
            cur=cur->next;
        }
        Linkednode*temp=cur->next;//这里指向删除节点就行,注意定义一个指针就行
        cur->next=cur->next->next;
        delete temp;

        temp=NULL;
        Lsize--;
    }
    private:
    int Lsize;//链表长度
    Linkednode*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);
 */

新知,删除节点那里,最后一步要把temp指针置空原因

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

 本题需要注意cur是指向虚拟头结点还是头结点,这样就不会搞错循环次数和index范围了。

结合画图比较好理解。

双链表待补充。。

206.反转链表

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

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

双指针法

思路:定义一个pre指针先指向null,然后cur指向头结点,额外一个临时指针保存cur的下一个节点。然后cur指向pre,然后pre和cur都下移一位。然后给一个循环过程遍历链表,当cur为空时就完成了链表的反转。(挺好理解)

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
    ListNode*pre=NULL;
    ListNode*cur=head;
    ListNode*temp;
    while(cur){
        temp=cur->next;//先保存下一个位置,因为等会cur转向时要斩断和next的连接
        cur->next=pre;
        pre=cur;
        cur=temp;
    }
    return pre;//最后pre指向的就是头结点

    }
};

递归法

 模拟的就是双指针法的操作过程,只要把while循环的一次次操作弄成递归操作的就行

区别就是反转操作让reverse函数来完成。然后reverselist调用。

传入新的cur和pre时,注意是cur移动到temp,pre移动到cur的位置。

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

总结:由于之前看过印度老哥的视频,所以思路是有点印象的,理解上不会太困难,不过代码还有很多小细节需要注意。

;