您现在的位置是:首页 >学无止境 >代码随想录算法训练营第三天|203. 移除链表元素 707. 设计链表 206.反转链表网站首页学无止境

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

Yuyu Tong 2024-06-17 10:22:07
简介代码随想录算法训练营第三天|203. 移除链表元素 707. 设计链表 206.反转链表

代码随想录算法训练营第三天|

1. 203. 移除链表元素

1.1 自己看到题目的第一想法

这题做过,但是一点都想不起来了,十几天没做题,链表操作都忘了

1.2 看完代码随想录之后的想法

1.3 自己实现过程中遇到哪些困难

  1. 不用删除cur节点嘛?
    答:不用,cur与head指向的是同一块空间,如果删除,head也被删除了。与tmp节点不同,tmp指向的就是需要删除的元素所在的空间,所以需要删除。
  2. 如何定义虚拟头结点,怎么初始化虚拟头结点,指向哪里?不能指向head
ListNode* vir = new ListNode(0);

当前节点肯定不是空,所以判断的时候只需判断当前节点是否指向nullptr就可以了

1.4 完整代码

/**
 * 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) {
        // 法1. 使用虚拟头结点
        ListNode* vir = new ListNode(0);
        vir->next = head;

        ListNode* cur = vir;
        while (cur->next != nullptr) {
            
            if (cur->next->val == val) {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            } else {
                cur = cur->next;
            }

        }

        head = vir->next;
        delete vir;
        return head;

        // 法2,分为头结点和非头结点,分别删除
        // //删除头结点
        // while ((head != nullptr) && (head->val == val)) {
        //     ListNode* tmp = head; 
        //     head = head->next;
        //     delete tmp;
        // }
        

        // ListNode* cur = head;
        // while ((cur != nullptr) && (cur->next != nullptr)) {
        //     if (cur->next->val == val) {
        //         ListNode* tmp = cur->next;
        //         cur->next = cur->next->next;
        //         delete tmp;
        //     } else {
        //         cur = cur->next;
        //     }        
        // }
        // return head;

    }
};

2. 707. 设计链表

2.1 自己看到题目的第一想法

如何构造一个链表?

2.2 看完代码随想录之后的想法

类里还可以定义结构体?那类里能定义类嘛?

C++中的类可以定义新的类或者结构体。这种情况被称为嵌套类(Nested Class)。
我们可以在一个类中定义另一个类。这个嵌套类具有与普通类相同的成员,如变量和函数,但也有一些不同之处。嵌套类可以被私有、公有或受保护,可以是静态或非静态的,可以是虚拟的或非虚拟的。

下面是一个示例:

class OuterClass 
{ 
    public: 
       // 嵌套类
        class InnerClass 
        { 
            public: 
                void display() 
                { 
                    cout << "This is Inner Class" << endl; 
                } 
        }; 
}; 

int main() 
{ 
    //创建外部类对象
    OuterClass outer; 

    // 在外部类中调用内部类方法
    OuterClass::InnerClass inner_obj;

    inner_obj.display(); 
    
    return 0; 
} 

输出:

This is Inner Class
在这里,我们定义了一个名为 OuterClass 的类,并在其中定义了一个名为 InnerClass 的嵌套类。InnerClass 中有一个名为 display 的函数,它将一个消息输出到屏幕上。

在主函数中,我们首先创建了 OuterClass 的对象 outer,然后创建了它的嵌套类 InnerClass 的对象 inner_obj,并使用该对象调用 display 函数,显示消息 “This is Inner Class”。

2.3 自己实现过程中遇到哪些困难

  1. 好难,只能照着敲,能看懂,自己不会写
  2. _dummyHead和_size不需要定义类型嘛?什么意思呢
    _dummyHead 和 _size 是类 MyLinkedList 的成员变量,在类的定义中已经定义过其类型。在构造函数中,不需要再次定义它们的类型。

_dummyHead 是一个指向链表头结点的指针,它是一个 LinkedNode* 类型的变量。在这个构造函数中,我们初始化了 _dummyHead,将它指向一个空的 LinkedNode 结点。

_size 是链表元素的数量,它是一个整数类型的变量。在构造函数中,我们将 _size 初始化为 0,因为此时链表中还没有元素。

2.4 完整代码

class MyLinkedList {
public:
    // 定义链表节点结构体
    struct LinkedNode {
        int val;
        LinkedNode* next;
        LinkedNode(int val):val(val), next(nullptr) {}
    };
    // 初始化链表
    MyLinkedList() {
        _dummyHead = new LinkedNode(0); //_dummyHead 是一个指向链表头结点的指针,它是一个 LinkedNode* 类型的变量。始在这个构造函数中,我们初化了 _dummyHead,将它指向一个空的 LinkedNode 结点。
        _size = 0;

    }
    // 获取下标为index的元素
    int get(int index) {
        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);
        newNode->next = _dummyHead->next;
        _dummyHead->next = newNode;
        _size++;
    }
    
    void addAtTail(int val) {
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
            cur = cur->next;
        }
        cur->next = newNode;
        _size++;
    }
    
    void addAtIndex(int index, int val) {
        if (index > _size) return;
        if (index < 0) index = 0;
        LinkedNode* newNode = new LinkedNode(val);
        LinkedNode* cur =_dummyHead;
        while (index--) {
            cur = cur->next;
        }
        newNode->next = cur->next;
        cur->next = newNode;
        _size++;

    }
    
    void deleteAtIndex(int index) {

        if (index >= _size || index < 0) {
            return;
        }
        LinkedNode* cur = _dummyHead;
        while (index--) {
            cur = cur->next;
        }
        LinkedNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        _size--;
    }
        // 打印链表
    void printLinkedList() {
        LinkedNode* cur = _dummyHead;
        while (cur->next != nullptr) {
            cout << cur->next->val << " ";
            cur = cur->next;
        }
        cout << endl;
    }
private:
    int _size;
    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);
 */

