代码随想录训练营Day3 | 链表理论基础 | 203.移除链表元素 | 707.设计链表 | 206.反转链表

今天任务:学习链表理论基础
链表的类型
链表的存储方式
链表的定义
链表的操作
性能分析

学习文档:代码随想录 (programmercarl.com)

链表的类型单链表、双链表、循环链表(区别就在于其结构不同)

链表是一种常用的数据结构,通过指针串联在一起,相对于数组有以下几方面优点:

  1. 动态大小:链表的大小是动态的,可以在运行时根据需要进行扩展或缩减。而数组的大小在声明时就固定了,不能动态改变。

  2. 内存利用率:链表不需要像数组那样预先分配一块连续的内存空间,因此可以更有效地利用内存,尤其是在内存碎片较多的情况下。

  3. 插入和删除操作搞笑:在链表中,插入和删除节点通常只需要改变指针,而不需要移动其他元素。这使得链表在插入和删除操作上比数组更高效,因为数组需要移动插入点或删除点之后的所有元素。

  4. 不需要初始化大小:在创建链表时,不需要指定链表的大小,可以根据需要逐步构建链表。

  5. 空间分配:链表的节点可以在需要时单独分配,这意味着即使链表很大,也不需要一次性分配大块内存,从而减少了内存的浪费。

  6. 灵活的数据结构 :链表可以很容易地构建成其他复杂的数据结构,如双向链表、循环链表

    等,这些结构可以支持更复杂的操作。

链表也有其缺点,比如访问元素时需要从头开始遍历,导致访问时间较长;指针的额外存储空间可能会增加内存的开销;以及由于指针的存在,可能会导致程序的复杂性增加

链表的存储方式

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

链表的定义

单链表的定义 (特别注意,在面试中可能需要自己定义链表)

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

链表的基本操作:删除、添加

删除节点

删除D节点,如图所示:

只要将C节点的next指针 指向E节点就可以了。注意此时D节点依旧留在内存中,只不过是没有在这个链表中而已,在使用C++最好手动释放这个D节点,释放这块内除。

添加节点

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)

性能分析

Leetcode: 203.移除链表元素

题目描述:

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

示例 1:

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

示例 2:

复制代码
输入:head = [], val = 1
输出:[]

示例 3:

复制代码
输入:head = [7,7,7,7], val = 7
输出:[]

解题思路:

这题就是简单的删除链表元素,但是要注意区分两种删除方式
1.删除头节点
2.删除非头节点

完整代码:

