Day3 链表理论基础,203.移除链表元素,707.设计链表,206.反转链表

基础知识(转载自代码随想录):

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

链表的入口节点称为链表的头结点也就是head。

如图所示: 链表1

链表的类型

接下来说一下链表的几种类型:

单链表

刚刚说的就是单链表。

双链表

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

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

双链表 既可以向前查询也可以向后查询。

如图所示: 链表2

循环链表

循环链表,顾名思义,就是链表首尾相连。

循环链表可以用来解决约瑟夫环问题。

链表4

链表的存储方式

了解完链表的类型,再来说一说链表在内存中的存储方式。

数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。

链表是通过指针域的指针链接在内存中各个节点。

所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。

如图所示:

链表3

这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。

链表的定义

接下来说一说链表的定义。

链表节点的定义,很多同学在面试的时候都写不好。

这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。

而在面试的时候,一旦要自己手写链表,就写的错漏百出。

这里我给出C/C++的定义链表节点方式,如下所示:

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

有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。

但是这个构造函数不会初始化任何成员变量,下面我来举两个例子:

通过自己定义构造函数初始化节点:

ListNode* head = new ListNode(5);

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

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

所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!

链表的操作

删除节点

删除D节点,如图所示:

链表-删除节点

只要将C节点的next指针 指向E节点就可以了。

那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。

是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。

其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。

添加节点

如图所示:

链表-添加节点

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。

性能分析

再把链表的特性和数组的特性进行一个对比,如图所示:

链表-链表与数据性能对比

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

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

相信大家已经对链表足够的了解,后面我会讲解关于链表的高频面试题目,我们下期见!

自定义链表结构整理:

c++单链表:

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

c++双链表:

struct ListNode {
    int val;              // 节点上存储的元素
    ListNode *prev;       // 指向前一个节点的指针
    ListNode *next;       // 指向下一个节点的指针

    ListNode(int x) : val(x), prev(nullptr), next(nullptr) {}  // 节点的构造函数
};

题目:

203.移除链表元素

在这里插入图片描述

突然想到多级指针,AI解答

指针指向指针是一种多级指针的概念,也叫作指向指针的指针。顾名思义,它是指一个指针变量存储了另一个指针变量的地址。这样的指针可以被用来访问、修改甚至创建其他指针变量。

以下是一个示例代码:

int main() {
    int number = 42;
    int *ptr1 = &number;  // 指针ptr1指向number
    int **ptr2 = &ptr1;  // 指针ptr2指向ptr1

    // 使用指针进行访问
    printf("number: %d\n", *ptr1);   // 输出: number: 42
    printf("number: %d\n", **ptr2);  // 输出: number: 42

    // 修改值
    *ptr1 = 24;
    printf("number: %d\n", *ptr1);   // 输出: number: 24
    printf("number: %d\n", **ptr2);  // 输出: number: 24

    return 0;
}

在上面的示例中,我们首先创建了一个整数变量 number 和一个指向 number 的指针变量 ptr1。然后,我们使用 & 符号获取 ptr1 的地址,并将其存储在另一个指针变量 ptr2 中。

通过使用多级指针,我们可以通过一个额外的间接层级来访问和修改所指向的数据。在示例中,*ptr1**ptr2 都可以访问并修改 number 的值。

多级指针在某些情况下很有用,比如在函数参数中传递指针的指针,或者动态分配内存等场景中。

方法一(手动删除头节点):

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        while(head !=nullptr && head->val == val) //注意这里不是if,因为下一个节点也可能需要删除
        {
            ListNode* tmp = head;
            head = head->next;
            delete tmp; //手动释放内存
        }
        ListNode* curhead = head;
        while(curhead != nullptr && curhead->next!=nullptr)
        {
            if(curhead->next->val == val)
            {
                ListNode* tmp = curhead->next;
                curhead->next = curhead->next->next;
                delete tmp; //手动释放内存
            }
            else 
            {
                curhead=curhead->next;
            }
        }

        return head;
    }
};

方法二(虚拟头节点):

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        /*
        *虚拟头节点
        */
        ListNode* dummyhead = new ListNode();
        dummyhead->next = head;
        ListNode* curhead = dummyhead;
        
        while(curhead != nullptr && curhead->next!=nullptr)
        {
            if(curhead->next->val == val)
            {
                ListNode* tmp = curhead->next;
                curhead->next = curhead->next->next;
                delete tmp; //手动释放内存
            }
            else 
            {
                curhead=curhead->next;
            }
        }

        //return dummyhead->next;  这样写有问题,我们没有delete dummyhead
        head = dummyhead->next;
        delete dummyhead;
        return head;
    }
};

