0.PTA得分截图
线性表题目集总得分,请截图,截图中必须有自己名字。题目至少完成2/3,否则本次作业最高分5分。
1.本周学习总结(0-4分)
1.1 总结线性表内容:
1.1.1顺序表
(1)顺序表的定义:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储;即通过数据元素物理存储的连续性来反应元素之间逻辑上的相邻关系。采用顺序存储结构存储的线性表通常简称为顺序表。
(2)顺序表的操作:
/*顺序表的建立操作;*/
void CreateList(SqList& L, int n)//建立有限结点的顺序表L,节点数n;
{
int i;
L = new List;
L->length = n;
for (i = 1; i <= n; i++)
{
cin >> L->data[i];
}
}
/*顺序表的(全部)删除操作:*/
void DeleteAllList()
{
for (int i = length; i > 0; i--) //从最后一个元素开始删除,长度减一
{
elem[i] = elem[i - 1]; //元素向前移位
length--; //长度减一
}
}
/*顺序表的插入操作:*/
void InsertSq(SqList& L, int x)
{
int index = 0;//设插入下标为index;
int i;
while (index < L->length && x > L->data[index])//寻找插入结点的下标;
index++;
for (i = L->length - 1; i >= index; i--)//将顺序表数据逐渐向后移动;
{
L->data[i + 1] = L->data[i];
}
L->data[index] = x;
L->length++;
}
/*顺序表删除重复数据的操作:*/
void DelSameNode(List& L)
{
if (L->length == 0) return;//判断是否为空表;
int i = 0, j, k;
for (; i < L->length - 1; i++)
{
for (j = i + 1; j < L->length; j++)
{
if (L->data[i] == L->data[j])
{
for (k = j; k < L->length - 1; k++)
{
L->data[k] = L->data[k + 1];
}
L->length--;
}
}
}
if (L->length == 2)
{
if (L->data[0] == L->data[1])
L->length--;
}
}
1.1.2 链表
(1)链表的结构定义:
链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。下面是链表的结构体定义:
typedef struct LNode //定义单链表结点类型
{
ElemType data; //这一部分主要存储数据,又叫数据域;
struct LNode *next; //指向后继结点,指针域;
} LNode,*LinkList;
(2)关于链表的各种操作:
/*尾插法建立链表*/
void CreateListR(LinkList& L, int n)
{
L = new LNode;
int i = 0;
L->next = NULL;
LinkList s;
LinkList r;
r = new LNode;
r = L;
for (i = 0; i < n; i++)
{
s = new LNode;
r->next = s;
s->next = NULL;
r = s;
cin >> s->data;
}
}
/*头插法建立链表*/
void CreateListF(LinkList& L, int n)
{
int i = 0;
L = new LNode;
L->next = NULL;
LinkList s;
for (i = 0; i < n; i++)
{
s = new LNode;
s->next = L->next;
L->next = s;
cin >> s->data;
}
}
/*(增序)有序链表的插入操作*/
void ListInsert(LinkList& L, ElemType e)
{
LinkList p = new(LNode);
p = L;
LinkList node = new(LNode);
while (1) {
if (p != NULL && p->next != NULL)
{
if (e >= p->data && e <= p->next->data)
{
node->data = e;
node->next = p->next;
p->next = node;
return;
}
p = p->next;
}
else break;
}
node->data = e;
p->next = node;
p = p->next;
p->next =NULL;
return;
}
/*有序链表的删除操作*/
void ListDelete(LinkList& L, ElemType e)
{
LinkList p = new(LNode);
p = L;
if (p->next == NULL) return;
while (1)
{
if (p != NULL && p->next != NULL)
{
if (e == p->next->data)
{
p->next = p->next->next;
return;
}
}
if (p == NULL)
break;
p = p->next;
}
cout << e << "找不到!" << endl;
}
/*有序链表的合并*/
void SplitList(LinkList& L, LinkList& L1, LinkList& L2)
{
int count = 1;
LinkList p = L;
LinkList temp;
L2 = new LNode;
L2->next = NULL;
while (p->next != NULL)
{
if (count % 2 == 0)
{
temp = p->next->next;
p->next->next = L2->next;
L2->next = p->next;
p->next = temp;
}
else
{
p = p->next;
}
count++;
}
L1 = L;
}
1.1.3循环链表、双链表
(1)循环链表:
循环链表是另一种形式的链式存储结构,将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。从表中任一结点出发均可找到表中其他结点。为了使空链表与非空链表处理一致,我们通常设一个头结点。并不一定所有循环链表都要头结点。
(2)双链表:
双向链表就是在单链表的每个结点中,在设置一个指向其前驱结点的指针域。 所以在双向链表的每个结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。结构如下:
/*线性表的双向链表存储结构*/
typedef struct DouLNode
{
ElemType data;
struct DouLNode *prior;/*直接前驱指针*/
struct DouLNode *next;/*直接后继指针*/
}DulNode,*DuLinkList;
双向链表是单链表中扩展出来的结构,所以它的很多操作是和单链表相同的,比如求长度,查找元素,获得元素位置等.但是在处理的时候需要注意对前驱指针的处理:
/*双向链表插入的主要代码*/
node->next = p->next;
p->next->prior = node;
p->next = node;
node->prior = p;
/*双向链表的删除*/
deleteNode->next ->prior = p;
p->next = deleteNode->next;
free(deleteNode);
1.2.谈谈你对线性表的认识及学习体会。
近期由于疫情的影响,不能及时开学,通过网上授课的方式学习。上学期最后阶段学习过链表的知识,还是有一些基础的,但是过了一个寒假,基本上忘记的差不多了。开学已经3周,慢慢的捡回来之前的知识。先开始了解的是顺序表,和数据的功能差不多,数据在储存器的位置是连续的,结构体定义中有顺序表的长度一项,对顺序表进行删除插入操作之后也要注意它长度的变化。单链表题在上个学期已经了解过,但是,现在做题的时候,经常会遇到非法访问内存的问题,问题是经常对空指针进行了操作,或者没有动态申请空间,或者链表前继后继关系丢失。双链表的结构体定义不难理解,在单链表的基础上增加了前驱指针,从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点,这也是双向链表相对于单链表的优点之一。循环链表的特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。循环链表中没有NULL指针。涉及遍历操作时,其终止条件就不再是像非循环链表那样判别p或p->next是否为空,而是判别它们是否等于某一指定指针,如头指针或尾指针等。
2.PTA实验作业(0-2分)
选3道PTA题目,不写伪代码,只贴代码截图,并记录代码提交碰到问题及解决方法。必须选择线性结构应用题目,不得选操作集类似题目,头插法、尾插法、链表逆转也不得选。(选,则为0分)选择难度越高题目得分越高。
2.1.题目1:6-9 jmu-ds-有序链表合并 (20分)
2.1.1代码截图(注意,截图,截图,截图。不要粘贴博客上。)
2.1.2本题PTA提交列表说明。
- Q:多种错误;
- A:大致思路没有问题,但是建立尾结点tail的时候我让他动态申请了空间,导致结果错误;
- Q:部分正确;
- A:改正之后大部分正确,空表的测试点没有过去,所以加了两条语句分别讨论A或B链走完的情况;
- Q:全部正确;
- A:经过改正全部通过;
2.2 题目2:7-1 两个有序链表序列的交集 (20分)
2.2.1代码截图(注意,截图,截图,截图。不要粘贴博客上。)
2.2.2本题PTA提交列表说明。
- Q:部分正确;
- A:该问题与之前做的有序链表的合并并且删除重复数据相似,但是之前都做的是函数题,对于整个程序的编写还是有点生疏;
- Q:部分正确;
- A:改正之后大部分正确,有一个链表的数据为空的测试点没有过去,与上面相同的情况,多加一条语句即可;
- Q:全部正确;
- A:经过改正全部通过;
2.3 题目3:6-3 jmu-ds-链表区间删除 (20分)
2.3.1代码截图(注意,截图,截图,截图。不要粘贴博客上。)
2.3.2本题PTA提交列表说明。
- 虽然提交列表上好像是一次就通过,但是,在VS里面调试了好多次才成功运行的;
(1)本题的难点在于建立链表的时候需要让原本没有顺序的链表变成有有序链表;链表顺序排列就需要大量的操作;所以我的做法是先按输入顺序建立一条单链表,然后重构一条新的链表,每次插入一个数据的时候,都要与之前已经插入的数据进行比较,小前大后。大致思路是这样,代码实现如下:
while (p1 != NULL)
{
flag = 1;
temp = p1;
for (pre2 = head, p2 = pre2->next; p2 != p1; pre2 = pre2->next, p2 = p2->next)
{
if (p2->data > p1->data)
{
p1 = p1->next;
pre1->next = p1;
pre2->next = temp;
temp->next = p2;
flag = 0;
break;
}
}
}
由于每次插入数据的时候需要与已经插入的数据进行大小的比较,所以需要保存每一项数据的前驱结点;
(2)删除区间的数据与链表的删除并没有太大的区别,重要的是如何找到区间中的数据;注意删除结点的时候要及时释放空间;
3.阅读代码(0--4分)
找2份优秀代码,理解代码功能,并讲出你所选代码优点及可以学习地方。主要找以下类型代码:
考研题种关于线性表内容。可以找参加过考研的学长学姐拿。尤其是想要考研同学,可以结合本章内容,用考研题训练学习。
3.1 题目及解题代码
/*官方题解代码*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
// base cases
if (head == null) return null;
if (head.next == null) return head;
// close the linked list into the ring
ListNode old_tail = head;
int n;
for(n = 1; old_tail.next != null; n++)
old_tail = old_tail.next;
old_tail.next = head;
// find new tail : (n - k % n - 1)th node
// and new head : (n - k % n)th node
ListNode new_tail = head;
for (int i = 0; i < n - k % n - 1; i++)
new_tail = new_tail.next;
ListNode new_head = new_tail.next;
// break the ring
new_tail.next = null;
return new_head;
}
}
/*我的代码*/
LinkList RotateList(LinkList L, int k)
{
LinkList head;
head = L->next;
if (head == NULL) return head;
LinkList p = head;
LinkList q = head;
int len = 1;
//求出链表长度,指针p指向当前链表尾部
while (p->next) {
len++;
p = p->next;
}
k %= len;
if (k == 0) return head; //当k为链表长度整数倍时返回原链表
p->next = head; //将原链表的尾节点与首节点链接
//寻找链表断开的位置
int index = 1;
while (index < (len - k)) {
index++;
q = q->next;
}
LinkList new_head = q->next;
q->next = NULL;
return new_head;
}
3.1.1 该题的设计思路
链表题目,请用图形方式展示解决方法。同时分析该题的算法时间复杂度和空间复杂度。
这道题是对链表的右移,并且要保持数据之间相对位置不变。相似的问题,之前学习数组的时候见过,数组经过移动,可以做到。但是对于链表来说,遍历链表时操作量太大,移动数据可操作性不太强,所以常规办法是行不通的。该题的官方解法是,先将链表封闭成环,再寻找结点断裂成新的头结点进行操作:
算法实现很直接:
(1)找到旧的尾部并将其与链表头相连 old_tail.next = head,整个链表闭合成环,同时计算出链表的长度 n。
(2)找到新的尾部,第 (n - k % n - 1) 个节点 ,新的链表头是第 (n - k % n) 个节点。
(3)断开环 new_tail.next = None,并返回新的链表头 new_head。
时间复杂度:O(N)其中N是链表中的元素个数
空间复杂度:O(1)因为只需要常数的空间
3.1.2 该题的伪代码
文字+代码简要介绍本题思路
定义指针head, p, q;
head = L->next;
p = q = head;
while p->next to end //求出链表长度;指针p指向当前链表尾部
k %= len;
if (k == 0) return head; //当k为链表长度整数倍时返回原链表
p->next = head; //将原链表的尾节点与首节点链接,形成环;
int index = 1; //寻找链表断开的位置
while to index < (len - k)
{
index++;
}
定义new_head = q->next;
return new_head;
3.1.3 运行结果
网上题解给的答案不一定能跑,请把代码复制自己运行完成,并截图。
3.1.4分析该题目解题优势及难点。
该题只要能想到封闭环,重新断开寻找结点,写代码的时候就不会太困难。难点是怎么样将单链表变成循环链表,以及如何准确的寻找到断开的结点,即新的链表头怎么找。通过该题目的学习,了解了循环链表如何使用,理解了它的优势。相对于单链表来说,能保持数据间相对位置不变化,从任意一个结点出发都能找到对应的数据。
3.2 题目及解题代码
/*官方题解代码*/
class Solution {
public:
int getDecimalValue(ListNode* head) {
ListNode* cur = head;
int ans = 0;
while (cur != nullptr) {
ans = ans * 2 + cur->val;
cur = cur->next;
}
return ans;
}
};
3.2.1 该题的设计思路
由于链表中从高位到低位存放了数字的二进制表示,因此我们可以使用二进制转十进制的方法,在遍历一遍链表的同时得到数字的十进制值。时间复杂度:O(N)其中N 是链表中的节点个数。
空间复杂度:O(1)。
3.1.2 该题的伪代码
int getDecimalValue(LinkList L)
{
定义遍历指针cur = L->next;
while cur to NULL
ans = ans * 2 + cur->data;//进位
return ans;
}
3.1.3 运行结果
3.1.4分析该题目解题优势及难点。
该问题是比较简单的链表题目,平常的思路就可以很好的解出。该题目的优势是好理解,空间复杂度比较友好。与此对比,还有一种递归的方法解决这道题目。这里还有一种递归思想解法:
class Solution {
public:
int sum = 0;
ListNode* p;
int getDecimalValue(ListNode* head) {
doit(head); //得到sum
return sum;
}
int doit(ListNode* p) { //递归调用计算sum
if (p==NULL) { //到二进制最后一位时返回
return 0;
}
int n = doit(p->next); //根据该位所处的位置加权
sum += (p->val)*pow(2, n);
return n+1;
}
};
在这里对递归与迭代的方法再做一次总结。
(1)递归的优点大问题转化为小问题,可以减少代码量,同时代码精简,可读性好;缺点就是递归调用浪费了空间,而且递归太深容易造成堆栈的溢出。
(2)迭代的好处就是代码运行效率好,因为时间只因循环次数增加而增加,而且没有额外的空间开销;缺点就是代码不如递归简洁