数据结构 – 王道课程笔记
本笔记参考王道数据结构考研复习指导2023版,对上面的知识点进行提炼,以及代码和习题的进一步解释,同时还添加了leetcode的一些例题帮助理解。
第一章 绪论
1.1 数据结构的基本概念
- 什么是数据?
数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并背计算机程序所识别和处理的符号的合集。 - 世界上第一台通用计算机 ENIAC – 数值型问题
- 现代计算机 – 经常处理非数值型问题
- 数据元素、数据项:数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由阮淦数据项组成,数据项是构成数据元素的不可分割的最小单位。
- 数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
例如:一个微博账号的基本内容为一个数据元素,其中用户名、密码、生日等是一个个数据项,所有微博账号是一个数据对象。
第一节总结:
1.2 数据结构的三要素
- 逻辑结构 – 数据之间的逻辑关系
- 集合(考纲不考)
- 线性结构:一对一
- 树形结构:一对多
- 图结构:多对多
- 数据的运算 – 针对某种逻辑结构,结合实际需求,定义基本运算
例如对于线性结构,基本运算可以有如下:
- 查找第i个数据元素
- 在第i个位置插入新元素
- 删除第i个位置的元素 等等
- 物理结构(存储结构)-- 如何用计算机表示这些逻辑结构
- 顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元里。
优点:可以实现随机存取,每个元素占用最少的存储空间
缺点:每个元素只能使用相邻一整块的存储单元,可能会产生较多的外部碎片 - 链式存储:逻辑上相邻的元素在物理位置上可以不相邻(通过指针)
优点:不会出现碎片现象,充分利用存储单元
缺点:每个元素的指针会占用额外的存储空间,而且只能实现顺序存取 - 索引存储:在存储信息的同时,还建立附加的索引表
优点:检索速度快所以表额外占用空间,在删除修改数据之后索引表的更新也会浪费时间
缺点:索引表额外占用空间,在删除修改数据之后索引表的更新也会浪费时间 - 散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希存储
优点:检索、增加和删除结点的操作都很快
缺点:若散列函数不好,可能出现元素冲突增加时间和空间的开销
若采用顺序存储,则各个数据元素在物理上必须是连续的
数据的存储结构会影响存储空间分配的方便程度
数据的存储结构会影响对数据运算的速度
运算的定义是针对对逻辑结构设计的,指出运算的功能;运算的实现是针对存储结构设计的
数据类型、抽象数据类型:
数据类型是一个值的集合和定义在此集合上的一组操作的总称
- 原子类型:其值不可再分的数据类型
- 结构类型:其值可以再分为若干成分的数据类型
- 抽象数据类型(ADT)是抽象数据组织以及与之相关的操作
1.3 算法的基本概念
程序=数据结构+算法
算法是对特定问题求解步骤的一种描述,他是指令的有限序列,其中的每条指令表示一个或多个操作
算法的特性:
- 有穷性:一个算法必须总在执行有穷步骤之后结束,且每一步都在有穷时间内完成
- 确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
例如对所有人年龄升序排序,第二行和第三行结果不同,所以这个算法有问题
- 可行性:算法在描述的操作都可以通过已经实现的基本运算执行有限次来实现
- 输入:一个算法有零个或多个输入
- 输出:一个算法至少有一个输出
“好”算法的特质
- 正确性,算法应能正确的解决求解问题
- 可读性:算法应具有良好的可读性,以帮助人理解
- 健壮性:输入非法数据时,算法能适当的做出反应或进行处理
- 高效率与低存储量需求:花的时间少(时间复杂度低),占用存储少(空间复杂度低)
1.1-1.3 习题:
1.4 时间复杂度
算法时间复杂度的概念:事先预估算法时间开销T(n)和问题规模n的关系
一个算法的时间复杂度可以只考虑阶数高的部分,用O表示
时间复杂度排序:常对幂指阶
- 顺序执行的代码只会影响常数项,可以忽略
- 只需挑选循环中的一个基本操作分析他的执行次数与n的关系即可
- 如果有多层嵌套循环,只需关注最深层的循环执行了几次
小练习:
在学习中,一般只考虑最坏时间复杂度和平均时间复杂度
1.5 空间复杂度
算法的空间复杂度是指执行这个算法所需的存储空间的量度。它是计算算法在执行过程中大约需要多少内存空间的一个标准。在算法分析中,我们常常同时考虑时间复杂度和空间复杂度,两者共同决定了算法的效率
空间复杂度的概念
- 总体空间需求:算法执行过程中总共需要的存储空间。它包括固定部分(如代码空间、简单变量和固定大小的结构变量等)与变化部分(如动态分配的空间、递归栈空间等)的总和
- 辅助空间需求:除了输入数据所占的空间外,算法运行过程中额外需要的存储空间
空间复杂度的计算
- 固定部分:通常包括算法中的变量(包括常量、简单变量和固定大小的结构类型变量等)
- 变化部分:主要包括动态分配的空间大小和递归栈的使用空间。例如,一个递归算法的空间复杂度通常与递归深度成正比
常见的空间复杂度
- O(1):如果算法所需要的临时空间不随着某个变量n的大小而变化,即空间复杂度是一个常量,这个算法的空间复杂度就是O(1)
- O(n):对于一些算法,需要的临时空间是随着某个变量n的大小而线性增长的,那么这类算法的空间复杂度是O(n)
示例
以递归算法为例:如果有一个递归函数,每次递归都需要一个新的变量空间,而且递归深度为n,那么该算法的空间复杂度是O(n)。因为在递归的最深处,会有n个变量同时存在。
第二章 线性表
2.1 线性表的定义
线性表是具有相同数据类型的n个数据元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。线性表L的一般表示为:
L=(a1,a2,… ,an)
- ai是线性表中第i个元素,是一个位序
- a1是表头元素,an是表尾元素
- 除表头元素外,每个元素有且仅有一个直接前驱;除表尾元素外,每个元素仅有一个直接后继
位序从1开始,但数组下标从0开始
线性表的基本操作
- 初始化表 – InitList(&L) :构造一个空的线性表L,分配内存空间
- 销毁表 – DestoryList(&L): 销毁线性表,并释放线性表L所占用的内存空间
- 插入元素 --ListInsert(&L,i,e): 在表中的第i个位置插入元素e
- 删除元素 – ListDelete(&L,i,&e): 删除表L的第i个元素,并用e返回删除元素的值
- 按值查找 – LocateElem(L,e): 在表中查找具有给定关键字值的元素
- 按位查找 – GetElem(L,i): 在表中查找第i个位置的元素
- 求表长 – Length(L): 返回线性表的长度
- 判断表空 – Empty(L): 若表为空,则返回true,否则返回false
- …(可自行定义)
在C++中,使用&符号作为函数参数,意味着参数是通过引用传递的。这允许函数直接修改外部变量的值,而不仅仅是它的副本。这样做可以提高效率,因为不需要复制数据,也允许函数改变实际传入的变量。简单来说,使用 & 可以让函数直接操作原始变量,而不是它的拷贝
2.2 顺序表
顺序表的定义
顺序表:是指用顺序存储的方式实现线性表顺序存储
静态实现顺序表
#define MaxSize 10
typedef struct{
ElemType data[MaxSize]; // 顺序表元素的数据类型
int length; // 顺序表的长度
}SqList;
void InitList(SqList &L){
for(int i=0;i<MaxSize;i++)
L.data[i]=0; // 初始化所有值为0
L.length=0; // 初始化长度为0
}
int main(){
SqList L;
InitList(L);
for(int i=0;i<MaxSize;i++)
printf("data[%d]=%d\n",i,L.data[i]);
return 0;
}
动态分配实现顺序表
#define InitSize 10
typedef struct{
int * data; // 顺序表元素的数据类型
int MaxSize; // 顺序表最大长度
int length; // 顺序表当前的长度
}SqList;
C语言:malloc、free函数
malloc 是 C 语言中用于动态内存分配的函数,声明在 <stdlib.h>。通过 malloc(size_t size) 分配 size 字节的未初始化内存,成功时返回指向内存的指针,失败时返回 NULL。使用 malloc 分配的内存应使用 free 函数释放,以避免内存泄漏。常与 sizeof 操作符结合使用,以确定所需内存大小。例如:int *a = malloc(10 * sizeof(int)); 表示分配了存储10个整数的内存空间。
// 初始化动态数组
void InitList(SqList &L){
L.data = (ElemType *) malloc (sizeof(ElemType) * InitSize);
L.length = 0;
L.MaxSize = InitSize;
}
// 增加动态数组的长度
void IncreaseSize(SqList &L, int len){
// 保存旧数组的地址
int *p = L.data;
// 重新分配更大的内存空间
L.data = (int*) malloc ((L.MaxSize+len)*sizeof(int));
// 将旧数组的数据复制到新数组
for(int i=0; i<L.length; i++){
L.data[i] = p[i];
}
// 更新数组最大容量
L.MaxSize = L.MaxSize + len;
// 释放旧数组的内存
free(p);
}
顺序表的特点
- 随机访问:通过索引直接访问元素,速度快
- 存储密度高:元素紧密排列,无额外空间
- 拓展容量不方便:增加空间需复制数据
- 插入删除不方便:需移动多个元素
*** 顺序表操作的实现 ***
- 插入操作
在顺序表L中的第i个位置插入元素e,如果i输入的不合法(** 1<= i <= L.length + 1 或者 顺序表已经达到最大长度**), 则返回false,否则将第i个元素后面的元素整体向后移一个长度,再将元素e插入到位置i中,返回true。
bool ListInsert(SqList &L,int i, Elemtype e){
if (i<1 || i>L.length +1)
return false;
if(L.length > = MaxSize)
return false;
for(int j=L.length; j>i ;j++){
L.data[j] = L.datta[j-1];
}
L.data[i] = e;
L.length++;
return true;
插入新元素的实例:
时间复杂度的分析:
最好情况:在表尾插入,无需移动元素,时间复杂度为O(1)
最坏情况:在表头插入,需要移动所有元素,时间复杂度为O(n)
平均情况:第i个位置插入元素的概率为pi,即每个位置插入元素概率都相等为
1
n
+
1
\frac{1}{n+1}
n+11,第i个位置插入后,需要移动的元素个数为$n则元素平均移动次数为 ** n-i+1 ,则平均次数为:
故顺序表插入操作时间复杂度为O(n)**
- 删除操作
删除在顺序表L中第i个位置元素,用引用变量e返回,如果i的输入不合法,则返回false,否则删除第i个元素后,将该元素后面的元素整体向前一个单位,返回true
bool ListDelete(SqList &L, int i, Elemtype &e){
if(i<1 || i>L.length)
return false;
e=L.data[i-1];
for(int j=i;j<L.length;j++)
L.data[j-1] = L.data[j];
L.length--;
return true;
}
删除元素实例:
时间复杂度分析:
最好情况:删除表尾,无需移动元素,时间复杂度为O(1)
最坏情况:删除表头,需移动所有元素,时间复杂度为O(n)
平均情况:和插入元素类似,不过每个节点被删除的概率是
1
n
\frac{1}{n}
n1,并且删除后移动元素的个数为n-i,因为被删除的位置的元素不参与移动了,所以平均移动次数为:
故顺序表删除操作时间复杂度为O(n)
- 按值查找
在顺序表L中查找第一个元素等于e的元素,并返回其位序
int LocateElem(SqList L,Elemtype e){
int i;
for(i=0; i<L.length;i++)
if(L.data[i] = e)
return i+1;
return 0;
}
时间复杂度分析:
最好情况:要找的元素就是表头,时间复杂度为O(1)
最坏情况:要找的元素在表尾,时间复杂度为O(n)
平均情况:要找的元素在每个位置出现的概率为
1
n
\frac{1}{n}
n1,则平均需要查找的次数为:
故线性表按值查找的平均时间复杂度为O(n)
2.2 习题
- A
线性表优点是存储密度大且连续,插入和删除不方便,因为要移动元素,树型和图型结构不适合用顺序表表示 - A
存取是指读写方式,而不是指元素排列顺序,顺序表支持随机存取,根据起始地址和元素序号就可以找到任意一个元素。 - B
存取空间 = 表长 ∗ s i z e o f ( 元素类型 ) 存取空间 = 表长 * sizeof(元素类型) 存取空间=表长∗sizeof(元素类型),和元素顺序无关 - D
已经知道了读取元素的标号,则顺序表可以直接进行读取,时间复杂度为O(1),最快 - A
顺序表在表尾插入和删除不需要移动任何元素,且可以按标号随机存取 - C
顺序表直接访问第i个元素、在表尾插入元素,删除表尾元素,时间复杂度为O(1) - C
对于I,输出第i个元素值,顺序表为O(1),链表为O(n)
对于II,交换元素顺序表只需要一个中间变量temp,然后三次交换就能实现了;链表需要先找到两个元素,在进行指针的修改,效率低
对于III,顺序输出都需要从头至尾进行读取,时间复杂度都为O(n) - C
顺序表删除第i个元素,需要移动n-i个元素 - C
- B
顺序表若有n个元素,则插入位置共有n+1个,相当于n个物体会产生n+1个空隙,1是表头左侧,n+1是表尾右侧 - D
顺序存储需要连续的存储空间,在申请时需申请n+m个连续的存储空间,然后将线性表原来的n个元素复制到新申请的n+m个连续的存储空间的前n个单元
2.3 链表
2.3.1 单链表
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据之间的线性关系,每个链表节点除了存放自身的元素信息外,还需要存放一个后继指针指向后继元素。
节点类型描述:
typedef struct LNode{
Elemtype data;
struct LNode *next;
}LNode, *LinkList;
单链表的元素离散的分布在存储空间中,是非随机存取的存储结构,即不能直接找到表中某个特定的节点。查找某个特定节点时,需要从表头开始遍历依次查找。
单链表需要一个头指针来标识,头指针是一个空表。头指针指向头节点,头节点可以不设任何信息,也可记录表的长度,头节点的指针域指向链表第一个元素。
头指针始终指向第一个节点,头节点是链表的第一个节点,通常不存储信息
头指针的优点:
- 头指针的引入使得存储链表中数据的节点具有相同的操作,如果没头节点,那么存储第一个数据的结点由于和头指针关联,需要单独进行处理
- 无论链表是否为空,有了头节点,头指针都能指向一个节点,空表和非空表的处理也得到了统一
单链表的建立
- 头插法建立单链表
每当插入一个新元素,就将该元素插入到头节点的后面。将头节点的指针指向新结点,新结点的指针指向原本头结点指向的结点。
LinkList List_HeadInsert(LinkList &L){
LNode *s, int x;
L=(LinkList)malloc(sizeof(LNode)); // 创建头结点
L->next=NULL; //头结点指向空,初始化为空链表
scanf("%d",&x); //输入第一个结点的值
while(x!=9999){ // 输入9999表示结束
s=(LNode*)malloc(sizeof(LNode));//创建新结点
s->data=x; // 新结点赋值
s->next = L->next; // 新结点指向头结点指向的结点
L->next = s; // 头结点指向新结点
scanf("%d",&x);// 输入下一个结点的值
}
return L;
}
头插法建立单链表时,读入数据的顺序和生成链表的数据是相反的,时间复杂度为O(n)
- 尾插法建立单链表
尾插法的思想是增加一个尾指针r,使其始终指向建立链表的尾结点,这样就能将新插入的节点每次都插入到链表的末尾
LinkList List_TailInsert(LinkList &L){
int x; // 设置元素类型为整形
L=(LinkList)malloc(sizeof(LNode)); // 创建头结点
LNode *s,*r = L; // 创建新结点和尾指针
scanf("%d",&x); // 输入新结点元素的值
while(x!9999){
s = (LNode *)malloc(sizeof(LNode)); //创建新节点
s->data =x; // 新结点赋值
r->next=s; // 尾指针指向新结点
r=s; // 更新尾指针为最后一个节点
scanf("%d",&x); // 输入下一个结点的元素值
}
r->next = NULL; // 尾指针指向空
return L;
}
- 按序号查找结点值
对于一个单链表,从第一个结点出发,顺着指针的next域逐个往下查,查到第i个结点为止,如果没查到,则返回NULL
LNode *GetElem(LinkList L,int i){ // LNode* 表示返回我们找到的结点的指针
int j=1;
LNode *p = L->next; //L是头结点,L->next是存储信息的第一个结点
if(i==0)
return 0;
if(i<1)
return NULL;
while(p&j<i){ // 当p不为空且j<i时进入循环
p=p->next;
j++;
}
return p;
}
按序号查找操作的时间复杂度为O(n)
- 按值查找表结点
从单链表的第一个结点开始,从前往后依次查找,找到第一个符合所找值的结点返回,若没查到则返回NULL
LNode *LocateElem(LinkList L,Elemtype E){
LNode *p=L->next;
while(p->data != e && p){ // p不为空且p所指的值不为e时进入循环
p=p->next;
return p;
}
按值查找操作的时间复杂度为O(n)
- 插入节点操作
插入节点是将一个新结点s插入到单链表L第i个位置上,解决办法为:先利用GetElem函数查找第i-1位置上的结点记为*p,然后将新结点s插入到p的后面,再将x的next域指向原本p的next指向的结点。
void ListInsert(&L,i,x){ // x为要插入的节点
if(i<1 || i>L.length+1)
return;
LNode *p = GetElem(L,i-1);
s->next = p->next;
p->next = s;
}
GetElem的时间复杂度为O(n),插入时间复杂度为O(1)
前插操作:
上面的例子是在节点后进行插入,如果想在第i个结点前插入,两种办法
- GetElem操作查找第i-1个结点,而不是第i个结点,再进行后插操作
- 可以正常的先进性后插操作,再将p->data和s->data相交换,这样也相当于在前面插入了
- 删除结点操作
删除单链表的第i个结点,如果删除位置合法,删除后应把后面的结点接到前面的结点上
void DeleteElem(&L,i,q){
LNode *p = GetElem(L,i-1); // p指向被删除节点的前驱节点
LNode *q = p->next; // q指向要删除的结点
p->next = q->next; // 这步是把被删除结点的前驱结点和后继结点连接起来
free(p); // 释放要被删除的节点
}
- 求表长的操作
求表长就是将链表从头到尾走下去,直到结点为NULL为止,统计结点的个数
int LinkListLength(L){
LNode *p = L->next;
int j = 0;
while(p!=NULL)
j++;
return j;
}
2.3.2 双链表
双链表就是在单链表的基础上增加头指针,这样每个结点既有next指针指向后继结点,又有prior指针指向前驱结点。
双链表的结点类型描述:
typedef struct DNode{
Elemtype data; // 数据域
struct DNode *prior,*next; // 前驱和后继指针
}DNode,*DLinkList;
相比于单链表,双链表可以向前查找任何前驱结点,而单链表只能再从头查找才能访问某个元素的前驱结点了
双链表操作举例:
- 插入操作
和单链表不同,双链表插入既要考虑next指针的连接,也要考虑prior指针的连接
void ListInsert(&L,i,x){ // x为要插入的节点
if(i<1 || i>L.length+1)
return;
LNode *p = GetElem(L,i-1);
s->next = p->next; //确定s的后继
p->next = s; // 确定p的后继
s->prior = p; // 确定s的前驱
s->next->piror = s; // 确定s的后继的前驱
}
- 删除操作
void DeleteElem(&L,i,q){
LNode *p = GetElem(L,i-1); // p指向被删除节点的前驱节点
p->next = q->next; //p的后继指向被删除的结点的后继,对应 ①
q->next->prior = p; //q的后继的前驱指向p,对应 ②
free(p); // 释放要被删除的节点
}
2.3.3 循环单链表
循环单链表是在单链表的基础上将最后一个结点指向的结点由NULL改为头结点,形成一个环。其中指针 r 始终指向表尾,r->next 始终指向表头
相比于单链表,循环链表对表头和表尾元素进行操作都只需要O(1)的时间复杂度。判断循环单链表是否为空是看头指针L和 r->next 是否相等
2.3.4 循环双链表
循环双链表就是双链表和循环链表的结合,当循环双链表为空时,其头结点的prior和next都为L
2.3.5 静态链表
静态链表时借助数组实现线性表的存储结构,也分数据域和指针域,不过静态链表的指针是相对地址,也就是数组下标
结构类型描述:
#define MaxSize 30
typedef struct{
Elemtype data;
int next;
}SLinkList[MaxSize];
静态链表以第一个数组元素作为头结点,以next==-1作为结束的标志。
2.4 顺序表和链表的比较
顺序表:
- 存取时间复杂度为 O(1),因为顺序表是数组结构,可以通过索引直接访问元素
- 插入和删除操作的时间复杂度较高,平均为 O(n),因为需要移动元素以维持元素的连续性
- 空间上更紧凑,因为元素紧密排列,不会有额外的空间开销
- 可以快速的访问任意数据,但是扩展大小不方便,当数组满了需要进行扩容,这个过程时间复杂度为 O(n)
链表:
- 存取时间复杂度为 O(n),因为需要从头节点开始遍历直到找到需要的元素
- 插入和删除操作的时间复杂度较低,平均为 O(1),因为只需要改变指针的指向即可,不需要移动其他元素
- 需要额外的空间来存储指针,因此相对顺序表来说空间使用率低
- 扩展大小非常方便,只需要改变指针的指向即可完成节点的插入和删除,不需要移动其他元素
2.3习题
解答:
2.4 链表leetcode刷题
题干模板:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* oddEvenList(struct ListNode* head) {
}
思路:
题目要求将链表中的奇数位置节点和偶数位置节点分开,然后首先连接所有奇数节点,其后连接所有偶数节点。例如,链表 1->2->3->4->5 应该被重新排列为 1->3->5->2->4
这个问题的关键是在一次遍历中改变节点的连接。为此,我们可以使用两个指针,一个指向当前的奇数节点,另一个指向当前的偶数节点。同时,我们需要记住偶数节点链的头部,这样在奇数节点链表遍历完成后,我们可以将两个链表连接起来
下面是题解的步骤解析:
- 首先检查 head 是否为空。如果为空,则直接返回 NULL
- 初始化两个指针 ji 和 ou。ji 指向链表的第一个节点(即头节点 head),ou 指向链表的第二个节点(即 head->next)。同时,保存偶数链的头部到 ouhead 指针,以便后续连接
- 使用 while 循环,条件是 ji->next 和 ou->next 都不为 NULL。在循环内部,我们将 ji->next 指向 ou->next,即跳过一个偶数位置节点到下一个奇数位置节点。然后 ji 指针移动到这个新的奇数位置节点上
- 接下来,我们需要处理 ou 指针。同样,我们将 ou->next 指向 ji->next,即跳过一个奇数位置节点到下一个偶数位置节点。然后 ou 指针移动到这个新的偶数位置节点上
- 当 while 循环完成时,所有的奇数位置节点已经连接在一起,所有的偶数位置节点也连接在一起。此时,ji->next 应该指向 ouhead,也就是偶数链的头部,这样就连接了两个链表
- 最后,返回修改后的链表头节点 head
答案:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* oddEvenList(struct ListNode* head) {
if(head==NULL)
return NULL;
struct ListNode *ji = head;
struct ListNode *ou = head->next;
struct LinkNode *ouhead = ou;
while(ji->next!=NULL && ou->next!=NULL){
ji->next = ou->next;
ji=ji->next;
ou->next = ji->next;
ou=ou->next;
}
ji->next = ouhead;
return head;
}
题目模板:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
void deleteNode(struct ListNode* node) {
}
思路
刚开始看这道题是懵的,要求删除一个结点,不知道头结点。想了半天,哦,原来是前面学到的,删除一个结点,可以把后继结点的值赋给自己,然后删除
后继结点就行了。
答案
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
void deleteNode(struct ListNode* node) {
node->val = node->next->val;
node->next = node->next->next;
}
思路:
我在自己尝试做的时候,大概能知道要去几个指针来指向当前元素,前驱元素和后继元素来实现反转,但自己弄了半天也没想出来,后来看了题解就觉得自己的脑子就总差一点弯转不过来,然后做不出来。。。
首先,要实现链表逆转,原本的表头变成表尾,所以要指向NULL;之后每次处理到第i个结点的时候,将curr指向当前结点,prev指向前驱,next指向后继,将curr的next指向prev就实现了翻转;然后更新prev和curr向后移动一个结点,也就是curr指向next,prev指向curr,然后重复同样的操作直到curr为空
官方答案:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode * prev = NULL;
struct ListNode * curr = head;
while(curr){
struct ListNode * next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
解析:
逻辑上分为几个主要步骤,下面是每个步骤的简要说明:
计算链表长度:首先,通过遍历链表计算出链表的总长度 j
定位中点:根据链表的长度,将指针 a 移动到链表的中间位置。如果链表长度为偶数,则 a 位于中间两个节点的后一个
反转后半部分链表:从中点开始,反转链表的后半部分。这里用到了三个指针:curr(当前节点)、prior(前一个节点)、next(下一个节点),以实现链表的反转
连接链表:将前半部分的最后一个节点(c 指向的节点)指向反转后的后半部分的起始节点(curr)。这一步在代码中似乎是为了重构原链表,但实际上这样做会破坏原链表的结构,对于回文判断不是必须的
回文判断:最后,同时遍历原链表的前半部分和反转后的后半部分,比较对应节点的值。如果所有对应节点的值都相同,则链表是回文的,返回 true;否则,返回 false
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
bool isPalindrome(struct ListNode* head) {
int j = 1;
struct ListNode *p = head;
struct ListNode *q = head;
while(p->next!=NULL){
j++;
p=p->next;
}
struct ListNode * a = head;
for(int i=1;i<=j/2;i++){
a = a->next;
}
struct ListNode *curr = a;
struct ListNode *prior, *next= NULL;
while(curr->next !=NULL){
next=curr->next;
curr->next = prior;
prior = curr;
curr = next;
}
curr->next = prior;
struct ListNode *c = head;
for(int i=1;i<=j/2-1;i++){
c=c->next;
}
c->next = curr;
struct ListNode * b = head;
for(int i=1;i<=j/2;i++){
b = b->next;
}
for(int i=1;i<=j/2;i++){
if(q->val != b->val){
return false;
}
else{
q=q->next;
b=b->next;
}
}
return true;
}