2.设计链表
题目要求:在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
题目理解
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
也就是实现五个函数。分别是获取第n个节点,头部插入节点,尾部插入节点,第n个节点前插入节点,删除第n个节点。
注意:题目的假设中,这道题目的n是从0开始的,首先需要获取第0个节点,也就是链表的头节点。
链表头节点”下标“的问题
链表和数组是两种不同的数据结构,它们在内存中的存储方式和访问方式也不同。链表中的每个节点都包含一个数据域和一个指针域,指针域指向下一个节点的地址。数组中的每个元素都有一个固定的下标,下标从0开始。
链表的头节点是链表中的第一个节点,它不一定是0,而是根据分配的内存地址决定的。链表没有下标的概念,要访问链表中的某个节点,需要从头节点开始遍历,直到找到目标节点为止。
因此,链表头节点和数组第一个元素不一样都是0,链表也没有从0开始排序的下标。
链表头节点的问题
q: 头节点head到底是一个链表对象还是一个指针?
a: 在 C++ 中,头节点 head
通常是一个指针,而不是一个链表对象。
头节点是链表中的第一个节点,它包含了链表的起始位置。在许多链表的实现中,头节点通常是一个指针,它指向链表的第一个节点。
这样的设计允许我们通过头节点的指针来访问和操作整个链表。通过头节点指针,我们可以遍历链表,访问每个节点,并进行插入、删除等操作。
因此,头节点 head
在大多数情况下是一个指针,它指向链表的第一个节点。通过这个指针,我们可以访问整个链表的结构和数据。
头节点介绍:
链表的头节点 head
通常是一个指针。这个指针指向链表中的第一个节点。
通常情况下,链表的每个节点由一个自定义的结构体或类来表示,这个结构体或类包含节点的值和指向下一个节点的指针。
因此,head
是一个指向链表第一个节点的指针,通过它可以遍历和访问整个链表。通过 head
,我们可以访问头节点的值以及通过头节点的 next
指针访问链表中的其他节点。
需要注意的是,C++ 中链表的头节点并不是固定要求为指针,可以使用其他方式来表示,例如使用模板类来实现链表。但在通常情况下,链表的头节点是一个指针。
注意点:链表操作中指针的重要性
链表的插入、提取、删除等操作基本都是通过指针完成的,而代码过程中不直接涉及链表对象本身。
链表的操作主要涉及节点之间指针的改变,通过改变指针的指向来实现插入、删除、提取等操作。具体而言:
- 插入操作:我们创建一个新节点,并将其插入到适当的位置,即修改相应节点的指针来连接新节点。
- 提取操作:我们可以通过指针访问到特定的节点,并提取所需的值或进行其他操作。
- 删除操作:我们通过修改指针的指向来绕过需要删除的节点,从而将其从链表中移除,并释放相应的内存。
在这些操作中,代码主要关注的是指针的操作,而不是直接操作链表对象本身。通过操作指针,我们可以有效地改变链表的结构和节点之间的连接关系。
因此,链表的操作是通过指针完成的,而链表对象本身在代码中的作用相对较小,主要是提供一个起始位置(头节点)来引导整个链表的访问。
问题:链表数据结构每次使用的时候都需要自己定义吗?
在C++和许多其他语言中,ListNode
这样的数据结构并不是内置的,所以在使用它们时,通常需要自己定义。例如,本题目中,就定义了一个ListNode
类来表示链表的节点。这个类包含了节点的值(val
)和一个指向下一个节点的指针(next
)。
在某些特定的情况下,如果你的代码库或者项目中经常需要使用链表,你可以将ListNode
类定义在一个公共的头文件中,这样在需要使用链表的时候,只需要包含这个头文件就可以了,不需要每次都重新定义。
在一些其他的编程语言中,例如Java或者Python,链表或者类似的数据结构(如ArrayList或者LinkedList)可能是语言的标准库的一部分,这样你就不需要自己定义这些数据结构了。但是,了解并能自己实现这些数据结构,仍然是非常有用的,因为这可以帮助你更好地理解它们的工作原理和性能特性。
1.获取第Index个节点的值
1.注意题目中的N是从0开始的,第0个节点也就是链表的头节点。
2.遍历链表的时候,先定义一个指针来遍历,而不是直接操作头指针。因为操作完链表之后需要返回头节点,上来就操作头节点的话,头节点的值变化,链表头节点无法返回。
class MyLinkedList {
public:
//定义链表节点结构体
struct ListNode{
int val;
LinkNode* next;
LinkNode(int val):val(val),next(nullptr){
//自己写一个空的构造函数
}
};
//初始化MyLinkedList 对象
//本题目中初始化的含义就是创建构造函数
MyLinkedList() { //构造函数
dummyHead = new LinkNode(0); //定义虚拟头节点
size = 0; //定义记录链表节点数量的变量
}
//获取下标为index的节点数值,index是从0开始的
int get(int index) {
ListNode* cur = dummyHead->next;
int count=0;
while(cur!=nullptr){
if(count==index){
return cur->val;
}
else{
count++;
cur=cur->next;
}
}
return -1;
}
void addAtHead(int val) {
}
};
2.在链表最前面插入一个节点
void addAtHead(int val) {
ListNode* list = new ListNode(val);
List = dummyHead; //与List->next = _dummyHead->next相同?
dummyHead->next = list;
size++;
}
3.在链表最后面添加一个节点
void addAtTail(int val) {
ListNode* list = new ListNode(val);
ListNode* cur = dummyHead; //需要遍历,定义遍历指针
while(cur->next!=nullptr){
cur=cur->next;
}
cur->next = list; //指针直接相等即可
size++;
}
cur->next = list
是将当前节点 cur
的 next
指针指向新创建的节点 list
。
在 addAtTail
方法中,我们希望将一个新的节点添加到链表的尾部。为了找到链表的尾部节点,我们需要遍历链表,找到最后一个节点。
首先,我们使用一个遍历指针 cur
来表示当前节点,初始时指向虚拟头节点 dummyHead
。然后,我们通过循环遍历链表,直到 cur
指针指向的节点的 next
指针为 nullptr
,即当前节点为链表的最后一个节点。
在循环结束后,我们创建一个新的节点 list
,并将当前节点 cur
的 next
指针指向新节点 list
,将新节点连接到链表的尾部。这样就完成了在链表尾部添加节点的操作。
最后,更新链表的节点数量 size
。
3.将节点插入index节点之前
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,那么新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index>size){
return; //返回空就是直接return; 即可
}
if(index=size){
ListNode* list = new ListNode(val);
ListNode* cur = dummyHead; //需要遍历,定义遍历指针
while(cur->next!=nullptr){
cur=cur->next;
}
cur->next = list; //指针直接相等即可
size++;
}
if(index<=0){
ListNode* list = new ListNode(val);
List = dummyHead; //与List->next = _dummyHead->next相同?
dummyHead->next = list;
size++;
}
}
4.删除第Index个节点
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
int count=0;
ListNode* cur = _dummyHead;
while(count!=index) {
cur = cur ->next;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
5.打印链表
void printLinkedList(){
ListNode* cur = dummyHead;
while(cur->next!=nullptr){
cout<<cur->next->val<<" ";
cur=cur->next;
}
cout<<endl;
}
6.完整版:
1.第一个版本:不用虚拟头节点
class ListNode { //class ListNode这种写法是可以的,ListNode是一种数据结构,它表示一个链表中的节点。可以用它来命名类,但是要注意和其他类名区分开,避免混淆。
public:
int val;
ListNode* next;
ListNode(int value) : val(value), next(nullptr) {}
};
class MyLinkedList {
//自己定义一个头节点head
private:
ListNode* head;
public:
MyLinkedList() : head(nullptr) {}
void addAtHead(int val) {
ListNode* newNode = new ListNode(val);//建立新的listNode节点
newNode->next = head;
head = newNode; //更新头节点
}
void addAtTail(int val) { //不用虚拟头节点的话,需要考虑两种情况
ListNode* newNode = new ListNode(val);
if (head == nullptr) {
head = newNode;
} else {
ListNode* curr = head;
while (curr->next != nullptr) {
curr = curr->next; //注意此处表达的意思就是cur++,但是不能写成cur++
}
curr->next = newNode; //此处cur指向的并不是空的内存,因此为了防止覆盖,需要放在next里面
}
}
int get(int index) { //获取链表第index个元素的值
ListNode* curr = head;
int count = 0;//定义一个整数来进行存储
while (curr != nullptr) { //注意此处的while循环条件,是curr!=nullptr而不是curr->next!=nullptr
if (count == index) {
return curr->val;
}
curr = curr->next;
count++;
}
return -1; // If index is out of range
}
void addAtIndex(int index, int val) {
if (index == 0) {
addAtHead(val);
} else {
ListNode* newNode = new ListNode(val);
ListNode* curr = head;
int count = 0;
while (curr != nullptr && count < index - 1) {
curr = curr->next;
count++;
}
if (curr != nullptr) {
newNode->next = curr->next;
curr->next = newNode;
}
}
}
void deleteAtIndex(int index) {
if (index == 0) {
if (head != nullptr) {
ListNode* temp = head;
head = head->next;
delete temp;
}
} else {
ListNode* curr = head;
int count = 0;
while (curr != nullptr && count < index - 1) {
curr = curr->next;
count++;
}
if (curr != nullptr && curr->next != nullptr) {
ListNode* temp = curr->next;
curr->next = curr->next->next;
delete temp;
}
}
}
};
上述代码实现了一个简单的单链表 MyLinkedList
,其中包含了常见的操作,如在头部添加节点、在尾部添加节点、获取指定位置的节点值、在指定位置插入节点和删除指定位置的节点。
你可以根据需要使用 MyLinkedList
类来初始化和操作链表对象。例如,使用以下代码来创建一个链表对象并进行操作:
MyLinkedList linkedList;
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1, 2);
int val = linkedList.get(1); // 获取索引 1 处的节点值
linkedList.deleteAtIndex(1);
val = linkedList.get(1); // 获取索引 1 处的节点值
这里定义了两个类,一个是ListNode
,另一个是MyLinkedList
。
ListNode
是一个简单的数据结构,表示链表中的一个节点。它包含一个整型值(val
)和一个指向下一个节点的指针(next
)。
注意:
cur++
的操作在链表里面是错误的,应该写成cur=cur->next
while (curr != nullptr)
和 while (curr->next != nullptr)
的区别是什么?
while (curr->next != nullptr)
会遍历链表直到curr
指向最后一个节点。这是因为,当curr
指向最后一个节点时,curr->next
会是nullptr
,此时循环就会结束。所以,循环结束后,curr
会指向链表的最后一个节点。
这种循环条件在你需要获取到最后一个节点,以便在其后添加新节点(如在尾部添加节点)或者修改其next
指针(如删除最后一个节点)时是非常有用的。
另一方面,**while (curr != nullptr)
**会遍历链表直到curr
指针已经越过最后一个节点,也就是curr
变成nullptr
。**循环结束后,curr
会指向链表最后一个节点的下一个。**这种循环条件在你需要遍历整个链表,包括访问最后一个节点时是非常有用的。
while (curr != nullptr)
适用于需要挨个链表元素(包括最后一个元素)的判断,例如本题中获取链表中下标为xx的元素的值,就需要用while (curr != nullptr)
而不是while (curr->next != nullptr)
2.第二个版本:使用虚拟头节点
class MyLinkedList {
public:
//定义链表节点结构体
struct ListNode{
int val;
ListNode* next;
ListNode(int val):val(val),next(nullptr){
//自己写一个空的构造函数
}
};
//初始化MyLinkedList 对象
//本题目中初始化的含义就是创建构造函数
MyLinkedList() { //构造函数
dummyHead = new ListNode(0); //定义虚拟头节点
size = 0; //定义记录链表节点数量的变量
}
//获取下标为index的节点数值,index是从0开始的
int get(int index) {
ListNode* cur = dummyHead->next;
int count=0;
if (index > (size - 1) || index < 0) {
return -1;
}
while(count<index&&cur!=nullptr){
if(count==index&&cur!=nullptr){
return cur->val;
}
else{
count++;
cur=cur->next;
}
}
return -1;
}
void addAtHead(int val) {
ListNode* list = new ListNode(val);
list->next = dummyHead->next; //与list= dummyHead不同,这一点一定要注意,这是要插入元素!
dummyHead->next = list;
size++;
}
void addAtTail(int val) {
ListNode* list = new ListNode(val);
ListNode* cur = dummyHead; //需要遍历,定义遍历指针
while(cur->next!=nullptr){
cur=cur->next;
}
cur->next = list; //指针直接相等即可
size++;
}
void addAtIndex(int index, int val) {
if(index>size){
return; //返回空就是直接return; 即可
}
if(index==size){
ListNode* list = new ListNode(val);
ListNode* cur = dummyHead; //需要遍历,定义遍历指针
while(cur->next!=nullptr){
cur=cur->next;
}
cur->next = list; //指针直接相等即可
size++;
}
if(index<=0){
ListNode* list = new ListNode(val);
list->next = dummyHead->next; //与List=dummyHead不同,这里也是插入元素!
dummyHead->next = list;
size++;
}
}
void deleteAtIndex(int index) {
if (index >= size || index < 0) {
return;
}
int count=0;
ListNode* cur = dummyHead;
while(count!=index) {
cur = cur ->next;
count++;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
size--;
}
private:
ListNode* dummyHead;
int size;
};class MyLinkedList {
public:
//定义链表节点结构体
struct ListNode{
int val;
LinkNode* next;
LinkNode(int val):val(val),next(nullptr){
//自己写一个空的构造函数
}
};
//初始化MyLinkedList 对象
//本题目中初始化的含义就是创建构造函数
MyLinkedList() { //构造函数
dummyHead = new LinkNode(0); //定义虚拟头节点
size = 0; //定义记录链表节点数量的变量
}
//获取下标为index的节点数值,index是从0开始的
int get(int index) { //这里的get测试有点问题,还需要再看
ListNode* cur = dummyHead->next;
int count=0;
while(cur!=nullptr){
if(count==index){
return cur->val;
}
else{
count++;
cur=cur->next;
}
}
return -1;
}
void addAtHead(int val) {
ListNode* list = new ListNode(val);
List = dummyHead; //与List->next = _dummyHead->next相同?
dummyHead->next = list;
size++;
}
void addAtTail(int val) {
ListNode* list = new ListNode(val);
ListNode* cur = dummyHead; //需要遍历,定义遍历指针
while(cur->next!=nullptr){
cur=cur->next;
}
cur->next = list; //指针直接相等即可
size++;
}
void addAtIndex(int index, int val) {
if(index>size){
return; //返回空就是直接return; 即可
}
if(index=size){
ListNode* list = new ListNode(val);
ListNode* cur = dummyHead; //需要遍历,定义遍历指针
while(cur->next!=nullptr){
cur=cur->next;
}
cur->next = list; //指针直接相等即可
size++;
}
if(index<=0){
ListNode* list = new ListNode(val);
List = dummyHead; //与List->next = _dummyHead->next相同?
dummyHead->next = list;
size++;
}
}
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
int count=0;
ListNode* cur = _dummyHead;
while(count!=index) {
cur = cur ->next;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
private:
ListNode* dummyHead;
int size;
};