算法训练第三天|203.移除链表元素 707.设计链表 206.反转链表

链表基础

1.链表的定义

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域,另一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表的入口节点称为链表的头结点也就是head。结构如图所示:

2.链表的类型

链表分为单链表、双链表、循环链表等。单链表如上图。

双链表

单链表中的指针域只能指向节点的下一个节点。

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表 既可以向前查询也可以向后查询。结构如图所示:

循环链表

尾节点指向头结点。如图所示:

3.链表节点的代码

对链表的表示是使用的节点,也就是说我们通过定义节点的结构就可以用其来表示链表。节点包含数据部分,指针部分,也可以加一个构造函数(赋初值的部分),代码如下:

struct ListNode{
    int val;  //节点上存的元素

    ListNode * next;  //指向下一节点的指针

    ListNode(int x) : val(x), next(NULL) {};  //节点构造函数,将val初始化为x,next指针初始化为NULL

};

C++结构体中支持写成员函数,所以我们添加了一个节点的构造函数用于初始化节点。

4.链表的操作

删除节点

删除D节点:

  • 记录D节点的地址即pos = C.next
  • C.next = D.next
  • delete释放第一步pos所在的内存

插入节点 

  • 记录手动开辟的F节点的地址pos
  • F.next = C.next
  • C.next = pos

5. 数组与链表的区别

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。但是数组查询很方便,因为数组支持下标访问。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景(查询需要一个一个的遍历)。

203.移除链表元素

力扣链接

题目描述:

正常情况下删除val所在的节点,则执行上文所述4.链表操作小节中删除节点的操作即可,但是当

被删除的值位于 头结点中时,我们就需要记录head的地址,然后head = head ->next,delete刚刚记录的head地址。相当于我们对head节点做了特殊的逻辑,这样的代码实现如下:

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 删除头结点
        while (head != NULL && head->val == val) { // 注意这里不是if
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }

        // 删除非头结点
        //此时head可能是原链表最后的null也可能不是(还有元素)
        ListNode* cur = head;
        while (cur != NULL && cur->next!= NULL) {
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }
        }
        return head;
    }
};

上述代码最开始遍历的视时候采用了一个while循环来处理val在头结点中的情景,没有采用if是为了防止链表前几个元素全是val的情况。

添加虚拟头结点的方法就不用对val在前几个节点做特殊操作了,代码如下:

