【题目】
假设有一个链表的节点定义如下:
struct Node {
int data;
Node* next;
};
现在有一个指向链表头部的指针:Node* head。如果想要在链表中插入一个新的节点,其成员 data 的值为 42,并使新节点成为链表的第一个节点,下面哪个操作是正确的?( )
A. Node* newNode = new Node; newNode->data = 42; newNode->next = head; head = newNode;
B. Node* newNode = new Node; head->data = 42; newNode->next = head; head = newNode;
C. Node* newNode = new Node; newNode->data = 42; head->next = newNode;
D. Node* newNode = new Node; newNode->data = 42; newNode->next = head;
【答案】
A
【解析】
一、链表与数组的区别及其存在意义
假设有一群盲人去看电影,为防走散,它们必须排成一队。如果能订到连在一起的座位最好,工作人员只需将领头的那个带到第一个座位,后面一个接一个摸过去即可。这种靠紧挨在一起找座的方式就是数组。
这种订座方式虽然方便,但也有缺陷:需要预留足够多的可以挨着坐的空座位。留多少空位合适就成了问题。留少了,人不够坐;留多了,浪费座位。如果看电影的盲人数是不固定的,这种方式根本不可行。
解决的办法只能是哪有空位订哪个,结果订的座位就会变成东一个西一个,对于盲人来说,找座就是一件麻烦的事。工作人员可以把订好的座位依次用链子连在一起,然后同样将领头的带到第一个座位,后面一个接一个顺着链子摸过去即可,这种靠链子找座的方式就是链表。
链表就是用一条条链子依次将各种东西连成一串,它们整体看起来就像一个列表一样。
与数组相同的是,它们的数据都是有顺序的,像排队一样,因此都属于线性表。
与数组不同的是,链表在内存中是分散存储的,它分的是一小块一小块的地,用多少分多少,随时用随时分;数组是连续存储的,它是一次分一大片地,估摸着用多少就分多少。同样要存100条数据,数组是一次分配足够的内存,链表是分100次一项一项地分配内存。
数组要访问数据只需要知道是第几项即可;链表要访问数据得从头捋着链子一点一点向后爬。
数组要插入或删除数据的话比较麻烦,需要将元素进行整体移动;链表要插入、删除数据不涉及任何数据移动,只需要重新拴一下链子。
因此,数组的读取速度比链表快,链表的插入和删除速度比数组快。
二、链表的结构
要靠链条找座位,每个座位必须有两样东西,一个就是要坐的人,一个就链子。
同样地,对于链表,每一项称为一个节点,每个节点也有两样东西:数据、指针。
座位就是节点,人就是数据,链子就是指针。
这个结构的关键是这条被叫做“指针”的链子,它包含着在何处能找到下一项的信息(确切地说是下一项的地址)。
对于本题而言,节点就是代码中的结构体Node,数据就是data,指针就是next。
struct Node { //定义了一个结构体Node
int data; //数据成员1:整型data,用于存储节点的数据
Node* next; //数据成员2:指向Node类型的指针next,用于指向下一节点
};
每个节点的指针next指向下一个节点的地址,这样就做成了链子。
要操作链表,必须先要找到领头的,才能顺藤摸瓜,因此需要给链表设置一个头指针(head pointer)。要判断什么时候摸到了队尾,需要有个特定的标志,方案是把最后一个节点的next指针设为NULL(表示空指针,可以把它理解为指针型的0)。
指针说到底也是变量,它的值其实就是整数,只不过这个整数用于表示内存的地址。上图是链表的逻辑结构,它在内存中是类似这样的:
定义头指针只需要一行代码:
Node* head; //定义一个指向Node类型的指针head
当然还要将第一个节点的地址赋给head;
head=firstNode;
三、链表的插入操作
要插入一个节点,只需要做两件事:将要插的节点指向下一个节点,上一个节点指向要插的节点。
下一个节点的地址是存在原链表的上一个节点中的,所以,只要知道上一个节点的地址,就能直接插入节点。比如将指针s指向的节点插入到指针p指向的节点之后:
1.本题的链表插入解析
本题要求在第一个节点之前插入节点,也就是在head之后插入节点。head指针指向的是原链表第一个节点,因此,只需要将head指针的值赋给新节点的next指针,再把新节点的地址赋给head即可完成插入。代码如下:
Node* newNode = new Node; //创建新节点newNode,并为其分配内存
newNode->data = 42; //将新的节点的数据成员 data 赋值为 42
newNode->next = head; //将head指针的值赋给新节点的next指针,即将原第一个节点的地址赋给新节点的next指针
head = newNode; //新节点的地址赋给head,即使head指向新节点
故选A。
其他选项:
B. 代码head->data = 42; 是错误的,head指向的是原链表的第一个节点,因此它会将42赋给了原链表的第一个节点,而不是新插入的节点。
C. 代码没有用head指针指向新节点,注意因为head指针本来是指向第一个节点的,所以head->next = newNode;是将第一个节点的next指针指向新节点,这相当于把新节点放在第一个节点之后,插错了位置。而且这个新节点只连了前面,没连后面。
D. 与选项C相反,新节点只连了后面,没连前面。
2.模拟真实链表插入
(1)添加链表元素。
要在链表中插入一个节点,首先链表中得有元素,所以先要在链表中添加元素。
添加元素主要有三步:
①创建节点。创建新节点newNode,并为其分配内存空间,如前面的Node* newNode = new Node;
②为节点赋值。分数据和指针两部分,为节点存入数据如前面的newNode->data = 42; 因为添加的节点是处于最后的,所以其next指针应设为NULL。
③链接节点。如果是第一次添加,就和head链接,否则就和尾节点链接。
代码如下:
//为链表添加元素
Node *tail, *newNode; //tail指向尾节点,newNode指向要添加的节点
head = NULL;
int t[]={9527, 9421, 2587, 2627, 1372, 1573, 1920};
for(int i=0; i<7; i++){
//创建节点
newNode = new Node; //创建新节点newNode,并为其分配内存
//为节点赋值
newNode->data = t[i]; //将新的节点的数据成员 data 赋值
newNode->next = NULL; //将当前节点的next指针设为空指针
//链接节点
if(head==NULL) head = newNode; //为头指针赋值
else tail->next = newNode; //为上个节点的next指针赋值
tail=newNode; //更新尾节点,将新节点赋给tail
}
代码说明:
①三个指针。为方便添加链表元素,需要3个指针:head、tail、newNode。head即是前面说的头指针,指向第一个节点;tail是尾指针,指向最后一个节点,有了这个指针才能方便在队尾添加新元素;newNode指向要添加的节点。
②指针的赋值。关于这三个指针的赋值要特别注意:head指针最开始赋值为NULL,表示链表是空的,当添加第一个元素时,就指向第一个元素。新添加的节点自然处于队尾,所以其next指针应设为NULL。tail的next指针指向新节点,并且每次添加完节点,别忘了还要更新tail(把新节点赋给tail)。
(2)插入节点
方法是通过遍历找到节点要插入的位置,然后用前面介绍的插入方法插入节点。
插入位置有两种情况:插入到第一个节点前、插入到其他节点前。前者涉及到head指针,与后者的处理是不一样的。
代码如下:
//链表插入节点的函数:在第i个节点前插入data
void ListInsert(Node** head, int i, int data){
//找到要插入的节点的前一个节点p
Node* p;
p = *head;
int j=1;
while(p&&j<i-1){
p=p->next; //指针移到下个节点
j++;
}
Node* newNode = new Node;
newNode->data = data;
if(i==1){ //处理插入到第一个节点前的情况
newNode->next = *head;
*head = newNode;
}else{ //处理插入到其他节点前的情况
newNode->next = p->next;
p->next = newNode;
}
}
代码说明:
① 遍历条件判定。变量j 表示要遍历的节点位置,它应该遍历到i-2的位置终止,此时通过p=p->next 正好将p指向第i-1个节点。while(p&&j<i-1)中的p是为了保证遍历到空指针时停止。
②指针的指针。插入到第一个节点时,需要更新head指针。这时将head作为参数传入函数时,就需要传入head指针的地址,也就是指针的指针。所以函数参数要用Node** head的形式,更新head的值时要用*head,而当调用该函数时参数要用&head。(如果不习惯于指针的指针这种表示,可以用C++的引用语法,只需要在定义函数时将Node** head改为Node* &head即可)
完整的程序代码如下:
#include<stdio.h>
struct Node{
int data;
Node* next;
};
Node* head;
//链表插入节点的函数:在第i个节点前插入data
void ListInsert(Node** head, int i, int data){
//找到要插入的节点的前一个节点p
Node* p;
p = *head;
int j=1;
while(p&&j<i-1){
p=p->next; //指针移到下个节点
j++;
}
Node* newNode = new Node;
newNode->data = data;
if(i==1){ //处理插入到第一个节点前的情况
newNode->next = *head;
*head = newNode;
}else{ //处理插入到其他节点前的情况
newNode->next = p->next;
p->next = newNode;
}
}
int main(){
//为链表添加元素
Node *tail, *newNode; //tail指向尾节点,newNode指向要添加的节点
head = NULL;
int t[]={9527, 9421, 2587, 2627, 1372, 1573, 1920};
for(int i=0; i<7; i++){
//创建节点
newNode = new Node; //创建新节点newNode,并为其分配内存
//为节点赋值
newNode->data = t[i]; //将新的节点的数据成员 data 赋值
newNode->next = NULL; //将当前节点的next指针设为空指针
//链接节点
if(head==NULL) head = newNode; //为头指针赋值
else tail->next = newNode; //为上个节点的next指针赋值
tail=newNode; //更新尾节点,将新节点赋给tail
}
//在链表中插入节点
ListInsert(&head, 1, 42); //在第1个节点前插入数值42
//打印链表
Node *current = head;
while(current){
printf("%d\n", current->data);
current = current->next; //指针移到下个节点
}
return 0;
}
【题目来源】
2023 CCF非专业级别软件能力认证第一轮 (CSP-J1) 入门级C++语言试题 第4题