3. 206.反转链表

3.1 自己看到题目的第一想法

遍历链表,用一个栈存链表中的每个元素,然后重新从链表投挨个节点赋值

3.2 看完代码随想录之后的想法

这样可以双指针啊,我先用栈实现吧,双指针得归纳一下了。

3.3 自己实现过程中遇到哪些困难(总结)

  • 法1 双指针法
  1. pre指针如何定义并初始化?
    答:应该初始化为nullptr,不应该初始化为0,否则反转之后,pre最后指向为0,会多一个值
  2. ListNode* tmp;定义一个不指向任何空间的指针可以嘛?
    答:在C++中,定义一个指针并不会自动为其分配内存空间。因此,你可以定义一个不指向任何空间的指针,但是在使用它之前,你应该为其分配内存空间或将其指向已经存在的有效对象。

在你的例子中,ListNode* tmp;定义了一个指向ListNode类型的指针变量tmp,但它没有指向任何有效的内存空间。如果你尝试在使用tmp之前对其进行操作(例如访问其值或在其上进行解引用),可能会导致未定义的行为。

为了避免悬空指针的问题,你可以选择在定义指针时将其初始化为nullptr,表示它不指向任何有效的内存空间,例如:ListNode* tmp = nullptr;。然后,你可以在后续的代码中为该指针分配内存空间或将其指向有效的对象。

  • 法2:递归
    暂时没动手实现;

  • 法3:用栈实现(自己最先想到)

  1. 如何定义栈?
    答:stack stk;
  2. 压栈函数是?push_back()还是push()?
    答:压栈:stk.push(); stk.pop();
  3. 判断栈空
    答:stk.empty();
  4. 我的代码为什么不行?
// 我的
        ListNode* cur2 = head;
        while (!stk.empty()) {
            cur2->val = stk.top();
            cur2 = cur2->next;
            stk.pop();
        }
        cur2->next = nullptr;

        return head;        