/**
 * 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 因为删除有可能需要一直删 不止一个val
        // 如果val是头节点 直接将head = head->next;即可
        while(head!=NULL && head->val == val) {
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }
        ListNode* cur = head;
        // 如果不是头节点,需要cur->next = cur->next->next; 这样就删除了cur->next这个节点
        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;
    }
};

使用虚拟头节点:

使用一个虚拟头节点,可以统一逻辑来删除链表节点

/**
 * 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!=NULL && cur->next!=NULL) {
            if(cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            }
            else {
                cur = cur->next;
            }
        }
        // 重新设置头节点
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};

Leetcode: 707.设计链表

题目描述

你可以选择使用单链表或者双链表,设计并实现自己的链表。单链表中的节点应该具备两个属性:valnextval 是当前节点的值,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 的节点。

解题思路

这道题目设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目

可以继续使用上面的操作,使用虚拟头节点来操作

class MyLinkedList {
public:
    // 定义链表节点的结构体
    struct LinkedNode {
        int val;
        LinkedNode* next;
        LinkedNode(int val) : val(val), next(nullptr){}
    };
    // 初始化链表 这里定义的头节点是一个虚拟头节点 而不是真正的链表头节点
    MyLinkedList() {
        _dummyhead = new LinkedNode(0);
        // 一个整型变量,用于存储链表中实际节点的数量
        _size = 0;
    }
    // 获取链表第index个节点的数值
    int get(int index) {
        // 如果索引无效(即超出链表范围),返回 -1
        if(index > (_size - 1) || index < 0) {
            return -1;
        }
        // 从虚拟头节点的下一个节点开始遍历,直到到达指定索引的节点,然后返回该节点的值
        LinkedNode* cur = _dummyhead->next;
        while(index--) {
            cur = cur->next;
        }
        return cur->val;
    }
    // 在链表最前面插入一个节点 ,插入完成后,新插入的节点为链表新的头节点
    void addAtHead(int val) {
        LinkedNode* newnode = new LinkedNode(val);
        // 注意这里的顺序不能改变 统一插入的赋值顺序 先将新头节点插入在head之前 然后将虚拟头节点依旧放在最前面
        newnode->next = _dummyhead->next;
        _dummyhead->next = newnode;
        _size++;
    }
    // 在链表最末尾插入节点
    void addAtTail(int val) {
        LinkedNode* newnode = new LinkedNode(val);
        LinkedNode* cur = _dummyhead;
        // 先将cur指向最后一个节点 判断条件cur->next != NULL
        while(cur->next != NULL) {
            cur = cur->next;
        }
        cur->next = newnode;
        _size++;
    }
    // 在第index个节点之前插入一个新节点 使用虚拟头节点就可以方便处理index为0 插入头节点的情况
    void addAtIndex(int index, int val) {
        if(index > _size) {
            return;
        }
        LinkedNode* newnode = new LinkedNode(val);
        LinkedNode* cur = _dummyhead;
        while(index--) {
            cur = cur->next;
        }
        newnode->next = cur->next;
        cur->next = newnode;
        _size++;
    }
    
    //删除第index个节点
    void deleteAtIndex(int index) {
        //当 index 等于链表长度时,cur->next 将为 nullptr,因此不能访问 cur->next->next
         if(index >= _size || index < 0) {
            return;
        }
        LinkedNode* cur = _dummyhead;
        // 注意index是从0开始的 这样刚好指向index前一个节点
        while(index--) {
            cur = cur->next;
        }
        LinkedNode* tmp = cur->next;
        // 删除index节点
        cur->next = cur->next->next;
        delete tmp;
        _size--;
    }
    private:
        int _size;
        LinkedNode* _dummyhead;
};

总结:要用意识去使用虚拟头节点,对于一些边界条件判断不到位!

Leetcode: 206.反转链表

题目描述

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

示例 1:

复制代码
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

解题思路

1.依次遍历链表 依次反转次序

/**
 * 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) {
        ListNode* former = NULL;
        ListNode* mid    = head;
        ListNode* latter = NULL;

        while(mid != NULL) {
        // 保存mid的下一个节点,因为接下来要改变mid->next的指向了
            latter = mid->next;
            mid->next = former;
            former = mid;
            mid = latter;
        }

        // 注意最后一次while循环 将latter赋给了mid 所以former是反转链表后的头节点
        return former;
    }
};

2.递归法

递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

关键是初始化的地方, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。

具体可以看代码(已经详细注释),双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。

class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};
相关推荐
c1assy9 分钟前
DP动态规划+贪心题目汇总
数据结构·算法·leetcode·贪心算法·动态规划
代码小将42 分钟前
PTA数据结构编程题7-1最大子列和问题
数据结构·c++·笔记·学习·算法
yangjiwei02071 小时前
数据结构-排序
数据结构·python
坊钰1 小时前
【Java 数据结构】合并两个有序链表
java·开发语言·数据结构·学习·链表
抓住鼹鼠不撒手1 小时前
力扣 429 场周赛-前两题
数据结构·算法·leetcode
南宫生2 小时前
力扣-数据结构-3【算法学习day.74】
java·数据结构·学习·算法·leetcode
向宇it3 小时前
【从零开始入门unity游戏开发之——C#篇30】C#常用泛型数据结构类——list<T>列表、`List<T>` 和数组 (`T[]`) 的选择
java·开发语言·数据结构·unity·c#·游戏引擎·list
A懿轩A3 小时前
C/C++ 数据结构与算法【树和二叉树】 树和二叉树,二叉树先中后序遍历详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·二叉树·
DogDaoDao5 小时前
leetcode 面试经典 150 题:矩阵置零
数据结构·c++·leetcode·面试·矩阵·二维数组·矩阵置零
徐子童5 小时前
二分查找算法专题
数据结构·算法