/**
 * 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 *vtr_head = new ListNode(0);
        //用虚拟头结点作为新的头结点
        vtr_head->next = head;

        //使用tmp指针从头开始遍历链表
        ListNode *tmp = vtr_head;
        //当tmp下一个节点有元素时
        while(tmp->next != nullptr) 
        {   
            //若该元素为val,需删除
            if(tmp->next->val == val)
            {
                //记录当前元素节点的内存
                ListNode *addr = tmp->next;
                //tmp->next = 该节点的next
                tmp->next = addr->next;
                //delete该元素节点的内存
                delete addr;
                
            }
            else
            {
                //更新遍历指针tmp
                tmp = tmp->next;
            }
        }

        
        //最后返回虚拟头结点的next即为新链表的头结点
        head = vtr_head->next;  //方便理解还是用head来记录头结点
        delete vtr_head;  //释放用于辅助的虚拟头结点
        return head;
    }
};

 707.设计链表

力扣链接

题目描述:

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

单链表中的节点应该具备两个属性: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 的节点。

1.补充节点定义及构造函数

2.链表要维护头结点指针和实际有效元素个数size

3.插入和删除元素时记得更新size

4.从虚拟头结点开始,遍历到末尾的循环条件就是cur->next != nullptr。因为最后一个节点的next 等于nullptr。

5.有虚头结点的链表,在index处插入或删除,遍历条件均为:

ListNode * cur = head;
while(index--)
{
    cur = cur->next;
}

整体代码: 

class MyLinkedList {
public:
    //添加节点结构体定义
    struct ListNode
    {
        int val;
        ListNode *next;
        ListNode(int x) : val(x), next(nullptr) {} 
    };
    
    //链表应该有哪些属性?链表头指针head、链表中节点的个数size
    MyLinkedList() {
        this->size = 0;
        this->head = new ListNode(0);  //创建了一个不存储有效元素的虚拟头结点,不计入长度
    }
    
    int get(int index) {
        //判断下标index是否超出链表size的范围,size-1为最大坐标,0为最小坐标
        if(index > size-1|| index < 0 )
        {
            return -1;
        }
        //下标为index,则需遍历index+1次
        ListNode * cur = head;  //保护head不改变,使用cur来遍历各节点
        while(index)
        {
            cur = cur->next;
            --index;
        }
        return cur->next->val;
    }
    
    void addAtHead(int val) {
        //先创建新节点记录val
        ListNode * newHead = new ListNode(val);
        //新节点指向原来的第一个节点即head->next(我们的头结点是虚拟头结点,所以实际第一个元素所在的节点为head->next)
        newHead->next = head->next;
        //虚拟头结点指向新节点
        head->next = newHead;
        //别忘记更新size
        size++;
    }
    
    void addAtTail(int val) {
        //先创建新节点记录val
        ListNode * newTail = new ListNode(val);
        //要找到原来的最后一个节点,将其next改为newTail。可以通过size找,也可以通过指针找
        //指针找
        ListNode * cur = head;
        //从虚拟头结点开始,遍历到末尾的循环条件就是cur->next != nullptr
        while(cur->next!=nullptr)
        {
            cur = cur->next;
        }
        cur->next = newTail;
        //size++
        size++;

    }
    
    void addAtIndex(int index, int val) {
        //先判断index是否有效
        if( index > size || index < 0)
        {
            return ;//无效则插入失败直接return
        }
        
        //先创建新节点记录val
        ListNode * newNode = new ListNode(val);
        ListNode * cur = head;
        //将一个值为 val 的节点插入到链表中下标为 index 的节点之前与当index 等于链表的长度追加到链表的末尾做的是相同的操作
        while(index--)
        {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        size++;


    }
    
    void deleteAtIndex(int index) {
        if(index > size-1|| index < 0 )
        {
            return ;
        }
        ListNode * cur = head;
        while(index--)
        {
            cur = cur->next;
        }
        ListNode *  tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        tmp = nullptr;
        size--;
    }

private:
    int size;
    ListNode * head;  //注意头结点指针类型为ListNode
};

206.反转链表

力扣原题

题目描述:

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

思路如下:

为什么要使用tmp记录cur-》next呢?因为题目要反转链表,如果我们不提前保存cur下一个节点的地址,那么cur-》next指向反转后,我们将无法再访问到下一个节点。

所以一旦我们记录了cur->next,我们就可以对cur->next做反转了:cur->next = prev。

下一步先更新prev = cur, 在更新cur=tmp。如果先更新cur,那么prev将无法指向节点1。

变化一个元素后的情况如下:

接着执行上面的4个操作,当cur指向最后一个节点时,还需要改变该节点的指向,所以还需要执行一轮的这4个操作。

执行完之后是这种情形:

所以我选择了用while(cur!=nullptr)作为循环遍历的控制条件。

返回prev即可。代码如下:

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode *prev = nullptr;
        ListNode *cur = head;
        while(cur!=nullptr)
        {
            ListNode * tmp = cur->next;
            cur->next = prev;
            prev = cur;
            cur = tmp;
        }
        return prev;
    }
};

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我知道了。以下是对您的问题的回答: a. 动态分配内存的方法有很多,可以使用C语言的malloc函数,也可以使用C++中的new操作符,具体方法如下: C语言的malloc函数: ```c struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode)); ``` C++中的new操作符: ```c++ ListNode* node = new ListNode(); ``` b. 初始化单链表需要对链表节点的成员变量进行赋值,以下是一个示例: ```c++ struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(NULL) {} }; ListNode* head = NULL; ``` c. 头插法创建单链表的示例代码如下: ```c++ ListNode* head = new ListNode(); head->next = NULL; for (int i = 0; i < n; i++) { int x; cin >> x; ListNode* node = new ListNode(x); node->next = head->next; head->next = node; } ``` 尾插法创建单链表的示例代码如下: ```c++ ListNode* head = new ListNode(); head->next = NULL; ListNode* tail = head; for (int i = 0; i < n; i++) { int x; cin >> x; ListNode* node = new ListNode(x); node->next = NULL; tail->next = node; tail = node; } ``` d. 取链表某一位的值可以通过遍历链表来实现,以下是一个示例: ```c++ ListNode* p = head->next; int pos = 3; for (int i = 1; i < pos && p != NULL; i++) { p = p->next; } if (p == NULL) { cout << "链表长度不足" << endl; } else { cout << p->val << endl; } ``` e. 在链表的某一位进行插入操作可以通过遍历链表找到插入位置,然后插入一个新节点来实现,以下是一个示例: ```c++ ListNode* p = head; int pos = 3; for (int i = 1; i < pos && p != NULL; i++) { p = p->next; } if (p == NULL) { cout << "链表长度不足" << endl; } else { int x = 100; ListNode* node = new ListNode(x); node->next = p->next; p->next = node; } ``` f. 遍历链表可以通过循环遍历链表节点来实现,以下是一个示例: ```c++ ListNode* p = head->next; while (p != NULL) { cout << p->val << " "; p = p->next; } cout << endl; ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值