//正确的
        ListNode* newHead = new ListNode(stk.top()); // 倒置之后的链表的头结点应该是原链表尾节点,所以先取出栈顶元素作为新头结点
        ListNode* cur2 = newHead; // 从新的头结点开始
        stk.pop(); // 把新的头结点从栈中剔除

        while (!stk.empty()) {
            ListNode* newNode = new ListNode(stk.top()); // 栈顶元素作为新节点值
            cur2->next = newNode; // 指向新节点
            cur2 = cur2->next; // 移动指针到新节点
            stk.pop(); // 弹出栈顶元素
        }
        cur2->next = nullptr; // 修改尾结点的next指针

        return newHead; // 返回新链表的头结点 
        

3.4 完整代码

#include <stack>

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (head == nullptr || head->next == nullptr) { //如果链表为空或只有一个节点,直接返回原链表;反转后仍为同一链表
            return head;
        }

        stack<int> stk; // 栈需要指定模板类型,这里是存储int的单链表
        ListNode* cur = head;
        while (cur != nullptr) {
            stk.push(cur->val); // 将链表的数值依次入栈
            cur = cur->next;
        }

        ListNode* newHead = new ListNode(stk.top()); // 倒置之后的链表的头结点应该是原链表尾节点,所以先取出栈顶元素作为新头结点
        ListNode* cur2 = newHead; // 从新的头结点开始
        stk.pop(); // 把新的头结点从栈中剔除

        while (!stk.empty()) {
            ListNode* newNode = new ListNode(stk.top()); // 栈顶元素作为新节点值
            cur2->next = newNode; // 指向新节点
            cur2 = cur2->next; // 移动指针到新节点
            stk.pop(); // 弹出栈顶元素
        }
        cur2->next = nullptr; // 修改尾结点的next指针

        return newHead; // 返回新链表的头结点 
    }
};


其他

ListNode *tmp 与ListNode* tmp	是等价的

内训泄漏

在C++中,**内存泄漏(Memory Leak)**是指在程序运行过程中,动态分配的内存没有被正确释放或回收的情况。当内存泄漏发生时,程序无法再访问或使用被泄漏的内存块,导致系统中的可用内存逐渐减少,直到最终耗尽可用内存,程序可能会崩溃或表现出异常的行为。

内存泄漏通常发生在以下情况下:

  1. 动态内存分配后未调用释放函数:在使用new关键字动态分配内存后,如果没有使用对应的delete或delete[]来释放内存,就会发生内存泄漏。
int* ptr = new int;		// 没有释放ptr指向的内存,造成内存泄漏
  1. 丢失对动态分配内存的引用:如果动态分配的内存的指针被覆盖或丢失,就无法再释放该内存,导致内存泄漏。
void function() {
    int* ptr = new int;
    ptr = nullptr;  // ptr丢失对动态内存的引用,无法释放内存
}
  1. 循环引用:在使用智能指针(如std::shared_ptr)时,如果存在循环引用(两个或多个对象彼此持有对方的智能指针),这些对象无法被正常释放,导致内存泄漏。
class Node {
public:
    std::shared_ptr<Node> next;
};

std::shared_ptr<Node> node1 = std::make_shared<Node>();
std::shared_ptr<Node> node2 = std::make_shared<Node>();
node1->next = node2;
node2->next = node1;  // 循环引用,导致两个节点无法被释放

内存泄漏会导致程序占用的内存越来越多,最终可能耗尽系统资源。为避免内存泄漏,需要确保在动态分配内存后及时释放,并避免循环引用等情况。使用智能指针、RAII(资源获取即初始化)等技术可以帮助自动管理内存,减少内存泄漏的风险。此外,内存泄漏检测工具和良好的代码设计和测试也能帮助发现和解决内存泄漏问题。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。