0.PTA得分截图
1.本周学习总结
1.1 总结线性表内容
1.1.1 顺序表
- 顺序表结构体定义
采用顺序存储结构的线性表通常称为顺序表,顺序表是将表中的结点依次存放在计算机内存中一组地址连续的存储单元中。
顺序表特点有:- 可以根据下标直接访问
- 不方便插入和删除元素
顺序表结构体定义一般有如下两种形式:
typedef int ElemType;
typedef struct
{
ElemType data[MaxSize]; //存放顺序表元素
int length ; //存放顺序表的长度
} List;
typedef List *SqList;
或者
typedef int ElemType;
typedef struct
{
ElemType *data; //存放顺序表元素,注意为data申请空间
int length ; //存放顺序表的长度
} List;
typedef List *SqList;
- 顺序表的基本操作
- 插入
思路:将插入位置及之后的元素右移,然后插入要求元素,最后让表长加一。
相关代码:
- 插入
Status ListInsert(SqList &L,int i,ElemType e)
{
// 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)+1
// 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1
ElemType *q,*p;
if(i<1||i>L.length+1) // i值不合法
return ERROR;
q=L.elem+i-1; // q为插入位置
for(p=L.elem+L.length-1;p>=q;--p) // 插入位置及之后的元素右移
*(p+1)=*p;
*q=e; // 插入e
++L.length; // 表长增1
return OK;
}
- 删除
思路:将被删除元素之后的元素左移即可
相关代码:
Status ListDelete(SqList &L,int i,ElemType &e)
{
// 初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
// 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
ElemType *p,*q;
if(i<1||i>L.length) // i值不合法
return ERROR;
p=L.elem+i-1; // p为被删除元素的位置
e=*p; // 被删除元素的值赋给e
q=L.elem+L.length-1; // 表尾元素的位置
for(++p;p<=q;++p) // 被删除元素之后的元素左移
*(p-1)=*p;
L.length--; // 表长减1
return OK;
}
- 删除区间数据重构做法
思路:在区间内的数放入数组,遇到不在区间内的数让数组长度减一
相关代码:
int DelData(Sqlist L, int min, int max)
{
int j = 0;
int i;
for (i = 0;i < L->length;i++)
{
if (!(L->data[i] >= min && L->data[i] <= max)) L->data[j++] = L->data[i];
else L->lenth--;
}
if (L->length == 0) return 0;
else return 1;
}
1.1.2 链表
- 链表结构体定义
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
链表结构体包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。其定义一般形式为:
typedef int ElemType;
typedef struct LNode //定义单链表结点类型
{
ElemType data;
struct LNode *next; //指向后继结点
} LNode,*LinkList;
- 链表的基本操作
- 头插法建链
思路:不断将新生成的结点接到头结点之后
相关代码:
- 头插法建链
void CreateListF(LinkList& L, int n)
{
int i;
LinkList ptr;
L = new LNode;
L->next = NULL;
for (i = 0;i < n;i++)
{
ptr = new LNode;
cin >> ptr->data;
ptr->next = L->next;
L->next = ptr;
}
}
- 尾插法建链
思路:不断将新生成的结点接到尾结点之后
相关代码:
void CreateListR(LinkList& L, int n)
{
int num = 0;
LinkList tail;
LinkList ptr;
L = new LNode;
L->next = NULL;
tail = L;
while (num < n)
{
ptr = new LNode;
cin >> ptr->data;
tail->next = ptr;
tail = ptr;
num++;
}
tail->next = NULL;
}
- 插入
思路:使用前驱指针去寻找插入位置,然后将相应元素插入到对应位置
相关代码:
void ListInsert(LinkList& L, ElemType e,int i)
{
LinkList pre;
LinkList node;
for (pre = L;i != 0;pre = pre->next)
{
i--;
}
node = new LNode;
node->data = e;
node->next = pre->next;
pre->next = node;
}
- 删除
思路:使用前驱指针寻找删除结点,记录下这个结点,让前驱指针的next指向删除结点的下一个结点,然后删除对应结点
相关代码:
void ListDelete(LinkList& L, ElemType e)
{
LinkList pre;
LinkList temp;
if (L->next == NULL) return;
for (pre = L;pre->next != NULL;pre = pre->next)
{
if (pre->next->data == e)
{
temp = pre->next;
pre->next = pre->next->next;
delete temp;
return;
}
}
cout << e << "找不到!" << endl;
}
- 链表逆置
思路:先将链表重构,之后将链表的各个结点依次接到头结点之后
相关代码:
void ReverseList(LinkList& L) //逆转链表
{
LinkList ptr_next;
LinkList ptr_cur;
ptr_cur = L->next;
L->next = NULL;
while (ptr_cur)
{
ptr_next = ptr_cur->next;
ptr_cur->next = L->next;
L->next = ptr_cur;
ptr_cur = ptr_next;
}
}
1.1.3 有序表
- 插入
思路:使用前驱指针去寻找插入位置,然后将相应元素插入到对应位置
相关代码:
void ListInsert(LinkList& L, ElemType e)
{
LinkList pre;
LinkList node;
for (pre = L;pre->next != NULL;pre = pre->next)
{
if (pre->next->data > e) break;
}
node = new LNode;
node->data = e;
node->next = pre->next;
pre->next = node;
}
- 删除
思路:使用前驱指针寻找删除结点,记录下这个结点,让前驱指针的next指向删除结点的下一个结点,然后删除对应结点
相关代码:
void ListDelete(LinkList& L, ElemType e)
{
LinkList pre;
LinkList temp;
if (L->next == NULL) return;
for (pre = L;pre->next != NULL;pre = pre->next)
{
if (pre->next->data == e)
{
temp = pre->next;
pre->next = pre->next->next;
delete temp;
return;
}
}
cout << e << "找不到!" << endl;
}
- 有序表合并
思路:遍历两条链表,哪个链表中的数据小(大)就将哪个结点接到目标链表上,一条链表遍历完之后,将另外一条链表的剩余结点都接到目标链表上
相关代码:
void MergeList(LinkList& L1, LinkList L2)
{
LinkList ptr1, ptr2, tail, temp;
ptr1 = L1->next;
ptr2 = L2->next;
L1->next = NULL;
tail = L1;
while (ptr1 != NULL && ptr2 != NULL)
{
if (ptr1->data < ptr2->data)
{
temp = ptr1;
ptr1 = ptr1->next;
}
else if (ptr2->data < ptr1->data)
{
temp = ptr2;
ptr2 = ptr2->next;
}
else
{
temp = ptr1;
ptr1 = ptr1->next;
ptr2 = ptr2->next;
}
tail->next = temp;
tail = temp;
}
if (ptr1) tail->next = ptr1;
if (ptr2) tail->next = ptr2;
}
1.1.4 循环链表
- 分类
- 单循环链表——在单链表中,将终端结点的指针域NULL改为指向表头结点或开始结点即可
- 多重链的循环链表——将表中结点链在多个环上
- 结构特点
- 表中最后一个结点的指针域指向头结点,整个链表形成一个环
- 判断空链表的条件是
head==head->next;
rear==rear->next;
- 无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活
- 创建循环链表的相关代码
void creat_list(list *p)//如果链表为空,则创建一个链表,指针域指向自己,否则寻找尾节点,将
{ //将尾节点的指针域指向这个新节点,新节点的指针域指向头结点
int item;
list *temp;
list *target;
printf("输入节点的值,输入0结束
");
while(1)
{
scanf("%d",&item);
if(item==0)return;
if(*p==NULL) //如果输入的链表是空。则创建一个新的节点,使其next指针指向自己 (*head)->next=*head;
{
*p=(list *)malloc(sizeof(list));
if(!*p)exit(0);
(*p)->data=item;
(*p)->next=*p;
}
else //输入的链表不是空的,寻找链表的尾节点,使尾节点的next=新节点。新节点的next指向头节点
{
for(target=*p;target->next!=*p;target=target->next);//寻找尾节点
temp=(list *)malloc(sizeof(list));
if(!temp)exit(0);
temp->data=item;
temp->next=*p; //新节点指向头节点
target->next=temp;//尾节点指向新节点
}
}
}
流程示意图如下:
1.1.5 双链表
-
定义
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱
示意图如下:
-
结构特点
- 从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点
-
创建双链表
- 头插法
思路:与单链表头插法相似,不过需要判断头节点后面是否有数据节点
相关代码:
- 头插法
void node_creat(node *head, int n)
{
node *p;
int i;
for (i = 1; i <= n; i++)
{
p = new node;
cin >> p->a;
p->next = head->next;
head->next = p;
if (p->next != NULL) //这里需要判断头节点后面是否有数据节点
p->next->pre = p;//如果有就让它的pre指针指向新插入的节点
p->pre = head;//新插入的节点的pre指向头节点
}
}
- 尾插法
思路:类比单链表尾插法
相关代码:
void node_creat(node *head, int n)
{
node *p;
node *q; //q用来指向每次创建好的数据节点
q = head;
int i;
for (i = 1; i <= n; i++)
{
p = new node;
cin >> p->a;
p->next = q->next;
q->next = p;
p->pre = q;
q = q->next;
}
}
1.2.谈谈你对线性表的认识及学习体会
- 对于线性表的理解
线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列
线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储,但是把最后一个数据元素的尾指针指向了首位结点)
线性表的优点是:线性表的逻辑结构简单,便于实现和操作。因此,线性表这种数据结构在实际应用中是广泛采用的一种数据结构 - 操作线性表常见的问题及其解决方法
序号 | 问题 | 解决方法 |
---|---|---|
1 | 引用空指针的分量 | 添加限制条件使其无法访问空指针的分量(例如while (ptr != NULL && ptr->data != e) ) |
2 | 有些情况下(例如删除,输出)忘记判断是否为空表 | 在写代码之前使用伪代码构建思路 |
3 | 合并链表时新建结点浪费空间 | 直接引用原有的结点 |
- 学习体会
其实上学期就已经涉及了链表,但是还不够深入,这学期更进一步学习了链表,那些在上学期看起来很难的操作我也能够逐渐掌握并使用,就这点我觉得就是很大的进步了。之后还要学习栈,队列,图,树等等数据结构,虽然可能会很难,不过只要用心学习了,肯定会有所收获的。还是要好好学习,天天向上。
2.PTA实验作业
2.1 一元多项式的乘法与加法运算
2.1.1代码截图
2.1.2本题PTA提交列表说明
-
提交列表
-
说明
结果 | 说明 |
---|---|
编译错误 | 均为选择了C编译器,而不是C++编译器 |
部分正确 | GetSum函数有误 |
部分正确 | 合并后的零多项式没有消除 |
部分正确 | 一个测试点段错误,于是重写 |
答案正确 | 重写后正确 |
2.2 jmu-ds-有序链表合并
2.2.1代码截图
2.2.2本题PTA提交列表说明
-
提交列表
-
说明
结果 | 说明 |
---|---|
运行超时 | 忘记最后给tail的next赋值为NULL |
答案正确 | 修改后正确 |
答案错误 | 尝试新的做法,最后将tail的next又置为空,导致连接之后的链表又断开 |
答案正确 | 修改后正确 |
2.3 jmu-ds-链表分割
2.3.1代码截图
2.3.2本题PTA提交列表说明
-
提交列表
-
说明
结果 | 说明 |
---|---|
段错误 | 没有判断ptr_cur是否为空 |
答案正确 | 添加判断条件后正确 |
3.阅读代码
3.1 环形链表
-
题干
-
题解
class Solution {
public:
bool hasCycle(ListNode* head)
{
//两个运动员位于同意起点head
ListNode* faster{ head }; //快的运动员
ListNode* slower{ head }; //慢的运动员
if (head == NULL) //输入链表为空,必然不是循环链表
return false;
while (faster != NULL && faster->next != NULL)
{
faster = faster->next->next; //快的运动员每次跑两步
slower = slower->next; //慢的运动员每次跑一步
if (faster == slower) //他们在比赛中相遇了
return true; //可以断定是环形道,直道不可能相遇
}
return false; //快的运动员到终点了,那就是直道,绕圈跑不会有终点
}
};
3.1.1 该题的设计思路
-
设计思路
假如该链表是循环链表,那我们可以定义两个指针,一个每次向前移动两个节点,另一个每次向前移动一个节点。这就和田径比赛是一样的,假如这两个运动员跑的是直道,那快的运动员和慢的运动员在起点位于同一位置,但快的运动员必将先到达终点,期间这两个运动员不会相遇。而如果绕圈跑的话(假设没有米数限制),跑的快的运动员在超过跑的慢的运动员一圈的时候,他们将会相遇,此刻就是循环链表。
动图方式展示解决方法如下:
-
算法复杂度
- 时间复杂度:O(n)。假定链表为循环单链表,则faster和slower必然在尾结点相遇,此时faster遍历链表两遍,slower遍历链表一遍
- 空间复杂度:O(1)。算法中只定义了两个指针,没有再额外申请空间
3.1.2 该题的伪代码
if 为空表 then
返回false
end if
while faster不为空且faster的下一结点亦不为空 then
faster每次移动两个结点
slower每次移动一个结点
if faster和slower相遇 then
返回true
end if
end while
返回false
3.1.3 运行结果
3.1.4分析该题目解题优势及难点
- 优势
- 使用快慢双指针,就不用思考各个结点是否已经遍历过
- 不用考虑哪里是环的一部分,哪里不是环的一部分
- 难点
- 难以判断各个结点是否遍历过
3.2 分隔链表
-
题干
-
题解
class Solution {
public:
//
vector<ListNode*> splitListToParts(ListNode* root, int k) {
int size=0;
ListNode *p=root;
while(p){
size++;
p=p->next;
}
int avg_size=size/k,mod=size%k;
vector<ListNode*> res(k,nullptr);
ListNode *cur=root,*pre=nullptr;
for(int i=0;i<k;++i)
{
res[i]=cur;
//mod为0时,即使平均长度为0,cur为nullpre,不能实现断链了,即添加为nullptr
int temp_size=mod?(avg_size+1):avg_size;
while(temp_size--){
pre=cur;
cur=cur->next;
}
//pre为cur的前驱节点,也就是当前长度的最后一个节点,作用是实现断链
if(pre)pre->next=nullptr;
if(mod)mod--;
}
return res;
}
};
3.2.1 该题的设计思路
-
设计思路
先求出链表的长度,然后求出链表的平均长度,以及余数。由于题目规定任意两部分的长度不能超过1,所以余数依次给排在前面的平均长度+1即可
动图方式展示解决方法如下:
-
算法复杂度
- 时间复杂度:O(n)。求链表长度遍历一次链表,分割链表时又遍历了一遍链表
- 空间复杂度:O(1)。额外分配的空间是固定的,与问题规模无关
3.2.2 该题的伪代码
while 遍历链表
求表长
end while
定义向量容器res(内有k个ListNode*类型元素,初始值为nullptr)
for i=0 to k
res[i]记录该段链表起始地址
temp_size为(余数为0,则为平均长度+1)或者(余数不为0,则为平均长度)
while temp_size--
pre移动到分割链表的最后一个结点
cur移动到分割链表的最后一个结点的下一结点
end while
if pre不为空 then
分割出该段链表
end if
if 余数不为0 then
余数--
end if
end for
返回向量容器res
3.2.3 运行结果
3.2.4分析该题目解题优势及难点
- 优势
- 先求平均长度和余数,保证了排在前面的部分的长度大于或等于后面的长度
- 将vector容器的元素初始值设为nullptr,就可以不用处理空链表的情况
- 使用pre前驱指针可以便捷地实现断链
- 难点
- 如何保证排在前面的部分的长度大于或等于后面的长度
- 如何判断应该断链的是哪些结点