链表基础
1.链表的定义
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域,另一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。结构如图所示:
2.链表的类型
链表分为单链表、双链表、循环链表等。单链表如上图。
双链表
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表 既可以向前查询也可以向后查询。结构如图所示:
循环链表
尾节点指向头结点。如图所示:
3.链表节点的代码
对链表的表示是使用的节点,也就是说我们通过定义节点的结构就可以用其来表示链表。节点包含数据部分,指针部分,也可以加一个构造函数(赋初值的部分),代码如下:
struct ListNode{
int val; //节点上存的元素
ListNode * next; //指向下一节点的指针
ListNode(int x) : val(x), next(NULL) {}; //节点构造函数,将val初始化为x,next指针初始化为NULL
};
C++结构体中支持写成员函数,所以我们添加了一个节点的构造函数用于初始化节点。
4.链表的操作
删除节点
删除D节点:
- 记录D节点的地址即pos = C.next
- C.next = D.next
- delete释放第一步pos所在的内存
插入节点
- 记录手动开辟的F节点的地址pos
- F.next = C.next
- C.next = pos
5. 数组与链表的区别
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。但是数组查询很方便,因为数组支持下标访问。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景(查询需要一个一个的遍历)。
203.移除链表元素
题目描述:
正常情况下删除val所在的节点,则执行上文所述4.链表操作小节中删除节点的操作即可,但是当
被删除的值位于 头结点中时,我们就需要记录head的地址,然后head = head ->next,delete刚刚记录的head地址。相当于我们对head节点做了特殊的逻辑,这样的代码实现如下:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) { // 注意这里不是if
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
//此时head可能是原链表最后的null也可能不是(还有元素)
ListNode* cur = head;
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;
}
};
上述代码最开始遍历的视时候采用了一个while循环来处理val在头结点中的情景,没有采用if是为了防止链表前几个元素全是val的情况。
添加虚拟头结点的方法就不用对val在前几个节点做特殊操作了,代码如下:
/**
* 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 *vtr_head = new ListNode(0);
//用虚拟头结点作为新的头结点
vtr_head->next = head;
//使用tmp指针从头开始遍历链表
ListNode *tmp = vtr_head;
//当tmp下一个节点有元素时
while(tmp->next != nullptr)
{
//若该元素为val,需删除
if(tmp->next->val == val)
{
//记录当前元素节点的内存
ListNode *addr = tmp->next;
//tmp->next = 该节点的next
tmp->next = addr->next;
//delete该元素节点的内存
delete addr;
}
else
{
//更新遍历指针tmp
tmp = tmp->next;
}
}
//最后返回虚拟头结点的next即为新链表的头结点
head = vtr_head->next; //方便理解还是用head来记录头结点
delete vtr_head; //释放用于辅助的虚拟头结点
return head;
}
};
707.设计链表
题目描述:
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,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
的节点。
1.补充节点定义及构造函数
2.链表要维护头结点指针和实际有效元素个数size
3.插入和删除元素时记得更新size
4.从虚拟头结点开始,遍历到末尾的循环条件就是cur->next != nullptr。因为最后一个节点的next 等于nullptr。
5.有虚头结点的链表,在index处插入或删除,遍历条件均为:
ListNode * cur = head;
while(index--)
{
cur = cur->next;
}
整体代码:
class MyLinkedList {
public:
//添加节点结构体定义
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
//链表应该有哪些属性?链表头指针head、链表中节点的个数size
MyLinkedList() {
this->size = 0;
this->head = new ListNode(0); //创建了一个不存储有效元素的虚拟头结点,不计入长度
}
int get(int index) {
//判断下标index是否超出链表size的范围,size-1为最大坐标,0为最小坐标
if(index > size-1|| index < 0 )
{
return -1;
}
//下标为index,则需遍历index+1次
ListNode * cur = head; //保护head不改变,使用cur来遍历各节点
while(index)
{
cur = cur->next;
--index;
}
return cur->next->val;
}
void addAtHead(int val) {
//先创建新节点记录val
ListNode * newHead = new ListNode(val);
//新节点指向原来的第一个节点即head->next(我们的头结点是虚拟头结点,所以实际第一个元素所在的节点为head->next)
newHead->next = head->next;
//虚拟头结点指向新节点
head->next = newHead;
//别忘记更新size
size++;
}
void addAtTail(int val) {
//先创建新节点记录val
ListNode * newTail = new ListNode(val);
//要找到原来的最后一个节点,将其next改为newTail。可以通过size找,也可以通过指针找
//指针找
ListNode * cur = head;
//从虚拟头结点开始,遍历到末尾的循环条件就是cur->next != nullptr
while(cur->next!=nullptr)
{
cur = cur->next;
}
cur->next = newTail;
//size++
size++;
}
void addAtIndex(int index, int val) {
//先判断index是否有效
if( index > size || index < 0)
{
return ;//无效则插入失败直接return
}
//先创建新节点记录val
ListNode * newNode = new ListNode(val);
ListNode * cur = head;
//将一个值为 val 的节点插入到链表中下标为 index 的节点之前与当index 等于链表的长度追加到链表的末尾做的是相同的操作
while(index--)
{
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
size++;
}
void deleteAtIndex(int index) {
if(index > size-1|| index < 0 )
{
return ;
}
ListNode * cur = head;
while(index--)
{
cur = cur->next;
}
ListNode * tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
tmp = nullptr;
size--;
}
private:
int size;
ListNode * head; //注意头结点指针类型为ListNode
};
206.反转链表
题目描述:
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
思路如下:
为什么要使用tmp记录cur-》next呢?因为题目要反转链表,如果我们不提前保存cur下一个节点的地址,那么cur-》next指向反转后,我们将无法再访问到下一个节点。
所以一旦我们记录了cur->next,我们就可以对cur->next做反转了:cur->next = prev。
下一步先更新prev = cur, 在更新cur=tmp。如果先更新cur,那么prev将无法指向节点1。
变化一个元素后的情况如下:
接着执行上面的4个操作,当cur指向最后一个节点时,还需要改变该节点的指向,所以还需要执行一轮的这4个操作。
执行完之后是这种情形:
所以我选择了用while(cur!=nullptr)作为循环遍历的控制条件。
返回prev即可。代码如下:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *prev = nullptr;
ListNode *cur = head;
while(cur!=nullptr)
{
ListNode * tmp = cur->next;
cur->next = prev;
prev = cur;
cur = tmp;
}
return prev;
}
};