题目描述
众所周知,第 0 次机考总有一道题是单链表,这次也不例外。
你需要实现一个单链表 L L L,一些需要实现的操作如下:
-
利用数组初始化该单链表,该操作只会在初始时被调用 1 次;
-
在单链表第 i i i 个元素后插入元素 x x x。特别地,如果 i = 0 i = 0 i=0,则表示在表头插入元素;如果 i = ∣ L ∣ i = |L| i=∣L∣,则表示在表尾插入元素;如果出现除 0 ≤ i ≤ ∣ L ∣ 0 \leq i \leq |L| 0≤i≤∣L∣之外的其他情况,无视当前操作;
注意:元素从 1 开始编号。
-
删除单链表的第 i i i 个元素。特别地,如果出现除 1 ≤ i ≤ ∣ L ∣ 1 \leq i \leq |L| 1≤i≤∣L∣之外的其他情况,无视当前操作;
-
将单链表奇偶交换,也就是说原来编号为 2 k 2k 2k的元素与原来编号为 2 k − 1 2k - 1 2k−1的元素交换。如果单链表长度为奇数,则剩下的最后一个元素不需要进行交换;你需要在原地进行该操作,也就是说,这个操作只能使用 O ( 1 ) O(1) O(1)的额外空间;
-
询问单链表的第 i i i个元素。特别地,保证不会出现除 1 ≤ i ≤ ∣ L ∣ 1 \leq i \leq |L| 1≤i≤∣L∣之外的其他情况。
-
询问整个单链表,即按编号顺序输出单链表中所有元素。
-
在程序的最后,你需要释放所有动态申请的内存。
本题提供代码模板,也欢迎直接从头开始写代码的选手。
输入格式
程序开始时,你需要读入一个数 n n n 和一个有 n n n 个数的数组;然后调用操作 0 进行初始化。接下来你会读入一个数 m m m 表示操作个数。接下来 m m m行,每行第一个数为 o p op op,表示操作类型。
- 如果 o p = 1 op = 1 op=1,表示当前操作为操作 1,接下来读入两个数 i,xi,x,含义如上文所示;
- 如果 o p = 2 op = 2 op=2,表示当前操作为操作 2,接下来读入一个数 ii,含义如上文所示;
- 如果 o p = 3 op = 3 op=3,表示当前操作为操作 3;
- 如果 o p = 4 op = 4 op=4,表示当前操作为操作 4,接下来读入一个数 i i i,含义如上文所示;
- 如果 o p = 5 op = 5 op=5,表示当前操作为操作 5。
在所有操作后,将会调用操作 6 来释放动态分配的内存。
输出格式
对于每一个操作 4 或操作 5,在屏幕上输出操作的答案。注意,每次操作 4 或操作 5 完成后,务必在输出后附带换行,以区分每个输出。具体参见样例输入输出。
样例输入
5
2 3 3 3 4
10
3
5
1 2 6
1 5 4
1 10 3
2 7
4 4
4 3
3
5
样例输出
3 2 3 3 4
3
6
2 3 3 6 4 3
数据范围
1
≤
n
,
m
≤
3000
1 \leq n, m \leq 3000
1≤n,m≤3000,链表中的所有数在 int
范围内。
请务必使用单链表来完成本题!!助教将会人工判断你的代码是否使用单链表,若否则本题计 0 分。
你可以使用如下的代码模板,程序的大部分已帮你写好,你只需要填写 TODO
中的内容即可;当然,你也可以自己从头开始写代码。
# include <iostream>
# include <stdio.h>
using namespace std;
struct LinkList {
// TODO: Define some variables of struct LinkList here.
void Initialize(int *a, int n) {
// TODO: use a[0 ... n-1] to initialize the link list.
}
void Insert(int i, int x) {
// TODO: insert element x after i-th element.
}
void Delete(int i) {
// TODO: delete element i
}
void EvenOddSwap() {
// TODO: Swap the even-indexed element with the corresponding odd-indexed element.
}
void Query(int i) {
// TODO: Print the value of the i-th element on the screen.
}
void QueryAll() {
// TODO: Print the link list on the screen.
}
void ClearMemory() {
// TODO: Clear the memory.
}
};
LinkList L;
int main() {
ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, *a;
cin >> n;
a = new int [n];
for (int i = 0; i < n; ++ i) cin >> a[i];
L.Initialize(a, n);
delete [] a;
int m, op, i, x;
cin >> m;
while(m --) {
cin >> op;
switch(op) {
case 1:
cin >> i >> x;
L.Insert(i, x);
break;
case 2:
cin >> i;
L.Delete(i);
break;
case 3:
L.EvenOddSwap();
break;
case 4:
cin >> i;
L.Query(i);
break;
case 5:
L.QueryAll();
break;
}
}
L.ClearMemory();
return 0;
}
题目分析
首先,作为一个机考题,如果是没有学习过的应该是蛮难了,但是似乎ACM班的大佬应该很多,这题肯定是小意思。~~(而且我0基础看了一下午也就学会了?这说明不难)~~不过要是有单链表以及指针的基础话并不困难。而且客观的说,本题考察的也很基础,就是普通的单链表操作,包括增加、删除、交换、修改。似乎看出来这个题目的目的,就是督促学生预习23333。
另外我觉得emmm……写代码的时候最重要的一点就是及时输出检测,在我第一次写这道题的时候,一口气写完了所有的功能。
没错,然后失败了,又重头写的2333。
所以写代码一定记得及时输出,写一个功能检测一个功能,不然,,,,嘿嘿,,,头发,你懂的。。。。
单链表
单链表嘛,从个人的感觉就是emmm在黑夜里面,操作一个黑箱子,看不见摸不着,全靠指针来指示方向。单链表的每一个节点包括两个部分,数据部分和指针部分。如下表所例。
数据部分 | 指针部分 |
---|---|
int data | int * p |
单链表是一个链子,前一个节点指向后一个节点,但是在内存中的位置我们不知道。这就是链表和数组的最大区别。数组在内存里面是连续的内存空间,但是链表不是。操作链表的时候一定要小小,应为一旦失误,谁都不知道这个指针会指向哪里,下一个结点会在哪里,下下个节点会在哪里也没有人知道。一旦断了或者地址丢失了,整个链表相当于作废了。
通常比较好的方法是定义单链表的时候开头定义一个指针head头指针,然后尾部定义一个NULL空指针,然后遍历的时候就比较方便啦!有始有终,便于查找。
题目解答
一、定义结构体
我们开始正经的看题目。首先定义一个结构体。
struct LinkList
{
int data; // 定义一个结构体,前一个部分存放数据
LinkList *next; // 定义一个结构体,后一个部分存放指针,指向下一个节点。
};
然后要定义的是一些全局变量,考虑到整个程序中要统计数组的个数,要有一个头指针和尾部指针,还有一个指针要跑腿,在创建和查找的时候遍历这个链表,我们定义为*p
;
LinkList *head,*rear,*p;
int n;
二、链表初始化
第一个要实现的功能是初始化单链表,题目是通过一个函数initialize实现的,而函数之前有一个数组a[n],初始化单链表有下面几个步骤
-
创建第一个节点,head指向表头,rear指向表尾,第一个节点是也是最后一个节点。
-
for循环读取数据,读入数据后要申请空间,空间里面的数据部分要存放新来的数据,前一个节点的指针部分要指向这个新的节点;
-
读取结束要把尾指针更新为NULL。
步骤一:创建空链表
head = rear = new LinkList; // 申请空间,头指针,尾指针都定义过来。(其实尾指针可以最后再说)
步骤二、三:空链表初始化
void Initialize(int a[], int n)
{
for (int i = 0; i < n; i++)
{
p = new LinkList; // 申请空间
p->data = a[i]; // data部分放数据
rear->next = p; // 之前的尾指针指示的是上一个节点,rear->next 表示上一个节点的指针域
rear = p; // 更新尾指针
}
rear->next = NULL; // for循环结束,尾指针也要更新为空指针 NULL
}
三、链表的输出
链表的输出由void QueryAll()
负责,输出就由一个for遍历一次即可。
void QueryAll()
{
p = head->next;
while (p != NULL)
{
if (p->next == NULL)
cout << p->data; // 输出结果
else
cout << p->data << " "; // 输出结果
p = p->next; // 考虑到输出的之间要空白 if实现
}
cout << endl;
}
四、链表的访问
链表的访问由void Query(int i)
负责,不妨我们修改一下,把这个函数功能改为把指针p定位到第i个元素。输出放在主函数里面。
void Query(int i) //这个函数用于把指针p定位到第i个元素
{
p = head->next;
for (int k = 0; k < i - 1; k++)
{
p = p->next;
}
// cout << p->data << endl; 测试有效!
}
五、链表的元素删除
链表的删除需要以下几个步骤:
-
定义一个临时指针,保存要删除节点前面的位置的地址。
-
更改要删除节点前面的节点的指针部分,使其指向后面节点的后面的节点。
-
通过指针回收要删除的节点,delete!
考虑到这个题目有点特殊,可能要删第一个,也可能要删除最后一个,也可能传入的数非法,那就不操作,所以if判断。
另外,后两个
if
可以合并。
void Delete(int i) // 删除第 i 个节点
{
LinkList *temp;
if (i == 1) // 删除首节点
{
LinkList *temp; // 定义一个临时指针
temp = head->next;
head->next = temp->next; // 更改要删除节点前面的节点的指针部分,使其指向后面节点的后面的节点。
delete temp; // 回收空间要记得哦
n--; // 更新链表元素的总数
}
if (i == n) // 删除尾部节点
{
LinkList *temp; // 定义一个临时指针
Query(n - 1); // 利用之前已经写好的函数,把temp指针定位到要删除指针的前面。
temp = p->next; // 临时指针保存要删除节点前面的位置的地址
p->next = temp->next; // 更改要删除节点前面的节点的指针部分,使其指向后面节点的后面的节点。
delete temp; // 回收空间要记得哦
n--; // 更新链表元素的总数
}
if (i > 1 && i < abs(n)) // 删除中间节点
{
Query(i - 1); // 利用之前已经写好的函数,把temp指针定位到要删除指针的前面。
LinkList *temp;
temp = p->next; // 临时指针保存要删除节点前面的位置的地址
p->next = temp->next; // 更改要删除节点前面的节点的指针部分,使其指向后面节点的后面的节点。
delete temp; // 回收空间要记得哦
n--; // 更新链表元素的总数
}
else
return;
}
六、链表的元素增加
要增加链表的元素,分三种情况:链表首元素前添加,链表中间元素添加,链表末元素前添加。
链表的添加步骤如下:
-
申请新空间,在data区域存放要添加的元素值
-
如果在第 i i i 个节点后面添加节点,可以调用我们之前写过的函数
Query(i);
把p指针指的第 i i i 个节点 -
注意顺序!先修改新空间的指针域的数据,使其指向第 i i i 个节点后面的节点
-
注意顺序!再修改第 i i i 个节点的指针域,使其指向新空间,注意,就这样就可以了。
-
结束!
不要画蛇添足回收空间
void Insert(int i, int x)
{
// TODO: insert element x after i-th element.
LinkList *temp;
if (i == 0)
{
temp = new LinkList;
temp->data = x;
temp->next = head->next;
head->next = temp;
n++;
}
if (i > 0 && i <= abs(n))
{
Query(i); //申请新空间
temp = new LinkList; //在data区域存放要添加的元素值
temp->data = x; //先修改新空间的指针域的数据,使其指向第i个节点后面的节点
temp->next = p->next;
p->next = temp; //再修改第 $i$ 个节点的指针域,使其指向新空间,注意,就这样就可以了。
n++; // 更新链表元素的总数n(n是全局变量)
}
else
return; // 非法数据不予处理
}
七、链表奇偶元素互换
老套路,把p指针先指向链表头部吧。
偷偷的开一个空间,临时存放数据,然后就交换数据,就行我们学过的交换数组的数据。
p指针最后往后移动两个位置,例如从第head->2,2->4……之后再开始循环
for循环中表示次数,考虑到n/2的特点,可以满足要求。
void EvenOddSwap()
{
// TODO: Swap the even-indexed element with the corresponding odd-indexed element.
p = head;
for (int j = 1; j <= n / 2; j++)
{
int tempnum = p->next->data;
p->next->data = p->next->next->data;
p->next->next->data = tempnum;
p = p->next->next;
}
}
其实最开始我是这么写的,先保存奇数节点的data数值,再调用delete删除节点,再把保存的数组添加到正确位置
但是明显很复杂,Query(k)的调用与j的遍历明显会超时,所以不建议使用。
void EvenOddSwap()
{
// TODO: Swap the even-indexed element with the corresponding odd-indexed element.
p = head;
LinkList *temp1,*temp2;
for (int j = 1; j <= n / 2; j++)
{
int k = 2 * j - 1;
Query(k);
int tempnum = p->data;
Delete(k);
Insert(k, tempnum);
}
}
八、完整代码
#include<iostream>
#include<stdio.h>
using namespace std;
struct LinkList
{
int data;
LinkList *next;
};
LinkList *head,*rear,*p;
int n;
void Initialize(int a[], int n)
{
for (int i = 0; i < n; i++)
{
p = new LinkList;
p->data = a[i];
rear->next = p;
rear = p;
}
rear->next = NULL;
}
void QueryAll()
{
p = head->next;
while (p != NULL)
{
if (p->next == NULL)
cout << p->data;
else
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
void Query(int i) //这个函数用于把指针p定位到第i个元素
{
p = head->next;
for (int k = 0; k < i - 1; k++)
{
p = p->next;
}
// cout << p->data << endl; 测试有效!
}
void Delete(int i)
{
LinkList *temp;
if (i == 1)
{
temp = new LinkList;
temp = head->next;
head->next = temp->next;
delete temp;
n--;
}
if (i == n)
{
LinkList *temp;
Query(n - 1);
temp = p->next;
p->next = temp->next;
delete temp;
n--;
}
if (i > 1 && i < abs(n))
{
Query(i - 1);
LinkList *temp;
temp = p->next;
p->next = temp->next;
delete temp;
n--;
}
else
return;
}
void Insert(int i, int x)
{
// TODO: insert element x after i-th element.
LinkList *temp;
if (i == 0)
{
temp = new LinkList;
temp->data = x;
temp->next = head->next;
head->next = temp;
n++;
}
// if (i == abs(n))
// {
// Query(n);
// temp = new LinkList;
// temp->data = x;
// temp->next = p->next;
// p->next = temp;
// n++;
// }
if (i > 0 && i <= abs(n))
{
Query(i);
temp = new LinkList;
temp->data = x;
temp->next = p->next;
p->next = temp;
n++;
}
else
return;
}
void EvenOddSwap()
{
// TODO: Swap the even-indexed element with the corresponding odd-indexed element.
p = head;
LinkList *temp1,*temp2;
// for (int j = 1; j <= n / 2; j++)
// {
// // int k = 2 * j - 1;
// // Query(k);
// // int tempnum = p->data;
// // Delete(k);
// // Insert(k, tempnum);
// }
for (int j = 1; j <= n / 2; j++)
{
int tempnum = p->next->data;
p->next->data = p->next->next->data;
p->next->next->data = tempnum;
p = p->next->next;
// temp1 = new LinkList;
// temp2 = new LinkList;
// temp1->next = temp2;
// temp1->data = p->next->data;
// temp2->data = p->next->next->data;
// temp2->next = p->next->next->next;
// LinkList *dp;
// dp = p->next->next;
// delete dp;
// dp = p->next;
// delete dp;
// p->next = temp1;
// p = temp2->next;
}
}
void ClearMemory()
{
// TODO: Clear the memory.
LinkList *q;
p = head;
while (p != NULL)
{
q = p->next;
delete p;
p = q;
}
}
int main()
{
head = rear = new LinkList;
cin >> n;
int a[n];
for (int i = 0; i < n; ++ i)
{
cin >> a[i];
}
int m, op, i, x;
cin >> m;
Initialize(a, n);
while(m--)
{
cin >> op;
switch (op)
{
case 1:
cin >> i >> x;
Insert(i, x);
break;
case 2:
cin >> i;
Delete(i);
break;
case 3:
EvenOddSwap();
break;
case 4:
cin >> i;
Query(i);
cout << p->data << endl;
break;
case 5:
QueryAll();
break;
default:
break;
}
// cout << "当前有" << n << endl;
}
ClearMemory();
system("pause");
return 0;
}
补充更新,考虑到我之前的做法没有用题目的模板,而且把函数都拆分了出来(当时确实不是很会操作,没有学习过类和结构体的知识)现在学了一丢丢再来补充更新一下,按照模板的写法。
之前的我也尝试过按照模板,但是在结构体里面我要定义单链表的每一个节点,那就是要一个int
整型变量和一个 next
指针变量,另外还要三个变量,分别是*head
,*rear
,*p
分别指示头结点,尾结点,还有用来操作的指针,定义的时候有点乱,解决方法是,在结构体里面定义一个结构体(事实证明可行)就像下面的代码一样,里面的结构体 LinkList
是用来描述节点的,每个节点有 data
和 next
组成,外面的结构体是一个包裹的,就相当于一个类,里面的变量是私有的 private
(为啥?因为外部的 main 函数不需要调用到里面的变量,所以用私有的)里面的函数是 public
(为啥?因为外部的 main 函数要调用这个里面的函数,所以是公有的。(关于私有共有还是看课本那个有理数计算的案例,并不是所有函数都是公有,课本里面那个有理数约分的那个函数就是私有的,因为这个函数外面不需要调用,所以设置为私有 private
)
写到这里我就有点生气了,那这个出题人就有点没搞好,直接定义一个类嘛,然后在类里面方函数不香嘛,偏偏要搞一个结构体,结构体里面再放一个结构体,都让人要混乱了
按照模板的代码如下:(通过结构体访问成员函数)
#include<iostream>
#include<stdio.h>
using namespace std;
int n,*a; // 由于n是链表里面元素的个数,所以我把它移到全局变量
int temp_query; // 由于我的Query()函数只能把指针定位到制定元素,不能输出,所以定义这个变量
struct test
{
struct LinkList
{
int data;
LinkList *next;
}; // 这个结构体是单链表的结构体
LinkList *head,*rear,*p; // 定义三个指针变量
void Initialize(int a[], int n)
{
head = rear = new LinkList;
for (int i = 0; i < n; i++)
{
p = new LinkList;
p->data = a[i];
rear->next = p;
rear = p;
}
rear->next = NULL;
}
void QueryAll()
{
p = head->next;
while (p != NULL)
{
if (p->next == NULL)
cout << p->data;
else
cout << p->data << " ";
p = p->next;
}
cout << endl;
}
void Query(int i) //这个函数用于把指针p定位到第i个元素
{
p = head->next;
for (int k = 0; k < i - 1; k++)
{
p = p->next;
}
temp_query = p->data;
}
void Delete(int i)
{
LinkList *temp;
if (i == 1)
{
temp = new LinkList;
temp = head->next;
head->next = temp->next;
delete temp;
n--;
}
if (i == n)
{
LinkList *temp;
Query(n - 1);
temp = p->next;
p->next = temp->next;
delete temp;
n--;
}
if (i > 1 && i < abs(n))
{
Query(i - 1);
LinkList *temp;
temp = p->next;
p->next = temp->next;
delete temp;
n--;
}
else
return;
}
void Insert(int i, int x)
{
// TODO: insert element x after i-th element.
LinkList *temp;
if (i == 0)
{
temp = new LinkList;
temp->data = x;
temp->next = head->next;
head->next = temp;
n++;
}
if (i > 0 && i <= abs(n))
{
Query(i);
temp = new LinkList;
temp->data = x;
temp->next = p->next;
p->next = temp;
n++;
}
else
return;
}
void EvenOddSwap()
{
// TODO: Swap the even-indexed element with the corresponding odd-indexed element.
p = head;
LinkList *temp1,*temp2;
for (int j = 1; j <= n / 2; j++)
{
int tempnum = p->next->data;
p->next->data = p->next->next->data;
p->next->next->data = tempnum;
p = p->next->next;
}
}
void ClearMemory()
{
// TODO: Clear the memory.
LinkList *q;
p = head;
while (p != NULL)
{
q = p->next;
delete p;
p = q;
}
}
};
struct test L;
int main()
{
cin >> n;
a = new int [n];
for (int i = 0; i < n; ++ i)
{
cin >> a[i];
}
L.Initialize(a, n);
delete [] a;
int m, op, i, x;
cin >> m;
while(m--)
{
cin >> op;
switch (op)
{
case 1:
cin >> i >> x;
L.Insert(i, x);
break;
case 2:
cin >> i;
L.Delete(i);
break;
case 3:
L.EvenOddSwap();
break;
case 4:
cin >> i;
L.Query(i);
cout << temp_query << endl;
break;
case 5:
L.QueryAll();
break;
default:
break;
}
}
L.ClearMemory();
system("pause");
return 0;
}
结构体默认是 public
所以按照题目的这个模板就有点不合适,不过这题共有私有不必纠结,直接做就可以。(2021-7-26更新)