目录
知识点:链表
结点定义
单链表的初始化
判断一个链表是否为空
单链表的销毁
清空单链表
求链表表长
取单链表中第i个元素
按值查找
插入第i个结点
删除第i个结点
移除列表元素
没有采用虚拟头结点的方法
采用虚拟头结点的方法
设计链表
反转链表
知识点:链表
链表:存储每一个元素的同时,还存储下一个元素的地址
结点包含数据域和指针域
链表有三种:单链表、双向链表和循环链表
头指针:指向链表第一个结点的指针
单链表由表头唯一确定,所以可以用头指针来代表整个链表
首元结点:链表中存储第一个数据元素a1的结点
空表:头结点的指针域为空
设置头结点的好处:1.方便处理首元结点;2.便于空表和非空表的统一处理
顺序表:随机存取;链表:顺序存取
头结点 -> 第1个结点 -> 第2个结点 -> ··· -> 第n个节点
-
构建结点 struct
-
生成头结点
-
生成子结点
-
向节点中存数据
-
构建结点之间的关系
结点定义
构建结点
用结构体struct来定义结点
指向头结点的指针,代表整个链表,使用LinkList L;定义 而指向某个结点的指针,使用Lnode* p;定义
typedef struct Lnode
{
ElemType data; // ElemType指数据类型
struct Lnode *next; // 自己定义自己,嵌套定义
} Lnode, *LinkList; // Lnode:指Lnode结点;LinkList:指向结构体Lnode的指针
// Lnode a; a.data来访问
// Lnode* L;和LinkList L;效果相同,都是定义一个指向链表的指针
为统一链表的操作,通常定义:
typedef struct
{
char num[8];
char name[8];
int score;
} ElemType;
typedef struct Lnode
{
ElemType data;
struct Lnode* next;
} Lnode, *LinkList;
单链表的初始化
即构造一个空表
Status initList(LinkList &L)
{
L = new Lnode; // 生成一个大小为Lnode的内存,将地址赋值给L
L->next = NULL;
return OK;
}
->操作指针指向对象的某一个成员
判断一个链表是否为空
头结点指针与是否为空
int ListEmpty(LinkList L)
{
if (L->next) // 非空
return 0;
else
return 1;
}
单链表的销毁
从头结点开始,依次释放所有结点
Status DestroyList(LinkList &L)
{
Lnode *p;
while L // 非空
{
p = L;
L = L->next;
delete p;
}
return OK;
}
清空单链表
依次释放所有结点,并将头结点指针域设置为空
int RemoveList(LinktList &L)
{
Londe *p = L->next;
Londe *q = p;
while p
{
q = p->next;
delete p;
p = q;
}
L->data = NULL;
return OK;
}
求链表表长
从首元结点开始,依次计数所有结点
Status ListLength(LinkList &L)
{
if (L)
return 0;
Londe *p = L->next;
int count = 1;
while p
{
p = p->next;
count++;
}
return OK;
}
取单链表中第i个元素
从链表的头指针出发,顺着next逐个结点向下搜索,直到搜索到第i个元素
-
声明一个指针p指向链表第一个结点,初始化j从1开始;
-
当j<i时,就遍历链表,让指针p向后移动,不断指向下一个结点,j累加1;
-
当j==i时,找到第i个结点
Status GetElem(LinkList L, int i, ElemType &e)
{
p = L->next;
int j = 1;
while (p && j < i)
{
p = p->next;
j++;
}
if (!p || j > i) // 第i个元素不存在
return ERROR;
e = p->data;
return OK;
}
按值查找
根据指定数据获取该数据所在的位置
int LocateElem(LinkList L, ElemType e)
{
Lnode *p = L->next; // 此时p指向第一个结点
count = 1;
while (p->data != e)
{
p = p->next;
count++;
}
if (p->data == e)
return count;
else
return 0;
}
插入第i个结点
在第i个结点前插入值为e的新结点
关键:找到第i-1个结点
Status InsertElem(LinkList &L, int i, ElemType e)
{
Lnode *p = L->next; // 此时p指向第一个结点
int j = 1;
while (j < i -1 && p) // 指针不为空
{
p = p->next;
j++;
}
// 循环完后,p指向第i-1个结点
if (!p || j > i - 1)
return ERROR;
Lnode *s = new Lnode; // 生成一个空结点
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
删除第i个结点
Status RemoveElem(LinkList &L, int i, ElemType &e)
{
Londe *p = L->next;
int j = 1;
while (j < i - 1 && p)
{
p = p->next;
j++;
}
// 循环完后,p指向第i-1个结点
if (!p || j > i - 1)
return ERROR;
Lnode *s = p->next; // s指向第i个结点
p->next = s->next;
e = s->data;
delete s;
return OK;
}
温馨提示:在力扣中,关于链表的题目,都是没有头结点的(也有的人叫做虚拟头结点)
操作指针时,一定要先判断指针是否为空,若为空,则编译报错(操作空指针)
移除列表元素
题目描述:给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点
。
示例:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
输入:head = [], val = 1
输出:[]
思路:这道题目还是比较简单的,主要要考虑要删除节点的位置,即要删除的节点是否是首元结点,所谓首元结点,是指储存元素的第一个节点,head指向该节点。为了统一删除操作,可以采用(虚拟)头节点的方式。
删除节点的流程:
假设cur指向我们要删除的节点,pre指向要删除节点的前一个节点,即pre->next等于cur
pre->next = cur->next; // 如果单次删除的话,这一句就可以了
cur = cur->next; // 为下一次删除做准备
没有采用虚拟头结点的方法
ListNode* removeElements(ListNode* head, int val) {
if (head == NULL) // 如果是空链表,直接返回
return head;
while (head->val == val) // 链表第一个元素是或前几个元素都是val
{
head = head->next;
if (head == NULL)
return head;
}
// 快慢指针
ListNode *pre = head;
ListNode *cur = head->next;
while (cur != NULL)
{
if (cur->val == val)
{
pre->next = cur->next; // 删除结点-步骤1
cur = cur->next; // 删除结点-步骤2
}
else
{
pre = cur;
cur = cur->next;
}
}
return head;
}
采用虚拟头结点的方法
ListNode* removeElements(ListNode* head, int val) {
if (head == NULL) // 如果是空链表,直接返回
return head;
ListNode *dummyNode = new ListNode(-1, head); // 虚拟头结点 dummyNode->val为-1,dummyNode->next为head(相当于调用了结构体ListNode的构造函数)
ListNode *pre = dummyNode;
ListNode *cur = dummyNode->next; // 或者ListNode *cur = head;
while (cur != NULL)
{
if (cur->val == val)
{
pre->next = cur->next; // 删除结点的两步操作 记住!
cur = cur->next;
}
else
{
pre = cur;
cur = cur->next;
}
}
// return head; // 注意:直接return head; 是错误的 我们想要的head应该是dummyNode的下一个结点,而非本来的head(这两个head有可能一样,有可能不同)
head = dummyNode->next;
return head;
}
设计链表
题目描述:设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev
以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
-
get(index):获取链表中第 index
个节点的值。如果索引无效,则返回-1
。
-
addAtHead(val):在链表的第一个元素之前添加一个值为 val
的节点。插入后,新节点将成为链表的第一个节点。
-
addAtTail(val):将值为 val
的节点追加到链表的最后一个元素。
-
addAtIndex(index,val):在链表中的第 index
个节点之前添加值为 val
的节点。如果 index
等于链表的长度,则该节点将附加到链表的末尾。如果 index
大于链表长度,则不会插入节点。如果index
小于0,则在头部插入节点。
-
deleteAtIndex(index):如果索引 index
有效,则删除链表中的第 index
个节点。
示例:
MyLinkedList linkedList; // 实例化类对象
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); // 链表变为1-> 2-> 3
linkedList.get(1); // 返回2
linkedList.deleteAtIndex(1); // 现在链表是1-> 3
linkedList.get(1); // 返回3
踩过的坑:
-
index从0开始;
-
写类方法时,考虑是否要判断index的范围;
-
最好采用虚拟头结点的方法;
-
在写addAtIndex方法时,index可以等于链表长度,相当于在链表尾插入;
具体代码:
class MyLinkedList {
public:
struct Node
{
int val;
Node *next;
Node(int val):val(val), next(nullptr){}
};
private:
// 私密属性前加 '_'
Node *_dummyNode; // 虚拟头结点
int _size; // 记录链表长度
public:
MyLinkedList()
{
_dummyNode = new Node(-1); // 调用构造函数
_size = 0;
}
int get(int index) // index从0开始
{
if (index < 0 || index > _size - 1)
{
return -1;
}
Node *p = _dummyNode->next;
for (int i = 0; i < index; i++)
{
p = p->next;
}
return p->val;
}
void addAtHead(int val)
{
Node *node = new Node(val);
node->next = _dummyNode->next;
_dummyNode->next = node;
_size++;
}
void addAtTail(int val)
{
Node *node = new Node(val);
if (_dummyNode->next == nullptr)
{
_dummyNode->next = node;
}
else
{
Node *p = _dummyNode->next;
while (p->next != nullptr)
{
p = p->next;
}
p->next = node;
}
_size++;
}
void addAtIndex(int index, int val)
{
if (index < 0 || index > _size) // 为什么这里不是大于_size - 1 原因:当index=_size时,相当于往链表尾添加元素
{
return;
}
Node *node = new Node(val);
Node *p = _dummyNode;
for (int i = 0; i < index; i++)
{
p = p->next;
}
node->next = p->next;
p->next = node;
_size++;
}
void deleteAtIndex(int index)
{
if (index < 0 || index > _size - 1)
return;
Node *p = _dummyNode;
for (int i = 0; i < index; i++)
{
p = p->next;
}
Node *de = p->next;
p->next = de->next;
delete de;
_size--;
}
};
反转链表
题目描述:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
输入:head = [1,2]
输出:[2,1]
输入:head = []
输出:[]
思路:采用双指针的方法,我代码里的lat指针其实就相当于卡哥视频里的临时指针。简单来说,pre指向cur的前一节点,lat最开始和cur指向同一个节点,在每次循环时,用lat来保存cur的下一节点。
具体代码:
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr)
return head;
ListNode *pre = head;
ListNode *cur = pre->next;
ListNode *lat = cur;
pre->next = nullptr; // 不能与上上一句调换顺序
while (cur->next != nullptr)
{
lat = cur->next;
cur->next = pre;
pre = cur;
cur = lat;
}
cur->next = pre;
head = cur;
return head;
}