707.设计链表

请添加图片描述

class MyLinkedList {
public:
    struct LinkedNode{
        int val;
        LinkedNode* next;
        //构造函数
        LinkedNode() : val(0),next(nullptr){} 
        LinkedNode(int x) : val(x), next(nullptr){}   
        LinkedNode(int x, LinkedNode* next) : val(x),next(next){}
    };
private:
    int size;
    LinkedNode* DummyHead; 
public:   
    MyLinkedList() {
        //构造函数赋值成员变量
        DummyHead = new LinkedNode();  //通过构造函数直接赋值val为0
        size = 0;
    }
    
    int get(int index) {
        if(index>(size - 1) || index<0)  //index范围[0,size-1]  超过size-1或者小于0 直接返回
        {
            return -1;
        }
        LinkedNode* cur = DummyHead->next;
        while(index--) //index = 0为头节点,index=size-1为尾节点,后自减:先取值再减少,index为0退出循环
        {
            cur = cur->next;
        }
        return cur->val;
    }
    
    void addAtHead(int val) {
        LinkedNode* AddedNode = new LinkedNode(val);
        AddedNode->next = DummyHead->next;  //新增节点尾部先连接
        DummyHead->next = AddedNode; //然后dummyhead尾部连接新增节点
        size++;  //链表长度+1
    }
    
    void addAtTail(int val) {
        
         LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = DummyHead;
        while(cur->next != nullptr){
            cur = cur->next;
        }
        cur->next = newNode;
        size++; //链表长度+1
        
        //下面代码出错,问题不明2023-12-15 2:35
       /* LinkedNode* AddedNode = new LinkedNode(val);
        LinkedNode* cur = DummyHead;
        while(size--) //遍历找到尾巴
        {
            cur =  cur->next;
        }
        cur->next = AddedNode;
        size++;*/
        //问题找到,我们不能直接操作size,要新建临时变量,2023-12-15 3:16
        /*  LinkedNode* AddedNode = new LinkedNode(val);
        LinkedNode* cur = DummyHead;
        int temp = size;
        while(temp--) //遍历找到尾巴
        {
            cur =  cur->next;
        }
        cur->next = AddedNode;
        size++;*/
    }
    
    void addAtIndex(int index, int val) {
        if(index > size) return;
        if(index < 0) index = 0;  
        LinkedNode* AddedNode = new LinkedNode(val);
        LinkedNode* cur = DummyHead; //局部变量,不要移动DummyHead!!!!!
        while(index--) //遍历找到插入位置,第n个节点前一个
        {
            cur =  cur->next;
        }
        AddedNode->next = cur->next;//新增节点尾部先连接
        cur->next = AddedNode;//然后cur尾部连接新增节点
        size++; //链表长度+1
    }
    
    void deleteAtIndex(int index) {
        if (index >= size || index < 0) {
            return;
        }
        LinkedNode* cur = DummyHead;
        while(index--) //遍历找到删除位置,第n个节点前一个
        {
            cur =  cur->next;
        }
        LinkedNode* tmp = cur->next; //先储存起来,之后手动清理内存
        cur->next = cur->next->next;
        delete tmp;
        tmp =nullptr;
        size--;
    }
};

206.反转链表

请添加图片描述

nullnullptr区别:

nullnullptr 都可以表示空指针,但它们是不同的概念。

null 是 C++ 标准库中提供的宏定义,它被定义为 0 或者 (void*)0。在 C++98 的标准中,null 被定义为 0,它可以被隐式地转换为任意指针类型。这种隐式转换可能会导致一些不安全的编程错误,例如再分配空指针时可能会触发未定义的行为。因此,在 C++11 的标准中,它被弃用了。

nullptr 是 C++11 引入的新关键字,它是一个特殊的空指针常量,可以被转换为任意指针类型,但不能转换为整数类型。它具有与字面量更加一致的语义,可以避免一些潜在的编程错误,因此更推荐使用。

在实际编程中,如果你需要表示空指针,应该尽量使用 nullptr,以避免隐式转换和意外的错误。如果你必须使用 null,则必须格外小心,确保不会引入不必要的错误。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* tmp;
        ListNode* cur = head;
        ListNode* pre = nullptr;

        while(cur)  {//cur为nullptr退出循环
            tmp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; 
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};
  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值