非科班学习算法day3 | LeetCode203: 移除链表元素,Leetcode707: 设计链表,Leetcode206: 反转链表
目录
介绍
包含LC的两道题目,还有相应概念的补充。
相关图解和更多版本:
代码随想录 (programmercarl.com)https://programmercarl.com/#%E6%9C%AC%E7%AB%99%E8%83%8C%E6%99%AF
一、基础概念补充:
1.c++中的new
在C++中,new
运算符用于动态地分配内存。它执行两个主要操作:首先,它从堆区域中分配一块内存,这块内存的大小足以容纳特定类型(或对象)的数据;其次,它调用该类型的构造函数来初始化这块内存中的对象(如果是类类型)
基本数据类型的动态分配:
int* p_int = new int; // 分配一个int大小的内存,并返回指向它的指针
类类型的动态分配:
// 假设有一个类MyClass
class MyClass {
public:
MyClass() { std::cout << "Constructor called\n"; }
~MyClass() { std::cout << "Destructor called\n"; }
};
MyClass* p_myclass = new MyClass(); // 分配MyClass对象的内存,并调用构造函数
new
与数组的动态分配:
int* arr = new int[10]; // 分配一个包含10个int的数组
类类型的动态分配数组:
// 假设有一个类MyClass
MyClass* arr_myclass = new MyClass[5]; // 分配一个包含5个MyClass对象的数组
new
与有参构造函数的结合使用:
// 假设有一个类MyClass,它有一个带参数的构造函数
class MyClass {
public:
MyClass(int value) : data(value) { std::cout << "Constructor with value: " << data << '\n'; }
~MyClass() { std::cout << "Destructor called\n"; }
private:
int data;
};
MyClass* p_myclass = new MyClass(42); // 分配MyClass对象的内存,并调用带参数的构造函数
new必须
与delete
的配对使用,否则可能造成内存泄漏
释放单个对象的内存:
delete p_int; // 释放p_int指向的int内存
delete p_myclass; // 释放p_myclass指向的MyClass对象的内存
释放数组的内存:
delete[] arr; // 释放arr指向的int数组的内存
delete[] arr_myclass; // 释放arr_myclass指向的MyClass数组的内存
2.链表
C++链表概述
链表是一种常见的线性数据结构,它由一系列节点组成,每个节点包含数据和指向下一个节点的指针。在C++中,链表通常通过结构体或类来实现,每个节点包含数据部分和指针部分。链表的特点是节点在内存中不连续存储,而是通过指针相互链接.
基本操作包括创建链表、插入节点、删除节点、查找节点和遍历链表等。插入和删除节点的时间复杂度通常为O(1),而查找节点的时间复杂度为O(n),因为需要从头节点开始逐个遍历。可以看出来频繁的插入和删除的操作用链表是有优势的。
C++单向链表构建
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
C++链表增删节点思路
强烈推荐统一养成虚拟头节点的方法解决问题。
二、LeetCode题目
1.LeetCode203: 移除链表元素
题目链接:203. 移除链表元素 - 力扣(LeetCode)
题目解析
首先,采用虚拟头节点的方法。创建一个新的节点,将节点的尾部指向现在的头部(head),并且创建遍历指针。
寻找目标值,调整链表指向。
c++代码如下:
/**
* 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)
{
//创建虚拟头节点,并设置为0
ListNode* dummyhead = new ListNode(0);
//将虚拟头节点添加到头部
dummyhead->next = head;
//设置循环指针指向虚拟头节点
ListNode* cur = dummyhead;
while(cur->next != nullptr)
{
if(cur->next->val == val)
{
//记录需要删除的节点位置
ListNode* temp = cur->next;
//更新链表顺序
cur->next = cur->next->next;
//手动释放内存
delete temp;
}
else
{
//移动检索指针
cur = cur->next;
}
}
//返回头部节点
head = dummyhead->next;
delete dummyhead;
return head;
}
};
注意点1:对于new出来的空间,要手动管理,所以在最后将dummyhead单独删除,防止内存泄漏;同样的,对于不需要的空间,手动删除不需要的节点temp
注意点2:关于边界的设置cur->next != nullptr,这里要注意检查的是下一个节点是否存在,cur != nullptr是检查指针指向是不是空,是完全不一样的概念
注意点3:关于cur指针,首先最后是需要返回头节点,也就是head,而head现在依靠的是dummyhead的指向,所以不要直接移动头节点,而是选择设立新的指针;其次指针重新指向的过程需要的是将cur节点的下一位替换为cur节点的下一位的下一位。
2.Leetcode707: 设计链表
题目链接:
题目解析
主要是借助虚拟头节点,将操作统一化
C++代码如下:
class MyLinkedList {
public:
//定义链表结构体
struct LinkedNode
{
int val;
LinkedNode* next;
LinkedNode(int x):val(x),next(nullptr){}
};
//初始化函数
MyLinkedList()
{
dummyhead = new LinkedNode(0);
size = 0;
}
//获取下标为index的节点的值
int get(int index)
{
if(index < 0 || index >= size)
{
return -1;
}
//设置检索指针
LinkedNode * cur = dummyhead->next;
//搜索!
while(index > 0)
{
cur = cur->next;
index--;
}
return cur->val;
}
//头部添加节点-ok
void addAtHead(int val)
{
LinkedNode * newnode = new LinkedNode(val);
newnode->next = dummyhead->next;
dummyhead->next = newnode;
size++;
}
//在尾部添加节点-ok
void addAtTail(int val)
{
LinkedNode * cur = dummyhead;
while(cur->next != nullptr)
{
cur = cur->next;
}
LinkedNode * newnode = new LinkedNode(val);
newnode->next = nullptr;
cur->next = newnode;
size++;
}
//在index插入节点val-ok
void addAtIndex(int index, int val)
{
if(index > size)
{
return;
}
LinkedNode * newnode = new LinkedNode(val);
LinkedNode * cur = dummyhead;
while(index > 0)
{
cur = cur->next;
index--;
}
newnode->next = cur->next;
cur->next = newnode;
size++;
}
//删除下标为index的节点 - ok
void deleteAtIndex(int index)
{
if(index >= size || index < 0)
{
return;
}
LinkedNode * cur = dummyhead;
while(index > 0)
{
cur = cur->next;
index--;
}
LinkedNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;
temp = nullptr;
size--;
}
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);
*/
定义结构体
属于常规操作
struct LinkedNode
{
int val;
LinkedNode* next;
LinkedNode(int x):val(x),next(nullptr){}
};
获取下标为index的节点的值
先对输入进行判定!如果不符合检索条件,直接返回-1;接下来首先将指针指向链表头部,因为这里的要求是,首位是0开始检索index(其实定义在dummyhead也可以写,不过个人感觉不够工整,和后面的条件也不够统一,书写过程中容易搞混乱),接下来借助于index--来进行链表的检索。
//获取下标为index的节点的值
int get(int index)
{
if(index < 0 || index >= size)
{
return -1;
}
//设置检索指针
LinkedNode * cur = dummyhead->next;
//搜索!
while(index > 0)
{
cur = cur->next;
index--;
}
return cur->val;
}
头部添加节点
先设置虚拟头节点,然后将新增节点的尾部指向头节点,再将虚拟头节点的尾部指向新增节点,构成新的链表,这样子做是为了防止next无法准确找到,所以一定要注意顺序。不要忘记调整链表长度便于维护。
//头部添加节点-ok
void addAtHead(int val)
{
LinkedNode * newnode = new LinkedNode(val);
newnode->next = dummyhead->next;
dummyhead->next = newnode;
size++;
}
在尾部添加节点
和头部添加节点的思路是一样的,不过这里显示要进行链表的遍历,先找到需要的位置,即尾部;然后再将新建节点的尾部指向空,最后添加到原有的链表中。不要忘记调整链表长度便于维护。
//在尾部添加节点-ok
void addAtTail(int val)
{
LinkedNode * cur = dummyhead;
while(cur->next != nullptr)
{
cur = cur->next;
}
LinkedNode * newnode = new LinkedNode(val);
newnode->next = nullptr;
cur->next = newnode;
size++;
}
在index插入节点val
这一步类似于之前的总结。首先检查条件是否满足检索要求,不满足直接返回;之后遍历链表直到检索位置,新建节点,将新节点插入。同样的,插入时候注意指向的顺序。不要忘记调整链表长度便于维护。
//在index插入节点val-ok
void addAtIndex(int index, int val)
{
if(index > size)
{
return;
}
LinkedNode * newnode = new LinkedNode(val);
LinkedNode * cur = dummyhead;
while(index > 0)
{
cur = cur->next;
index--;
}
newnode->next = cur->next;
cur->next = newnode;
size++;
}
删除下标为index的节点
//删除下标为index的节点 - ok
void deleteAtIndex(int index)
{
if(index >= size || index < 0)
{
return;
}
LinkedNode * cur = dummyhead;
while(index > 0)
{
cur = cur->next;
index--;
}
LinkedNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;
temp = nullptr;
size--;
3.Leetcode206:反转链表
题目解析
首先基于链表的数据结构,可以知道,反转链表可以采用逐个,更改指向。那么问题是如果我定义一个指针cur,当我用cur->next指向了cur,因为指向发生了变化,那么cur->next->next就会造成丢失或者错位!所以可以采用两个指针来进行分别的检索和保存。下面具体实现一下。
C++代码如下:
/**
* 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 * cur1 = head;
//定义反向链表的尾部
ListNode * cur2 = nullptr;
//定义临时变量
ListNode * temp;
while(cur1 != nullptr)
{
//保存顺序链表下一位
temp = cur1->next;
//重新指向
cur1->next = cur2;
//移动指针
cur2 = cur1;
cur1 = temp;
}
//返回新链表的尾部
return cur2;
}
};
注意点1:补充一下,这里定义temp应该指向空,防止造成野指针。
//定义临时变量
ListNode * temp = nullptr;
注意点2:cur1指向起始位置,进行链表的正向检索;cur2指向空,用来作为新链表的尾部来接受指向;想移动cur1要借助提前保存好下一位置的指针。
注意点3:最后的返回条件是cur2,因为此时,cur1已经指向了链表原来顺序的尾部(null)
更为详细的图解见:代码随想录 (programmercarl.com)
总结
打卡第三天,坚持!!!